Skip to content

Commit 70e6261

Browse files
feat(tracing): Add JS Bundle Execution to the App Start span (#3857)
1 parent 0884076 commit 70e6261

File tree

6 files changed

+200
-2
lines changed

6 files changed

+200
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Add native application start spans ([#3855](https://github.com/getsentry/sentry-react-native/pull/3855))
88
- This doesn't change the app start measurement length, but add child spans (more detail) into the existing app start span
9+
- Added JS Bundle Execution start information to the application start measurements ([#3857](https://github.com/getsentry/sentry-react-native/pull/3857))
910

1011
### Dependencies
1112

src/js/tracing/reactnativeprofiler.tsx

Lines changed: 9 additions & 1 deletion
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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { cancelInBackground, onlySampleIfChildSpans } from './transaction';
2424
import type { BeforeNavigate, RouteChangeContextData } from './types';
2525
import {
2626
adjustTransactionDuration,
27+
getBundleStartTimestampMs,
2728
getTimeOriginMilliseconds,
2829
isNearToNow,
2930
setSpanDurationAsMeasurement,
@@ -153,6 +154,7 @@ export class ReactNativeTracing implements Integration {
153154
private _hasSetTracePropagationTargets: boolean;
154155
private _hasSetTracingOrigins: boolean;
155156
private _currentViewName: string | undefined;
157+
private _firstConstructorCallTimestampMs: number | undefined;
156158

157159
public constructor(options: Partial<ReactNativeTracingOptions> = {}) {
158160
this._hasSetTracePropagationTargets = !!(
@@ -295,6 +297,13 @@ export class ReactNativeTracing implements Integration {
295297
this._appStartFinishTimestamp = endTimestamp;
296298
}
297299

300+
/**
301+
* Sets the root component first constructor call timestamp.
302+
*/
303+
public setRootComponentFirstConstructorCallTimestampMs(timestamp: number): void {
304+
this._firstConstructorCallTimestampMs = timestamp;
305+
}
306+
298307
/**
299308
* Starts a new transaction for a user interaction.
300309
* @param userInteractionId Consists of `op` representation UI Event and `elementId` unique element identifier on current screen.
@@ -499,12 +508,41 @@ export class ReactNativeTracing implements Integration {
499508
startTimestamp: appStartTimeSeconds,
500509
endTimestamp: this._appStartFinishTimestamp,
501510
});
511+
this._addJSExecutionBeforeRoot(appStartSpan);
502512
this._addNativeSpansTo(appStartSpan, appStart.spans);
503513

504514
const measurement = appStart.type === 'cold' ? APP_START_COLD : APP_START_WARM;
505515
transaction.setMeasurement(measurement, appStartDurationMilliseconds, 'millisecond');
506516
}
507517

518+
/**
519+
* Adds JS Execution before React Root. If `Sentry.wrap` is not used, create a span for the start of JS Bundle execution.
520+
*/
521+
private _addJSExecutionBeforeRoot(appStartSpan: Span): void {
522+
const bundleStartTimestampMs = getBundleStartTimestampMs();
523+
if (!bundleStartTimestampMs) {
524+
return;
525+
}
526+
527+
if (!this._firstConstructorCallTimestampMs) {
528+
logger.warn('Missing the root component first constructor call timestamp.');
529+
appStartSpan.startChild({
530+
description: 'JS Bundle Execution Start',
531+
op: appStartSpan.op,
532+
startTimestamp: bundleStartTimestampMs / 1000,
533+
endTimestamp: bundleStartTimestampMs / 1000,
534+
});
535+
return;
536+
}
537+
538+
appStartSpan.startChild({
539+
description: 'JS Bundle Execution Before React Root',
540+
op: appStartSpan.op,
541+
startTimestamp: bundleStartTimestampMs / 1000,
542+
endTimestamp: this._firstConstructorCallTimestampMs / 1000,
543+
});
544+
}
545+
508546
/**
509547
* Adds native spans to the app start span.
510548
*/

src/js/tracing/utils.ts

Lines changed: 25 additions & 1 deletion
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

Lines changed: 2 additions & 0 deletions
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 */

test/tracing/reactnativetracing.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,114 @@ describe('ReactNativeTracing', () => {
331331
expect(transaction).toBeUndefined();
332332
});
333333

334+
describe('bundle execution spans', () => {
335+
afterEach(() => {
336+
clearReactNativeBundleExecutionStartTimestamp();
337+
});
338+
339+
it('does not add bundle executions span if __BUNDLE_START_TIME__ is undefined', async () => {
340+
const integration = new ReactNativeTracing();
341+
342+
mockAppStartResponse({ cold: true });
343+
344+
setup(integration);
345+
346+
await jest.advanceTimersByTimeAsync(500);
347+
await jest.runOnlyPendingTimersAsync();
348+
349+
const transaction = client.event;
350+
351+
const bundleStartSpan = transaction!.spans!.find(
352+
({ description }) =>
353+
description === 'JS Bundle Execution Start' || description === 'JS Bundle Execution Before React Root',
354+
);
355+
356+
expect(bundleStartSpan).toBeUndefined();
357+
});
358+
359+
it('adds bundle execution span', async () => {
360+
const integration = new ReactNativeTracing();
361+
362+
const [timeOriginMilliseconds] = mockAppStartResponse({ cold: true });
363+
mockReactNativeBundleExecutionStartTimestamp();
364+
365+
setup(integration);
366+
integration.onAppStartFinish(timeOriginMilliseconds + 200);
367+
368+
await jest.advanceTimersByTimeAsync(500);
369+
await jest.runOnlyPendingTimersAsync();
370+
371+
const transaction = client.event;
372+
373+
const appStartRootSpan = transaction!.spans!.find(({ description }) => description === 'Cold App Start');
374+
const bundleStartSpan = transaction!.spans!.find(
375+
({ description }) => description === 'JS Bundle Execution Start',
376+
);
377+
const appStartRootSpanJSON = spanToJSON(appStartRootSpan!);
378+
const bundleStartSpanJSON = spanToJSON(bundleStartSpan!);
379+
380+
expect(appStartRootSpan).toBeDefined();
381+
expect(bundleStartSpan).toBeDefined();
382+
expect(appStartRootSpanJSON).toEqual(
383+
expect.objectContaining(<SpanJSON>{
384+
description: 'Cold App Start',
385+
span_id: expect.any(String),
386+
op: APP_START_COLD_OP,
387+
}),
388+
);
389+
expect(bundleStartSpanJSON).toEqual(
390+
expect.objectContaining(<SpanJSON>{
391+
description: 'JS Bundle Execution Start',
392+
start_timestamp: expect.closeTo((timeOriginMilliseconds - 50) / 1000),
393+
timestamp: expect.closeTo((timeOriginMilliseconds - 50) / 1000),
394+
parent_span_id: spanToJSON(appStartRootSpan!).span_id, // parent is the root app start span
395+
op: spanToJSON(appStartRootSpan!).op, // op is the same as the root app start span
396+
}),
397+
);
398+
});
399+
400+
it('adds bundle execution before react root', async () => {
401+
const integration = new ReactNativeTracing();
402+
403+
const [timeOriginMilliseconds] = mockAppStartResponse({ cold: true });
404+
mockReactNativeBundleExecutionStartTimestamp();
405+
406+
setup(integration);
407+
integration.setRootComponentFirstConstructorCallTimestampMs(timeOriginMilliseconds - 10);
408+
409+
await jest.advanceTimersByTimeAsync(500);
410+
await jest.runOnlyPendingTimersAsync();
411+
412+
const transaction = client.event;
413+
414+
const appStartRootSpan = transaction!.spans!.find(({ description }) => description === 'Cold App Start');
415+
const bundleStartSpan = transaction!.spans!.find(
416+
({ description }) => description === 'JS Bundle Execution Before React Root',
417+
);
418+
const appStartRootSpanJSON = spanToJSON(appStartRootSpan!);
419+
const bundleStartSpanJSON = spanToJSON(bundleStartSpan!);
420+
421+
expect(appStartRootSpan).toBeDefined();
422+
expect(bundleStartSpan).toBeDefined();
423+
expect(appStartRootSpanJSON).toEqual(
424+
expect.objectContaining(<SpanJSON>{
425+
description: 'Cold App Start',
426+
span_id: expect.any(String),
427+
op: APP_START_COLD_OP,
428+
}),
429+
);
430+
expect(bundleStartSpanJSON).toEqual(
431+
expect.objectContaining(<SpanJSON>{
432+
description: 'JS Bundle Execution Before React Root',
433+
start_timestamp: expect.closeTo((timeOriginMilliseconds - 50) / 1000),
434+
timestamp: (timeOriginMilliseconds - 10) / 1000,
435+
parent_span_id: spanToJSON(appStartRootSpan!).span_id, // parent is the root app start span
436+
op: spanToJSON(appStartRootSpan!).op, // op is the same as the root app start span
437+
}),
438+
);
439+
});
440+
});
441+
334442
it('adds native spans as a child of the main app start span', async () => {
335443
const integration = new ReactNativeTracing();
336444

@@ -991,3 +1099,20 @@ function mockAppStartResponse({
9911099
function setup(integration: ReactNativeTracing) {
9921100
integration.setupOnce(addGlobalEventProcessor, getCurrentHub);
9931101
}
1102+
1103+
/**
1104+
* Mocks RN Bundle Start Module
1105+
* `var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now()`
1106+
*/
1107+
function mockReactNativeBundleExecutionStartTimestamp() {
1108+
RN_GLOBAL_OBJ.nativePerformanceNow = () => 100; // monotonic clock like `performance.now()`
1109+
RN_GLOBAL_OBJ.__BUNDLE_START_TIME__ = 50; // 50ms after time origin
1110+
}
1111+
1112+
/**
1113+
* Removes mock added by mockReactNativeBundleExecutionStartTimestamp
1114+
*/
1115+
function clearReactNativeBundleExecutionStartTimestamp() {
1116+
delete RN_GLOBAL_OBJ.nativePerformanceNow;
1117+
delete RN_GLOBAL_OBJ.__BUNDLE_START_TIME__;
1118+
}

0 commit comments

Comments
 (0)