-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(tracing-internal): Fix express middleware path param parsing #8668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Lms24
merged 23 commits into
getsentry:develop
from
LubomirIgonda1:fix-express-path-param
Oct 16, 2023
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
93b1942
fix(tracing-internal): Fix express middleware path param parsing
LubomirIgonda1 b2a7825
Merge branch 'develop' into fix-express-path-param
LubomirIgonda1 2ee2167
fix(tracing-internal): Fix lint errors
LubomirIgonda1 ac325e9
fix(tracing-internal): Add test for middlelayer path param
LubomirIgonda1 176d70c
Merge branch 'develop' into fix-express-path-param
LubomirIgonda1 0b33605
Merge branch 'develop' into fix-express-path-param
LubomirIgonda1 366d563
Merge branch 'develop' into fix-express-path-param
LubomirIgonda1 731cec4
fix(tracing-internal): remove useless cors from test
LubomirIgonda1 c456133
fix(tracing-internal): fix req._reconstructedRoute param if express u…
LubomirIgonda1 a242cc7
fix(tracing-internal): fix eslint errors
LubomirIgonda1 602594b
Merge branch 'develop' into fix-express-path-param
LubomirIgonda1 a21e8bb
Merge branch 'develop' into fix-express-path-param
LubomirIgonda1 9964521
Merge branch 'develop' into fix-express-path-param
LubomirIgonda1 5736c28
fix(tracing-internal) fix duplicating route segment in _reconstructRo…
LubomirIgonda1 4df8c5b
Merge branch 'getsentry:develop' into fix-express-path-param
LubomirIgonda1 a746d54
Merge branch 'develop' into fix-express-path-param
LubomirIgonda1 81ef719
fix(tracing-internal): add unit test for preventDuplicateSegments fn
LubomirIgonda1 aa23c41
fix(tracing-internal): call preventDuplicateSegments only when it is …
LubomirIgonda1 a0fe20d
Merge branch 'getsentry:develop' into fix-express-path-param
LubomirIgonda1 16ff8fa
fix(tracing-internal): add regex indices polyfill to make nodejs comp…
LubomirIgonda1 bf813f6
fix merge conflict
LubomirIgonda1 df3faf6
remove regex d flag polyfill and aply fix only if node is 16+add new …
LubomirIgonda1 740ef63
fix(tracing-internal): Fix tests for node <=14
LubomirIgonda1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
35 changes: 35 additions & 0 deletions
35
packages/node-integration-tests/suites/express/multiple-routers/complex-router/server.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import * as Sentry from '@sentry/node'; | ||
import * as Tracing from '@sentry/tracing'; | ||
import express from 'express'; | ||
|
||
const app = express(); | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
release: '1.0', | ||
// eslint-disable-next-line deprecation/deprecation | ||
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })], | ||
tracesSampleRate: 1.0, | ||
}); | ||
|
||
app.use(Sentry.Handlers.requestHandler()); | ||
app.use(Sentry.Handlers.tracingHandler()); | ||
|
||
const APIv1 = express.Router(); | ||
|
||
APIv1.use( | ||
'/users/:userId', | ||
APIv1.get('/posts/:postId', (_req, res) => { | ||
Sentry.captureMessage('Custom Message'); | ||
return res.send('Success'); | ||
}), | ||
); | ||
|
||
const router = express.Router(); | ||
|
||
app.use('/api', router); | ||
app.use('/api/api/v1', APIv1.use('/sub-router', APIv1)); | ||
|
||
app.use(Sentry.Handlers.errorHandler()); | ||
|
||
export default app; |
27 changes: 27 additions & 0 deletions
27
packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { assertSentryEvent, TestEnv } from '../../../../utils/index'; | ||
|
||
test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route', async () => { | ||
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`); | ||
const event = await env.getEnvelopeRequest({ | ||
url: env.url.replace('test', 'api/api/v1/sub-router/users/123/posts/456'), | ||
envelopeType: 'transaction', | ||
}); | ||
// parse node.js major version | ||
const [major] = process.versions.node.split('.').map(Number); | ||
// Split test result base on major node version because regex d flag is support from node 16+ | ||
if (major >= 16) { | ||
assertSentryEvent(event[2] as any, { | ||
transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', | ||
transaction_info: { | ||
source: 'route', | ||
}, | ||
}); | ||
} else { | ||
assertSentryEvent(event[2] as any, { | ||
transaction: 'GET /api/api/v1/sub-router/users/123/posts/:postId', | ||
transaction_info: { | ||
source: 'route', | ||
}, | ||
}); | ||
} | ||
}); |
35 changes: 35 additions & 0 deletions
35
...de-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/server.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import * as Sentry from '@sentry/node'; | ||
import * as Tracing from '@sentry/tracing'; | ||
import express from 'express'; | ||
|
||
const app = express(); | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
release: '1.0', | ||
// eslint-disable-next-line deprecation/deprecation | ||
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })], | ||
tracesSampleRate: 1.0, | ||
}); | ||
|
||
app.use(Sentry.Handlers.requestHandler()); | ||
app.use(Sentry.Handlers.tracingHandler()); | ||
|
||
const APIv1 = express.Router(); | ||
|
||
APIv1.use( | ||
'/users/:userId', | ||
APIv1.get('/posts/:postId', (_req, res) => { | ||
Sentry.captureMessage('Custom Message'); | ||
return res.send('Success'); | ||
}), | ||
); | ||
|
||
const root = express.Router(); | ||
|
||
app.use('/api/v1', APIv1); | ||
app.use('/api', root); | ||
|
||
app.use(Sentry.Handlers.errorHandler()); | ||
|
||
export default app; |
28 changes: 28 additions & 0 deletions
28
...node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/test.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { assertSentryEvent, TestEnv } from '../../../../utils/index'; | ||
|
||
test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route', async () => { | ||
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`); | ||
const event = await env.getEnvelopeRequest({ | ||
url: env.url.replace('test', 'api/v1/users/123/posts/456'), | ||
envelopeType: 'transaction', | ||
}); | ||
|
||
// parse node.js major version | ||
const [major] = process.versions.node.split('.').map(Number); | ||
// Split test result base on major node version because regex d flag is support from node 16+ | ||
if (major >= 16) { | ||
assertSentryEvent(event[2] as any, { | ||
transaction: 'GET /api/v1/users/:userId/posts/:postId', | ||
transaction_info: { | ||
source: 'route', | ||
}, | ||
}); | ||
} else { | ||
assertSentryEvent(event[2] as any, { | ||
transaction: 'GET /api/v1/users/123/posts/:postId', | ||
transaction_info: { | ||
source: 'route', | ||
}, | ||
}); | ||
} | ||
}); |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,6 +64,8 @@ type Layer = { | |
handle_request: (req: PatchedRequest, res: ExpressResponse, next: () => void) => void; | ||
route?: { path: RouteType | RouteType[] }; | ||
path?: string; | ||
regexp?: RegExp; | ||
keys?: { name: string; offset: number; optional: boolean }[]; | ||
}; | ||
|
||
type RouteType = string | RegExp; | ||
|
@@ -318,7 +320,24 @@ function instrumentRouter(appOrRouter: ExpressRouter): void { | |
} | ||
|
||
// Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path | ||
const partialRoute = layerRoutePath || layer.path || ''; | ||
let partialRoute; | ||
|
||
if (layerRoutePath) { | ||
partialRoute = layerRoutePath; | ||
} else { | ||
/** | ||
* prevent duplicate segment in _reconstructedRoute param if router match multiple routes before final path | ||
* example: | ||
* original url: /api/v1/1234 | ||
* prevent: /api/api/v1/:userId | ||
* router structure | ||
* /api -> middleware | ||
* /api/v1 -> middleware | ||
* /1234 -> endpoint with param :userId | ||
* final _reconstructedRoute is /api/v1/:userId | ||
*/ | ||
partialRoute = preventDuplicateSegments(req.originalUrl, req._reconstructedRoute, layer.path) || ''; | ||
} | ||
|
||
// Normalize the partial route so that it doesn't contain leading or trailing slashes | ||
// and exclude empty or '*' wildcard routes. | ||
|
@@ -370,6 +389,79 @@ type LayerRoutePathInfo = { | |
numExtraSegments: number; | ||
}; | ||
|
||
/** | ||
* Recreate layer.route.path from layer.regexp and layer.keys. | ||
* Works until express.js used package [email protected] | ||
* or until layer.keys contain offset attribute | ||
* | ||
* @param layer the layer to extract the stringified route from | ||
* | ||
* @returns string in layer.route.path structure 'router/:pathParam' or undefined | ||
*/ | ||
export const extractOriginalRoute = ( | ||
path?: Layer['path'], | ||
regexp?: Layer['regexp'], | ||
keys?: Layer['keys'], | ||
): string | undefined => { | ||
if (!path || !regexp || !keys || Object.keys(keys).length === 0 || !keys[0]?.offset) { | ||
return undefined; | ||
} | ||
|
||
const orderedKeys = keys.sort((a, b) => a.offset - b.offset); | ||
|
||
// add d flag for getting indices from regexp result | ||
const pathRegex = new RegExp(regexp, `${regexp.flags}d`); | ||
/** | ||
* use custom type cause of TS error with missing indices in RegExpExecArray | ||
*/ | ||
const execResult = pathRegex.exec(path) as (RegExpExecArray & { indices: [number, number][] }) | null; | ||
|
||
if (!execResult || !execResult.indices) { | ||
return undefined; | ||
} | ||
/** | ||
* remove first match from regex cause contain whole layer.path | ||
*/ | ||
const [, ...paramIndices] = execResult.indices; | ||
|
||
if (paramIndices.length !== orderedKeys.length) { | ||
return undefined; | ||
} | ||
let resultPath = path; | ||
let indexShift = 0; | ||
|
||
/** | ||
* iterate param matches from regexp.exec | ||
*/ | ||
paramIndices.forEach(([startOffset, endOffset], index: number) => { | ||
/** | ||
* isolate part before param | ||
*/ | ||
const substr1 = resultPath.substring(0, startOffset - indexShift); | ||
/** | ||
* define paramName as replacement in format :pathParam | ||
*/ | ||
const replacement = `:${orderedKeys[index].name}`; | ||
|
||
/** | ||
* isolate part after param | ||
*/ | ||
const substr2 = resultPath.substring(endOffset - indexShift); | ||
|
||
/** | ||
* recreate original path but with param replacement | ||
*/ | ||
resultPath = substr1 + replacement + substr2; | ||
|
||
/** | ||
* calculate new index shift after resultPath was modified | ||
*/ | ||
indexShift = indexShift + (endOffset - startOffset - replacement.length); | ||
}); | ||
|
||
return resultPath; | ||
}; | ||
|
||
/** | ||
* Extracts and stringifies the layer's route which can either be a string with parameters (`users/:id`), | ||
* a RegEx (`/test/`) or an array of strings and regexes (`['/path1', /\/path[2-5]/, /path/:id]`). Additionally | ||
|
@@ -382,11 +474,24 @@ type LayerRoutePathInfo = { | |
* if the route was an array (defaults to 0). | ||
*/ | ||
function getLayerRoutePathInfo(layer: Layer): LayerRoutePathInfo { | ||
const lrp = layer.route?.path; | ||
let lrp = layer.route?.path; | ||
|
||
const isRegex = isRegExp(lrp); | ||
const isArray = Array.isArray(lrp); | ||
|
||
if (!lrp) { | ||
// parse node.js major version | ||
const [major] = process.versions.node.split('.').map(Number); | ||
|
||
// allow call extractOriginalRoute only if node version support Regex d flag, node 16+ | ||
if (major >= 16) { | ||
/** | ||
* If lrp does not exist try to recreate original layer path from route regexp | ||
*/ | ||
lrp = extractOriginalRoute(layer.path, layer.regexp, layer.keys); | ||
} | ||
} | ||
|
||
if (!lrp) { | ||
return { isRegex, isArray, numExtraSegments: 0 }; | ||
} | ||
|
@@ -424,3 +529,28 @@ function getLayerRoutePathString(isArray: boolean, lrp?: RouteType | RouteType[] | |
} | ||
return lrp && lrp.toString(); | ||
} | ||
|
||
/** | ||
* remove duplicate segment contain in layerPath against reconstructedRoute, | ||
* and return only unique segment that can be added into reconstructedRoute | ||
*/ | ||
export function preventDuplicateSegments( | ||
originalUrl?: string, | ||
reconstructedRoute?: string, | ||
layerPath?: string, | ||
): string | undefined { | ||
const originalUrlSplit = originalUrl?.split('/').filter(v => !!v); | ||
let tempCounter = 0; | ||
const currentOffset = reconstructedRoute?.split('/').filter(v => !!v).length || 0; | ||
const result = layerPath | ||
?.split('/') | ||
.filter(segment => { | ||
if (originalUrlSplit?.[currentOffset + tempCounter] === segment) { | ||
tempCounter += 1; | ||
return true; | ||
} | ||
return false; | ||
}) | ||
.join('/'); | ||
return result; | ||
} |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.