@@ -6,6 +6,12 @@ const globalWithInjectedManifest = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
6
6
_sentryRouteManifest : RouteManifest | undefined ;
7
7
} ;
8
8
9
+ // Some performance caches
10
+ let cachedManifest : RouteManifest | null = null ;
11
+ let cachedManifestString : string | undefined = undefined ;
12
+ const compiledRegexCache : Map < string , RegExp > = new Map ( ) ;
13
+ const routeResultCache : Map < string , string | undefined > = new Map ( ) ;
14
+
9
15
/**
10
16
* Calculate the specificity score for a route path.
11
17
* Lower scores indicate more specific routes.
@@ -35,65 +41,130 @@ function getRouteSpecificity(routePath: string): number {
35
41
}
36
42
37
43
/**
38
- * Parameterize a route using the route manifest.
39
- *
40
- * @param route - The route to parameterize.
41
- * @returns The parameterized route or undefined if no parameterization is needed.
44
+ * Get compiled regex from cache or create and cache it.
42
45
*/
43
- export const maybeParameterizeRoute = ( route : string ) : string | undefined => {
46
+ function getCompiledRegex ( regexString : string ) : RegExp | null {
47
+ if ( compiledRegexCache . has ( regexString ) ) {
48
+ return compiledRegexCache . get ( regexString ) ?? null ;
49
+ }
50
+
51
+ try {
52
+ // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- regex patterns are from build-time route manifest, not user input
53
+ const regex = new RegExp ( regexString ) ;
54
+ compiledRegexCache . set ( regexString , regex ) ;
55
+ return regex ;
56
+ } catch ( error ) {
57
+ DEBUG_BUILD && logger . warn ( 'Could not compile regex' , { regexString, error } ) ;
58
+ // Cache the failure to avoid repeated attempts by storing undefined
59
+ return null ;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get and cache the route manifest from the global object.
65
+ * @returns The parsed route manifest or null if not available/invalid.
66
+ */
67
+ function getManifest ( ) : RouteManifest | null {
44
68
if (
45
- ! globalWithInjectedManifest . _sentryRouteManifest ||
69
+ ! globalWithInjectedManifest ? ._sentryRouteManifest ||
46
70
typeof globalWithInjectedManifest . _sentryRouteManifest !== 'string'
47
71
) {
48
- return undefined ;
72
+ return null ;
73
+ }
74
+
75
+ const currentManifestString = globalWithInjectedManifest . _sentryRouteManifest ;
76
+
77
+ // Return cached manifest if the string hasn't changed
78
+ if ( cachedManifest && cachedManifestString === currentManifestString ) {
79
+ return cachedManifest ;
49
80
}
50
81
82
+ // Clear caches when manifest changes
83
+ compiledRegexCache . clear ( ) ;
84
+ routeResultCache . clear ( ) ;
85
+
51
86
let manifest : RouteManifest = {
52
87
staticRoutes : [ ] ,
53
88
dynamicRoutes : [ ] ,
54
89
} ;
55
90
56
91
// Shallow check if the manifest is actually what we expect it to be
57
92
try {
58
- manifest = JSON . parse ( globalWithInjectedManifest . _sentryRouteManifest ) ;
93
+ manifest = JSON . parse ( currentManifestString ) ;
59
94
if ( ! Array . isArray ( manifest . staticRoutes ) || ! Array . isArray ( manifest . dynamicRoutes ) ) {
60
- return undefined ;
95
+ return null ;
61
96
}
97
+ // Cache the successfully parsed manifest
98
+ cachedManifest = manifest ;
99
+ cachedManifestString = currentManifestString ;
100
+ return manifest ;
62
101
} catch ( error ) {
63
- DEBUG_BUILD && logger . warn ( 'Could not extract route manifest' ) ;
64
102
// Something went wrong while parsing the manifest, so we'll fallback to no parameterization
65
- return undefined ;
66
- }
67
-
68
- // Static path: no parameterization needed
69
- if ( manifest . staticRoutes . some ( r => r . path === route ) ) {
70
- return undefined ;
103
+ DEBUG_BUILD && logger . warn ( 'Could not extract route manifest' ) ;
104
+ return null ;
71
105
}
106
+ }
72
107
108
+ /**
109
+ * Find matching routes from static and dynamic route collections.
110
+ * @param route - The route to match against.
111
+ * @param staticRoutes - Array of static route objects.
112
+ * @param dynamicRoutes - Array of dynamic route objects.
113
+ * @returns Array of matching route paths.
114
+ */
115
+ function findMatchingRoutes (
116
+ route : string ,
117
+ staticRoutes : RouteManifest [ 'staticRoutes' ] ,
118
+ dynamicRoutes : RouteManifest [ 'dynamicRoutes' ] ,
119
+ ) : string [ ] {
73
120
const matches : string [ ] = [ ] ;
74
121
122
+ // Static path: no parameterization needed, return empty array
123
+ if ( staticRoutes . some ( r => r . path === route ) ) {
124
+ return matches ;
125
+ }
126
+
75
127
// Dynamic path: find the route pattern that matches the concrete route
76
- for ( const dynamicRoute of manifest . dynamicRoutes ) {
128
+ for ( const dynamicRoute of dynamicRoutes ) {
77
129
if ( dynamicRoute . regex ) {
78
- try {
79
- // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- regex patterns are from build-time route manifest, not user input
80
- const regex = new RegExp ( dynamicRoute . regex ) ;
81
- if ( regex . test ( route ) ) {
82
- matches . push ( dynamicRoute . path ) ;
83
- }
84
- } catch ( error ) {
85
- // Just skip this route in case of invalid regex
86
- continue ;
130
+ const regex = getCompiledRegex ( dynamicRoute . regex ) ;
131
+ if ( regex ?. test ( route ) ) {
132
+ matches . push ( dynamicRoute . path ) ;
87
133
}
88
134
}
89
135
}
90
136
91
- if ( matches . length === 1 ) {
92
- return matches [ 0 ] ;
93
- } else if ( matches . length > 1 ) {
94
- // Only calculate specificity when we have multiple matches like [param] and [...params]
95
- return matches . sort ( ( a , b ) => getRouteSpecificity ( a ) - getRouteSpecificity ( b ) ) [ 0 ] ;
137
+ return matches ;
138
+ }
139
+
140
+ /**
141
+ * Parameterize a route using the route manifest.
142
+ *
143
+ * @param route - The route to parameterize.
144
+ * @returns The parameterized route or undefined if no parameterization is needed.
145
+ */
146
+ export const maybeParameterizeRoute = ( route : string ) : string | undefined => {
147
+ const manifest = getManifest ( ) ;
148
+ if ( ! manifest ) {
149
+ return undefined ;
150
+ }
151
+
152
+ // Check route result cache after manifest validation
153
+ if ( routeResultCache . has ( route ) ) {
154
+ return routeResultCache . get ( route ) ;
96
155
}
97
156
98
- return undefined ;
157
+ const { staticRoutes, dynamicRoutes } = manifest ;
158
+ if ( ! Array . isArray ( staticRoutes ) || ! Array . isArray ( dynamicRoutes ) ) {
159
+ return undefined ;
160
+ }
161
+
162
+ const matches = findMatchingRoutes ( route , staticRoutes , dynamicRoutes ) ;
163
+
164
+ // We can always do the `sort()` call, it will short-circuit when it has one array item
165
+ const result = matches . sort ( ( a , b ) => getRouteSpecificity ( a ) - getRouteSpecificity ( b ) ) [ 0 ] ;
166
+
167
+ routeResultCache . set ( route , result ) ;
168
+
169
+ return result ;
99
170
} ;
0 commit comments