From 477b221007f99e9e89a393d64231a9901dd1feb1 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Tue, 24 Feb 2026 10:02:58 +0100 Subject: [PATCH 1/7] Add Sentry span for JS bundle parse time Uses native `runJsBundleStart`/`runJsBundleEnd` performance marks to create a `ManualJsParseTime` span parented to `ManualAppStartup`, making pre-JS startup time visible in Sentry traces. Co-Authored-By: Claude Sonnet 4.6 --- src/CONST/index.ts | 1 + src/libs/telemetry/activeSpans.ts | 6 +++--- src/setup/telemetry/index.ts | 33 ++++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 72ede8cbdaae1..7a2cad434186c 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1777,6 +1777,7 @@ const CONST = { // Span names SPAN_OPEN_REPORT: 'ManualOpenReport', SPAN_APP_STARTUP: 'ManualAppStartup', + SPAN_JS_PARSE_TIME: 'ManualJsParseTime', SPAN_NAVIGATE_TO_REPORTS_TAB: 'ManualNavigateToReportsTab', SPAN_NAVIGATE_TO_REPORTS_TAB_RENDER: 'ManualNavigateToReportsTabRender', SPAN_ON_LAYOUT_SKELETON_REPORTS: 'ManualOnLayoutSkeletonReports', diff --git a/src/libs/telemetry/activeSpans.ts b/src/libs/telemetry/activeSpans.ts index 57694ff3b5c09..fe3e5cca7664d 100644 --- a/src/libs/telemetry/activeSpans.ts +++ b/src/libs/telemetry/activeSpans.ts @@ -1,4 +1,4 @@ -import type {StartSpanOptions} from '@sentry/core'; +import type {SpanTimeInput, StartSpanOptions} from '@sentry/core'; import * as Sentry from '@sentry/react-native'; import CONST from '@src/CONST'; @@ -25,7 +25,7 @@ function startSpan(spanId: string, options: StartSpanOptions, extraOptions: Star return span; } -function endSpan(spanId: string) { +function endSpan(spanId: string, endTime?: SpanTimeInput) { const span = activeSpans.get(spanId); if (!span) { @@ -33,7 +33,7 @@ function endSpan(spanId: string) { } span.setStatus({code: 1}); span.setAttribute(CONST.TELEMETRY.ATTRIBUTE_FINISHED_MANUALLY, true); - span.end(); + span.end(endTime); activeSpans.delete(spanId); } diff --git a/src/setup/telemetry/index.ts b/src/setup/telemetry/index.ts index deb9fac1f50fa..088673c1e9f5e 100644 --- a/src/setup/telemetry/index.ts +++ b/src/setup/telemetry/index.ts @@ -1,7 +1,8 @@ import * as Sentry from '@sentry/react-native'; import {Platform} from 'react-native'; +import performance, {PerformanceObserver} from 'react-native-performance'; import {isDevelopment} from '@libs/Environment/Environment'; -import {startSpan} from '@libs/telemetry/activeSpans'; +import {endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; import {breadcrumbsIntegration, browserProfilingIntegration, consoleIntegration, navigationIntegration, tracingIntegration} from '@libs/telemetry/integrations'; import processBeforeSendTransactions from '@libs/telemetry/middlewares'; import CONFIG from '@src/CONFIG'; @@ -39,4 +40,34 @@ export default function (): void { name: CONST.TELEMETRY.SPAN_APP_STARTUP, op: CONST.TELEMETRY.SPAN_APP_STARTUP, }); + + const runJsBundleStartEntries = performance.getEntriesByName('runJsBundleStart'); + if (runJsBundleStartEntries.length > 0) { + const jsParseStartSecs = (runJsBundleStartEntries.at(0)?.startTime ?? 0) / 1000; + + startSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, { + name: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, + op: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, + startTime: jsParseStartSecs, + parentSpan: getSpan(CONST.TELEMETRY.SPAN_APP_STARTUP), + }); + + const finishJsParseSpan = (endTimeSecs: number) => { + endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, endTimeSecs); + }; + + const runJsBundleEndEntries = performance.getEntriesByName('runJsBundleEnd'); + if (runJsBundleEndEntries.length > 0) { + finishJsParseSpan((runJsBundleEndEntries.at(0)?.startTime ?? 0) / 1000); + } else { + const observer = new PerformanceObserver((list) => { + const entries = list.getEntriesByName('runJsBundleEnd'); + if (entries.length > 0) { + finishJsParseSpan((entries.at(0)?.startTime ?? 0) / 1000); + observer.disconnect(); + } + }); + observer.observe({type: 'mark', buffered: true}); + } + } } From a59a35fed079c2d7c208041a6c49698c9bbd5116 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Tue, 24 Feb 2026 16:33:41 +0100 Subject: [PATCH 2/7] Track JS bundle parse time as a Sentry span on Android HybridApp Co-Authored-By: Claude Sonnet 4.6 --- src/setup/telemetry/index.ts | 55 ++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/src/setup/telemetry/index.ts b/src/setup/telemetry/index.ts index 088673c1e9f5e..fcc77259472d6 100644 --- a/src/setup/telemetry/index.ts +++ b/src/setup/telemetry/index.ts @@ -1,8 +1,8 @@ import * as Sentry from '@sentry/react-native'; import {Platform} from 'react-native'; -import performance, {PerformanceObserver} from 'react-native-performance'; +import {PerformanceObserver} from 'react-native-performance'; import {isDevelopment} from '@libs/Environment/Environment'; -import {endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; +import {endSpan, startSpan} from '@libs/telemetry/activeSpans'; import {breadcrumbsIntegration, browserProfilingIntegration, consoleIntegration, navigationIntegration, tracingIntegration} from '@libs/telemetry/integrations'; import processBeforeSendTransactions from '@libs/telemetry/middlewares'; import CONFIG from '@src/CONFIG'; @@ -41,33 +41,28 @@ export default function (): void { op: CONST.TELEMETRY.SPAN_APP_STARTUP, }); - const runJsBundleStartEntries = performance.getEntriesByName('runJsBundleStart'); - if (runJsBundleStartEntries.length > 0) { - const jsParseStartSecs = (runJsBundleStartEntries.at(0)?.startTime ?? 0) / 1000; - - startSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, { - name: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, - op: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, - startTime: jsParseStartSecs, - parentSpan: getSpan(CONST.TELEMETRY.SPAN_APP_STARTUP), - }); - - const finishJsParseSpan = (endTimeSecs: number) => { - endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, endTimeSecs); - }; - - const runJsBundleEndEntries = performance.getEntriesByName('runJsBundleEnd'); - if (runJsBundleEndEntries.length > 0) { - finishJsParseSpan((runJsBundleEndEntries.at(0)?.startTime ?? 0) / 1000); - } else { - const observer = new PerformanceObserver((list) => { - const entries = list.getEntriesByName('runJsBundleEnd'); - if (entries.length > 0) { - finishJsParseSpan((entries.at(0)?.startTime ?? 0) / 1000); - observer.disconnect(); - } - }); - observer.observe({type: 'mark', buffered: true}); + // getEntriesByName() queries the JS-side entry store, which is only populated after + // CONTENT_APPEARED fires (emitBufferedMarks). Since this code runs during JS bundle + // execution — before content appears — the sync check always returns []. + // Use a PerformanceObserver with type 'react-native-mark' (native marks arrive as + // PerformanceReactNativeMark, not PerformanceMark) and buffered: true so it fires + // when CONTENT_APPEARED flushes the native mark buffer to JS. + let jsParseStartMs: number | undefined; + const observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + if (entry.name === 'runJsBundleStart' && jsParseStartMs === undefined) { + jsParseStartMs = entry.startTime; + startSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, { + name: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, + op: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, + startTime: jsParseStartMs / 1000, + }); + } + if (entry.name === 'runJsBundleEnd' && jsParseStartMs !== undefined) { + endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, entry.startTime / 1000); + observer.disconnect(); + } } - } + }); + observer.observe({type: 'react-native-mark', buffered: true}); } From 5e9aecc65484f44441ba223db1b98b751c2a5942 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Thu, 26 Feb 2026 11:11:02 +0100 Subject: [PATCH 3/7] removing comments --- src/setup/telemetry/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/setup/telemetry/index.ts b/src/setup/telemetry/index.ts index fcc77259472d6..217d4a4927c22 100644 --- a/src/setup/telemetry/index.ts +++ b/src/setup/telemetry/index.ts @@ -41,12 +41,6 @@ export default function (): void { op: CONST.TELEMETRY.SPAN_APP_STARTUP, }); - // getEntriesByName() queries the JS-side entry store, which is only populated after - // CONTENT_APPEARED fires (emitBufferedMarks). Since this code runs during JS bundle - // execution — before content appears — the sync check always returns []. - // Use a PerformanceObserver with type 'react-native-mark' (native marks arrive as - // PerformanceReactNativeMark, not PerformanceMark) and buffered: true so it fires - // when CONTENT_APPEARED flushes the native mark buffer to JS. let jsParseStartMs: number | undefined; const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { From 8cc37062fbec7697c38c36b42af5d7d9ce496425 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Thu, 26 Feb 2026 12:24:50 +0100 Subject: [PATCH 4/7] save js parse time in sentry --- src/setup/telemetry/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/setup/telemetry/index.ts b/src/setup/telemetry/index.ts index 217d4a4927c22..d9613de324c51 100644 --- a/src/setup/telemetry/index.ts +++ b/src/setup/telemetry/index.ts @@ -43,7 +43,8 @@ export default function (): void { let jsParseStartMs: number | undefined; const observer = new PerformanceObserver((list) => { - for (const entry of list.getEntries()) { + const entries = list.getEntries(); + for (const entry of entries) { if (entry.name === 'runJsBundleStart' && jsParseStartMs === undefined) { jsParseStartMs = entry.startTime; startSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, { @@ -52,8 +53,11 @@ export default function (): void { startTime: jsParseStartMs / 1000, }); } - if (entry.name === 'runJsBundleEnd' && jsParseStartMs !== undefined) { - endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, entry.startTime / 1000); + if (entry.name === 'runJsBundleEnd') { + const durationMs = jsParseStartMs !== undefined ? Math.round(entry.startTime - jsParseStartMs) : 'n/a'; + if (jsParseStartMs !== undefined && durationMs !== 'n/a') { + endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, durationMs); + } observer.disconnect(); } } From 9726052bc218061fca75087b5ef9812a15e7a7c8 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Thu, 26 Feb 2026 12:45:12 +0100 Subject: [PATCH 5/7] fix span --- src/setup/telemetry/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/setup/telemetry/index.ts b/src/setup/telemetry/index.ts index d9613de324c51..d09ed507d3031 100644 --- a/src/setup/telemetry/index.ts +++ b/src/setup/telemetry/index.ts @@ -54,9 +54,8 @@ export default function (): void { }); } if (entry.name === 'runJsBundleEnd') { - const durationMs = jsParseStartMs !== undefined ? Math.round(entry.startTime - jsParseStartMs) : 'n/a'; - if (jsParseStartMs !== undefined && durationMs !== 'n/a') { - endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, durationMs); + if (jsParseStartMs !== undefined) { + endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, entry.startTime / 1000); } observer.disconnect(); } From a951c6c81013ec68c9d09f85ed856f319254c1b3 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Fri, 27 Feb 2026 12:28:30 +0100 Subject: [PATCH 6/7] remove performance module --- src/setup/telemetry/index.ts | 36 ++++++++++++++---------------------- src/types/global.d.ts | 4 ++++ 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/setup/telemetry/index.ts b/src/setup/telemetry/index.ts index d09ed507d3031..2bec99f6573e5 100644 --- a/src/setup/telemetry/index.ts +++ b/src/setup/telemetry/index.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/react-native'; +import {getBundleStartTimestampMs} from '@sentry/react-native/dist/js/tracing/utils'; import {Platform} from 'react-native'; -import {PerformanceObserver} from 'react-native-performance'; import {isDevelopment} from '@libs/Environment/Environment'; import {endSpan, startSpan} from '@libs/telemetry/activeSpans'; import {breadcrumbsIntegration, browserProfilingIntegration, consoleIntegration, navigationIntegration, tracingIntegration} from '@libs/telemetry/integrations'; @@ -10,6 +10,8 @@ import CONST from '@src/CONST'; import pkg from '../../../package.json'; import makeDebugTransport from './debugTransport'; +const bundleEndMs = Date.now(); + export default function (): void { // With Sentry enabled in dev mode, profiling on iOS and Android does not work // If you want to enable Sentry in dev, set ENABLE_SENTRY_ON_DEV=true in .env @@ -41,25 +43,15 @@ export default function (): void { op: CONST.TELEMETRY.SPAN_APP_STARTUP, }); - let jsParseStartMs: number | undefined; - const observer = new PerformanceObserver((list) => { - const entries = list.getEntries(); - for (const entry of entries) { - if (entry.name === 'runJsBundleStart' && jsParseStartMs === undefined) { - jsParseStartMs = entry.startTime; - startSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, { - name: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, - op: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, - startTime: jsParseStartMs / 1000, - }); - } - if (entry.name === 'runJsBundleEnd') { - if (jsParseStartMs !== undefined) { - endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, entry.startTime / 1000); - } - observer.disconnect(); - } - } - }); - observer.observe({type: 'react-native-mark', buffered: true}); + const bundleStartMs = getBundleStartTimestampMs(); + if (bundleStartMs) { + const durationMs = bundleEndMs - bundleStartMs; + console.debug(`[Telemetry] JS parse time: ${durationMs}ms`); + startSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, { + name: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, + op: CONST.TELEMETRY.SPAN_JS_PARSE_TIME, + startTime: bundleStartMs / 1000, + }); + endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, bundleEndMs / 1000); + } } diff --git a/src/types/global.d.ts b/src/types/global.d.ts index c5df07aa2ca81..26e0a4e7d9e60 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -45,3 +45,7 @@ interface ArrayBuffer { // Might be defined in browsers, in RN hermes it's not implemented yet transfer?: (length: number) => ArrayBuffer; } + +// Set by the React Native native layer (epoch ms) right before the JS bundle starts executing +// eslint-disable-next-line no-var, no-underscore-dangle, @typescript-eslint/naming-convention +declare var __BUNDLE_START_TIME__: number | undefined; From b0abe0de186186dbeac8b6b61863708344088da2 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Fri, 27 Feb 2026 13:14:45 +0100 Subject: [PATCH 7/7] remove global ts value --- src/types/global.d.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 26e0a4e7d9e60..c5df07aa2ca81 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -45,7 +45,3 @@ interface ArrayBuffer { // Might be defined in browsers, in RN hermes it's not implemented yet transfer?: (length: number) => ArrayBuffer; } - -// Set by the React Native native layer (epoch ms) right before the JS bundle starts executing -// eslint-disable-next-line no-var, no-underscore-dangle, @typescript-eslint/naming-convention -declare var __BUNDLE_START_TIME__: number | undefined;