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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

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

- 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:
- `DenoAmqplib` => `Amqplib`
- `DenoKoa` => `Koa`
- `DenoMongodb` => `Mongodb`
- `DenoMongoose` => `Mongoose`
- `DenoMysql` => `Mysql`
- `DenoPostgres` => `Postgres`

## 10.67.0

### Important Changes
Expand Down
13 changes: 13 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,19 @@ The `childProcessIntegration` was split into a `childProcessIntegration` (for `c
> **TODO(v11):** Document how the two integrations are configured and what users who customized
> `childProcessIntegration` need to change.

### Deno default integrations renamed to match the other SDKs

Affected SDKs: `@sentry/deno`.

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:

- `DenoAmqplib` => `Amqplib`
- `DenoKoa` => `Koa`
- `DenoMongodb` => `Mongodb`
- `DenoMongoose` => `Mongoose`
- `DenoMysql` => `Mysql`
- `DenoPostgres` => `Postgres`

## 6. Type Changes

- Several public types that used `any` now use `unknown` — including `StackFrame`, `SamplingContext`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Spawned by test.ts via `deno run`, in a fresh process so nothing else has
// installed the AsyncLocalStorage context strategy.
//
// This builds a `DenoClient` DIRECTLY — `new DenoClient(...)` + `client.init()`
// instead of calling `Sentry.init()`, then drives the mysql orchestrion channel
// The mysql subscriber only binds once the ALS context strategy is installed
// (it waits for the tracing-channel binding), so a nested db span here proves
// `DenoClient.init()` installs that strategy on the direct-construction path.
// Without it, the subscriber never binds and no span is produced.
import { createStackParser, nodeStackLineParser } from '@sentry/core';
import { DenoClient, getCurrentScope, getDefaultIntegrations, startSpan } from '@sentry/deno';
import { tracingChannel } from 'node:diagnostics_channel';

let nested = false;

const client = new DenoClient({
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
integrations: getDefaultIntegrations({}),
stackParser: createStackParser(nodeStackLineParser()),
beforeSendTransaction(event) {
const spans = event.spans ?? [];
if (spans.some(s => s.op === 'db' && s.data?.['sentry.origin'] === 'auto.db.orchestrion.mysql')) {
nested = true;
}
return null;
},
transport: () => ({ send: () => Promise.resolve({}), flush: () => Promise.resolve(true) }),
});

client.init();
getCurrentScope().setClient(client);

const channel = tracingChannel('orchestrion:mysql:query');
const ctx = {
arguments: ['SELECT 1 AS solution'],
self: { config: { host: '127.0.0.1', port: 3306, database: 'mydb', user: 'root' } },
};

startSpan({ name: 'parent', op: 'test' }, () => {
channel.start.runStores(ctx, () => {
channel.end.publish(ctx);
});
channel.asyncStart.runStores(ctx, () => {
channel.asyncEnd.publish(ctx);
});
});

await client.flush(2000);

// eslint-disable-next-line no-console
console.log(`SCENARIO nested=${nested}`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// <reference lib="deno.ns" />

import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';

// A directly-constructed `DenoClient` (no `Sentry.init()`) is a supported path.
// The SDK's own tests use it. It must still install the AsyncLocalStorage
// context strategy, which the channel integrations depend on. We run it in a
// fresh process so no prior `init()` has installed the strategy already, then
// assert a nested mysql span appears (see scenario.mjs for why that proves the
// strategy was installed by `client.init()`).
Deno.test('DenoClient.init installs the AsyncLocalStorage strategy on the direct-construction path', async () => {
const scenario = new URL('./scenario.mjs', import.meta.url);

// The package root — where `node_modules` (and thus `@sentry/deno`) resolves
// for the spawned `deno run`.
const cwd = new URL('../../', import.meta.url);

const command = new Deno.Command('deno', {
args: ['run', '--allow-all', scenario.pathname],
cwd: cwd.pathname,
stdout: 'piped',
stderr: 'piped',
});

const { code, stdout, stderr } = await command.output();
const out = new TextDecoder().decode(stdout);
const err = new TextDecoder().decode(stderr);

assertEquals(code, 0, `scenario exited ${code}\nstdout:\n${out}\nstderr:\n${err}`);

const line = out.split('\n').find(l => l.startsWith('SCENARIO')) ?? '';
assert(line, `no SCENARIO line in output:\n${out}\nstderr:\n${err}`);
assert(
line.includes('nested=true'),
`expected a nested mysql span via the direct client path (ACS must be installed by client.init), got: ${line}`,
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// <reference lib="deno.ns" />

import { tracingChannel } from 'node:diagnostics_channel';
import type { TransactionEvent } from '@sentry/core';
import type { DenoClient } from '@sentry/deno';
import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';

function resetGlobals(): void {
getCurrentScope().clear();
getCurrentScope().setClient(undefined);
getIsolationScope().clear();
getGlobalScope().clear();
}

/** See deno-redis.test.ts — same sink shape, deduped for clarity. */
function transactionSink(): {
beforeSendTransaction: (event: TransactionEvent) => null;
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
} {
const transactions: TransactionEvent[] = [];
const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = [];
return {
beforeSendTransaction(event) {
transactions.push(event);
for (let i = waiters.length - 1; i >= 0; i--) {
const w = waiters[i]!;
if (w.predicate(event)) {
waiters.splice(i, 1);
w.resolve(event);
}
}
return null;
},
waitFor(predicate) {
const already = transactions.find(predicate);
if (already) return Promise.resolve(already);
return new Promise<TransactionEvent>(resolve => {
waiters.push({ predicate, resolve });
});
},
};
}

function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<T>((_, reject) => {
timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms);
});
return Promise.race([p, timeout]).finally(() => {
if (timer !== undefined) clearTimeout(timer);
});
}

Deno.test('amqplib instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
const names = client.getOptions().integrations.map(i => i.name);
assert(names.includes('Amqplib'), `Amqplib should be in defaults, got ${names.join(', ')}`);
});

// Exercises the SDK path end-to-end: `init()` installs the AsyncLocalStorage
// context strategy and wires the default `amqplibChannelIntegration` (which
// subscribes to the channel), and we drive the `orchestrion:amqplib:publish`
// channel manually — the same events the orchestrion transform publishes around
// `Channel.prototype.publish` — so no live broker is needed. Asserting a nested
// producer `message` span proves the subscriber, the emitted attributes, AND the
// context-strategy wiring all work.
Deno.test('amqplib instrumentation: orchestrion:amqplib:publish channel produces a nested message span', async () => {
resetGlobals();
const sink = transactionSink();
init({
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
beforeSendTransaction: sink.beforeSendTransaction,
});

const channel = tracingChannel('orchestrion:amqplib:publish');

// `publish(exchange, routingKey, content, options)`; `self.connection` carries
// the server product used for `messaging.system`.
const ctx = {
self: { connection: { serverProperties: { product: 'RabbitMQ' } } },
arguments: ['my-exchange', 'my.routing.key', new Uint8Array(), { messageId: 'msg-1' }],
};

startSpan({ name: 'parent', op: 'test' }, () => {
channel.start.runStores(ctx, () => {
channel.end.publish(ctx);
});
channel.asyncStart.runStores(ctx, () => {
channel.asyncEnd.publish(ctx);
});
});

const parent = await withTimeout(
sink.waitFor(t => t.transaction === 'parent'),
5000,
"'parent' transaction",
);

const publishSpan = parent.spans?.find(s => s.op === 'message');
assertExists(publishSpan, `expected a message child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
assertEquals(publishSpan!.description, 'publish my-exchange');
assertEquals(publishSpan!.data?.['messaging.destination.name'], 'my-exchange');
assertEquals(publishSpan!.data?.['messaging.system'], 'rabbitmq');
assertEquals(publishSpan!.data?.['sentry.origin'], 'auto.amqplib.orchestrion.publisher');
});
103 changes: 103 additions & 0 deletions dev-packages/deno-integration-tests/suites/orchestrion-koa/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// <reference lib="deno.ns" />

