Skip to content

Commit df6100a

Browse files
feat(tracing): Add JS Bundle Execution to the App Start span
1 parent 202f386 commit df6100a

File tree

5 files changed

+78
-3
lines changed

5 files changed

+78
-3
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535

3636
Note that the `Sentry.BrowserIntegrations`, `Sentry.Integration` and the Class style integrations will be removed in the next major version of the SDK.
3737

38+
- Added JS Bundle Execution start information to the application start measurements ([#3857](https://github.com/getsentry/sentry-react-native/pull/3857))
39+
3840
### Fixes
3941

4042
- Remove unused `rnpm` config ([#3811](https://github.com/getsentry/sentry-react-native/pull/3811))

src/js/tracing/reactnativeprofiler.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { getCurrentHub, Profiler } from '@sentry/react';
1+
import { getClient, getCurrentHub, Profiler } from '@sentry/react';
2+
import { timestampInSeconds } from '@sentry/utils';
23

34
import { createIntegration } from '../integrations/factory';
45
import { ReactNativeTracing } from './reactnativetracing';
@@ -13,6 +14,13 @@ const ReactNativeProfilerGlobalState = {
1314
export class ReactNativeProfiler extends Profiler {
1415
public readonly name: string = 'ReactNativeProfiler';
1516

17+
public constructor(props: ConstructorParameters<typeof Profiler>[0]) {
18+
const client = getClient();
19+
const integration = client && client.getIntegrationByName && client.getIntegrationByName<ReactNativeTracing>('ReactNativeTracing');
20+
integration && integration.setRootComponentFirstConstructorCallTimestampMs(timestampInSeconds() * 1000);
21+
super(props);
22+
}
23+
1624
/**
1725
* Get the app root mount time.
1826
*/

src/js/tracing/reactnativetracing.ts

+40-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
Event,
88
EventProcessor,
99
Integration,
10+
Span,
1011
Transaction as TransactionType,
1112
TransactionContext,
1213
} from '@sentry/types';
@@ -23,6 +24,7 @@ import { cancelInBackground, onlySampleIfChildSpans } from './transaction';
2324
import type { BeforeNavigate, RouteChangeContextData } from './types';
2425
import {
2526
adjustTransactionDuration,
27+
getBundleStartTimestampMs,
2628
getTimeOriginMilliseconds,
2729
isNearToNow,
2830
setSpanDurationAsMeasurement,
@@ -152,6 +154,7 @@ export class ReactNativeTracing implements Integration {
152154
private _hasSetTracePropagationTargets: boolean;
153155
private _hasSetTracingOrigins: boolean;
154156
private _currentViewName: string | undefined;
157+
private _firstConstructorCallTimestampMs: number | undefined;
155158

156159
public constructor(options: Partial<ReactNativeTracingOptions> = {}) {
157160
this._hasSetTracePropagationTargets = !!(
@@ -294,6 +297,13 @@ export class ReactNativeTracing implements Integration {
294297
this._appStartFinishTimestamp = endTimestamp;
295298
}
296299

300+
/**
301+
* Sets the root component first constructor call timestamp.
302+
*/
303+
public setRootComponentFirstConstructorCallTimestampMs(timestamp: number): void {
304+
this._firstConstructorCallTimestampMs = timestamp;
305+
}
306+
297307
/**
298308
* Starts a new transaction for a user interaction.
299309
* @param userInteractionId Consists of `op` representation UI Event and `elementId` unique element identifier on current screen.
@@ -478,17 +488,46 @@ export class ReactNativeTracing implements Integration {
478488
}
479489

480490
const op = appStart.isColdStart ? APP_START_COLD_OP : APP_START_WARM_OP;
481-
transaction.startChild({
491+
const appStartSpan = transaction.startChild({
482492
description: appStart.isColdStart ? 'Cold App Start' : 'Warm App Start',
483493
op,
484494
startTimestamp: appStartTimeSeconds,
485495
endTimestamp: this._appStartFinishTimestamp,
486496
});
497+
this._addJSExecutionBeforeRoot(appStartSpan);
487498

488499
const measurement = appStart.isColdStart ? APP_START_COLD : APP_START_WARM;
489500
transaction.setMeasurement(measurement, appStartDurationMilliseconds, 'millisecond');
490501
}
491502

503+
/**
504+
* Adds JS Execution before React Root. If `Sentry.wrap` is not used, create a span for the start of JS Bundle execution.
505+
*/
506+
private _addJSExecutionBeforeRoot(appStartSpan: Span): void {
507+
const bundleStartTimestampMs = getBundleStartTimestampMs();
508+
if (!bundleStartTimestampMs) {
509+
return;
510+
}
511+
512+
if (!this._firstConstructorCallTimestampMs) {
513+
logger.warn('Missing the root component first constructor call timestamp.');
514+
appStartSpan.startChild({
515+
description: 'JS Bundle Execution Start',
516+
op: appStartSpan.op,
517+
startTimestamp: bundleStartTimestampMs / 1000,
518+
endTimestamp: bundleStartTimestampMs / 1000,
519+
});
520+
return;
521+
}
522+
523+
appStartSpan.startChild({
524+
description: 'JS Bundle Execution Before React Root',
525+
op: appStartSpan.op,
526+
startTimestamp: bundleStartTimestampMs / 1000,
527+
endTimestamp: this._firstConstructorCallTimestampMs / 1000,
528+
});
529+
}
530+
492531
/** To be called when the route changes, but BEFORE the components of the new route mount. */
493532
private _onRouteWillChange(context: TransactionContext): TransactionType | undefined {
494533
return this._createRouteTransaction(context);

src/js/tracing/utils.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
spanToJSON,
77
} from '@sentry/core';
88
import type { Span, TransactionContext, TransactionSource } from '@sentry/types';
9-
import { timestampInSeconds } from '@sentry/utils';
9+
import { logger, timestampInSeconds } from '@sentry/utils';
10+
11+
import { RN_GLOBAL_OBJ } from '../utils/worldwide';
1012

1113
export const defaultTransactionSource: TransactionSource = 'component';
1214
export const customTransactionSource: TransactionSource = 'custom';
@@ -111,3 +113,25 @@ export function setSpanDurationAsMeasurement(name: string, span: Span): void {
111113

112114
setMeasurement(name, (spanEnd - spanStart) * 1000, 'millisecond');
113115
}
116+
117+
/**
118+
* Returns unix timestamp in ms of the bundle start time.
119+
*
120+
* If not available, returns undefined.
121+
*/
122+
export function getBundleStartTimestampMs(): number | undefined {
123+
const bundleStartTime = RN_GLOBAL_OBJ.__BUNDLE_START_TIME__;
124+
if (!bundleStartTime) {
125+
logger.warn('Missing the bundle start time on the global object.');
126+
return undefined;
127+
}
128+
129+
if (!RN_GLOBAL_OBJ.nativePerformanceNow) {
130+
// bundleStartTime is Date.now() in milliseconds
131+
return bundleStartTime;
132+
}
133+
134+
// nativePerformanceNow() is monotonic clock like performance.now()
135+
const approxStartingTimeOrigin = Date.now() - RN_GLOBAL_OBJ.nativePerformanceNow();
136+
return approxStartingTimeOrigin + bundleStartTime;
137+
}

src/js/utils/worldwide.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface ReactNativeInternalGlobal extends InternalGlobal {
2222
___SENTRY_METRO_DEV_SERVER___?: string;
2323
};
2424
};
25+
__BUNDLE_START_TIME__?: number;
26+
nativePerformanceNow?: () => number;
2527
}
2628

2729
/** Get's the global object for the current JavaScript runtime */

0 commit comments

Comments
 (0)