豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 8ee8dce

Browse files
Merge branch 'main' into lz/expo-cons
2 parents 6dcc0f4 + a6ce440 commit 8ee8dce

File tree

10 files changed

+303
-94
lines changed

10 files changed

+303
-94
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
### Features
1212

1313
- Add expo constants on event context ([#5748](https://github.com/getsentry/sentry-react-native/pull/5748))
14+
- Capture dynamic route params as span attributes for Expo Router navigations ([#5750](https://github.com/getsentry/sentry-react-native/pull/5750))
1415

1516
### Fixes
1617

@@ -20,9 +21,9 @@
2021

2122
### Dependencies
2223

23-
- Bump JavaScript SDK from v10.39.0 to v10.41.0 ([#5715](https://github.com/getsentry/sentry-react-native/pull/5715), [#5744](https://github.com/getsentry/sentry-react-native/pull/5744))
24-
- [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#10410)
25-
- [diff](https://github.com/getsentry/sentry-javascript/compare/10.39.0...10.41.0)
24+
- Bump JavaScript SDK from v10.39.0 to v10.42.0 ([#5715](https://github.com/getsentry/sentry-react-native/pull/5715), [#5744](https://github.com/getsentry/sentry-react-native/pull/5744), [#5753](https://github.com/getsentry/sentry-react-native/pull/5753))
25+
- [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#10420)
26+
- [diff](https://github.com/getsentry/sentry-javascript/compare/10.39.0...10.42.0)
2627
- Bump Bundler Plugins from v4.9.1 to v5.1.1 ([#5700](https://github.com/getsentry/sentry-react-native/pull/5700))
2728
- [changelog](https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/CHANGELOG.md#511)
2829
- [diff](https://github.com/getsentry/sentry-javascript-bundler-plugins/compare/4.9.1...5.1.1)

dev-packages/e2e-tests/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"devDependencies": {
1414
"@babel/preset-env": "^7.25.3",
1515
"@babel/preset-typescript": "^7.18.6",
16-
"@sentry/core": "10.41.0",
16+
"@sentry/core": "10.42.0",
1717
"@sentry/react-native": "8.2.0",
1818
"@types/node": "^20.9.3",
1919
"@types/react": "^18.2.64",

packages/core/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,20 @@
6969
},
7070
"dependencies": {
7171
"@sentry/babel-plugin-component-annotate": "5.1.1",
72-
"@sentry/browser": "10.41.0",
72+
"@sentry/browser": "10.42.0",
7373
"@sentry/cli": "3.2.3",
74-
"@sentry/core": "10.41.0",
75-
"@sentry/react": "10.41.0",
76-
"@sentry/types": "10.41.0"
74+
"@sentry/core": "10.42.0",
75+
"@sentry/react": "10.42.0",
76+
"@sentry/types": "10.42.0"
7777
},
7878
"devDependencies": {
7979
"@babel/core": "^7.26.7",
8080
"@expo/metro-config": "~0.20.0",
8181
"@mswjs/interceptors": "^0.25.15",
8282
"@react-native/babel-preset": "0.80.0",
83-
"@sentry-internal/eslint-config-sdk": "10.41.0",
84-
"@sentry-internal/eslint-plugin-sdk": "10.41.0",
85-
"@sentry-internal/typescript": "10.41.0",
83+
"@sentry-internal/eslint-config-sdk": "10.42.0",
84+
"@sentry-internal/eslint-plugin-sdk": "10.42.0",
85+
"@sentry-internal/typescript": "10.42.0",
8686
"@sentry/wizard": "6.12.0",
8787
"@testing-library/react-native": "^13.2.2",
8888
"@types/jest": "^29.5.13",

packages/core/src/js/tracing/reactnavigation.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,52 @@ export const INTEGRATION_NAME = 'ReactNavigation';
3434

3535
const NAVIGATION_HISTORY_MAX_SIZE = 200;
3636

37+
/**
38+
* Extracts dynamic route parameters from a route name and its params.
39+
* Matches Expo Router style dynamic segments like `[id]` and `[...slug]`.
40+
*
41+
* Only params whose keys appear as dynamic segments in the route name are returned,
42+
* filtering out non-structural params (query params, etc.) that may contain PII.
43+
*
44+
* Note: dynamic segment values (e.g. the `123` in `profile/[id]`) may be user-identifiable.
45+
* This function only extracts params — callers are responsible for checking `sendDefaultPii`
46+
* before including the result in span attributes.
47+
*
48+
* Previous route params are intentionally not captured — only the current route's
49+
* structural params are needed for trace attribution.
50+
*/
51+
export function extractDynamicRouteParams(
52+
routeName: string,
53+
params?: Record<string, unknown>,
54+
): Record<string, string> | undefined {
55+
if (!params) {
56+
return undefined;
57+
}
58+
59+
const dynamicKeys = new Set<string>();
60+
const pattern = /\[(?:\.\.\.)?(\w+)\]/g;
61+
let match: RegExpExecArray | null;
62+
while ((match = pattern.exec(routeName)) !== null) {
63+
if (match[1]) {
64+
dynamicKeys.add(match[1]);
65+
}
66+
}
67+
68+
if (dynamicKeys.size === 0) {
69+
return undefined;
70+
}
71+
72+
const result: Record<string, string> = {};
73+
for (const key of dynamicKeys) {
74+
if (key in params) {
75+
const value = params[key];
76+
result[`route.params.${key}`] = Array.isArray(value) ? value.join('/') : String(value ?? '');
77+
}
78+
}
79+
80+
return Object.keys(result).length > 0 ? result : undefined;
81+
}
82+
3783
/**
3884
* Builds a full path from the navigation state by traversing nested navigators.
3985
* For example, with nested navigators: "Home/Settings/Profile"
@@ -412,16 +458,14 @@ export const reactNavigationIntegration = ({
412458
if (spanToJSON(latestNavigationSpan).description === DEFAULT_NAVIGATION_SPAN_NAME) {
413459
latestNavigationSpan.updateName(routeName);
414460
}
461+
const sendDefaultPii = getClient()?.getOptions()?.sendDefaultPii ?? false;
415462
latestNavigationSpan.setAttributes({
416463
'route.name': routeName,
417464
'route.key': route.key,
418-
// TODO: filter PII params instead of dropping them all
419-
// 'route.params': {},
465+
...(sendDefaultPii ? extractDynamicRouteParams(routeName, route.params) : undefined),
420466
'route.has_been_seen': routeHasBeenSeen,
421467
'previous_route.name': previousRoute?.name,
422468
'previous_route.key': previousRoute?.key,
423-
// TODO: filter PII params instead of dropping them all
424-
// 'previous_route.params': {},
425469
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
426470
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
427471
});

packages/core/test/tracing/reactnavigation.test.ts

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { nativeFramesIntegration, reactNativeTracingIntegration } from '../../src/js';
1414
import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION } from '../../src/js/tracing/origin';
1515
import type { NavigationRoute } from '../../src/js/tracing/reactnavigation';
16-
import { reactNavigationIntegration } from '../../src/js/tracing/reactnavigation';
16+
import { extractDynamicRouteParams, reactNavigationIntegration } from '../../src/js/tracing/reactnavigation';
1717
import {
1818
SEMANTIC_ATTRIBUTE_PREVIOUS_ROUTE_KEY,
1919
SEMANTIC_ATTRIBUTE_PREVIOUS_ROUTE_NAME,
@@ -59,6 +59,54 @@ class MockNavigationContainer {
5959
}
6060
}
6161

62+
describe('extractDynamicRouteParams', () => {
63+
it('returns undefined when params is undefined', () => {
64+
expect(extractDynamicRouteParams('profile/[id]', undefined)).toBeUndefined();
65+
});
66+
67+
it('returns undefined when route name has no dynamic segments', () => {
68+
expect(extractDynamicRouteParams('StaticScreen', { foo: 'bar' })).toBeUndefined();
69+
});
70+
71+
it('extracts single dynamic segment [id]', () => {
72+
expect(extractDynamicRouteParams('profile/[id]', { id: '123' })).toEqual({
73+
'route.params.id': '123',
74+
});
75+
});
76+
77+
it('extracts catch-all segment [...slug] and joins array values with /', () => {
78+
expect(extractDynamicRouteParams('posts/[...slug]', { slug: ['tech', 'react-native'] })).toEqual({
79+
'route.params.slug': 'tech/react-native',
80+
});
81+
});
82+
83+
it('extracts multiple dynamic segments', () => {
84+
expect(
85+
extractDynamicRouteParams('[org]/[project]/issues/[id]', { org: 'sentry', project: 'react-native', id: '42' }),
86+
).toEqual({
87+
'route.params.org': 'sentry',
88+
'route.params.project': 'react-native',
89+
'route.params.id': '42',
90+
});
91+
});
92+
93+
it('ignores params not matching dynamic segments', () => {
94+
expect(extractDynamicRouteParams('profile/[id]', { id: '123', utm_source: 'email' })).toEqual({
95+
'route.params.id': '123',
96+
});
97+
});
98+
99+
it('returns undefined when dynamic segment key is missing from params', () => {
100+
expect(extractDynamicRouteParams('profile/[id]', { name: 'test' })).toBeUndefined();
101+
});
102+
103+
it('converts non-string param values to strings', () => {
104+
expect(extractDynamicRouteParams('items/[count]', { count: 42 })).toEqual({
105+
'route.params.count': '42',
106+
});
107+
});
108+
});
109+
62110
describe('ReactNavigationInstrumentation', () => {
63111
let client: TestClient;
64112
let mockNavigation: ReturnType<typeof createMockNavigationAndAttachTo>;
@@ -1004,10 +1052,98 @@ describe('ReactNavigationInstrumentation', () => {
10041052
});
10051053
});
10061054

1055+
describe('dynamic route params', () => {
1056+
it('includes dynamic route params from [id] route when sendDefaultPii is true', async () => {
1057+
setupTestClient({ sendDefaultPii: true });
1058+
jest.runOnlyPendingTimers(); // Flush the init transaction
1059+
1060+
// Navigate to a static screen first so previous_route.name is set to a known value
1061+
mockNavigation.navigateToNewScreen();
1062+
jest.runOnlyPendingTimers(); // Flush the navigation transaction
1063+
1064+
mockNavigation.navigateToDynamicRoute();
1065+
jest.runOnlyPendingTimers();
1066+
1067+
await client.flush();
1068+
1069+
const actualEvent = client.event;
1070+
expect(actualEvent).toEqual(
1071+
expect.objectContaining({
1072+
type: 'transaction',
1073+
transaction: 'profile/[id]',
1074+
contexts: expect.objectContaining({
1075+
trace: expect.objectContaining({
1076+
data: expect.objectContaining({
1077+
[SEMANTIC_ATTRIBUTE_ROUTE_NAME]: 'profile/[id]',
1078+
'route.params.id': '123',
1079+
[SEMANTIC_ATTRIBUTE_PREVIOUS_ROUTE_NAME]: 'New Screen',
1080+
}),
1081+
}),
1082+
}),
1083+
}),
1084+
);
1085+
});
1086+
1087+
it('includes dynamic route params from [...slug] catch-all route joined with / when sendDefaultPii is true', async () => {
1088+
setupTestClient({ sendDefaultPii: true });
1089+
jest.runOnlyPendingTimers(); // Flush the init transaction
1090+
1091+
mockNavigation.navigateToCatchAllRoute();
1092+
jest.runOnlyPendingTimers();
1093+
1094+
await client.flush();
1095+
1096+
const actualEvent = client.event;
1097+
expect(actualEvent).toEqual(
1098+
expect.objectContaining({
1099+
type: 'transaction',
1100+
transaction: 'posts/[...slug]',
1101+
contexts: expect.objectContaining({
1102+
trace: expect.objectContaining({
1103+
data: expect.objectContaining({
1104+
[SEMANTIC_ATTRIBUTE_ROUTE_NAME]: 'posts/[...slug]',
1105+
'route.params.slug': 'tech/react-native',
1106+
}),
1107+
}),
1108+
}),
1109+
}),
1110+
);
1111+
});
1112+
1113+
it('does not include dynamic route params when sendDefaultPii is false', async () => {
1114+
setupTestClient({ sendDefaultPii: false });
1115+
jest.runOnlyPendingTimers(); // Flush the init transaction
1116+
1117+
mockNavigation.navigateToDynamicRoute();
1118+
jest.runOnlyPendingTimers();
1119+
1120+
await client.flush();
1121+
1122+
const traceData = client.event?.contexts?.trace?.data as Record<string, unknown>;
1123+
expect(traceData[SEMANTIC_ATTRIBUTE_ROUTE_NAME]).toBe('profile/[id]');
1124+
expect(traceData['route.params.id']).toBeUndefined();
1125+
});
1126+
1127+
it('does not include non-dynamic params from static routes', async () => {
1128+
setupTestClient({ sendDefaultPii: true });
1129+
jest.runOnlyPendingTimers(); // Flush the init transaction
1130+
1131+
mockNavigation.navigateToStaticRouteWithParams();
1132+
jest.runOnlyPendingTimers();
1133+
1134+
await client.flush();
1135+
1136+
const traceData = client.event?.contexts?.trace?.data as Record<string, unknown>;
1137+
expect(traceData[SEMANTIC_ATTRIBUTE_ROUTE_NAME]).toBe('StaticScreen');
1138+
expect(traceData['route.params.utm_source']).toBeUndefined();
1139+
});
1140+
});
1141+
10071142
function setupTestClient(
10081143
setupOptions: {
10091144
beforeSpanStart?: (options: StartSpanOptions) => StartSpanOptions;
10101145
useDispatchedActionData?: boolean;
1146+
sendDefaultPii?: boolean;
10111147
} = {},
10121148
) {
10131149
const rNavigation = reactNavigationIntegration({
@@ -1026,6 +1162,7 @@ describe('ReactNavigationInstrumentation', () => {
10261162
tracesSampleRate: 1.0,
10271163
integrations: [rNavigation, rnTracing],
10281164
enableAppStartTracking: false,
1165+
sendDefaultPii: setupOptions.sendDefaultPii,
10291166
});
10301167
client = new TestClient(options);
10311168
setCurrentClient(client);

packages/core/test/tracing/reactnavigationutils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,33 @@ export function createMockNavigationAndAttachTo(sut: ReturnType<typeof reactNavi
6868
// this object is not used by the instrumentation
6969
});
7070
},
71+
navigateToDynamicRoute: () => {
72+
mockedNavigationContained.listeners['__unsafe_action__'](navigationAction);
73+
mockedNavigationContained.currentRoute = {
74+
key: 'profile_123',
75+
name: 'profile/[id]',
76+
params: { id: '123' },
77+
};
78+
mockedNavigationContained.listeners['state']({});
79+
},
80+
navigateToCatchAllRoute: () => {
81+
mockedNavigationContained.listeners['__unsafe_action__'](navigationAction);
82+
mockedNavigationContained.currentRoute = {
83+
key: 'posts_slug',
84+
name: 'posts/[...slug]',
85+
params: { slug: ['tech', 'react-native'] },
86+
};
87+
mockedNavigationContained.listeners['state']({});
88+
},
89+
navigateToStaticRouteWithParams: () => {
90+
mockedNavigationContained.listeners['__unsafe_action__'](navigationAction);
91+
mockedNavigationContained.currentRoute = {
92+
key: 'static_screen',
93+
name: 'StaticScreen',
94+
params: { utm_source: 'email', referrer: 'homepage' },
95+
};
96+
mockedNavigationContained.listeners['state']({});
97+
},
7198
emitNavigationWithUndefinedRoute: () => {
7299
mockedNavigationContained.listeners['__unsafe_action__'](navigationAction);
73100
mockedNavigationContained.currentRoute = undefined as any;

samples/expo/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"eas-build-development-android": "eas build --profile development --platform android"
2323
},
2424
"dependencies": {
25-
"@sentry/core": "10.41.0",
25+
"@sentry/core": "10.42.0",
2626
"@sentry/react-native": "8.2.0",
2727
"@types/react": "~19.1.10",
2828
"expo": "^54.0.0",

samples/react-native-macos/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
"@react-navigation/bottom-tabs": "^6.5.12",
1717
"@react-navigation/native": "^6.1.9",
1818
"@react-navigation/stack": "^6.3.20",
19-
"@sentry/core": "10.41.0",
20-
"@sentry/react": "10.41.0",
19+
"@sentry/core": "10.42.0",
20+
"@sentry/react": "10.42.0",
2121
"@sentry/react-native": "8.2.0",
2222
"delay": "^6.0.0",
2323
"react": "18.2.0",

samples/react-native/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
"@react-navigation/native-stack": "^7.3.24",
4848
"@react-navigation/stack": "^7.4.5",
4949
"@reduxjs/toolkit": "^2.8.2",
50-
"@sentry/core": "10.41.0",
51-
"@sentry/react": "10.41.0",
50+
"@sentry/core": "10.42.0",
51+
"@sentry/react": "10.42.0",
5252
"@sentry/react-native": "8.2.0",
5353
"@shopify/flash-list": "^2.0.2",
5454
"delay": "^6.0.0",

0 commit comments

Comments
 (0)