Skip to content
Merged
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 @@ -15,6 +15,7 @@
### Fixes

- 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))
- Attach App Start spans to the first created not the first processed root span ([#4618](https://github.com/getsentry/sentry-react-native/pull/4618))

### Dependencies

Expand Down
46 changes: 41 additions & 5 deletions packages/core/src/js/tracing/integrations/appStart.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable complexity, max-lines */
import type { Client, Event, Integration, SpanJSON, TransactionEvent } from '@sentry/core';
import type { Client, Event, Integration, Span, SpanJSON, TransactionEvent } from '@sentry/core';
import {
getCapturedScopesOnSpan,
getClient,
Expand All @@ -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,
Expand Down Expand Up @@ -137,16 +137,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 => {
Expand All @@ -168,6 +170,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<void> {
if (!standalone) {
logger.debug(
Expand Down Expand Up @@ -213,11 +236,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.');
Expand Down Expand Up @@ -333,7 +368,8 @@ export const appStartIntegration = ({
afterAllSetup,
processEvent,
captureStandaloneAppStart,
};
setFirstStartedActiveRootSpanId,
} as AppStartIntegration;
};

function setSpanDurationAsMeasurementOnTransactionEvent(event: TransactionEvent, label: string, span: SpanJSON): void {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/test/profiling/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 9 additions & 1 deletion packages/core/test/tracing/integrations/appStart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import { NATIVE } from '../../../src/js/wrapper';
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
import { mockFunction } from '../../testutils';

type AppStartIntegrationTest = ReturnType<typeof appStartIntegration> & {
setFirstStartedActiveRootSpanId: (spanId: string | undefined) => void;
};

let dateNowSpy: jest.SpyInstance;

jest.mock('../../../src/js/wrapper', () => {
Expand Down Expand Up @@ -692,7 +696,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 }),
);
Expand Down Expand Up @@ -725,6 +732,7 @@ describe('App Start Integration', () => {

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

Expand Down
1 change: 1 addition & 0 deletions packages/core/test/tracing/reactnavigation.ttid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,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

Expand Down
Loading