feat(core): Capture dynamic route params as span attributes#5750
Merged
feat(core): Capture dynamic route params as span attributes#5750
Conversation
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>
Contributor
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
Plus 3 more 🤖 This preview updates automatically when you update the PR. |
Contributor
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
antonis
commented
Mar 3, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
Author
|
@sentry review |
Contributor
Author
|
@claude review |
- 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>
f1e06fc to
35badfe
Compare
There was a problem hiding this comment.
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
sendDefaultPiigate- Dynamic route param span attributes are now only added when
sendDefaultPiiis explicitly enabled, and tests were updated to cover both gated and enabled behavior.
- Dynamic route param span attributes are now only added when
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,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>
alwx
approved these changes
Mar 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

📢 Type of change
📜 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 captureroute.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 duringsetAttributeson 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')💡 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?
extractDynamicRouteParams()covering: single dynamic segments, catch-all segments, multiple segments, non-dynamic param filtering, missing params, and type coercion[id]routes,[...slug]routes, and static routes with non-dynamic paramsyarn test,yarn build,yarn lint)📝 Checklist
sendDefaultPIIis enabled🔮 Next steps