Skip to content

chore: INFRA-297 Telemetry must never gate the crisis dial + real tap→render measurement - #224

Merged
MP2EZ merged 1 commit into
developmentfrom
chore/INFRA-297-crisis-tap-trace-anchor
Jul 27, 2026
Merged

chore: INFRA-297 Telemetry must never gate the crisis dial + real tap→render measurement#224
MP2EZ merged 1 commit into
developmentfrom
chore/INFRA-297-crisis-tap-trace-anchor

Conversation

@MP2EZ

@MP2EZ MP2EZ commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Closes INFRA-297 (narrowed scope). Production Sentry tracing stays OFF — the RUM half is INFRA-310, gated on real App Store traffic. AC3 is discharged by reference to INFRA-306, which shipped the breathing 60fps control.

Deliverable 1 — telemetry must never gate the dial

CollapsibleCrisisButton called onNavigate() from inside a Sentry.startSpan callback. startSpan does real work before it ever invokes that callback — async-context-strategy dispatch, a scope fork, a sampling decision, span creation. If any of it throws, the callback never runs, so the crisis tap produces nothing at all: no navigate, no dial, and no log, because the audit call lived inside the same callback. On the CrisisErrorBoundary mount that is the last-resort tel:988 dial in an already-crashed app — the one context where Sentry is itself a likely cause of the crash.

try/catch is not the fix. It converts a visible crash into a silently swallowed crisis tap and silences the audit trail. The navigate is moved out of the callback entirely; Sentry is no longer imported by that component. resetFade() also moves after onNavigate() — nothing runs ahead of the crisis action for a fade animation.

Pinned by __tests__/safety/crisis-button-telemetry-ordering.test.tsx, named and placed to be gated by both precommit (test:safety) and the CI crisis-validation job ([Cc]risis). It asserts order, not merely occurrence — occurrence alone would pass a try/catch non-fix. 7 of its 8 cases failed before this change.

Deliverable 2 — a measurement that can actually fire

The span bracketed the synchronous return of onNavigate(), which for RootCrisisButton is a bare navigationRef.navigate() dispatch (~9ms per performance-baselines.json). It measured dispatch, not tap→render, so a p95 > 200ms alert on it could never fire regardless of real regressions — false assurance on a safety budget, and the identical flaw the monitoring runbook already condemns in the jest proxy.

New features/crisis/services/crisisTapTrace.ts measures to the point the user can act, with terminals that are never aggregated: screen_commit (CrisisResourcesScreen useLayoutEffect) and url_open (openCrisisUrl resolve — required because the error boundary dials without ever rendering that screen), plus manual_fallback for the canOpenURL-false path, which is a legitimate non-success terminal rather than a dropped tap.

useLayoutEffect was chosen over InteractionManager.runAfterInteractions, which would fold the modal transition animation into the number and put any future p95 permanently over budget for reasons unrelated to responsiveness; and over onLayout, which is per-view and refires on re-layout.

begin() is synchronous, does no I/O, touches no Sentry, no logging, no store, no React state, uses performance.now() never Date.now(), keeps one module-scope slot rather than a list, and is internally guarded so it cannot throw onto the crisis path. All emission moved to end(), off the tap frame — so this path is now faster than before.

Byproduct: the watchdog makes an existing silent false negative observable for the first time. RootCrisisButton's navigationRef.isReady() guard means an early tap produces no navigate, no dial and no log — the tap simply vanished. That is now logSecurity(…, 'high') as a dropped crisis tap. The existing overrun record keeps its verbatim event string and {responseTime, threshold: 200} shape so downstream log history survives, and now fires independently of Sentry state.

Also fixed — a live false safety claim

CrisisErrorBoundary's rendering branch said "Use the crisis button at the top of your screen for immediate support." False twice: this boundary replaces its children and RootCrisisButton suppresses the root overlay on AssessmentFlow (the only route it wraps), so no button was on screen; and the button is bottom-right, not top. Corrected to point at the "All Emergency Options" control immediately below, which does route 988/741741/911 through openCrisisUrl.

(The originally-reported "988 dial inside a Sentry callback" is latent, not live — it sits inside if (this.props.fallbackComponent) and fallbackComponent has zero callers. One prop away from live. Fixed anyway.)

Verification

npm run precommit exit=0
  safety 116 · clinical 106 · unit 592 · privacy 51
  lint:baseline → No new lint errors. 555/558 across 173 files
jest --testPathPattern="[Cc]risis" → 258 passed, 26 suites
npm run e2e:safety:crisis-button → exit=0 (on-device, iPhone 16 Plus sim)

Maestro confirms crisis-button-rootcrisis-resources-screen from every tab and Profile route depth. Note per crisis-button-reachability.yaml:33-35 that no Maestro flow carries ms assertions — the e2e gate proves reachability, not the new measurement. crisisTapTrace is the first thing in the repo capable of producing a real tap→render number on device.

Scope held

