diff --git a/plugins/node/opentelemetry-instrumentation-express/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-express/src/instrumentation.ts index bb29f52cbf..d8b7369142 100644 --- a/plugins/node/opentelemetry-instrumentation-express/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-express/src/instrumentation.ts @@ -197,7 +197,9 @@ export class ExpressInstrumentation extends InstrumentationBase< storeLayerPath(req, layerPath); const route = (req[_LAYERS_STORE_PROPERTY] as string[]) .filter(path => path !== '/' && path !== '/*') - .join(''); + .join('') + // remove duplicate slashes to normalize route + .replace(/\/{2,}/g, '/'); const attributes: Attributes = { [SemanticAttributes.HTTP_ROUTE]: route.length > 0 ? route : '/', diff --git a/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts b/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts index a442ddbaa0..7866333125 100644 --- a/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts +++ b/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts @@ -455,6 +455,39 @@ describe('ExpressInstrumentation', () => { } ); }); + + it('should ignore double slashes in routes', async () => { + const rootSpan = tracer.startSpan('rootSpan'); + let rpcMetadata: RPCMetadata | undefined; + const httpServer = await serverWithMiddleware(tracer, rootSpan, app => { + app.use(express.json()); + app.use((req, res, next) => { + rpcMetadata = getRPCMetadata(context.active()); + next(); + }); + }); + server = httpServer.server; + port = httpServer.port; + assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); + await context.with( + trace.setSpan(context.active(), rootSpan), + async () => { + const response = await httpRequest.get( + `http://localhost:${port}/double-slashes/foo` + ); + assert.strictEqual(response, 'foo'); + rootSpan.end(); + const requestHandlerSpan = memoryExporter + .getFinishedSpans() + .find(span => span.name.includes('request handler')); + assert.strictEqual( + requestHandlerSpan?.attributes[SemanticAttributes.HTTP_ROUTE], + '/double-slashes/:id' + ); + assert.strictEqual(rpcMetadata?.route, '/double-slashes/:id'); + } + ); + }); }); describe('Disabling plugin', () => { diff --git a/plugins/node/opentelemetry-instrumentation-express/test/utils.ts b/plugins/node/opentelemetry-instrumentation-express/test/utils.ts index 92a3313e62..f557d48fcd 100644 --- a/plugins/node/opentelemetry-instrumentation-express/test/utils.ts +++ b/plugins/node/opentelemetry-instrumentation-express/test/utils.ts @@ -66,6 +66,7 @@ export async function serverWithMiddleware( const router = express.Router(); app.use('/toto', router); + app.use('/double-slashes/', router); router.get('/:id', (req, res) => { setImmediate(() => { res.status(200).end(req.params.id);