Skip to content

feat(core): Capture dynamic route params as span attributes#5750

Merged
antonis merged 8 commits intomainfrom
antonis/expo-dynamic-route-params
Mar 3, 2026
Merged

feat(core): Capture dynamic route params as span attributes#5750
antonis merged 8 commits intomainfrom
antonis/expo-dynamic-route-params

Conversation

@antonis
Copy link
Contributor

@antonis antonis commented Mar 3, 2026

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

Extracts dynamic route parameters ([id], [...slug]) from Expo Router style route names and includes them as span attributes (route.params.*).

Only structural params matching dynamic segments in the route name are captured, filtering out non-structural params that may contain PII. For example, a route named profile/[id] with params { id: '123', utm_source: 'email' } will only capture route.params.id = '123'.

How it works

A new extractDynamicRouteParams() function parses the route name for [key] and [...key] patterns, then returns only those matching params from the route's params object. This is called during setAttributes on the navigation span.

  • [id] segments: value is stringified (route.params.id = '123')
  • [...slug] catch-all segments: array values are joined with / (route.params.slug = 'tech/react-native')
  • Non-dynamic params (query params, etc.) are excluded

💡 Motivation and Context

Closes #5422

Dynamic route params are structural parts of the URL pattern (like path variables in a REST API). Capturing them as span tags allows users to filter and search navigation spans by specific route param values in Sentry's UI, which is useful for debugging and performance analysis.

💚 How did you test it?

  • Added 8 unit tests for extractDynamicRouteParams() covering: single dynamic segments, catch-all segments, multiple segments, non-dynamic param filtering, missing params, and type coercion
  • Added 3 integration tests verifying span attributes are correctly set for [id] routes, [...slug] routes, and static routes with non-dynamic params
  • All existing tests continue to pass (yarn test, yarn build, yarn lint)

📝 Checklist

  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • All tests passing
  • No breaking changes

🔮 Next steps

Extract dynamic route parameters ([id], [...slug]) from Expo Router
style route names and include them as span attributes (route.params.*).
Only structural params matching dynamic segments in the route name are
captured, filtering out non-structural params that may contain PII.

Closes #5422

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • feat(core): Capture dynamic route params as span attributes by antonis in #5750
  • chore(deps): bump brace-expansion from 1.1.11 to 1.1.12 by dependabot in #5751
  • chore(deps): bump minimatch to fix ReDoS vulnerabilities and tmp to ^0.2.4 by antonis in #5749
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.21.7 to 2.23.1 by dependabot in #5738
  • chore(deps): update Wizard to v6.12.0 by github-actions in #5747
  • chore(deps): update JavaScript SDK to v10.41.0 by github-actions in #5744
  • chore(deps): bump tar to ^7.5.8 by antonis in #5703
  • chore(deps): bump js-yaml to fix prototype pollution in merge by antonis in #5709
  • chore(deps): bump ajv to fix ReDoS in $data option by antonis in #5710
  • chore(deps): update CLI to v3.2.3 by github-actions in #5743
  • Fixes the issue with unit mismatch in adjustTransactionDuration by alwx in #5740
  • Handle inactive state for spans by alwx in #5742
  • chore(deps): bump actions/github-script from 7 to 8 by dependabot in #5737
  • chore(deps): bump actions/upload-artifact from 6 to 7 by dependabot in #5739
  • chore(deps): bump futureware-tech/simulator-action from 4 to 5 by dependabot in #5735
  • chore(deps): bump actions/download-artifact from 7 to 8 by dependabot in #5736
  • chore(deps): bump path-to-regexp to 0.1.12 by antonis in #5706
  • fix(ios): resolve relative SOURCEMAP_FILE against project root in Xcode build script by antonis in #5730
  • test(metro): Add type tests for SentryExpoConfigOptions.getDefaultConfig by antonis in #5733
  • chore(deps): bump axios to ^1.13.5 by antonis in #5708
  • chore(deps): bump on-headers to ^1.1.0 by antonis in #5704
  • chore(deps): bump dottie from 2.0.6 to 2.0.7 by dependabot in #5731
  • Cirrus Labs runners for other important workflows (where it makes sense to do so) + Ubuntu update (22.04 -> 24.04) by alwx in #5696
  • chore(deps): bump diff to ^5.2.2 by antonis in #5705

Plus 3 more


🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

Fails
🚫 Pull request is not ready for merge, please add the "ready-to-merge" label to the pull request
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 83ea3e5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
antonis and others added 2 commits March 3, 2026 11:27
@antonis
Copy link
Contributor Author

antonis commented Mar 3, 2026

@sentry review

@antonis
Copy link
Contributor Author

antonis commented Mar 3, 2026

@claude review

