@@ -13,7 +13,7 @@ import {
1313import { nativeFramesIntegration , reactNativeTracingIntegration } from '../../src/js' ;
1414import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION } from '../../src/js/tracing/origin' ;
1515import type { NavigationRoute } from '../../src/js/tracing/reactnavigation' ;
16- import { reactNavigationIntegration } from '../../src/js/tracing/reactnavigation' ;
16+ import { extractDynamicRouteParams , reactNavigationIntegration } from '../../src/js/tracing/reactnavigation' ;
1717import {
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+
62110describe ( '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 ) ;
0 commit comments