Skip to content
Open
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
2 changes: 2 additions & 0 deletions dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@growthbook/growthbook": "^1.6.5",
"@hapi/hapi": "^21.3.10",
"@hono/node-server": "^1.19.13",
"@koa/router": "^12.0.1",
"@langchain/anthropic": "^0.3.10",
"@langchain/core": "^0.3.80",
"@langchain/openai": "^0.5.0",
Expand Down Expand Up @@ -76,6 +77,7 @@
"ioredis-5": "npm:ioredis@^5.11.0",
"kafkajs": "2.2.4",
"knex": "^2.5.1",
"koa": "^2.15.2",
"lru-memoizer": "2.3.0",
"mongodb": "^3.7.3",
"mongodb-memory-server-global": "^11.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Router from '@koa/router';
import * as Sentry from '@sentry/node';
import { sendPortToRunner } from '@sentry-internal/node-integration-tests';
import Koa from 'koa';

const port = 5698;

const app = new Koa();

// Registered first so it wraps every downstream middleware/route in its try/catch.
Sentry.setupKoaErrorHandler(app);

// Plain middleware -> produces a `middleware.koa` span named after the function.
app.use(async function simpleMiddleware(ctx, next) {
await next();
});

const router = new Router();

router.get('/', ctx => {
ctx.body = 'Hello World!';
});

router.get('/test-param/:id', ctx => {
ctx.body = { id: ctx.params.id };
});

router.get('/error', () => {
throw new Error('Sentry Test Error');
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(port, () => {
sendPortToRunner(port);
});
103 changes: 103 additions & 0 deletions dev-packages/node-integration-tests/suites/tracing/koa/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { afterAll, describe, expect } from 'vitest';
import { isOrchestrionEnabled } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

describe('koa auto-instrumentation', () => {
afterAll(async () => {
cleanupChildProcesses();
});

// `createEsmAndCjsTests` auto-runs this suite with orchestrion on CI. The
// orchestrion path keeps span ops/attributes identical to the OTel path; only
// the origin differs to signal the injection mechanism, so we branch on
// `isOrchestrionEnabled()`.
const origin = isOrchestrionEnabled() ? 'auto.http.orchestrion.koa' : 'auto.http.otel.koa';

const EXPECTED_ERROR_EVENT = {
exception: {
values: [
{
type: 'Error',
value: 'Sentry Test Error',
},
],
},
};

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('should auto-instrument `koa` router and middleware layers.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /',
spans: expect.arrayContaining([
// Router layer span (from `@koa/router`), carrying the matched route.
expect.objectContaining({
description: '/',
op: 'router.koa',
origin,
data: expect.objectContaining({
'http.route': '/',
'koa.type': 'router',
'koa.name': '/',
'sentry.op': 'router.koa',
'sentry.origin': origin,
}),
}),
// Plain middleware span.
expect.objectContaining({
description: 'simpleMiddleware',
op: 'middleware.koa',
origin,
data: expect.objectContaining({
'koa.type': 'middleware',
'koa.name': 'simpleMiddleware',
'code.function.name': 'simpleMiddleware',
'sentry.op': 'middleware.koa',
'sentry.origin': origin,
}),
}),
]),
},
})
.start();
runner.makeRequest('get', '/');
await runner.completed();
});

test('should assign a parameterized transaction name.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /test-param/:id',
spans: expect.arrayContaining([
expect.objectContaining({
description: '/test-param/:id',
op: 'router.koa',
origin,
data: expect.objectContaining({
'http.route': '/test-param/:id',
'koa.type': 'router',
'koa.name': '/test-param/:id',
'sentry.op': 'router.koa',
'sentry.origin': origin,
}),
}),
]),
},
})
.start();
runner.makeRequest('get', '/test-param/123');
await runner.completed();
});

