Skip to content

Commit 11da461

Browse files
authored
Merge 9429126 into 209f66f
2 parents 209f66f + 9429126 commit 11da461

File tree

4 files changed

+253
-16
lines changed

4 files changed

+253
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- crashedLastRun now returns the correct value ([#4829](https://github.com/getsentry/sentry-react-native/pull/4829))
2121
- Use engine-specific promise rejection tracking ([#4826](https://github.com/getsentry/sentry-react-native/pull/4826))
2222
- Fixes Feedback Widget accessibility issue on iOS ([#4739](https://github.com/getsentry/sentry-react-native/pull/4739))
23+
- Report slow and frozen frames as app start span data ([#4865](https://github.com/getsentry/sentry-react-native/pull/4865))
2324

2425
### Dependencies
2526

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

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
APP_START_COLD as APP_START_COLD_MEASUREMENT,
1717
APP_START_WARM as APP_START_WARM_MEASUREMENT,
1818
} from '../../measurements';
19-
import type { NativeAppStartResponse } from '../../NativeRNSentry';
19+
import type { NativeAppStartResponse, NativeFramesResponse } from '../../NativeRNSentry';
2020
import type { ReactNativeClientOptions } from '../../options';
2121
import { convertSpanToTransaction, isRootSpan, setEndTimeValue } from '../../utils/span';
2222
import { NATIVE } from '../../wrapper';
@@ -49,7 +49,12 @@ const MAX_APP_START_AGE_MS = 60_000;
4949
/** App Start transaction name */
5050
const APP_START_TX_NAME = 'App Start';
5151

52-
let recordedAppStartEndTimestampMs: number | undefined = undefined;
52+
interface AppStartEndData {
53+
timestampMs: number;
54+
endFrames: NativeFramesResponse | null;
55+
}
56+
57+
let appStartEndData: AppStartEndData | undefined = undefined;
5358
let isRecordedAppStartEndTimestampMsManual = false;
5459

5560
let rootComponentCreationTimestampMs: number | undefined = undefined;
@@ -76,7 +81,24 @@ export async function _captureAppStart({ isManual }: { isManual: boolean }): Pro
7681
}
7782

7883
isRecordedAppStartEndTimestampMsManual = isManual;
79-
_setAppStartEndTimestampMs(timestampInSeconds() * 1000);
84+
85+
const timestampMs = timestampInSeconds() * 1000;
86+
let endFrames: NativeFramesResponse | null = null;
87+
88+
if (NATIVE.enableNative) {
89+
try {
90+
endFrames = await NATIVE.fetchNativeFrames();
91+
logger.debug('[AppStart] Captured end frames for app start.', endFrames);
92+
} catch (error) {
93+
logger.debug('[AppStart] Failed to capture end frames for app start.', error);
94+
}
95+
}
96+
97+
_setAppStartEndData({
98+
timestampMs,
99+
endFrames,
100+
});
101+
80102
await client.getIntegrationByName<AppStartIntegration>(INTEGRATION_NAME)?.captureStandaloneAppStart();
81103
}
82104

@@ -85,8 +107,7 @@ export async function _captureAppStart({ isManual }: { isManual: boolean }): Pro
85107
* Used automatically by `Sentry.wrap` and `Sentry.ReactNativeProfiler`.
86108
*/
87109
export function setRootComponentCreationTimestampMs(timestampMs: number): void {
88-
recordedAppStartEndTimestampMs &&
89-
logger.warn('Setting Root component creation timestamp after app start end is set.');
110+
appStartEndData?.timestampMs && logger.warn('Setting Root component creation timestamp after app start end is set.');
90111
rootComponentCreationTimestampMs && logger.warn('Overwriting already set root component creation timestamp.');
91112
rootComponentCreationTimestampMs = timestampMs;
92113
isRootComponentCreationTimestampMsManual = true;
@@ -107,9 +128,9 @@ export function _setRootComponentCreationTimestampMs(timestampMs: number): void
107128
*
108129
* @private
109130
*/
110-
export const _setAppStartEndTimestampMs = (timestampMs: number): void => {
111-
recordedAppStartEndTimestampMs && logger.warn('Overwriting already set app start.');
112-
recordedAppStartEndTimestampMs = timestampMs;
131+
export const _setAppStartEndData = (data: AppStartEndData): void => {
132+
appStartEndData && logger.warn('Overwriting already set app start end data.');
133+
appStartEndData = data;
113134
};
114135

115136
/**
@@ -121,6 +142,25 @@ export function _clearRootComponentCreationTimestampMs(): void {
121142
rootComponentCreationTimestampMs = undefined;
122143
}
123144

145+
/**
146+
* Attaches frame data to a span's data object.
147+
*/
148+
function attachFrameDataToSpan(span: SpanJSON, frames: NativeFramesResponse): void {
149+
span.data = span.data || {};
150+
span.data['frames.total'] = frames.totalFrames;
151+
span.data['frames.slow'] = frames.slowFrames;
152+
span.data['frames.frozen'] = frames.frozenFrames;
153+
154+
logger.debug('[AppStart] Attached frame data to span.', {
155+
spanId: span.span_id,
156+
frameData: {
157+
total: frames.totalFrames,
158+
slow: frames.slowFrames,
159+
frozen: frames.frozenFrames,
160+
},
161+
});
162+
}
163+
124164
/**
125165
* Adds AppStart spans from the native layer to the transaction event.
126166
*/
@@ -220,6 +260,21 @@ export const appStartIntegration = ({
220260

221261
logger.debug('[AppStart] App start tracking standalone root span (transaction).');
222262

263+
if (!appStartEndData?.endFrames && NATIVE.enableNative) {
264+
try {
265+
const endFrames = await NATIVE.fetchNativeFrames();
266+
logger.debug('[AppStart] Captured end frames for standalone app start.', endFrames);
267+
268+
const currentTimestamp = appStartEndData?.timestampMs || timestampInSeconds() * 1000;
269+
_setAppStartEndData({
270+
timestampMs: currentTimestamp,
271+
endFrames,
272+
});
273+
} catch (error) {
274+
logger.debug('[AppStart] Failed to capture frames for standalone app start.', error);
275+
}
276+
}
277+
223278
const span = startInactiveSpan({
224279
forceTransaction: true,
225280
name: APP_START_TX_NAME,
@@ -288,10 +343,10 @@ export const appStartIntegration = ({
288343
return;
289344
}
290345

291-
const appStartEndTimestampMs = recordedAppStartEndTimestampMs || getBundleStartTimestampMs();
346+
const appStartEndTimestampMs = appStartEndData?.timestampMs || getBundleStartTimestampMs();
292347
if (!appStartEndTimestampMs) {
293348
logger.warn(
294-
'[AppStart] Javascript failed to record app start end. `setAppStartEndTimestampMs` was not called nor could the bundle start be found.',
349+
'[AppStart] Javascript failed to record app start end. `_setAppStartEndData` was not called nor could the bundle start be found.',
295350
);
296351
return;
297352
}
@@ -368,6 +423,11 @@ export const appStartIntegration = ({
368423
parent_span_id: event.contexts.trace.span_id,
369424
origin,
370425
});
426+
427+
if (appStartEndData?.endFrames) {
428+
attachFrameDataToSpan(appStartSpanJSON, appStartEndData.endFrames);
429+
}
430+
371431
const jsExecutionSpanJSON = createJSExecutionStartSpan(appStartSpanJSON, rootComponentCreationTimestampMs);
372432

373433
const appStartSpans = [

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

Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import {
2020
UI_LOAD,
2121
} from '../../../src/js/tracing';
2222
import {
23+
_captureAppStart,
2324
_clearRootComponentCreationTimestampMs,
24-
_setAppStartEndTimestampMs,
25+
_setAppStartEndData,
2526
_setRootComponentCreationTimestampMs,
2627
appStartIntegration,
2728
setRootComponentCreationTimestampMs,
@@ -788,6 +789,169 @@ describe('App Start Integration', () => {
788789
});
789790
});
790791

792+
describe('Frame Data Integration', () => {
793+
it('attaches frame data to standalone cold app start span', async () => {
794+
const mockEndFrames = {
795+
totalFrames: 150,
796+
slowFrames: 5,
797+
frozenFrames: 2,
798+
};
799+
800+
mockFunction(NATIVE.fetchNativeFrames).mockResolvedValue(mockEndFrames);
801+
802+
mockAppStart({ cold: true });
803+
804+
const actualEvent = await captureStandAloneAppStart();
805+
806+
const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start');
807+
808+
expect(appStartSpan).toBeDefined();
809+
expect(appStartSpan!.data).toEqual(
810+
expect.objectContaining({
811+
'frames.total': 150,
812+
'frames.slow': 5,
813+
'frames.frozen': 2,
814+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_COLD_OP,
815+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START,
816+
}),
817+
);
818+
});
819+
820+
it('attaches frame data to standalone warm app start span', async () => {
821+
const mockEndFrames = {
822+
totalFrames: 200,
823+
slowFrames: 8,
824+
frozenFrames: 1,
825+
};
826+
827+
mockFunction(NATIVE.fetchNativeFrames).mockResolvedValue(mockEndFrames);
828+
829+
mockAppStart({ cold: false });
830+
831+
const actualEvent = await captureStandAloneAppStart();
832+
833+
const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Warm App Start');
834+
835+
expect(appStartSpan).toBeDefined();
836+
expect(appStartSpan!.data).toEqual(
837+
expect.objectContaining({
838+
'frames.total': 200,
839+
'frames.slow': 8,
840+
'frames.frozen': 1,
841+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_WARM_OP,
842+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START,
843+
}),
844+
);
845+
});
846+
847+
it('attaches frame data to attached cold app start span', async () => {
848+
const mockEndFrames = {
849+
totalFrames: 120,
850+
slowFrames: 3,
851+
frozenFrames: 0,
852+
};
853+
854+
mockFunction(NATIVE.fetchNativeFrames).mockResolvedValue(mockEndFrames);
855+
856+
mockAppStart({ cold: true });
857+
858+
await _captureAppStart({ isManual: false });
859+
860+
const actualEvent = await processEvent(getMinimalTransactionEvent());
861+
862+
const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start');
863+
864+
expect(appStartSpan).toBeDefined();
865+
expect(appStartSpan!.data).toEqual(
866+
expect.objectContaining({
867+
'frames.total': 120,
868+
'frames.slow': 3,
869+
'frames.frozen': 0,
870+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_COLD_OP,
871+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START,
872+
}),
873+
);
874+
});
875+
876+
it('attaches frame data to attached warm app start span', async () => {
877+
const mockEndFrames = {
878+
totalFrames: 180,
879+
slowFrames: 12,
880+
frozenFrames: 3,
881+
};
882+
883+
mockFunction(NATIVE.fetchNativeFrames).mockResolvedValue(mockEndFrames);
884+
885+
mockAppStart({ cold: false });
886+
887+
await _captureAppStart({ isManual: false });
888+
889+
const actualEvent = await processEvent(getMinimalTransactionEvent());
890+
891+
const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Warm App Start');
892+
893+
expect(appStartSpan).toBeDefined();
894+
expect(appStartSpan!.data).toEqual(
895+
expect.objectContaining({
896+
'frames.total': 180,
897+
'frames.slow': 12,
898+
'frames.frozen': 3,
899+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_WARM_OP,
900+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START,
901+
}),
902+
);
903+
});
904+
905+
it('does not attach frame data when native frames are not available', async () => {
906+
mockFunction(NATIVE.fetchNativeFrames).mockRejectedValue(new Error('Native frames not available'));
907+
908+
mockAppStart({ cold: true });
909+
910+
const actualEvent = await captureStandAloneAppStart();
911+
912+
const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start');
913+
914+
expect(appStartSpan).toBeDefined();
915+
expect(appStartSpan!.data).toEqual(
916+
expect.objectContaining({
917+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_COLD_OP,
918+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START,
919+
}),
920+
);
921+
922+
expect(appStartSpan!.data).not.toHaveProperty('frames.total');
923+
expect(appStartSpan!.data).not.toHaveProperty('frames.slow');
924+
expect(appStartSpan!.data).not.toHaveProperty('frames.frozen');
925+
});
926+
927+
it('does not attach frame data when NATIVE is not enabled', async () => {
928+
const originalEnableNative = NATIVE.enableNative;
929+
(NATIVE as any).enableNative = false;
930+
931+
try {
932+
mockAppStart({ cold: true });
933+
934+
const actualEvent = await captureStandAloneAppStart();
935+
936+
const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start');
937+
938+
expect(appStartSpan).toBeDefined();
939+
expect(appStartSpan!.data).toEqual(
940+
expect.objectContaining({
941+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_COLD_OP,
942+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START,
943+
}),
944+
);
945+
946+
expect(appStartSpan!.data).not.toHaveProperty('frames.total');
947+
expect(appStartSpan!.data).not.toHaveProperty('frames.slow');
948+
expect(appStartSpan!.data).not.toHaveProperty('frames.frozen');
949+
} finally {
950+
(NATIVE as any).enableNative = originalEnableNative;
951+
}
952+
});
953+
});
954+
791955
function setupIntegration() {
792956
const client = new TestClient(getDefaultTestClientOptions());
793957
const integration = appStartIntegration();
@@ -1095,7 +1259,10 @@ function mockAppStart({
10951259
: [],
10961260
};
10971261

1098-
_setAppStartEndTimestampMs(appStartEndTimestampMs || timeOriginMilliseconds);
1262+
_setAppStartEndData({
1263+
timestampMs: appStartEndTimestampMs || timeOriginMilliseconds,
1264+
endFrames: null,
1265+
});
10991266
mockFunction(getTimeOriginMilliseconds).mockReturnValue(timeOriginMilliseconds);
11001267
mockFunction(NATIVE.fetchNativeAppStart).mockResolvedValue(mockAppStartResponse);
11011268

@@ -1112,7 +1279,10 @@ function mockTooLongAppStart() {
11121279
spans: [],
11131280
};
11141281

1115-
_setAppStartEndTimestampMs(timeOriginMilliseconds);
1282+
_setAppStartEndData({
1283+
timestampMs: timeOriginMilliseconds,
1284+
endFrames: null,
1285+
});
11161286
mockFunction(getTimeOriginMilliseconds).mockReturnValue(timeOriginMilliseconds);
11171287
mockFunction(NATIVE.fetchNativeAppStart).mockResolvedValue(mockAppStartResponse);
11181288

@@ -1134,7 +1304,10 @@ function mockTooOldAppStart() {
11341304

11351305
// App start finish timestamp
11361306
// App start length is 5 seconds
1137-
_setAppStartEndTimestampMs(appStartEndTimestampMilliseconds);
1307+
_setAppStartEndData({
1308+
timestampMs: appStartEndTimestampMilliseconds,
1309+
endFrames: null,
1310+
});
11381311
mockFunction(getTimeOriginMilliseconds).mockReturnValue(timeOriginMilliseconds - 64000);
11391312
mockFunction(NATIVE.fetchNativeAppStart).mockResolvedValue(mockAppStartResponse);
11401313
// Transaction start timestamp

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jest.mock('../../src/js/tracing/timetodisplaynative', () => mockedtimetodisplayn
1414
import * as Sentry from '../../src/js';
1515
import { startSpanManual } from '../../src/js';
1616
import { TimeToFullDisplay, TimeToInitialDisplay } from '../../src/js/tracing';
17-
import { _setAppStartEndTimestampMs } from '../../src/js/tracing/integrations/appStart';
17+
import { _setAppStartEndData } from '../../src/js/tracing/integrations/appStart';
1818
import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION, SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from '../../src/js/tracing/origin';
1919
import { SPAN_THREAD_NAME, SPAN_THREAD_NAME_JAVASCRIPT } from '../../src/js/tracing/span';
2020
import { isHermesEnabled, notWeb } from '../../src/js/utils/environment';
@@ -47,7 +47,10 @@ describe('React Navigation - TTID', () => {
4747
type: 'cold',
4848
spans: [],
4949
});
50-
_setAppStartEndTimestampMs(mockedAppStartTimeSeconds * 1000);
50+
_setAppStartEndData({
51+
timestampMs: mockedAppStartTimeSeconds * 1000,
52+
endFrames: null
53+
});
5154

5255
const sut = createTestedInstrumentation({ enableTimeToInitialDisplay: true });
5356
transportSendMock = initSentry(sut).transportSendMock;

0 commit comments

Comments
 (0)