Skip to content

Commit 7d161c0

Browse files
fix(appStart): Attach App Start spans to the first created not the first processed root span (#4618)
1 parent 8ba5377 commit 7d161c0

File tree

5 files changed

+54
-7
lines changed

5 files changed

+54
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
### Fixes
1616

1717
- Considers the `SENTRY_DISABLE_AUTO_UPLOAD` and `SENTRY_DISABLE_NATIVE_DEBUG_UPLOAD` environment variables in the configuration of the Sentry Android Gradle Plugin for Expo plugin ([#4583](https://github.com/getsentry/sentry-react-native/pull/4583))
18+
- Attach App Start spans to the first created not the first processed root span ([#4618](https://github.com/getsentry/sentry-react-native/pull/4618))
1819

1920
### Dependencies
2021

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable complexity, max-lines */
2-
import type { Client, Event, Integration, SpanJSON, TransactionEvent } from '@sentry/core';
2+
import type { Client, Event, Integration, Span, SpanJSON, TransactionEvent } from '@sentry/core';
33
import {
44
getCapturedScopesOnSpan,
55
getClient,
@@ -17,7 +17,7 @@ import {
1717
} from '../../measurements';
1818
import type { NativeAppStartResponse } from '../../NativeRNSentry';
1919
import type { ReactNativeClientOptions } from '../../options';
20-
import { convertSpanToTransaction, setEndTimeValue } from '../../utils/span';
20+
import { convertSpanToTransaction, isRootSpan, setEndTimeValue } from '../../utils/span';
2121
import { NATIVE } from '../../wrapper';
2222
import {
2323
APP_START_COLD as APP_START_COLD_OP,
@@ -137,16 +137,18 @@ export const appStartIntegration = ({
137137
let _client: Client | undefined = undefined;
138138
let isEnabled = true;
139139
let appStartDataFlushed = false;
140+
let firstStartedActiveRootSpanId: string | undefined = undefined;
140141

141142
const setup = (client: Client): void => {
142143
_client = client;
143-
const clientOptions = client.getOptions() as ReactNativeClientOptions;
144+
const { enableAppStartTracking } = client.getOptions() as ReactNativeClientOptions;
144145

145-
const { enableAppStartTracking } = clientOptions;
146146
if (!enableAppStartTracking) {
147147
isEnabled = false;
148148
logger.warn('[AppStart] App start tracking is disabled.');
149149
}
150+
151+
client.on('spanStart', recordFirstStartedActiveRootSpanId);
150152
};
151153

152154
const afterAllSetup = (_client: Client): void => {
@@ -168,6 +170,27 @@ export const appStartIntegration = ({
168170
return event;
169171
};
170172

173+
const recordFirstStartedActiveRootSpanId = (rootSpan: Span): void => {
174+
if (firstStartedActiveRootSpanId) {
175+
return;
176+
}
177+
178+
if (!isRootSpan(rootSpan)) {
179+
return;
180+
}
181+
182+
setFirstStartedActiveRootSpanId(rootSpan.spanContext().spanId);
183+
};
184+
185+
/**
186+
* For testing purposes only.
187+
* @private
188+
*/
189+
const setFirstStartedActiveRootSpanId = (spanId: string | undefined): void => {
190+
firstStartedActiveRootSpanId = spanId;
191+
logger.debug('[AppStart] First started active root span id recorded.', firstStartedActiveRootSpanId);
192+
};
193+
171194
async function captureStandaloneAppStart(): Promise<void> {
172195
if (!standalone) {
173196
logger.debug(
@@ -213,11 +236,23 @@ export const appStartIntegration = ({
213236
return;
214237
}
215238

239+
if (!firstStartedActiveRootSpanId) {
240+
logger.warn('[AppStart] No first started active root span id recorded. Can not attach app start.');
241+
return;
242+
}
243+
216244
if (!event.contexts || !event.contexts.trace) {
217245
logger.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.');
218246
return;
219247
}
220248

249+
if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) {
250+
logger.warn(
251+
'[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.',
252+
);
253+
return;
254+
}
255+
221256
const appStart = await NATIVE.fetchNativeAppStart();
222257
if (!appStart) {
223258
logger.warn('[AppStart] Failed to retrieve the app start metrics from the native layer.');
@@ -333,7 +368,8 @@ export const appStartIntegration = ({
333368
afterAllSetup,
334369
processEvent,
335370
captureStandaloneAppStart,
336-
};
371+
setFirstStartedActiveRootSpanId,
372+
} as AppStartIntegration;
337373
};
338374

339375
function setSpanDurationAsMeasurementOnTransactionEvent(event: TransactionEvent, label: string, span: SpanJSON): void {

packages/core/test/profiling/integration.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,9 @@ describe('profiling integration', () => {
262262
const transaction1 = Sentry.startSpanManual({ name: 'test-name-1' }, span => span);
263263
const transaction2 = Sentry.startSpanManual({ name: 'test-name-2' }, span => span);
264264
transaction1.end();
265-
transaction2.end();
265+
jest.runOnlyPendingTimers();
266266

267+
transaction2.end();
267268
jest.runAllTimers();
268269

269270
expectEnvelopeToContainProfile(

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ import { NATIVE } from '../../../src/js/wrapper';
3434
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
3535
import { mockFunction } from '../../testutils';
3636

37+
type AppStartIntegrationTest = ReturnType<typeof appStartIntegration> & {
38+
setFirstStartedActiveRootSpanId: (spanId: string | undefined) => void;
39+
};
40+
3741
let dateNowSpy: jest.SpyInstance;
3842

3943
jest.mock('../../../src/js/wrapper', () => {
@@ -692,7 +696,10 @@ describe('App Start Integration', () => {
692696
const integration = appStartIntegration();
693697
const client = new TestClient(getDefaultTestClientOptions());
694698

695-
const actualEvent = await integration.processEvent(getMinimalTransactionEvent(), {}, client);
699+
const firstEvent = getMinimalTransactionEvent();
700+
(integration as AppStartIntegrationTest).setFirstStartedActiveRootSpanId(firstEvent.contexts?.trace?.span_id);
701+
702+
const actualEvent = await integration.processEvent(firstEvent, {}, client);
696703
expect(actualEvent).toEqual(
697704
expectEventWithAttachedColdAppStart({ timeOriginMilliseconds, appStartTimeMilliseconds }),
698705
);
@@ -725,6 +732,7 @@ describe('App Start Integration', () => {
725732

726733
function processEvent(event: Event): PromiseLike<Event | null> | Event | null {
727734
const integration = appStartIntegration();
735+
(integration as AppStartIntegrationTest).setFirstStartedActiveRootSpanId(event.contexts?.trace?.span_id);
728736
return integration.processEvent(event, {}, new TestClient(getDefaultTestClientOptions()));
729737
}
730738

packages/core/test/tracing/reactnavigation.ttid.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ describe('React Navigation - TTID', () => {
367367
});
368368

369369
test('idle transaction should cancel the ttid span if new frame not received', () => {
370+
jest.runOnlyPendingTimers(); // Flush app start transaction
370371
mockedNavigation.navigateToNewScreen();
371372
jest.runOnlyPendingTimers(); // Flush ttid transaction
372373

0 commit comments

Comments
 (0)