import { tracingChannel } from 'node:diagnostics_channel';
import type { TransactionEvent } from '@sentry/core';
import type { DenoClient } from '@sentry/deno';
import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';

function resetGlobals(): void {
getCurrentScope().clear();
getCurrentScope().setClient(undefined);
getIsolationScope().clear();
getGlobalScope().clear();
}

/** See deno-redis.test.ts — same sink shape, deduped for clarity. */
function transactionSink(): {
beforeSendTransaction: (event: TransactionEvent) => null;
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
} {
const transactions: TransactionEvent[] = [];
const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = [];
return {
beforeSendTransaction(event) {
transactions.push(event);
for (let i = waiters.length - 1; i >= 0; i--) {
const w = waiters[i]!;
if (w.predicate(event)) {
waiters.splice(i, 1);
w.resolve(event);
}
}
return null;
},
waitFor(predicate) {
const already = transactions.find(predicate);
if (already) return Promise.resolve(already);
return new Promise<TransactionEvent>(resolve => {
waiters.push({ predicate, resolve });
});
},
};
}

function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<T>((_, reject) => {
timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms);
});
return Promise.race([p, timeout]).finally(() => {
if (timer !== undefined) clearTimeout(timer);
});
}

Deno.test('koa instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
const names = client.getOptions().integrations.map(i => i.name);
assert(names.includes('Koa'), `Koa should be in defaults, got ${names.join(', ')}`);
});

// Exercises the SDK path end-to-end. Unlike the db integrations, koa's channel
// doesn't build a span directly: its `start` handler wraps the registered
// middleware (arg 0) in a span-creating proxy, and the span opens when that
// middleware later runs under an active span. So we publish `orchestrion:koa:use`
// with a middleware, then invoke the wrapped middleware inside a parent span —
// the same shape `app.use(fn)` then a request produces. Asserting a nested
// `middleware.koa` span proves the subscriber and context wiring work.
Deno.test('koa instrumentation: orchestrion:koa:use channel wraps middleware into a span', async () => {
resetGlobals();
const sink = transactionSink();
init({
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
beforeSendTransaction: sink.beforeSendTransaction,
});

function myMiddleware(_context: unknown, next: () => Promise<unknown>): Promise<unknown> {
return next();
}

// Publishing `start` runs the subscriber, which patches `arguments[0]` in place.
const ctx = { arguments: [myMiddleware] as unknown[] };
tracingChannel('orchestrion:koa:use').start.publish(ctx);
const wrappedMiddleware = ctx.arguments[0] as typeof myMiddleware;

await startSpan({ name: 'parent', op: 'test' }, async () => {
await wrappedMiddleware({}, () => Promise.resolve());
});

const parent = await withTimeout(
sink.waitFor(t => t.transaction === 'parent'),
5000,
"'parent' transaction",
);

const koaSpan = parent.spans?.find(s => s.op === 'middleware.koa');
assertExists(koaSpan, `expected a middleware.koa child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
assertEquals(koaSpan!.description, 'myMiddleware');
assertEquals(koaSpan!.data?.['sentry.origin'], 'auto.http.orchestrion.koa');
});
Loading
Loading