Skip to content

Track JS bundle parse time as a Sentry span#39

Open
elirangoshen wants to merge 8 commits intomainfrom
eliran/sentry-js-parse-time
Open

Track JS bundle parse time as a Sentry span#39
elirangoshen wants to merge 8 commits intomainfrom
eliran/sentry-js-parse-time

Conversation

@elirangoshen
Copy link

@elirangoshen elirangoshen commented Feb 24, 2026

Explanation of Change

Adds a Sentry span to track JS bundle parse time on Android HybridApp.

Start: getBundleStartTimestampMs() from Sentry's tracing utils. This is a Sentry-internal function that already knows how to read the RN native global set before bundle execution — including handling the two
possible formats (epoch ms vs process-relative ms) across different RN versions. No declaration needed on our side.

End: Date.now() captured at module load time.

The result is a SPAN_JS_PARSE_TIME span with accurate start and approximate end, with no native module dependency — only Sentry and standard RN globals.

Fixed Issues

$Expensify#82975

Tests

  • Verify that no errors appear in the JS console

Offline tests

N/A — telemetry only, no functional change.

QA Steps

  • Verify that no errors appear in the JS console

  • Install the HybridApp build

    • Force-stop the app and cold-launch it
    • Validate app is launched as expected

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Summary by CodeRabbit

  • Chores
    • Enhanced telemetry instrumentation with new span tracking for performance metrics.
    • Improved timing precision for span measurements to capture more granular performance data.

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

Adds telemetry instrumentation for measuring JavaScript parse time and reporting tab navigation events. New span name constants are defined, the endSpan function is extended to support custom end times, and a PerformanceObserver monitors bundle parsing events to capture parse duration metrics.

Changes

Cohort / File(s) Summary
Telemetry Constants
src/CONST/index.ts
Adds four new telemetry span name constants: SPAN_JS_PARSE_TIME, SPAN_NAVIGATE_TO_REPORTS_TAB, SPAN_NAVIGATE_TO_REPORTS_TAB_RENDER, and SPAN_ON_LAYOUT_SKELETON_REPORTS.
Telemetry Utility Enhancement
src/libs/telemetry/activeSpans.ts
Extends endSpan function signature to accept optional endTime parameter of type SpanTimeInput, forwarding it to the underlying span.end() call.
JS Parse Time Measurement
src/setup/telemetry/index.ts
Implements PerformanceObserver to detect runJsBundleStart and runJsBundleEnd events, capturing JavaScript parse duration and creating telemetry spans with measured timing.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Hops with glee through parse-time spans,
Measuring bundles in swift JavaScript clans,
Start and end times now dance in the light,
Telemetry whispers make performance right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Track JS bundle parse time as a Sentry span' directly and accurately summarizes the main change across all modified files: adding telemetry constants, extending the endSpan function signature, and implementing JS parse time measurement via PerformanceObserver.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch eliran/sentry-js-parse-time

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@elirangoshen elirangoshen changed the title Track JS bundle parse time as a Sentry span on Android HybridApp Track JS bundle parse time as a Sentry span Feb 25, 2026
@elirangoshen elirangoshen marked this pull request as ready for review February 26, 2026 11:49
@elirangoshen elirangoshen changed the title Track JS bundle parse time as a Sentry span [Do not merge] Track JS bundle parse time as a Sentry span Feb 26, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/setup/telemetry/index.ts (1)

44-64: Consider defensive handling for entry ordering.

The implementation correctly tracks JS bundle parse time using the PerformanceObserver API. However, there's a subtle edge case: if runJsBundleEnd appears before runJsBundleStart in the entries array (e.g., if entries aren't guaranteed to be chronologically ordered), the observer would disconnect without recording the span.

While this is unlikely in practice since entries should arrive in order, you could add robustness by deferring the disconnect until both events are processed:

♻️ Optional defensive fix
     let jsParseStartMs: number | undefined;
+    let jsParseEndMs: 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();
+            if (entry.name === 'runJsBundleEnd' && jsParseEndMs === undefined) {
+                jsParseEndMs = entry.startTime;
             }
         }
+        if (jsParseStartMs !== undefined && jsParseEndMs !== undefined) {
+            endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, jsParseEndMs / 1000);
+            observer.disconnect();
+        }
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/setup/telemetry/index.ts` around lines 44 - 64, The observer loop can
disconnect before the start span is recorded if a runJsBundleEnd entry is seen
before runJsBundleStart; update the handler in the PerformanceObserver callback
to track both start and end events (e.g., use jsParseStartMs and a
jsParseEndTime variable or a boolean jsParseEndSeen), on runJsBundleStart set
jsParseStartMs and call startSpan, on runJsBundleEnd set the end time and only
call endSpan and observer.disconnect() once jsParseStartMs is defined (or when
both start and end flags/times are present), ensuring you still call
endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME, ...) with the end timestamp and then
observer.disconnect().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Mobile-Expensify`:
- Line 1: The PR contains a submodule pointer referencing the orphaned commit
ec6cc1176b78f7a7216c1bd4171b377d5da85e9f which is unreachable from the remote;
fix by either pushing the intended commit to the Mobile-Expensify remote so that
ec6cc11 becomes reachable and update the submodule pointer accordingly, or
change the submodule pointer (the gitlink entry for the Mobile-Expensify
submodule in the repository index/.gitmodules) to a commit that is present on a
tracked remote branch or tag; ensure the updated pointer references an auditable
commit on the remote before re-running the CI and merging.

---

Nitpick comments:
In `@src/setup/telemetry/index.ts`:
- Around line 44-64: The observer loop can disconnect before the start span is
recorded if a runJsBundleEnd entry is seen before runJsBundleStart; update the
handler in the PerformanceObserver callback to track both start and end events
(e.g., use jsParseStartMs and a jsParseEndTime variable or a boolean
jsParseEndSeen), on runJsBundleStart set jsParseStartMs and call startSpan, on
runJsBundleEnd set the end time and only call endSpan and observer.disconnect()
once jsParseStartMs is defined (or when both start and end flags/times are
present), ensuring you still call endSpan(CONST.TELEMETRY.SPAN_JS_PARSE_TIME,
...) with the end timestamp and then observer.disconnect().

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ca0810 and a11e4c9.

📒 Files selected for processing (4)
  • Mobile-Expensify
  • src/CONST/index.ts
  • src/libs/telemetry/activeSpans.ts
  • src/setup/telemetry/index.ts

elirangoshen and others added 5 commits February 26, 2026 13:00
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 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@elirangoshen elirangoshen force-pushed the eliran/sentry-js-parse-time branch from 543ade2 to 9726052 Compare February 26, 2026 12:00
# Conflicts:
#	src/CONST/index.ts
@elirangoshen elirangoshen changed the title [Do not merge] Track JS bundle parse time as a Sentry span Track JS bundle parse time as a Sentry span Feb 27, 2026
@elirangoshen
Copy link
Author

I removed the dependancy of react-native-performance so its ready for review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant