Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .oxlintrc.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@
"**/integrations/tracing/knex/vendored/**/*.ts",
"**/integrations/tracing/mongo/vendored/**/*.ts",
"**/integrations/tracing/graphql/vendored/**/*.ts",
"**/integrations/tracing/koa/vendored/**/*.ts",
"**/integrations/tracing/mysql2/vendored/**/*.ts",
"**/integration/aws/vendored/**/*.ts",
"**/integrations/tracing/kafka/vendored/**/*.ts",
Expand Down
22 changes: 22 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-koa/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,30 @@ router1.post('/test-post', async ctx => {
ctx.body = { status: 'ok', body: ctx.request.body };
});

// RegExp route - exercises the @koa/router dispatch patch with a non-string layer path.
router1.get(/^\/test-regexp/, ctx => {
ctx.body = { matched: 'regexp' };
});

// Same middleware instance passed twice - the second occurrence must be skipped (kLayerPatched dedup).
const sharedRouteMiddleware = async (ctx, next) => {
await next();
};
router1.get('/test-dedup', sharedRouteMiddleware, sharedRouteMiddleware, ctx => {
ctx.body = { ok: true };
});

app1.use(router1.routes()).use(router1.allowedMethods());

// Nested router - the routed span's http.route is the composed parent + child path.
const nestedRouter = new Router();
nestedRouter.get('/details/:id', ctx => {
ctx.body = { id: ctx.params.id };
});
const outerRouter = new Router();
outerRouter.use('/:first', nestedRouter.routes());
app1.use(outerRouter.routes()).use(outerRouter.allowedMethods());

app1.listen(port1);

const app2 = new Koa();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('instruments RegExp router routes', async ({ baseURL }) => {
const transactionPromise = waitForTransaction('node-koa', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' && !!transactionEvent.transaction?.includes('test-regexp')
);
});

await fetch(`${baseURL}/test-regexp`);

const transactionEvent = await transactionPromise;

expect(transactionEvent.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
op: 'router.koa',
origin: 'auto.http.otel.koa',
data: expect.objectContaining({
'koa.type': 'router',
'sentry.op': 'router.koa',
'sentry.origin': 'auto.http.otel.koa',
'http.route': '/^\\/test-regexp/',
}),
}),
]),
);
});

test('instruments nested routers with the composed http.route', async ({ baseURL }) => {
const transactionPromise = waitForTransaction('node-koa', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /:first/details/:id'
);
});

await fetch(`${baseURL}/shop/details/1`);

const transactionEvent = await transactionPromise;

expect(transactionEvent.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
op: 'router.koa',
description: '/:first/details/:id',
data: expect.objectContaining({
'koa.type': 'router',
'http.route': '/:first/details/:id',
'sentry.op': 'router.koa',
'sentry.origin': 'auto.http.otel.koa',
}),
}),
]),
);
});

test('does not instrument the same middleware twice', async ({ baseURL }) => {
const transactionPromise = waitForTransaction('node-koa', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /test-dedup'
);
});

await fetch(`${baseURL}/test-dedup`);

const transactionEvent = await transactionPromise;

// The route stack is [sharedRouteMiddleware, sharedRouteMiddleware, handler]; the repeated
// middleware instance is skipped, leaving one span for it plus the handler span.
const dedupSpans = transactionEvent.spans?.filter(
span => span.op === 'router.koa' && span.description === '/test-dedup',
);
expect(dedupSpans).toHaveLength(2);
});

test('marks the layer span as errored when a handler throws', async ({ baseURL }) => {
const transactionPromise = waitForTransaction('node-koa', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /test-exception/:id'
);
});

await fetch(`${baseURL}/test-exception/123`);

const transactionEvent = await transactionPromise;

expect(transactionEvent.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
op: 'router.koa',
origin: 'auto.http.otel.koa',
status: 'internal_error',
}),
]),
);
});
44 changes: 2 additions & 42 deletions packages/node/src/integrations/tracing/koa/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
import type { KoaInstrumentationConfig, KoaLayerType } from './vendored/types';
import { KoaInstrumentation } from './vendored/instrumentation';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import type { IntegrationFn } from '@sentry/core';
import {
captureException,
debug,
defineIntegration,
getDefaultIsolationScope,
getIsolationScope,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
spanToJSON,
} from '@sentry/core';
import { addOriginToSpan, ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
import { DEBUG_BUILD } from '../../../debug-build';
import { captureException, defineIntegration } from '@sentry/core';
import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';

interface KoaOptions {
/**
Expand All @@ -29,36 +19,6 @@ export const instrumentKoa = generateInstrumentOnce(
(options: KoaOptions = {}) => {
return {
ignoreLayersType: options.ignoreLayersType as KoaLayerType[],
requestHook(span, info) {
addOriginToSpan(span, 'auto.http.otel.koa');

const attributes = spanToJSON(span).data;

// this is one of: middleware, router
const type = attributes['koa.type'];
if (type) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.koa`);
}

// Also update the name
const name = attributes['koa.name'];
if (typeof name === 'string') {
// Somehow, name is sometimes `''` for middleware spans
// See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220
span.updateName(name || '< unknown >');
}

if (getIsolationScope() === getDefaultIsolationScope()) {
DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName');
return;
}
const route = attributes[ATTR_HTTP_ROUTE];
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const method = info.context?.request?.method?.toUpperCase() || 'GET';
if (route) {
getIsolationScope().setTransactionName(`${method} ${route}`);
}
},
} satisfies KoaInstrumentationConfig;
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-koa
* - Upstream version: @opentelemetry/instrumentation-koa@0.66.0
*/
/* eslint-disable */

export enum AttributeNames {
KOA_TYPE = 'koa.type',
Expand Down
Loading
Loading