antonis and others added 2 commits March 3, 2026 12:03
- Guard against null param values with String(value ?? '')
- Add JSDoc note on PII scope and why previous route params are omitted
- Use it() consistently in describe blocks (was test())
- Add comment explaining the navigateToNewScreen step in test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@antonis antonis force-pushed the antonis/expo-dynamic-route-params branch from f1e06fc to 35badfe Compare March 3, 2026 12:56
@antonis antonis marked this pull request as ready for review March 3, 2026 12:59
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Route param values sent without sendDefaultPii gate
    • Dynamic route param span attributes are now only added when sendDefaultPii is explicitly enabled, and tests were updated to cover both gated and enabled behavior.

Create PR

Or push these changes by commenting:

@cursor push ff1303bcee
Preview (ff1303bcee)
diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts
--- a/packages/core/src/js/tracing/reactnavigation.ts
+++ b/packages/core/src/js/tracing/reactnavigation.ts
@@ -197,6 +197,7 @@
   let initialStateHandled: boolean = false;
   let stateChangeTimeout: ReturnType<typeof setTimeout> | undefined;
   let recentRouteKeys: string[] = [];
+  let sendDefaultPii: boolean = false;
 
   if (enableTimeToInitialDisplay) {
     NATIVE.initNativeReactNavigationNewFrameTracking().catch((reason: unknown) => {
@@ -208,6 +209,7 @@
    * Set the initial state and start initial navigation span for the current screen.
    */
   const afterAllSetup = (client: Client): void => {
+    sendDefaultPii = client.getOptions().sendDefaultPii === true;
     tracing = getReactNativeTracingIntegration(client);
     if (tracing) {
       idleSpanOptions = {
@@ -461,7 +463,7 @@
     latestNavigationSpan.setAttributes({
       'route.name': routeName,
       'route.key': route.key,
-      ...extractDynamicRouteParams(routeName, route.params),
+      ...(sendDefaultPii ? extractDynamicRouteParams(routeName, route.params) : {}),
       'route.has_been_seen': routeHasBeenSeen,
       'previous_route.name': previousRoute?.name,
       'previous_route.key': previousRoute?.key,

diff --git a/packages/core/test/tracing/reactnavigation.test.ts b/packages/core/test/tracing/reactnavigation.test.ts
--- a/packages/core/test/tracing/reactnavigation.test.ts
+++ b/packages/core/test/tracing/reactnavigation.test.ts
@@ -1054,7 +1054,7 @@
 
   describe('dynamic route params', () => {
     it('includes dynamic route params from [id] route', async () => {
-      setupTestClient();
+      setupTestClient({ sendDefaultPii: true });
       jest.runOnlyPendingTimers(); // Flush the init transaction
 
       // Navigate to a static screen first so previous_route.name is set to a known value
@@ -1085,7 +1085,7 @@
     });
 
     it('includes dynamic route params from [...slug] catch-all route joined with /', async () => {
-      setupTestClient();
+      setupTestClient({ sendDefaultPii: true });
       jest.runOnlyPendingTimers(); // Flush the init transaction
 
       mockNavigation.navigateToCatchAllRoute();
@@ -1110,6 +1110,22 @@
       );
     });
 
+    it('does not include dynamic route params when sendDefaultPii is disabled', async () => {
+      setupTestClient();
+      jest.runOnlyPendingTimers(); // Flush the init transaction
+
+      mockNavigation.navigateToDynamicRoute();
+      jest.runOnlyPendingTimers();
+
+      await client.flush();
+
+      const actualEvent = client.event;
+      const traceData = actualEvent?.contexts?.trace?.data as Record<string, unknown>;
+
+      expect(traceData[SEMANTIC_ATTRIBUTE_ROUTE_NAME]).toBe('profile/[id]');
+      expect(traceData['route.params.id']).toBeUndefined();
+    });
+
     it('does not include non-dynamic params from static routes', async () => {
       setupTestClient();
       jest.runOnlyPendingTimers(); // Flush the init transaction
@@ -1131,6 +1147,7 @@
     setupOptions: {
       beforeSpanStart?: (options: StartSpanOptions) => StartSpanOptions;
       useDispatchedActionData?: boolean;
+      sendDefaultPii?: boolean;
     } = {},
   ) {
     const rNavigation = reactNavigationIntegration({
@@ -1146,6 +1163,7 @@
     const options = getDefaultTestClientOptions({
       enableNativeFramesTracking: false,
       enableStallTracking: false,
+      sendDefaultPii: setupOptions.sendDefaultPii,
       tracesSampleRate: 1.0,
       integrations: [rNavigation, rnTracing],
       enableAppStartTracking: false,
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Route param values may be user-identifiable (user IDs, slugs, etc.).
Gate their capture behind the sendDefaultPii SDK option, consistent
with how other potentially-PII data is handled in the SDK.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@antonis antonis merged commit 870ee06 into main Mar 3, 2026
39 of 44 checks passed
@antonis antonis deleted the antonis/expo-dynamic-route-params branch March 3, 2026 17:07
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.

[ExpoRouter] Dynamic route parameter tracking

2 participants