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 @@ -10,6 +10,7 @@

### Features

- Copy `app.vitals.start.screen` and `app.vitals.start.type` onto standalone `app.start` children, including user spans under `app.start.extended` ([#6631](https://github.com/getsentry/sentry-react-native/pull/6631))
- `featureFlagsIntegration` now forwards flag evaluations to the native SDKs, so flags are attached to native crashes too ([#6613](https://github.com/getsentry/sentry-react-native/pull/6613))

### Dependencies
Expand Down
32 changes: 32 additions & 0 deletions packages/core/src/js/tracing/integrations/appStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,10 @@ export const appStartIntegration = ({
children.push(...appStartSpans);
debug.log('[AppStart] Added app start spans to transaction event.', JSON.stringify(appStartSpans, undefined, 2));

if (standalone) {
copyStandaloneAppStartVitalsToChildren(event);
}

if (!standalone && !suppressMeasurement) {
const measurementKey = appStart.type === 'cold' ? APP_START_COLD_MEASUREMENT : APP_START_WARM_MEASUREMENT;
const measurementValue = {
Expand Down Expand Up @@ -1059,6 +1063,34 @@ export const appStartIntegration = ({
} as AppStartIntegration;
};

/**
* Copies `app.vitals.start.type` and `app.vitals.start.screen` from the standalone `app.start`
* root onto every child span so mobile-vitals queries can group/drill down by those dimensions.
* `app.vitals.start.value` stays on the root only. Screen is omitted when the root has none.
*/
function copyStandaloneAppStartVitalsToChildren(event: TransactionEvent): void {
const rootData = event.contexts?.trace?.data;
if (!rootData) {
return;
}

const type = rootData[SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE];
const screen = rootData[SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN];
if (type === undefined && screen === undefined) {
return;
}

for (const span of event.spans || []) {
span.data = span.data || {};
if (type !== undefined) {
span.data[SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE] = type;
}
if (screen !== undefined) {
span.data[SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN] = screen;
}
}
}

function setSpanDurationAsMeasurementOnTransactionEvent(event: TransactionEvent, label: string, span: SpanJSON): void {
if (!span.timestamp || !span.start_timestamp) {
debug.warn('Span is missing start or end timestamp. Cam not set measurement on transaction event.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ exports[`App Start Integration Standalone App Start matches the locked standalon
"spans": [
{
"data": {
"app.vitals.start.type": "cold",
"sentry.op": "app.start",
"sentry.origin": "auto.app.start",
},
Expand Down
71 changes: 71 additions & 0 deletions packages/core/test/tracing/integrations/appStart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ describe('App Start Integration', () => {
expectEventWithStandaloneColdAppStart(actualEvent, { timeOriginMilliseconds, appStartTimeMilliseconds }),
);
expect(actualEvent?.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN]).toBe('HomeScreen');
expectStandaloneChildrenHaveAppStartVitals(actualEvent, { type: 'cold', screen: 'HomeScreen' });
} finally {
screenSpy.mockRestore();
}
Expand All @@ -178,6 +179,14 @@ describe('App Start Integration', () => {

const actualEvent = await captureStandAloneAppStart();
expect(actualEvent?.contexts?.trace?.data).not.toHaveProperty(SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN);
expectStandaloneChildrenHaveAppStartVitals(actualEvent, { type: 'cold' });
});

it('copies app.vitals.start.type onto standalone children including native spans', async () => {
mockAppStart({ cold: false, enableNativeSpans: true });

const actualEvent = await captureStandAloneAppStart();
expectStandaloneChildrenHaveAppStartVitals(actualEvent, { type: 'warm' });
});

it('Does not add any spans or measurements when App Start Span is longer than threshold', async () => {
Expand Down Expand Up @@ -271,6 +280,7 @@ describe('App Start Integration', () => {
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_OP,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START,
[SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE]: 'cold',
},
}),
);
Expand Down Expand Up @@ -299,6 +309,7 @@ describe('App Start Integration', () => {
data: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_OP,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START,
[SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE]: 'cold',
},
}),
);
Expand Down Expand Up @@ -327,6 +338,7 @@ describe('App Start Integration', () => {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_OP,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START,
[SPAN_THREAD_NAME]: SPAN_THREAD_NAME_MAIN,
[SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE]: 'cold',
},
}),
);
Expand Down Expand Up @@ -1259,6 +1271,42 @@ describe('Extended App Start', () => {
expect(childSpan?.parent_span_id).toBe(extended?.span_id);
});

it('copies app start vitals onto the extended span, user children, and nested descendants', async () => {
mockAppStart({ cold: true });
const screenSpy = jest
.spyOn(ReactNativeTracing, 'getCurrentReactNativeTracingIntegration')
.mockReturnValue({ state: { currentRoute: 'HomeScreen' } } as ReturnType<
typeof ReactNativeTracing.getCurrentReactNativeTracingIntegration
>);
const { integration, client } = setupStandaloneIntegration();

try {
integration.extendAppStart();
const extendedSpan = integration.getExtendedAppStartSpan();
const child = startInactiveSpan({ parentSpan: extendedSpan, op: 'app.init', name: 'load config' });
const grandchild = startInactiveSpan({ parentSpan: child, op: 'app.init', name: 'parse flags' });
grandchild.end();
child.end();

await integration.finishExtendedAppStart();

const event = client.event as TransactionEvent;
expect(event?.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE]).toBe('cold');
expect(event?.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN]).toBe('HomeScreen');
expectStandaloneChildrenHaveAppStartVitals(event, { type: 'cold', screen: 'HomeScreen' });

const extended = event?.spans?.find(s => s.op === APP_START_EXTENDED_OP);
const childSpan = event?.spans?.find(s => s.description === 'load config');
const grandchildSpan = event?.spans?.find(s => s.description === 'parse flags');
expect(extended).toBeDefined();
expect(childSpan).toBeDefined();
expect(grandchildSpan).toBeDefined();
expect(grandchildSpan?.parent_span_id).toBe(childSpan?.span_id);
} finally {
screenSpy.mockRestore();
}
});

it('trims the transaction end to the last child span', async () => {
const [timeOriginMilliseconds] = mockAppStart({ cold: true });
const { integration, client } = setupStandaloneIntegration();
Expand Down Expand Up @@ -1358,6 +1406,9 @@ describe('Extended App Start', () => {
const event = client.eventQueue[0] as TransactionEvent;
expect(event?.contexts?.trace?.op).toBe(APP_START_OP);
expect(event?.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_APP_VITALS_START_VALUE]).toBeUndefined();
expect(event?.contexts?.trace?.data).not.toHaveProperty(SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE);
expect(event?.contexts?.trace?.data).not.toHaveProperty(SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN);
expectStandaloneChildrenHaveAppStartVitals(event, {});
});

it('does not claim the run when the standalone transaction is not recording (falls back to normal capture)', async () => {
Expand Down Expand Up @@ -2281,6 +2332,26 @@ function processEvent(event: Event): PromiseLike<Event | null> | Event | null {
return processEventWithIntegration(integration, event);
}

function expectStandaloneChildrenHaveAppStartVitals(
event: Event | null | undefined,
{ type, screen }: { type?: string; screen?: string } = {},
): void {
expect(event?.spans?.length).toBeGreaterThan(0);
for (const span of event!.spans!) {
if (type !== undefined) {
expect(span.data?.[SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE]).toBe(type);
} else {
expect(span.data).not.toHaveProperty(SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE);
}
if (screen !== undefined) {
expect(span.data?.[SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN]).toBe(screen);
} else {
expect(span.data).not.toHaveProperty(SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN);
}
expect(span.data).not.toHaveProperty(SEMANTIC_ATTRIBUTE_APP_VITALS_START_VALUE);
}
}
Comment thread
buenaflor marked this conversation as resolved.

async function captureStandAloneAppStart(): Promise<PromiseLike<Event | null> | Event | null> {
getCurrentScope().clear();
getIsolationScope().clear();
Expand Down
Loading