Skip to content

Commit af33f3b

Browse files
antonisclaude
andauthored
feat(tracing): Add extend app start API for standalone app start (#6392)
* feat(tracing): Add extend app start API for standalone app start (RN-675) Add experimental `extendAppStart()`, `finishExtendedAppStart()`, and `getExtendedAppStartSpan()` to keep the standalone `app.start` transaction open past the auto-detected end so post-init work (remote config, session restore, etc.) is included and can be broken down into child spans under an `app.start.extended` span. - `extendAppStart()` cancels the deferred auto-capture, holds the transaction open, and starts a 30s deadline. - `finishExtendedAppStart()` finalizes: trims the end to the last child (floored at the default app start end) and sets the measurement at finalization. - On the deadline the transaction is captured but the `app.vitals.start` measurement is suppressed (never emit a ~30s app start). - `attachAppStartToTransactionEvent` now takes `suppressMeasurement` and returns whether it attached, so the finalize path decides send vs skip. Standalone-only. `appLoaded()` stays as-is for now; its deprecation and removal, plus making standalone the default, are handled together in RN-676 (v9). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update changelog * fix(tracing): Address extend app start review feedback (RN-675) - extendAppStart(): claim the run only after confirming a recording span, so a sampled-out/disabled run falls back to the normal capture path. - Cap the 60s app start sanity check on the native window, not the extended end, so a legitimately extended app start is not dropped. - Thread the extended end explicitly instead of mutating shared appStartEndData, so the extended end survives without wrap()/appLoaded(). - Guard app start attach with a run generation so a finish suspended at the native-data await bails when a new runApplication run starts, instead of corrupting the new run's state. - finishExtendedAppStart() returns Promise<void> so callers can await before flush(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(tracing): Re-check run generation after frames-delay await (RN-675) The app start attach also awaits fetchNativeFramesDelay (~2s) after the first generation check; a runApplication reset during that window could still send a stale standalone transaction. Re-check the generation before reporting success. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(tracing): Trim extended app start to last child, not finish() time (RN-675) - getLatestChildSpanEndTimestamp was computed on the root transaction, which includes the extended (wrapper) span itself; since the wrapper ends at finalization time, the transaction was pinned to the finishExtendedAppStart() call rather than the last instrumented child. Compute the trim from the wrapper's children (excluding the wrapper) on the explicit-finish path, end the wrapper at that trimmed time so it never outlives the root, and keep the full-window end on the deadline path. Floored at the default app start end and the wrapper start. - Regenerate the API report against @sentry/core 10.63.0 (restores the deeplinkIntegration signature that a stale local build had reverted). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: Merge main and move extend app start changelog entry to Unreleased Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 513aeb3 commit af33f3b

7 files changed

Lines changed: 758 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
### Features
12+
13+
- Add experimental `extendAppStart`/`finishExtendedAppStart`/`getExtendedAppStartSpan` to extend the standalone app start window and instrument post-init work ([#6392](https://github.com/getsentry/sentry-react-native/pull/6392))
14+
1115
### Fixes
1216

1317
- Skip iOS source maps upload on `Debug` builds ([#6405](https://github.com/getsentry/sentry-react-native/pull/6405))

packages/core/etc/sentry-react-native.api.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ export const expoRouterIntegration: (options?: ExpoRouterIntegrationOptions) =>
305305
// @public
306306
export const expoUpdatesListenerIntegration: () => Integration;
307307

308+
// @public
309+
export function extendAppStart(): void;
310+
308311
export { extraErrorDataIntegration }
309312

310313
export { FeatureFlagsIntegration }
@@ -353,6 +356,9 @@ export const feedbackIntegration: (initOptions?: Partial<FeedbackFormProps> & {
353356
enableShakeToReport?: boolean;
354357
}) => FeedbackIntegration;
355358

359+
// @public
360+
export function finishExtendedAppStart(): Promise<void>;
361+
356362
// @public
357363
export function flush(): Promise<boolean>;
358364

@@ -384,6 +390,9 @@ export function getDataFromUri(uri: string): Promise<Uint8Array | null>;
384390
// @public
385391
export function getDefaultIdleNavigationSpanOptions(): StartSpanOptions;
386392

393+
// @public
394+
export function getExtendedAppStartSpan(): Span;
395+
387396
export { getGlobalScope }
388397

389398
export { getIsolationScope }

packages/core/src/js/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ export {
109109
withScope,
110110
crashedLastRun,
111111
appLoaded,
112+
extendAppStart,
113+
getExtendedAppStartSpan,
114+
finishExtendedAppStart,
112115
pauseAppHangTracking,
113116
resumeAppHangTracking,
114117
} from './sdk';

packages/core/src/js/sdk.tsx

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* oxlint-disable eslint(complexity) */
2-
import type { Breadcrumb, BreadcrumbHint, Integration, Scope } from '@sentry/core';
2+
import type { Breadcrumb, BreadcrumbHint, Integration, Scope, Span } from '@sentry/core';
33

44
import {
55
debug,
@@ -25,7 +25,12 @@ import { shouldEnableNativeNagger } from './options';
2525
import { enableSyncToNative } from './scopeSync';
2626
import { TouchEventBoundary } from './touchevents';
2727
import { ReactNativeProfiler } from './tracing';
28-
import { _appLoaded } from './tracing/integrations/appStart';
28+
import {
29+
_appLoaded,
30+
_extendAppStart,
31+
_finishExtendedAppStart,
32+
_getExtendedAppStartSpan,
33+
} from './tracing/integrations/appStart';
2934
import { useEncodePolyfill } from './transports/encodePolyfill';
3035
import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/native';
3136
import { getDefaultEnvironment, isExpoGo, isRunningInMetroDevServer, isWeb } from './utils/environment';
@@ -250,6 +255,64 @@ export function appLoaded(): void {
250255
_appLoaded();
251256
}
252257

258+
/**
259+
* Extends the app start window so work done after initialization (remote config, session restore,
260+
* splash screen dismissal, etc.) is included in the app start measurement. Call
261+
* {@link finishExtendedAppStart} when the app is ready, or attach child spans via
262+
* {@link getExtendedAppStartSpan} to break the extended work down.
263+
*
264+
* Requires standalone app start tracing (`_experiments.enableStandaloneAppStartTracing`). No-ops if
265+
* the app start transaction was already created, if extend was already called, or if called before
266+
* `Sentry.init()`.
267+
*
268+
* @experimental This API is subject to change in future versions.
269+
*
270+
* @example
271+
* ```ts
272+
* Sentry.extendAppStart();
273+
* await initializeRemoteConfig();
274+
* Sentry.finishExtendedAppStart();
275+
* ```
276+
*/
277+
export function extendAppStart(): void {
278+
_extendAppStart();
279+
}
280+
281+
/**
282+
* Returns the extended app start span for attaching child spans, or a no-op span when there is no
283+
* active extension. Only meaningful between {@link extendAppStart} and {@link finishExtendedAppStart}.
284+
*
285+
* @experimental This API is subject to change in future versions.
286+
*
287+
* @example
288+
* ```ts
289+
* Sentry.extendAppStart();
290+
* const parentSpan = Sentry.getExtendedAppStartSpan();
291+
* const child = Sentry.startInactiveSpan({ parentSpan, op: 'app.init', name: 'fetch remote config' });
292+
* await loadRemoteConfig();
293+
* child.end();
294+
* Sentry.finishExtendedAppStart();
295+
* ```
296+
*/
297+
export function getExtendedAppStartSpan(): Span {
298+
return _getExtendedAppStartSpan();
299+
}
300+
301+
/**
302+
* Finishes the app start extension started with {@link extendAppStart}, finalizing the app start
303+
* transaction (its duration is trimmed to the last child span). No-ops if there is no active
304+
* extension.
305+
*
306+
* Returns a promise that resolves once the app start transaction has been captured. `await` it
307+
* before {@link flush} (e.g. before a code-push/expo update) to make sure the app start data is
308+
* queued.
309+
*
310+
* @experimental This API is subject to change in future versions.
311+
*/
312+
export function finishExtendedAppStart(): Promise<void> {
313+
return _finishExtendedAppStart();
314+
}
315+
253316
/**
254317
* Flushes all pending events in the queue to disk.
255318
* Use this before applying any realtime updates such as code-push or expo updates.

0 commit comments

Comments
 (0)