Skip to content

feat(native): make the deferred-link match rate measurable - #2831

Merged
innolope-dev merged 3 commits into
devfrom
feat/deferred-link-telemetry-dev
Aug 27, 2026
Merged

innolope-dev merged 3 commits into
devfrom
feat/deferred-link-telemetry-dev

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Replaces #2587 — re-authored onto dev (the original branch predates the mobile-release retirement and is 800+ commits behind; force-push is blocked). One test in the original re-author was corrected: on Android a null install-referrer read is deliberately uncounted (transient failure — the one-shot survives for the next launch), so the organic-install no_handoff case now asserts the iOS empty-clipboard path, and a new case pins the android retry semantics.

Summary

Follow-up to #2560, stacked on feat/deferred-deep-link-main. Adds the telemetry the deferred-link restore path is missing.

⚠️ Base: targets feat/deferred-deep-link-main, so the diff shown includes #2560's commits until that merges. Retarget to main once #2560 lands — this branch's own change is 3 files / +157.

Why

The restore path currently emits nothing, and the iOS clipboard catch {} swallows the one distinction that matters:

} catch {
    // user declined the paste prompt, or clipboard unavailable
}

A declined paste prompt and an organic install both end up as raw === null. So a hand-off that never works on iOS is indistinguishable from one nobody used — and the only signal either way is the absence of restores, which looks the same as success. The download modal (TASK-20769) is about to drive real volume through this, and the plan was always to measure the match rate before escalating to a blocking interstitial.

What

DEFERRED_LINK_RESTORED, fired once per fresh install with channel (referrer / clipboard / none) and one of four outcomes:

outcome meaning
restored payload found and applied
no_handoff nothing there — organic install
marker_missing payload present but no pnutdl=1 — organic Play referrer, or unrelated clipboard text that passed the URL gate
clipboard_unavailable paste prompt declined, or plugin missing — the broken-hand-off signal

On restored it also reports has_dest / has_locale / has_invite / has_campaign.

Plus trackDeferredHandoffCreated(platform) as the seam for the modal to report the denominator. Deliberately not fired inside playStoreUrlWithReferrer — that's a pure URL builder invoked on render, so it would count impressions as taps.

Privacy

Only booleans and the channel leave the device. The invite code, campaign tag and destination never do — putting a user's inviter and intended screen into analytics would be a real regression. There's a test asserting the payload values are absent from the captured events.

Safety

posthog.capture is wrapped in try {} catch {} at both call sites. This runs on the first-launch path, where a telemetry failure must not cost the user their restored context — covered by a test that makes capture throw and asserts the restore still returns.

Tests

4 added to the existing suite (25 pass total, including all 21 from #2560 unchanged):

  • successful restore reports the right fields, and no payload content
  • organic install vs declined paste prompt land on different outcomes
  • present-but-unmarked referrer is marker_missing, not no_handoff
  • a throwing posthog doesn't lose the restored context

tsc --noEmit clean on the touched files; prettier clean.

The restore path had no telemetry, and its clipboard catch swallowed the
one distinction that matters: a declined iOS paste prompt and an organic
install both end up as `raw === null`. A hand-off that never works would
therefore look exactly like one nobody used, and the download modal
(TASK-20769) is about to put real volume through it.

Split the empty cases into distinct outcomes — clipboard_unavailable,
no_handoff, marker_missing — and emit them with the channel alongside
which fields actually came back.

Only booleans and the channel are sent; the invite code, campaign tag and
destination never leave the device (asserted in a test). Capture is fully
guarded: this runs on first launch, where a telemetry failure must not
cost the user their restored context.

trackDeferredHandoffCreated is the seam for the modal to report the
denominator — deliberately not fired inside playStoreUrlWithReferrer,
which is a pure builder invoked on render and would count impressions.
@innolope-dev innolope-dev self-assigned this Aug 26, 2026
@vercel

vercel Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 26, 2026 7:27pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5355041b-25ac-4c62-9f79-56599a67900a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7451.65 → 7452.72 (+1.07)
Findings: +1 net (+10 new, -9 resolved)

🆕 New findings (10)

  • critical complexity — src/utils/deferred-link.ts — CC 66, MI 57.55, SLOC 231
  • high hotspot — src/constants/analytics.consts.ts — 53 commits, +445/-23 lines since 6 months ago
  • high complexity — src/utils/migration.utils.ts — CC 35, MI 63.55, SLOC 89
  • high complexity — src/constants/analytics.consts.ts — CC 1, MI 31, SLOC 204
  • medium high-mdd — src/utils/deferred-link.ts:248 — doRestore: MDD 28.6 (uses across many lines from declarations)
  • medium method-complexity — src/utils/deferred-link.ts:248 — doRestore CC 16 SLOC 68
  • low high-mdd — src/utils/migration.utils.ts:86 — openStore: MDD 11.2 (uses across many lines from declarations)
  • low missing-return-type — src/utils/migration.utils.ts:70 — trackStoreClick: exported fn missing return type annotation
  • low missing-return-type — src/utils/migration.utils.ts:86 — openStore: exported fn missing return type annotation
  • low missing-return-type — src/utils/migration.utils.ts:135 — onStoreAnchorClick: exported fn missing return type annotation

✅ Resolved (9)

  • src/utils/deferred-link.ts — CC 58, MI 57.88, SLOC 206
  • src/constants/analytics.consts.ts — 52 commits, +420/-23 lines since 6 months ago
  • src/utils/migration.utils.ts — CC 30, MI 63.15, SLOC 77
  • src/constants/analytics.consts.ts — CC 1, MI 31.44, SLOC 197
  • src/utils/deferred-link.ts:213 — doRestore: MDD 19.8 (uses across many lines from declarations)
  • src/utils/migration.utils.ts:81 — openStore: MDD 10.3 (uses across many lines from declarations)
  • src/utils/migration.utils.ts:65 — trackStoreClick: exported fn missing return type annotation
  • src/utils/migration.utils.ts:81 — openStore: exported fn missing return type annotation
  • src/utils/migration.utils.ts:125 — onStoreAnchorClick: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/deferred-link.ts 8.8 9.5 +0.7

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • ✅ unit: 3606 ran, 0 failed, 0 skipped, 47.4s

📊 Coverage (unit)

metric %
statements 69.6%
branches 54.9%
functions 61.2%
lines 70.5%
⏱ 10 slowest test cases
time test
2.8s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
0.4s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.4s src/utils/__tests__/crisp.test.ts › settles, and hands back a usable plugin, against a real-shaped plugin proxy
0.3s scripts/__tests__/capgo-bundle-version.test.js › derives .. from package.json
0.3s src/app/[locale]/(marketing)/status/StatusBoard.test.tsx › renders one 72-hour bar row per service, in user-facing language
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › the lazy bank view survives a re-render without blanking
0.3s src/hooks/__tests__/useCrispTokenId.test.ts › retries then stays undefined when the endpoint keeps failing (no fallback token)
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@innolope-dev
innolope-dev merged commit 779b598 into dev Aug 27, 2026
21 checks passed

This branch was successfully deployed

1 active deployment
Preview — e4867a00 Deployed Aug 26, 2026 by vercel[bot]
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