diff --git a/CHANGELOG.md b/CHANGELOG.md index 200574ec81..aee1d42906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixes - Fixes missing Cold Start measurements by bumping the Android SDK version to v7.22.1 ([#4643](https://github.com/getsentry/sentry-react-native/pull/4643)) +- Attach App Start spans to the first created not the first processed root span ([#4618](https://github.com/getsentry/sentry-react-native/pull/4618), [#4644](https://github.com/getsentry/sentry-react-native/pull/4644)) ### Dependencies diff --git a/packages/core/src/js/tracing/integrations/appStart.ts b/packages/core/src/js/tracing/integrations/appStart.ts index 0f96557d4c..8a35e1aa01 100644 --- a/packages/core/src/js/tracing/integrations/appStart.ts +++ b/packages/core/src/js/tracing/integrations/appStart.ts @@ -1,5 +1,5 @@ -/* eslint-disable complexity */ -import type { Client, Event, Integration, SpanJSON, TransactionEvent } from '@sentry/core'; +/* eslint-disable complexity, max-lines */ +import type { Client, Event, Integration, Span, SpanJSON, TransactionEvent } from '@sentry/core'; import { getCapturedScopesOnSpan, getClient, @@ -17,7 +17,7 @@ import { } from '../../measurements'; import type { NativeAppStartResponse } from '../../NativeRNSentry'; import type { ReactNativeClientOptions } from '../../options'; -import { convertSpanToTransaction, setEndTimeValue } from '../../utils/span'; +import { convertSpanToTransaction, isRootSpan, setEndTimeValue } from '../../utils/span'; import { NATIVE } from '../../wrapper'; import { APP_START_COLD as APP_START_COLD_OP, @@ -136,16 +136,18 @@ export const appStartIntegration = ({ let _client: Client | undefined = undefined; let isEnabled = true; let appStartDataFlushed = false; + let firstStartedActiveRootSpanId: string | undefined = undefined; const setup = (client: Client): void => { _client = client; - const clientOptions = client.getOptions() as ReactNativeClientOptions; + const { enableAppStartTracking } = client.getOptions() as ReactNativeClientOptions; - const { enableAppStartTracking } = clientOptions; if (!enableAppStartTracking) { isEnabled = false; logger.warn('[AppStart] App start tracking is disabled.'); } + + client.on('spanStart', recordFirstStartedActiveRootSpanId); }; const afterAllSetup = (_client: Client): void => { @@ -167,6 +169,27 @@ export const appStartIntegration = ({ return event; }; + const recordFirstStartedActiveRootSpanId = (rootSpan: Span): void => { + if (firstStartedActiveRootSpanId) { + return; + } + + if (!isRootSpan(rootSpan)) { + return; + } + + setFirstStartedActiveRootSpanId(rootSpan.spanContext().spanId); + }; + + /** + * For testing purposes only. + * @private + */ + const setFirstStartedActiveRootSpanId = (spanId: string | undefined): void => { + firstStartedActiveRootSpanId = spanId; + logger.debug('[AppStart] First started active root span id recorded.', firstStartedActiveRootSpanId); + }; + async function captureStandaloneAppStart(): Promise { if (!standalone) { logger.debug( @@ -212,11 +235,23 @@ export const appStartIntegration = ({ return; } + if (!firstStartedActiveRootSpanId) { + logger.warn('[AppStart] No first started active root span id recorded. Can not attach app start.'); + return; + } + if (!event.contexts || !event.contexts.trace) { logger.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.'); return; } + if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) { + logger.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(); if (!appStart) { logger.warn('[AppStart] Failed to retrieve the app start metrics from the native layer.'); @@ -332,7 +367,8 @@ export const appStartIntegration = ({ afterAllSetup, processEvent, captureStandaloneAppStart, - }; + setFirstStartedActiveRootSpanId, + } as AppStartIntegration; }; function setSpanDurationAsMeasurementOnTransactionEvent(event: TransactionEvent, label: string, span: SpanJSON): void { diff --git a/packages/core/test/profiling/integration.test.ts b/packages/core/test/profiling/integration.test.ts index 83da5cc53d..7462d94c6e 100644 --- a/packages/core/test/profiling/integration.test.ts +++ b/packages/core/test/profiling/integration.test.ts @@ -262,8 +262,9 @@ describe('profiling integration', () => { const transaction1 = Sentry.startSpanManual({ name: 'test-name-1' }, span => span); const transaction2 = Sentry.startSpanManual({ name: 'test-name-2' }, span => span); transaction1.end(); - transaction2.end(); + jest.runOnlyPendingTimers(); + transaction2.end(); jest.runAllTimers(); expectEnvelopeToContainProfile( diff --git a/packages/core/test/tracing/integrations/appStart.test.ts b/packages/core/test/tracing/integrations/appStart.test.ts index 4337e3e2b3..bd01beb503 100644 --- a/packages/core/test/tracing/integrations/appStart.test.ts +++ b/packages/core/test/tracing/integrations/appStart.test.ts @@ -33,6 +33,10 @@ import { NATIVE } from '../../../src/js/wrapper'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; import { mockFunction } from '../../testutils'; +type AppStartIntegrationTest = ReturnType & { + setFirstStartedActiveRootSpanId: (spanId: string | undefined) => void; +}; + let dateNowSpy: jest.SpyInstance; jest.mock('../../../src/js/wrapper', () => { @@ -689,7 +693,10 @@ describe('App Start Integration', () => { const integration = appStartIntegration(); const client = new TestClient(getDefaultTestClientOptions()); - const actualEvent = await integration.processEvent(getMinimalTransactionEvent(), {}, client); + const firstEvent = getMinimalTransactionEvent(); + (integration as AppStartIntegrationTest).setFirstStartedActiveRootSpanId(firstEvent.contexts?.trace?.span_id); + + const actualEvent = await integration.processEvent(firstEvent, {}, client); expect(actualEvent).toEqual( expectEventWithAttachedColdAppStart({ timeOriginMilliseconds, appStartTimeMilliseconds }), ); @@ -722,6 +729,7 @@ describe('App Start Integration', () => { function processEvent(event: Event): PromiseLike | Event | null { const integration = appStartIntegration(); + (integration as AppStartIntegrationTest).setFirstStartedActiveRootSpanId(event.contexts?.trace?.span_id); return integration.processEvent(event, {}, new TestClient(getDefaultTestClientOptions())); } diff --git a/packages/core/test/tracing/reactnavigation.ttid.test.tsx b/packages/core/test/tracing/reactnavigation.ttid.test.tsx index a0245cff12..50efa84fca 100644 --- a/packages/core/test/tracing/reactnavigation.ttid.test.tsx +++ b/packages/core/test/tracing/reactnavigation.ttid.test.tsx @@ -359,6 +359,7 @@ describe('React Navigation - TTID', () => { }); test('idle transaction should cancel the ttid span if new frame not received', () => { + jest.runOnlyPendingTimers(); // Flush app start transaction mockedNavigation.navigateToNewScreen(); jest.runOnlyPendingTimers(); // Flush ttid transaction