Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
### Fixes

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

### Dependencies

Expand Down
25 changes: 15 additions & 10 deletions packages/core/src/js/tracing/integrations/appStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,21 +320,26 @@ export const appStartIntegration = ({
return;
}

if (!firstStartedActiveRootSpanId) {
debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.');
return;
}

if (!event.contexts?.trace) {
debug.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.');
return;
}

if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) {
debug.warn(
'[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.',
);
return;
// When standalone is true, we create our own transaction and don't need to verify
// it matches the first navigation transaction. When standalone is false, we need to
// ensure we're attaching app start to the first transaction (not a later one).
if (!standalone) {
if (!firstStartedActiveRootSpanId) {
debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.');
return;
}

if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) {
debug.warn(
'[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.',
);
return;
}
}

const appStart = await NATIVE.fetchNativeAppStart();
Expand Down
64 changes: 64 additions & 0 deletions packages/core/test/tracing/integrations/appStart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
setCurrentClient,
startInactiveSpan,
timestampInSeconds,
} from '@sentry/core';
import {
Expand Down Expand Up @@ -385,6 +386,69 @@ describe('App Start Integration', () => {
expect(actualEvent).toStrictEqual(undefined);
expect(NATIVE.fetchNativeAppStart).toHaveBeenCalledTimes(1);
});

it('Attaches app start to standalone transaction even when navigation transaction starts first', async () => {
// This test simulates the Android scenario where React Navigation auto-instrumentation
// starts a navigation transaction before the standalone app start transaction is created.
// The fix ensures that when standalone: true, the span ID check is skipped so app start
// can be attached to the standalone transaction even if a navigation transaction started first.
getCurrentScope().clear();
getIsolationScope().clear();
getGlobalScope().clear();

mockAppStart({ cold: true });

const integration = appStartIntegration({
standalone: true,
});
const client = new TestClient({
...getDefaultTestClientOptions(),
enableAppStartTracking: true,
tracesSampleRate: 1.0,
});
setCurrentClient(client);
integration.setup(client);

// Simulate a navigation transaction starting first (like React Navigation auto-instrumentation)
// This will set firstStartedActiveRootSpanId to the navigation span's ID
const navigationSpan = startInactiveSpan({
name: 'calendar/home',
op: 'navigation',
forceTransaction: true,
});
const navigationSpanId = navigationSpan?.spanContext().spanId;
if (navigationSpan) {
navigationSpan.end();
}

// Now capture standalone app start - it should still work even though navigation span started first
// The standalone transaction will have a different span ID, but the fix skips the check
await integration.captureStandaloneAppStart();

const actualEvent = client.event as TransactionEvent | undefined;
expect(actualEvent).toBeDefined();
expect(actualEvent?.spans).toBeDefined();
expect(actualEvent?.spans?.length).toBeGreaterThan(0);

// Verify that app start was attached successfully
const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start');
expect(appStartSpan).toBeDefined();
expect(appStartSpan).toEqual(
expect.objectContaining<Partial<SpanJSON>>({
description: 'Cold Start',
op: APP_START_COLD_OP,
}),
);

// Verify the standalone transaction has a different span ID than the navigation transaction
// This confirms that the span ID check was skipped (otherwise app start wouldn't be attached)
expect(actualEvent?.contexts?.trace?.span_id).toBeDefined();
if (navigationSpanId) {
expect(actualEvent?.contexts?.trace?.span_id).not.toBe(navigationSpanId);
}

expect(actualEvent?.measurements?.[APP_START_COLD_MEASUREMENT]).toBeDefined();
});
});

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