Skip to content

Commit 536eb3b

Browse files
authored
feat(deno)!: rename orchestrion integrations to match other SDKs (#22404)
Move the `setAsyncLocalStorageAsyncContextStrategy()` into the Deno SDK client init, since it's eventually called by any client that uses even the default set of integrations anyway. The previous renamed integrations are replaced by deprecated wrappers that do not change the integration name, maintained for backwards compatibility. Add tests for all imported integrations. This is the first step towards importing more (eventually, all) orchestrion integrations into the Deno SDK in a straightforward and well-tested way. Re: JS-2634 Re: #21225
1 parent 96e36bc commit 536eb3b

25 files changed

Lines changed: 687 additions & 310 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66

77
Work in this release was contributed by @psh4607, @trinitiwowka, @nehaprasad-dev, and @JealousGx. Thank you for your contributions!
88

9+
- feat(deno)!: Rename several default integrations to match the other SDKs ([#22404](https://github.com/getsentry/sentry-javascript/pull/22404)). The `deno*Integration` exports are kept as deprecated aliases. If you were relying on the names (for example, to disable them), then note that these have changed:
10+
- `DenoAmqplib` => `Amqplib`
11+
- `DenoKoa` => `Koa`
12+
- `DenoMongodb` => `Mongodb`
13+
- `DenoMongoose` => `Mongoose`
14+
- `DenoMysql` => `Mysql`
15+
- `DenoPostgres` => `Postgres`
16+
917
## 10.67.0
1018

1119
### Important Changes

MIGRATION.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,19 @@ The `childProcessIntegration` was split into a `childProcessIntegration` (for `c
527527
> **TODO(v11):** Document how the two integrations are configured and what users who customized
528528
> `childProcessIntegration` need to change.
529529
530+
### Deno default integrations renamed to match the other SDKs
531+
532+
Affected SDKs: `@sentry/deno`.
533+
534+
Several default integrations were renamed to match the names used by the other SDKs. The old `deno*Integration` exports are kept as deprecated aliases. If you relied on the old names (for example, to disable an integration), update them:
535+
536+
- `DenoAmqplib` => `Amqplib`
537+
- `DenoKoa` => `Koa`
538+
- `DenoMongodb` => `Mongodb`
539+
- `DenoMongoose` => `Mongoose`
540+
- `DenoMysql` => `Mysql`
541+
- `DenoPostgres` => `Postgres`
542+
530543
## 6. Type Changes
531544

532545
- Several public types that used `any` now use `unknown` — including `StackFrame`, `SamplingContext`,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Spawned by test.ts via `deno run`, in a fresh process so nothing else has
2+
// installed the AsyncLocalStorage context strategy.
3+
//
4+
// This builds a `DenoClient` DIRECTLY — `new DenoClient(...)` + `client.init()`
5+
// instead of calling `Sentry.init()`, then drives the mysql orchestrion channel
6+
// The mysql subscriber only binds once the ALS context strategy is installed
7+
// (it waits for the tracing-channel binding), so a nested db span here proves
8+
// `DenoClient.init()` installs that strategy on the direct-construction path.
9+
// Without it, the subscriber never binds and no span is produced.
10+
import { createStackParser, nodeStackLineParser } from '@sentry/core';
11+
import { DenoClient, getCurrentScope, getDefaultIntegrations, startSpan } from '@sentry/deno';
12+
import { tracingChannel } from 'node:diagnostics_channel';
13+
14+
let nested = false;
15+
16+
const client = new DenoClient({
17+
dsn: 'https://username@domain/123',
18+
tracesSampleRate: 1,
19+
integrations: getDefaultIntegrations({}),
20+
stackParser: createStackParser(nodeStackLineParser()),
21+
beforeSendTransaction(event) {
22+
const spans = event.spans ?? [];
23+
if (spans.some(s => s.op === 'db' && s.data?.['sentry.origin'] === 'auto.db.orchestrion.mysql')) {
24+
nested = true;
25+
}
26+
return null;
27+
},
28+
transport: () => ({ send: () => Promise.resolve({}), flush: () => Promise.resolve(true) }),
29+
});
30+
31+
client.init();
32+
getCurrentScope().setClient(client);
33+
34+
const channel = tracingChannel('orchestrion:mysql:query');
35+
const ctx = {
36+
arguments: ['SELECT 1 AS solution'],
37+
self: { config: { host: '127.0.0.1', port: 3306, database: 'mydb', user: 'root' } },
38+
};
39+
40+
startSpan({ name: 'parent', op: 'test' }, () => {
41+
channel.start.runStores(ctx, () => {
42+
channel.end.publish(ctx);
43+
});
44+
channel.asyncStart.runStores(ctx, () => {
45+
channel.asyncEnd.publish(ctx);
46+
});
47+
});
48+
49+
await client.flush(2000);
50+
51+
// eslint-disable-next-line no-console
52+
console.log(`SCENARIO nested=${nested}`);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// <reference lib="deno.ns" />
2+
3+
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
4+
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
5+
6+
// A directly-constructed `DenoClient` (no `Sentry.init()`) is a supported path.
7+
// The SDK's own tests use it. It must still install the AsyncLocalStorage
8+
// context strategy, which the channel integrations depend on. We run it in a
9+
// fresh process so no prior `init()` has installed the strategy already, then
10+
// assert a nested mysql span appears (see scenario.mjs for why that proves the
11+
// strategy was installed by `client.init()`).
12+
Deno.test('DenoClient.init installs the AsyncLocalStorage strategy on the direct-construction path', async () => {
13+
const scenario = new URL('./scenario.mjs', import.meta.url);
14+
15+
// The package root — where `node_modules` (and thus `@sentry/deno`) resolves
16+
// for the spawned `deno run`.
17+
const cwd = new URL('../../', import.meta.url);
18+
19+
const command = new Deno.Command('deno', {
20+
args: ['run', '--allow-all', scenario.pathname],
21+
cwd: cwd.pathname,
22+
stdout: 'piped',
23+
stderr: 'piped',
24+
});
25+
26+
const { code, stdout, stderr } = await command.output();
27+
const out = new TextDecoder().decode(stdout);
28+
const err = new TextDecoder().decode(stderr);
29+
30+
assertEquals(code, 0, `scenario exited ${code}\nstdout:\n${out}\nstderr:\n${err}`);
31+
32+
const line = out.split('\n').find(l => l.startsWith('SCENARIO')) ?? '';
33+
assert(line, `no SCENARIO line in output:\n${out}\nstderr:\n${err}`);
34+
assert(
35+
line.includes('nested=true'),
36+
`expected a nested mysql span via the direct client path (ACS must be installed by client.init), got: ${line}`,
37+
);
38+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// <reference lib="deno.ns" />
2+
3+
import { tracingChannel } from 'node:diagnostics_channel';
4+
import type { TransactionEvent } from '@sentry/core';
5+
import type { DenoClient } from '@sentry/deno';
6+
import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '@sentry/deno';
7+
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
8+
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
9+
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
10+
11+
function resetGlobals(): void {
12+
getCurrentScope().clear();
13+
getCurrentScope().setClient(undefined);
14+
getIsolationScope().clear();
15+
getGlobalScope().clear();
16+
}
17+
18+
/** See deno-redis.test.ts — same sink shape, deduped for clarity. */
19+
function transactionSink(): {
20+
beforeSendTransaction: (event: TransactionEvent) => null;
21+
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
22+
} {
23+
const transactions: TransactionEvent[] = [];
24+
const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = [];
25+
return {
26+
beforeSendTransaction(event) {
27+
transactions.push(event);
28+
for (let i = waiters.length - 1; i >= 0; i--) {
29+
const w = waiters[i]!;
30+
if (w.predicate(event)) {
31+
waiters.splice(i, 1);
32+
w.resolve(event);
33+
}
34+
}
35+
return null;
36+
},
37+
waitFor(predicate) {
38+
const already = transactions.find(predicate);
39+
if (already) return Promise.resolve(already);
40+
return new Promise<TransactionEvent>(resolve => {
41+
waiters.push({ predicate, resolve });
42+
});
43+
},
44+
};
45+
}
46+
47+
function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
48+
let timer: ReturnType<typeof setTimeout> | undefined;
49+
const timeout = new Promise<T>((_, reject) => {
50+
timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms);
51+
});
52+
return Promise.race([p, timeout]).finally(() => {
53+
if (timer !== undefined) clearTimeout(timer);
54+
});
55+
}
56+
57+
Deno.test('amqplib instrumentation: included in default integrations (Deno 2.8.0+)', () => {
58+
resetGlobals();
59+
const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
60+
const names = client.getOptions().integrations.map(i => i.name);
61+
assert(names.includes('Amqplib'), `Amqplib should be in defaults, got ${names.join(', ')}`);
62+
});
63+
64+
// Exercises the SDK path end-to-end: `init()` installs the AsyncLocalStorage
65+
// context strategy and wires the default `amqplibChannelIntegration` (which
66+
// subscribes to the channel), and we drive the `orchestrion:amqplib:publish`
67+
// channel manually — the same events the orchestrion transform publishes around
68+
// `Channel.prototype.publish` — so no live broker is needed. Asserting a nested
69+
// producer `message` span proves the subscriber, the emitted attributes, AND the
70+
// context-strategy wiring all work.
71+
Deno.test('amqplib instrumentation: orchestrion:amqplib:publish channel produces a nested message span', async () => {
72+
resetGlobals();
73+
const sink = transactionSink();
74+
init({
75+
dsn: 'https://username@domain/123',
76+
tracesSampleRate: 1,
77+
beforeSendTransaction: sink.beforeSendTransaction,
78+
});
79+
80+
const channel = tracingChannel('orchestrion:amqplib:publish');
81+
82+
// `publish(exchange, routingKey, content, options)`; `self.connection` carries
83+
// the server product used for `messaging.system`.
84+
const ctx = {
85+
self: { connection: { serverProperties: { product: 'RabbitMQ' } } },
86+
arguments: ['my-exchange', 'my.routing.key', new Uint8Array(), { messageId: 'msg-1' }],
87+
};
88+
89+
startSpan({ name: 'parent', op: 'test' }, () => {
90+
channel.start.runStores(ctx, () => {
91+
channel.end.publish(ctx);
92+
});
93+
channel.asyncStart.runStores(ctx, () => {
94+
channel.asyncEnd.publish(ctx);
95+
});
96+
});
97+
98+
const parent = await withTimeout(
99+
sink.waitFor(t => t.transaction === 'parent'),
100+
5000,
101+
"'parent' transaction",
102+
);
103+
104+
const publishSpan = parent.spans?.find(s => s.op === 'message');
105+
assertExists(publishSpan, `expected a message child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
106+
assertEquals(publishSpan!.description, 'publish my-exchange');
107+
assertEquals(publishSpan!.data?.['messaging.destination.name'], 'my-exchange');
108+
assertEquals(publishSpan!.data?.['messaging.system'], 'rabbitmq');
109+
assertEquals(publishSpan!.data?.['sentry.origin'], 'auto.amqplib.orchestrion.publisher');
110+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// <reference lib="deno.ns" />
2+
3+
import { tracingChannel } from 'node:diagnostics_channel';
4+
import type { TransactionEvent } from '@sentry/core';
5+
import type { DenoClient } from '@sentry/deno';
6+
import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '@sentry/deno';
7+
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
8+
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
9+
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
10+
11+
function resetGlobals(): void {
12+
getCurrentScope().clear();
13+
getCurrentScope().setClient(undefined);
14+
getIsolationScope().clear();
15+
getGlobalScope().clear();
16+
}
17+
18+
/** See deno-redis.test.ts — same sink shape, deduped for clarity. */
19+
function transactionSink(): {
20+
beforeSendTransaction: (event: TransactionEvent) => null;
21+
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
22+
} {
23+
const transactions: TransactionEvent[] = [];
24+
const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = [];
25+
return {
26+
beforeSendTransaction(event) {
27+
transactions.push(event);
28+
for (let i = waiters.length - 1; i >= 0; i--) {
29+
const w = waiters[i]!;
30+
if (w.predicate(event)) {
31+
waiters.splice(i, 1);
32+
w.resolve(event);
33+
}
34+
}
35+
return null;
36+
},
37+
waitFor(predicate) {
38+
const already = transactions.find(predicate);
39+
if (already) return Promise.resolve(already);
40+
return new Promise<TransactionEvent>(resolve => {
41+
waiters.push({ predicate, resolve });
42+
});
43+
},
44+
};
45+
}
46+
47+
function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
48+
let timer: ReturnType<typeof setTimeout> | undefined;
49+
const timeout = new Promise<T>((_, reject) => {
50+
timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms);
51+
});
52+
return Promise.race([p, timeout]).finally(() => {
53+
if (timer !== undefined) clearTimeout(timer);
54+
});
55+
}
56+
57+
Deno.test('koa instrumentation: included in default integrations (Deno 2.8.0+)', () => {
58+
resetGlobals();
59+
const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
60+
const names = client.getOptions().integrations.map(i => i.name);
61+
assert(names.includes('Koa'), `Koa should be in defaults, got ${names.join(', ')}`);
62+
});
63+
64+
// Exercises the SDK path end-to-end. Unlike the db integrations, koa's channel
65+
// doesn't build a span directly: its `start` handler wraps the registered
66+
// middleware (arg 0) in a span-creating proxy, and the span opens when that
67+
// middleware later runs under an active span. So we publish `orchestrion:koa:use`
68+
// with a middleware, then invoke the wrapped middleware inside a parent span —
69+
// the same shape `app.use(fn)` then a request produces. Asserting a nested
70+
// `middleware.koa` span proves the subscriber and context wiring work.
71+
Deno.test('koa instrumentation: orchestrion:koa:use channel wraps middleware into a span', async () => {
72+
resetGlobals();
73+
const sink = transactionSink();
74+
init({
75+
dsn: 'https://username@domain/123',
76+
tracesSampleRate: 1,
77+
beforeSendTransaction: sink.beforeSendTransaction,
78+
});
79+
80+
function myMiddleware(_context: unknown, next: () => Promise<unknown>): Promise<unknown> {
81+
return next();
82+
}
83+
84+
// Publishing `start` runs the subscriber, which patches `arguments[0]` in place.
85+
const ctx = { arguments: [myMiddleware] as unknown[] };
86+
tracingChannel('orchestrion:koa:use').start.publish(ctx);
87+
const wrappedMiddleware = ctx.arguments[0] as typeof myMiddleware;
88+
89+
await startSpan({ name: 'parent', op: 'test' }, async () => {
90+
await wrappedMiddleware({}, () => Promise.resolve());
91+
});
92+
93+
const parent = await withTimeout(
94+
sink.waitFor(t => t.transaction === 'parent'),
95+
5000,
96+
"'parent' transaction",
97+
);
98+
99+
const koaSpan = parent.spans?.find(s => s.op === 'middleware.koa');
100+
assertExists(koaSpan, `expected a middleware.koa child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
101+
assertEquals(koaSpan!.description, 'myMiddleware');
102+
assertEquals(koaSpan!.data?.['sentry.origin'], 'auto.http.orchestrion.koa');
103+
});

0 commit comments

Comments
 (0)