test('should capture errors thrown in routes via the koa error handler.', async () => {
const runner = createRunner()
.expect({ transaction: { transaction: 'GET /error' } })
.expect({ event: EXPECTED_ERROR_EVENT })
.start();
runner.makeRequest('get', '/error', { expectError: true });
await runner.completed();
});
});
});
1 change: 1 addition & 0 deletions packages/deno/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export type { DenoRedisIntegrationOptions } from './integrations/redis';
export { denoMysqlIntegration } from './integrations/mysql';
export { denoPostgresIntegration } from './integrations/postgres';
export { denoAmqplibIntegration } from './integrations/amqplib';
export { denoKoaIntegration } from './integrations/koa';
export { denoContextIntegration } from './integrations/context';
export { globalHandlersIntegration } from './integrations/globalhandlers';
export { normalizePathsIntegration } from './integrations/normalizepaths';
Expand Down
28 changes: 28 additions & 0 deletions packages/deno/src/integrations/koa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { koaChannelIntegration } from '@sentry/server-utils/orchestrion';
import type { Integration, IntegrationFn } from '@sentry/core';
import { defineIntegration, extendIntegration } from '@sentry/core';
import { setAsyncLocalStorageAsyncContextStrategy } from '../async';

const INTEGRATION_NAME = 'DenoKoa' as const;

/**
* Create spans for `koa` middleware/router layers under Deno. Requires the
* `@sentry/deno/import` loader. Delegates to the shared subscriber in
* `@sentry/server-utils`, adding Deno's `AsyncLocalStorage` context strategy so
* spans nest under the active HTTP server span.
*/
const _denoKoaIntegration = (() => {
const inner = koaChannelIntegration();

return extendIntegration(inner, {
name: INTEGRATION_NAME,
setupOnce() {
setAsyncLocalStorageAsyncContextStrategy();
},
});
}) satisfies IntegrationFn;

export const denoKoaIntegration = defineIntegration(_denoKoaIntegration) as () => Integration & {
name: 'DenoKoa';
setupOnce: () => void;
};
3 changes: 2 additions & 1 deletion packages/deno/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { denoServeIntegration } from './integrations/deno-serve';
import { denoHttpIntegration } from './integrations/http';
import { denoAmqplibIntegration } from './integrations/amqplib';
import { denoKoaIntegration } from './integrations/koa';
import { denoMysqlIntegration } from './integrations/mysql';
import { denoPostgresIntegration } from './integrations/postgres';
import { denoRedisIntegration } from './integrations/redis';
Expand Down Expand Up @@ -62,7 +63,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
// (or in parallel to) loading the SDK, so we only gate on whether the
// feature is possible. If they're never loaded, it'll just be a no-op.
...(MODULE_REGISTER_HOOKS_SUPPORTED
? [denoMysqlIntegration(), denoPostgresIntegration(), denoAmqplibIntegration()]
? [denoMysqlIntegration(), denoPostgresIntegration(), denoAmqplibIntegration(), denoKoaIntegration()]
: []),
contextLinesIntegration(),
normalizePathsIntegration(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { KoaLayerType, type KoaInstrumentationConfig } from './types';
import type { KoaContext, KoaMiddleware } from './internal-types';
import { AttributeNames } from './enums/AttributeNames';
import type { Attributes } from '@opentelemetry/api';
import { HTTP_ROUTE } from '@sentry/conventions/attributes';
import { CODE_FUNCTION_NAME, HTTP_ROUTE } from '@sentry/conventions/attributes';

export const getMiddlewareMetadata = (
context: KoaContext,
Expand All @@ -25,7 +25,7 @@ export const getMiddlewareMetadata = (
if (isRouter) {
return {
attributes: {
[AttributeNames.KOA_NAME]: layerPath?.toString(),
[AttributeNames.KOA_NAME]: layerPath?.toString(), // TODO(v11): remove, replaced by http.route
[AttributeNames.KOA_TYPE]: KoaLayerType.ROUTER,
[HTTP_ROUTE]: layerPath?.toString(),
},
Expand All @@ -34,8 +34,9 @@ export const getMiddlewareMetadata = (
} else {
return {
attributes: {
[AttributeNames.KOA_NAME]: layer.name ?? 'middleware',
[AttributeNames.KOA_NAME]: layer.name ?? 'middleware', // TODO(v11): remove, replaced by code.function.name
[AttributeNames.KOA_TYPE]: KoaLayerType.MIDDLEWARE,
[CODE_FUNCTION_NAME]: layer.name ?? 'middleware',
},
name: `middleware - ${layer.name}`,
};
Expand Down
Loading
Loading