Skip to content

Commit bb4ea33

Browse files
authored
fix(appstart): Skip span ID check when standalone mode is enabled (#5493)
* fix(appstart): Skip span ID check when standalone mode is enabled * Add changelog
1 parent 780b283 commit bb4ea33

3 files changed

Lines changed: 80 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
### Fixes
1212

1313
- Fix for missing `replay_id` from metrics ([#5483](https://github.com/getsentry/sentry-react-native/pull/5483))
14+
- Skip span ID check when standalone mode is enabled ([#5493](https://github.com/getsentry/sentry-react-native/pull/5493))
1415

1516
### Dependencies
1617

packages/core/src/js/tracing/integrations/appStart.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -320,21 +320,26 @@ export const appStartIntegration = ({
320320
return;
321321
}
322322

323-
if (!firstStartedActiveRootSpanId) {
324-
debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.');
325-
return;
326-
}
327-
328323
if (!event.contexts?.trace) {
329324
debug.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.');
330325
return;
331326
}
332327

333-
if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) {
334-
debug.warn(
335-
'[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.',
336-
);
337-
return;
328+
// When standalone is true, we create our own transaction and don't need to verify
329+
// it matches the first navigation transaction. When standalone is false, we need to
330+
// ensure we're attaching app start to the first transaction (not a later one).
331+
if (!standalone) {
332+
if (!firstStartedActiveRootSpanId) {
333+
debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.');
334+
return;
335+
}
336+
337+
if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) {
338+
debug.warn(
339+
'[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.',
340+
);
341+
return;
342+
}
338343
}
339344

340345
const appStart = await NATIVE.fetchNativeAppStart();

packages/core/test/tracing/integrations/appStart.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
SEMANTIC_ATTRIBUTE_SENTRY_OP,
77
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
88
setCurrentClient,
9+
startInactiveSpan,
910
timestampInSeconds,
1011
} from '@sentry/core';
1112
import {
@@ -385,6 +386,69 @@ describe('App Start Integration', () => {
385386
expect(actualEvent).toStrictEqual(undefined);
386387
expect(NATIVE.fetchNativeAppStart).toHaveBeenCalledTimes(1);
387388
});
389+
390+
it('Attaches app start to standalone transaction even when navigation transaction starts first', async () => {
391+
// This test simulates the Android scenario where React Navigation auto-instrumentation
392+
// starts a navigation transaction before the standalone app start transaction is created.
393+
// The fix ensures that when standalone: true, the span ID check is skipped so app start
394+
// can be attached to the standalone transaction even if a navigation transaction started first.
395+
getCurrentScope().clear();
396+
getIsolationScope().clear();
397+
getGlobalScope().clear();
398+
399+
mockAppStart({ cold: true });
400+
401+
const integration = appStartIntegration({
402+
standalone: true,
403+
});
404+
const client = new TestClient({
405+
...getDefaultTestClientOptions(),
406+
enableAppStartTracking: true,
407+
tracesSampleRate: 1.0,
408+
});
409+
setCurrentClient(client);
410+
integration.setup(client);
411+
412+
// Simulate a navigation transaction starting first (like React Navigation auto-instrumentation)
413+
// This will set firstStartedActiveRootSpanId to the navigation span's ID
414+
const navigationSpan = startInactiveSpan({
415+
name: 'calendar/home',
416+
op: 'navigation',
417+
forceTransaction: true,
418+
});
419+
const navigationSpanId = navigationSpan?.spanContext().spanId;
420+
if (navigationSpan) {
421+
navigationSpan.end();
422+
}
423+
424+
// Now capture standalone app start - it should still work even though navigation span started first
425+
// The standalone transaction will have a different span ID, but the fix skips the check
426+
await integration.captureStandaloneAppStart();
427+
428+
const actualEvent = client.event as TransactionEvent | undefined;
429+
expect(actualEvent).toBeDefined();
430+
expect(actualEvent?.spans).toBeDefined();
431+
expect(actualEvent?.spans?.length).toBeGreaterThan(0);
432+
433+
// Verify that app start was attached successfully
434+
const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start');
435+
expect(appStartSpan).toBeDefined();
436+
expect(appStartSpan).toEqual(
437+
expect.objectContaining<Partial<SpanJSON>>({
438+
description: 'Cold Start',
439+
op: APP_START_COLD_OP,
440+
}),
441+
);
442+
443+
// Verify the standalone transaction has a different span ID than the navigation transaction
444+
// This confirms that the span ID check was skipped (otherwise app start wouldn't be attached)
445+
expect(actualEvent?.contexts?.trace?.span_id).toBeDefined();
446+
if (navigationSpanId) {
447+
expect(actualEvent?.contexts?.trace?.span_id).not.toBe(navigationSpanId);
448+
}
449+
450+
expect(actualEvent?.measurements?.[APP_START_COLD_MEASUREMENT]).toBeDefined();
451+
});
388452
});
389453

390454
describe('App Start Attached to the First Root Span', () => {

0 commit comments

Comments
 (0)