Skip to content

feat: [SDK-5088] add device gesture that marks the user as a test user - #2727

Draft
nan-li wants to merge 1 commit into
mainfrom
nan/sdk-5088
Draft

feat: [SDK-5088] add device gesture that marks the user as a test user#2727
nan-li wants to merge 1 commit into
mainfrom
nan/sdk-5088

Conversation

@nan-li

@nan-li nan-li commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Six background/foreground cycles within 30 seconds mark the current user as a test user.

Details

Motivation

Finding your own device in the dashboard is the slowest part of sending a first test push. Today that means pulling a subscription ID out of verbose logs, or searching by external ID. This adds a gesture anyone can perform on a production build, with no app changes and no new permissions. Background and foreground the app six times within 30 seconds and the SDK sets a test_user_name property on the user, which the dashboard can then surface.

Scope

DeviceGestureDetector registers as an IApplicationLifecycleHandler and counts background-to-foreground pairs on a monotonic clock. A cycle needs a background phase of at least 250ms. That floor filters the synthetic unfocus/focus pair ApplicationService.onOrientationChanged fires when an activity declaring orientation in configChanges rotates, which would otherwise let six rotations fake the gesture. The 30 second sliding window is the only rate rule, so completing it takes sustained five-second round trips. Normal app switching never gets there.

On completion the SDK sends PATCH apps/{appId}/users/by/onesignal_id/{id} with test_user_name set to the device-local time. It goes straight through IHttpClient instead of the operation repo, because a queued operation can replay hours later and a stale mark is worse than one that failed outright. The JWT comes from the same JwtTokenStore lookup the update-user executors already use.

Nothing is sent when privacy consent is withheld, when the user has no backend ID yet, or when sdk_device_gesture_disabled is in the app's enabled feature keys. That key is an inverted kill switch, so absent means enabled and a device that has never fetched flags still has a working gesture. It reads the raw ConfigModel.sdkRemoteFeatureFlags list rather than IFeatureManager, because the manager only resolves keys the KMP catalog registers and this key is deliberately not in the catalog. Registering it there stays optional follow-up.

CoreModule.register moved its misconfigured-fallback registrations into a private helper to stay under detekt's LongMethod cap after the new line. No behavior change.

Other

The backend does not accept test_user_name yet and the dashboard does not surface test users yet, both tracked separately. Until that lands this ships inert, which is why it is a draft.

Testing

Unit testing

DeviceGestureDetectorTests drives synthetic focus/unfocus sequences against a controlled clock and a mocked IHttpClient, asserting the request path, the payload, and the JWT header. Fifteen tests cover the counting rules, including six cycles firing exactly once, five firing nothing, the detector re-arming afterward, sub-250ms blips not counting, cycles spread wider than the window never accumulating six, and a long pause mid-gesture keeping earlier progress. The rest cover every skip condition.

Manual testing

Not yet exercised on an Android device. The iOS counterpart was hand-tested, and that testing is what drove the timing rules here from a stricter dwell fingerprint down to the single window rule, since the strict version proved nearly impossible to perform by hand.

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

📊 Diff Coverage Report

Diff Coverage Report (Changed Lines Only)

Gate: aggregate coverage on changed executable lines must be ≥ 80% (JaCoCo line data for lines touched in the diff).

Changed Files Coverage

  • DeviceGestureDetector.kt: 0/59 touched executable lines (0.0%) (168 touched lines in diff)
    • 59 uncovered touched lines in this file

Overall (aggregate gate)

0/59 touched executable lines covered (0.0% — requires ≥ 80%)

Per-file detail (informational; gate is aggregate above):

  • DeviceGestureDetector.kt: 0.0% (59 uncovered touched lines)

❌ Coverage Check Failed

Aggregate coverage on touched lines is 0.0% (minimum 80%).

📥 View workflow run

Backgrounding and foregrounding the app 6 times within 30 seconds sends
an Update User PATCH setting the test_user_name property to the
device-local time, so the dashboard can surface the device as a test
user. The request goes straight through IHttpClient, bypassing the
operation repo; a queued operation can replay hours later, and a stale
raise-hand signal is worse than a failed one. The JWT rides along via
the same JwtTokenStore lookup the update-user executors use.

Cycles are counted on a monotonic clock. A cycle needs a real
background phase of at least 250ms, which filters the synthetic
rotation unfocus/focus pair from ApplicationService.onOrientationChanged,
and the 30s sliding window is the only rate rule. Each counted cycle
logs at verbose so manual testing can watch progress.

The gesture skips when privacy consent is withheld or the user does not
exist on the backend yet, and adding sdk_device_gesture_disabled to an
app's enabled feature keys turns it off remotely. The raw
ConfigModel.sdkRemoteFeatureFlags list is checked instead of
IFeatureManager because the KMP catalog hides unregistered keys.

CoreModule.register moved its misconfigured-fallback block into a helper
to stay under detekt's LongMethod cap after the new registration.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant