Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
- Use engine-specific promise rejection tracking ([#4826](https://github.com/getsentry/sentry-react-native/pull/4826))
- Fixes Feedback Widget accessibility issue on iOS ([#4739](https://github.com/getsentry/sentry-react-native/pull/4739))
- Measuring TTID or TTFD could cause a crash when `parentSpanId` was removed ([#4881](https://github.com/getsentry/sentry-react-native/pull/4881))
- Report slow and frozen frames as app start span data ([#4865](https://github.com/getsentry/sentry-react-native/pull/4865))
- Report slow and frozen frames in TTID and TTFD span data ([#4871](https://github.com/getsentry/sentry-react-native/pull/4871))

### Dependencies

Expand Down
84 changes: 74 additions & 10 deletions packages/core/src/js/tracing/integrations/appStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
APP_START_COLD as APP_START_COLD_MEASUREMENT,
APP_START_WARM as APP_START_WARM_MEASUREMENT,
} from '../../measurements';
import type { NativeAppStartResponse } from '../../NativeRNSentry';
import type { NativeAppStartResponse, NativeFramesResponse } from '../../NativeRNSentry';
import type { ReactNativeClientOptions } from '../../options';
import { convertSpanToTransaction, isRootSpan, setEndTimeValue } from '../../utils/span';
import { NATIVE } from '../../wrapper';
Expand Down Expand Up @@ -49,7 +49,12 @@ const MAX_APP_START_AGE_MS = 60_000;
/** App Start transaction name */
const APP_START_TX_NAME = 'App Start';

let recordedAppStartEndTimestampMs: number | undefined = undefined;
interface AppStartEndData {
timestampMs: number;
endFrames: NativeFramesResponse | null;
}

let appStartEndData: AppStartEndData | undefined = undefined;
let isRecordedAppStartEndTimestampMsManual = false;

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

isRecordedAppStartEndTimestampMsManual = isManual;
_setAppStartEndTimestampMs(timestampInSeconds() * 1000);

const timestampMs = timestampInSeconds() * 1000;
let endFrames: NativeFramesResponse | null = null;

if (NATIVE.enableNative) {
try {
endFrames = await NATIVE.fetchNativeFrames();
logger.debug('[AppStart] Captured end frames for app start.', endFrames);
} catch (error) {
logger.debug('[AppStart] Failed to capture end frames for app start.', error);
}
}

_setAppStartEndData({
timestampMs,
endFrames,
});

await client.getIntegrationByName<AppStartIntegration>(INTEGRATION_NAME)?.captureStandaloneAppStart();
}

Expand All @@ -85,8 +107,7 @@ export async function _captureAppStart({ isManual }: { isManual: boolean }): Pro
* Used automatically by `Sentry.wrap` and `Sentry.ReactNativeProfiler`.
*/
export function setRootComponentCreationTimestampMs(timestampMs: number): void {
recordedAppStartEndTimestampMs &&
logger.warn('Setting Root component creation timestamp after app start end is set.');
appStartEndData?.timestampMs && logger.warn('Setting Root component creation timestamp after app start end is set.');
rootComponentCreationTimestampMs && logger.warn('Overwriting already set root component creation timestamp.');
rootComponentCreationTimestampMs = timestampMs;
isRootComponentCreationTimestampMsManual = true;
Expand All @@ -107,9 +128,9 @@ export function _setRootComponentCreationTimestampMs(timestampMs: number): void
*
* @private
*/
export const _setAppStartEndTimestampMs = (timestampMs: number): void => {
recordedAppStartEndTimestampMs && logger.warn('Overwriting already set app start.');
recordedAppStartEndTimestampMs = timestampMs;
export const _setAppStartEndData = (data: AppStartEndData): void => {
appStartEndData && logger.warn('Overwriting already set app start end data.');
appStartEndData = data;
};

/**
Expand All @@ -121,6 +142,29 @@ export function _clearRootComponentCreationTimestampMs(): void {
rootComponentCreationTimestampMs = undefined;
}

/**
* Attaches frame data to a span's data object.
*/
function attachFrameDataToSpan(span: SpanJSON, frames: NativeFramesResponse): void {
if (frames.totalFrames <= 0 && frames.slowFrames <= 0 && frames.totalFrames <= 0) {
logger.warn(`[AppStart] Detected zero slow or frozen frames. Not adding measurements to spanId (${span.span_id}).`);
return;
}
span.data = span.data || {};
span.data['frames.total'] = frames.totalFrames;
span.data['frames.slow'] = frames.slowFrames;
span.data['frames.frozen'] = frames.frozenFrames;

logger.debug('[AppStart] Attached frame data to span.', {
spanId: span.span_id,
frameData: {
total: frames.totalFrames,
slow: frames.slowFrames,
frozen: frames.frozenFrames,
},
});
}

/**
* Adds AppStart spans from the native layer to the transaction event.
*/
Expand Down Expand Up @@ -220,6 +264,21 @@ export const appStartIntegration = ({

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

if (!appStartEndData?.endFrames && NATIVE.enableNative) {
try {
const endFrames = await NATIVE.fetchNativeFrames();
logger.debug('[AppStart] Captured end frames for standalone app start.', endFrames);

const currentTimestamp = appStartEndData?.timestampMs || timestampInSeconds() * 1000;
_setAppStartEndData({
timestampMs: currentTimestamp,
endFrames,
});
} catch (error) {
logger.debug('[AppStart] Failed to capture frames for standalone app start.', error);
}
}

const span = startInactiveSpan({
forceTransaction: true,
name: APP_START_TX_NAME,
Expand Down Expand Up @@ -288,10 +347,10 @@ export const appStartIntegration = ({
return;
}

const appStartEndTimestampMs = recordedAppStartEndTimestampMs || getBundleStartTimestampMs();
const appStartEndTimestampMs = appStartEndData?.timestampMs || getBundleStartTimestampMs();
if (!appStartEndTimestampMs) {
logger.warn(
'[AppStart] Javascript failed to record app start end. `setAppStartEndTimestampMs` was not called nor could the bundle start be found.',
'[AppStart] Javascript failed to record app start end. `_setAppStartEndData` was not called nor could the bundle start be found.',
);
return;
}
Expand Down Expand Up @@ -368,6 +427,11 @@ export const appStartIntegration = ({
parent_span_id: event.contexts.trace.span_id,
origin,
});

if (appStartEndData?.endFrames) {
attachFrameDataToSpan(appStartSpanJSON, appStartEndData.endFrames);
}

const jsExecutionSpanJSON = createJSExecutionStartSpan(appStartSpanJSON, rootComponentCreationTimestampMs);

const appStartSpans = [
Expand Down
Loading
Loading