chore: INFRA-297 Telemetry must never gate the crisis dial + real tap→render measurement - #224
Merged
Merged
Conversation
…->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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
CollapsibleCrisisButtoncalledonNavigate()from inside aSentry.startSpancallback.startSpandoes 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 theCrisisErrorBoundarymount that is the last-resorttel:988dial in an already-crashed app — the one context where Sentry is itself a likely cause of the crash.try/catchis 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 afteronNavigate()— 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 CIcrisis-validationjob ([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 forRootCrisisButtonis a barenavigationRef.navigate()dispatch (~9ms perperformance-baselines.json). It measured dispatch, not tap→render, so ap95 > 200msalert 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.tsmeasures to the point the user can act, with terminals that are never aggregated:screen_commit(CrisisResourcesScreenuseLayoutEffect) andurl_open(openCrisisUrlresolve — required because the error boundary dials without ever rendering that screen), plusmanual_fallbackfor thecanOpenURL-false path, which is a legitimate non-success terminal rather than a dropped tap.useLayoutEffectwas chosen overInteractionManager.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 overonLayout, 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, usesperformance.now()neverDate.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 toend(), 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'snavigationRef.isReady()guard means an early tap produces no navigate, no dial and no log — the tap simply vanished. That is nowlogSecurity(…, '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 andRootCrisisButtonsuppresses the root overlay onAssessmentFlow(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 throughopenCrisisUrl.(The originally-reported "988 dial inside a Sentry callback" is latent, not live — it sits inside
if (this.props.fallbackComponent)andfallbackComponenthas zero callers. One prop away from live. Fixed anyway.)Verification
Maestro confirms
crisis-button-root→crisis-resources-screenfrom every tab and Profile route depth. Note percrisis-button-reachability.yaml:33-35that no Maestro flow carries ms assertions — the e2e gate proves reachability, not the new measurement.crisisTapTraceis the first thing in the repo capable of producing a real tap→render number on device.Scope held
No
tracesSampleRate, noenableAutoPerformanceTracing, nobeforeSendTransaction, no DPIA or privacy-policy change.__tests__/privacy/releaseHealthSession.contract.test.tsassertions 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
openCrisisUrland can fail silently mid-crisis.🤖 Generated with Claude Code