P-2207: Web-to-mobile attribution (Android)#51
Conversation
Fix three mobile analytics gaps found while testing the RN SDK against the ingestion pipeline (the SDK was already ~80% complete; the real issues were at the SDK <-> Tinybird seams): - session_id: the SDK sent none, so the pipeline (which groups sessions by session_id with no fallback) collapsed all mobile users of a project into one session. Now generated with a 30-min inactivity timeout. - user_agent: empty on the Expo path, so device/OS classification (done purely from the UA string) resolved to "unknown". Now synthesized from the device info the SDK already collects. - screen page_url: was app://<name>, making the screen name the origin (fragmenting sessions) and leaving page_path empty. Now app://<bundleId>/<name> - stable origin per app, real path per screen. Add per-event debug logging and 3 test suites pinning each fix to the pipeline's classifier. 226 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
session_id/anonymous_id now use Web Crypto (getRandomValues/randomUUID) when available, addressing the CodeQL js/insecure-randomness alert. Falls back to Math.random only on runtimes without Web Crypto so the SDK never throws; these IDs are analytics identifiers, not security tokens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend overrides to bump the fixable transitive deps (shell-quote, tmp, ws@6/7, ws@8, js-yaml@3) and document-ignore the 3 residuals with no clean fix (@babel/core, ws, js-yaml) - all in react-native/wagmi build tooling, never shipped in the SDK runtime. pnpm audit --prod now passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the no-Web-Crypto fallback (previously Math.random) with a monotonic-counter + timestamp hashed via SHA-256, so there is no PRNG in the code at all — clearing the residual CodeQL js/insecure-randomness alert. Secure path (Web Crypto) unchanged; fallback stays crash-free and collision-free within a process. Adds tests exercising the fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Attribute mobile installs to where the user came from (e.g. a link on example.com -> Play Store -> install), via the Google Play Install Referrer: - declare react-native-play-install-referrer as an optional peer dep so the (already-written) Android capture actually runs - await the install-referrer capture before lifecycle fires, so the referrer is available when Application Installed is tracked - attach the captured referrer/utm_* to the Application Installed event properties (referrer=example.com), filtering empty fields - remove the dead iOS AdServices path (referenced a non-existent npm package; Apple exposes no install-referrer API, so iOS web-to-mobile attribution is not possible from the SDK) Android is deterministic; iOS is documented as unsupported. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
- Pin @babel/plugin-transform-modules-systemjs override below v8. The lockfile regen let it float to 8.0.1 (peer @babel/core@^8, Node >=22.18) while the tree uses @babel/core@7 and CI runs Node 22.14 (P1). - Restore Math.random in the no-Web-Crypto UUID fallback for cross-process entropy; two processes starting in the same millisecond otherwise collide (P2). Math.random is fine here (analytics IDs, not security tokens); the CodeQL insecure-randomness alert on this line is dismissed as a false positive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bundle-id-host format (app://<bundleId>/<name>) conflicts with the Tinybird mobile page-event handling in formono#2033, whose page_path parser strips the app:// scheme and would leak the bundle id into the path (/com.formo.app/home instead of /home). The backend derives origin from the app identifier in context and page_path from the URL, so the SDK emits the screen name as-is. Test updated to guard against re-encoding a host. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 86ac51ad64
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await captureInstallReferrer({ | ||
| customRefParams: analytics.options.referral?.queryParams, | ||
| pathPattern: analytics.options.referral?.pathPattern, | ||
| }); |
There was a problem hiding this comment.
Add a timeout to the install-referrer await
When the Play Install Referrer native callback never arrives (for example, a stalled Play Store/service connection), captureAndroidReferrer leaves its promise pending indefinitely. This new await makes FormoAnalytics.init hang in that case; consequently the provider never sets a real SDK instance and all tracking remains on the no-op context. Keep the attribution fetch bounded and continue initialization after the timeout, as the previous fire-and-forget path did.
Useful? React with 👍 / 👎.
| "react-native-play-install-referrer": { | ||
| "optional": true | ||
| }, |
There was a problem hiding this comment.
Document installation of the Android referrer module
For Android consumers following the README installation command, this optional peer is not installed, so the lazy require in captureAndroidReferrer resolves to null and the new attribution path silently skips. Because marking a peer optional also suppresses the missing-peer warning, users receive neither the P-2207 behavior nor an actionable indication of what to add; document react-native-play-install-referrer as an Android setup dependency (including native rebuild instructions).
Useful? React with 👍 / 👎.
- Add a 3s timeout to the Play Install Referrer native call. init() awaits this capture, so a stalled Play Store service connection would otherwise leave the promise pending forever and hang SDK initialization, leaving every consumer on the no-op context with tracking dead (P1). On timeout we skip and retry next launch rather than marking attribution resolved. - Document react-native-play-install-referrer as an Android setup step in the README (install + native rebuild + referrer link format), and warn instead of debug-log when it is missing on Android, since marking the peer optional suppresses the missing-peer warning (P2). - Add regression tests for the hung-callback path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # pnpm-lock.yaml
The events-gateway authorizer computes session_id as hash(dailySalt + domain + sourceIp + userAgent) — a privacy-friendly, daily-rotating identifier (Plausible-style, see apps/aws functions/events-gateway/authorizer.ts). handlerV0 then applies `obj?.session_id || session_id`, so a body-provided session_id overrides it; that escape hatch exists for server-side SDKs whose request IP/UA would otherwise collapse every call into one session. Emitting session_id from the mobile SDK hijacked that escape hatch and replaced the daily-rotating hash with a client UUID, giving web and mobile two different definitions of a session. Reverted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # pnpm-lock.yaml # src/constants/storage.ts
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3bc68ed386
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "react-native-play-install-referrer": { | ||
| "optional": true |
There was a problem hiding this comment.
Make the native referrer module non-optional
When an Android consumer does not install this peer, Metro will fail while bundling: src/lib/installReferrer/index.ts contains a statically analyzable require("react-native-play-install-referrer"), and Metro resolves such dependencies before the try/catch can run. Marking the peer optional therefore makes the documented no-module path a build-time Unable to resolve module failure rather than a runtime warning/no-op. Make it required, or replace the static dependency with an integration approach that Metro can safely omit.
Useful? React with 👍 / 👎.
| if (analytics.isAttributionEnabled("installReferrer")) { | ||
| captureInstallReferrer({ | ||
| customRefParams: analytics.options.referral?.queryParams, | ||
| pathPattern: analytics.options.referral?.pathPattern, | ||
| }).catch((error) => { | ||
| try { | ||
| await captureInstallReferrer({ | ||
| customRefParams: analytics.options.referral?.queryParams, | ||
| pathPattern: analytics.options.referral?.pathPattern, | ||
| }); |
There was a problem hiding this comment.
Avoid blocking provider initialization on the referrer timeout
When the Play Store service stalls and never invokes its callback, this awaits the full three-second timeout before the SDK is available. FormoAnalyticsProvider keeps supplying its no-op context until FormoAnalytics.init() resolves, so screens or startup interactions during that interval silently drop their analytics events—the same failure mode the timeout comment identifies, just bounded to three seconds. Capture the referrer without holding up the usable SDK (or otherwise preserve events while it is pending).
Useful? React with 👍 / 👎.
Team decision: the ingestion edge's session derivation is a web design. The authorizer computes hash(dailySalt + domain + sourceIp + userAgent), and for a native app all three inputs degenerate at once — no Origin header (domain is the constant 'unknown'), the HTTP User-Agent is the client library's and near-identical across users on the same app build, and carrier CGNAT shares IPs — so unrelated mobile users collapse into a single session. handlerV0 already honours a body-provided session_id (obj?.session_id || session_id), so the mobile SDK supplies its own with a 30-minute inactivity window. Reverts 3847a8a and corrects the rationale comment, which previously described the edge as cookie-based. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # src/constants/storage.ts
Init awaits this capture, and the provider serves a no-op context until init resolves — so the timeout is also the window in which startup events are dropped. The Play bind is normally sub-second, so bound it tightly rather than generously. The cost is first-launch-only: the capture is one-shot and later launches short-circuit before the native call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks — conflict resolved, and both addressed. One of these I tested rather than reasoned about, and it came out the other way. P1 — optional peer / Metro resolution: not reproducibleI built the exact failing condition and Metro bundled fine. Setup: the
Result: And the code was in the graph — the shipped bundle contains the module's own log strings: So this isn't a case of the file being tree-shaken out; The reason is that Metro treats a This is also the pattern the SDK already relies on for Keeping it optional. Happy to revisit if you've seen it fail on a specific Metro/RN version — I only proved it for the version this example app pins. P2 — blocking init on the timeout: valid, tightenedYou're right about the mechanism: Two things narrow it:
Still, the worst case was larger than it needed to be, so I've cut the timeout to 1.5s ( I deliberately didn't make it fire-and-forget again — that's what caused the race this PR fixes (the referrer wouldn't be in storage when Worth flagging separately: the provider dropping events during init is pre-existing and broader than this PR — |
|
@codex review |
yosriady
left a comment
There was a problem hiding this comment.
feel free to merge after resolving the codex review comments
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
#50 landed the installReferrer module with the dead iOS AdServices path still in it (react-native-ad-services-attribution does not exist on npm; the require is try/caught so it only ever no-ops). Resolved the storage.ts comment conflict in favour of this branch, which removes that path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Adds web-to-mobile attribution for Android — so we know where a user came from before they installed (e.g. tapped a link on example.com → Play Store → install). The scenario from P-2207 now works end-to-end on Android.
The platform reality
Changes
react-native-play-install-referreras an optional peer dep, so the (already-written) Android capture actually runs instead of silently no-op'ing.Application Installedis tracked (was fire-and-forget → race). Safe to await now: it's a fast one-shot native call that no-ops instantly off-Android.referrer/utm_*to theApplication Installedevent (the issue's explicitreferrer=example.com), filtering empty fields.react-native-ad-services-attribution, which doesn't exist on npm, and only ever covered Apple Search Ads anyway. iOS is now documented as unsupported for web→mobile.Testing
Application Installed(with empty fields filtered).pnpm test— 229 passing;pnpm typecheckclean;pnpm audit --prodclean.How it works (Android)
A marketer's link points at
play.google.com/store/apps/details?id=…&referrer=utm_source%3Dexample.com…. On first launch the SDK reads that back, stores it as the traffic source, and stamps it onApplication Installed+ subsequent event context.P-2207
🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.