No tracesSampleRate, no enableAutoPerformanceTracing, no beforeSendTransaction, no DPIA or privacy-policy change. __tests__/privacy/releaseHealthSession.contract.test.ts assertions untouched and green — it is the pin proving tracing is still dark.

Follow-up filed as DEBUG-314: 12 crisis dial/text call sites bypass openCrisisUrl and can fail silently mid-crisis.

🤖 Generated with Claude Code

…->render measure

Scoped down by /b-batch: production Sentry tracing stays OFF. The RUM half
(tracesSampleRate, enableAutoPerformanceTracing, beforeSendTransaction, the p95
alert, DPIA amendment) is INFRA-310, gated on real App Store traffic. AC3 is
discharged by reference to INFRA-306, which shipped the breathing 60fps control.

DELIVERABLE 1 -- ordering. CollapsibleCrisisButton called onNavigate() from INSIDE
a Sentry.startSpan callback. startSpan does real work before invoking that
callback (async-context-strategy dispatch, scope fork, sampling decision, span
creation). If any of it throws, the callback never runs, so the crisis tap
produces NOTHING: no navigate, no dial, and no log -- the audit call lived inside
the same callback. On the CrisisErrorBoundary mount that is the last-resort
tel:988 dial in an already-crashed app, i.e. the one context where Sentry is
itself a likely cause of the crash.

try/catch is NOT the fix: it converts a visible crash into a silently swallowed
tap, which is worse, and it silences the audit trail. The navigate is moved out
of the callback entirely; Sentry is no longer imported by that component at all.
resetFade() also moves after onNavigate() -- nothing runs ahead of the crisis
action for a fade animation.

Pinned by __tests__/safety/crisis-button-telemetry-ordering.test.tsx, named and
placed to be gated by BOTH precommit (test:safety) and the CI crisis-validation
job ([Cc]risis pattern). It asserts ORDER, not merely occurrence -- occurrence
alone would pass a try/catch non-fix. 7 of its 8 cases failed before this change.

DELIVERABLE 2 -- measurement. The span bracketed the synchronous return of
onNavigate(), which for RootCrisisButton is a bare navigationRef.navigate()
dispatch (~9ms per performance-baselines.json). It measured dispatch, not
tap->render, so a p95 > 200ms alert on it could never fire regardless of real
regressions -- false assurance on a safety budget, and the identical flaw the
monitoring runbook already condemns in the jest proxy.

New features/crisis/services/crisisTapTrace.ts measures tap to the point the user
can act, with two distinct terminals that are never aggregated: 'screen_commit'
(CrisisResourcesScreen useLayoutEffect -- after commit, so the view hierarchy
exists) and 'url_open' (openCrisisUrl's resolve path, needed because the error
boundary dials without ever rendering that screen). Also 'manual_fallback' for
openCrisisUrl's canOpenURL-false path, which is a legitimate non-success terminal
rather than a dropped tap.

useLayoutEffect was chosen over InteractionManager.runAfterInteractions, which
would fold the modal transition animation into the number and put the p95
permanently over budget for reasons unrelated to responsiveness, and over
onLayout, which is per-view and refires on re-layout.

begin() is synchronous, does no I/O, touches no Sentry, no logging, no store, no
React state, uses performance.now() never Date.now(), keeps one module-scope slot
rather than a list, and is internally guarded so it cannot throw onto the crisis
path. All emission moved to end(), off the tap frame -- so the tap path is now
FASTER than before, not slower.

Watchdog makes an existing silent false negative observable for the first time:
RootCrisisButton's navigationRef.isReady() guard means an early tap produces no
navigate, no dial and no log. A tap that yields neither a screen nor a dial is now
logSecurity(...,'high') as a dropped crisis tap, not a perf datapoint. The
existing overrun record keeps its verbatim event string and {responseTime,
threshold: 200} shape so downstream log history is preserved, and it now fires
independently of Sentry state.

ALSO FIXED -- a live false safety claim in CrisisErrorBoundary. The branch that
actually renders said "Use the crisis button at the top of your screen for
immediate support". False twice: this boundary replaces its children and
RootCrisisButton suppresses the root overlay on 'AssessmentFlow' (the only route
it wraps), so no button was on screen; and the button is bottom-right, not top.
Corrected to point at the "All Emergency Options" control immediately below it,
which does route 988/741741/911 through openCrisisUrl.

Scope held: no tracesSampleRate, no enableAutoPerformanceTracing, no
beforeSendTransaction, no DPIA or privacy-policy change.
__tests__/privacy/releaseHealthSession.contract.test.ts assertions are untouched
and still green -- it is the pin proving tracing is still dark.

npm run precommit exit=0: safety 116, clinical 106, unit 592, privacy 51.
Crisis gate (jest --testPathPattern="[Cc]risis"): 258 passed, 26 suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MP2EZ
MP2EZ merged commit 84d2db3 into development Jul 27, 2026
31 of 33 checks passed
@MP2EZ
MP2EZ deleted the chore/INFRA-297-crisis-tap-trace-anchor branch July 27, 2026 01:51
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