Skip to content

Commit 2011bb7

Browse files
committed
feat(deno): Add orchestrion deno runtime hook
Use the orchestrion loader hook defined in server-utils, and create a loader for Deno that detects the presence of the hooks, and instruments the channels added to the mysql module. Documentation added to call out the caveat of usage in Deno v2.8.0 through 2.8.2, which is fixed in 2.8.3.
1 parent b34aa87 commit 2011bb7

11 files changed

Lines changed: 243 additions & 2 deletions

File tree

packages/deno/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,46 @@ Sentry.captureEvent({
5656
],
5757
});
5858
```
59+
60+
## Auto-instrumentation (experimental)
61+
62+
Some libraries (e.g. `mysql`) don't emit tracing signals on their
63+
own. To instrument them, Sentry uses
64+
[orchestrion](https://github.com/getsentry/sentry-javascript) to
65+
transform them at load time so they publish to
66+
`node:diagnostics_channel`.
67+
68+
In Deno versions prior to 2.8.0, this is not available, as it
69+
relies on `Module.registerHooks`, which was added in that
70+
version.
71+
72+
As of Deno 2.8.3, you can use the `--import` or `--preload`
73+
argument to `deno run` in order to enable these instrumentations.
74+
75+
```bash
76+
$ deno run --import=@sentry/deno/import app.ts
77+
```
78+
79+
> [!NOTE]
80+
> In Deno versions **2.8.0** through **2.8.2**, a bug causes Deno
81+
> to deadlock when a module hook is added in this way. As a
82+
> workaround, you can import the loader explicitly, and then
83+
> dynamically import your app to take advantage of the added
84+
> module loading hooks.
85+
>
86+
> ```ts
87+
> import 'npm:@sentry/deno/import';
88+
> await import('./app.ts');
89+
> ```
90+
91+
In both cases, your `app.ts` should simply load Sentry as usual:
92+
93+
```ts
94+
// app.ts
95+
96+
// initialize Sentry as early as possible
97+
import * as Sentry from 'npm:@sentry/deno';
98+
Sentry.init({ dsn: '__DSN__' });
99+
100+
// ... the rest of the app...
101+
```

packages/deno/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"types": "./build/esm/index.d.ts",
1616
"default": "./build/esm/index.js"
1717
}
18+
},
19+
"./import": {
20+
"import": "./build/import.mjs"
1821
}
1922
},
2023
"publishConfig": {
@@ -28,6 +31,9 @@
2831
"@sentry/core": "10.57.0",
2932
"@sentry/server-utils": "10.57.0"
3033
},
34+
"devDependencies": {
35+
"mysql": "^2.18.1"
36+
},
3137
"scripts": {
3238
"deno-types": "node ./scripts/download-deno-types.mjs",
3339
"build": "run-s build:transpile build:types",
@@ -51,7 +57,9 @@
5157
"volta": {
5258
"extends": "../../package.json"
5359
},
54-
"sideEffects": false,
60+
"sideEffects": [
61+
"./build/import.mjs"
62+
],
5563
"nx": {
5664
"targets": {
5765
"build:transpile": {
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import { defineConfig } from 'rollup';
12
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
23

3-
export default makeNPMConfigVariants(makeBaseNPMConfig(), { emitCjs: false });
4+
const orchestrionRuntimeHooks = [
5+
defineConfig({
6+
input: 'src/import.mjs',
7+
external: /.*/,
8+
output: { format: 'esm', file: 'build/import.mjs' },
9+
}),
10+
];
11+
12+
export default [...orchestrionRuntimeHooks, ...makeNPMConfigVariants(makeBaseNPMConfig(), { emitCjs: false })];

packages/deno/src/denoVersion.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,10 @@ export const HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED = gte(2, 8, 0);
2323

2424
/** Whether `node:diagnostics_channel.tracingChannel` exists (Deno 1.44.3+). */
2525
export const TRACING_CHANNEL_SUPPORTED = gte(1, 44, 3);
26+
27+
/**
28+
* Whether `Module.registerHooks` is available (Deno 2.8.0+), which the
29+
* orchestrion runtime hook (`@sentry/deno/import`) needs to transform libraries
30+
* like `mysql` so they publish to their tracing channels.
31+
*/
32+
export const MODULE_REGISTER_HOOKS_SUPPORTED = gte(2, 8, 0);

packages/deno/src/import.mjs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* EXPERIMENTAL: orchestrion runtime hook for Deno.
3+
*
4+
* In Deno versions prior to 2.8.0, this will crash, as it
5+
* relies on `Module.registerHooks`, which was added in that
6+
* version.
7+
*
8+
* As of Deno 2.8.3, this can be loaded via `--import` or `--preload`
9+
* argument to `deno run` in order to enable these instrumentations.
10+
*
11+
* For example:
12+
*
13+
* ```bash
14+
* $ deno run --import=@sentry/deno/import app.ts
15+
* ```
16+
*
17+
* In Deno 2.8.0 through 2.8.2, it can be loaded directly in an
18+
* `init.ts` file that then loads the app via dynamic import.
19+
*
20+
* For example:
21+
*
22+
* ```ts
23+
* // init.ts
24+
* import '@sentry/deno/import';
25+
* await import('./app.ts');
26+
* ```
27+
*
28+
* @module
29+
*/
30+
import '@sentry-internal/server-utils/orchestrion/import-hook';

packages/deno/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export { denoHttpIntegration } from './integrations/http';
108108
export type { DenoHttpIntegrationOptions } from './integrations/http';
109109
export { denoRedisIntegration } from './integrations/redis';
110110
export type { DenoRedisIntegrationOptions } from './integrations/redis';
111+
export { denoMysqlIntegration } from './integrations/mysql';
111112
export { denoContextIntegration } from './integrations/context';
112113
export { globalHandlersIntegration } from './integrations/globalhandlers';
113114
export { normalizePathsIntegration } from './integrations/normalizepaths';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { mysqlChannelIntegration } from '@sentry-internal/server-utils/orchestrion';
2+
import type { Integration, IntegrationFn } from '@sentry/core';
3+
import { defineIntegration } from '@sentry/core';
4+
import { setAsyncLocalStorageAsyncContextStrategy } from '../async';
5+
6+
const INTEGRATION_NAME = 'DenoMysql';
7+
8+
/**
9+
* Create spans for `mysql` queries under Deno.
10+
*
11+
* `mysql` channels are injected by the orchestrion runtime hook at load time.
12+
* The `@sentry/deno/import` loader must be active for this integration to
13+
* record anything.
14+
*
15+
* The channel-subscription logic is shared with the other server runtimes in
16+
* `@sentry-internal/server-utils`. This just installs Deno's
17+
* `AsyncLocalStorage` context strategy (so spans nest under the active
18+
* span and survive mysql's internal callback dispatch) before delegating.
19+
*/
20+
const _denoMysqlIntegration = (() => {
21+
const inner = mysqlChannelIntegration();
22+
return {
23+
name: INTEGRATION_NAME,
24+
setupOnce() {
25+
setAsyncLocalStorageAsyncContextStrategy();
26+
inner.setupOnce?.();
27+
},
28+
};
29+
}) satisfies IntegrationFn;
30+
31+
export const denoMysqlIntegration = defineIntegration(_denoMysqlIntegration) as () => Integration & {
32+
name: 'DenoMysql';
33+
setupOnce: () => void;
34+
};

packages/deno/src/sdk.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import { contextLinesIntegration } from './integrations/contextlines';
1919
import {
2020
HTTP_CLIENT_DIAGNOSTICS_CHANNEL_SUPPORTED,
2121
HTTP_SERVER_DIAGNOSTICS_CHANNEL_SUPPORTED,
22+
MODULE_REGISTER_HOOKS_SUPPORTED,
2223
TRACING_CHANNEL_SUPPORTED,
2324
} from './denoVersion';
2425
import { denoServeIntegration } from './integrations/deno-serve';
2526
import { denoHttpIntegration } from './integrations/http';
27+
import { denoMysqlIntegration } from './integrations/mysql';
2628
import { denoRedisIntegration } from './integrations/redis';
2729
import { globalHandlersIntegration } from './integrations/globalhandlers';
2830
import { normalizePathsIntegration } from './integrations/normalizepaths';
@@ -54,6 +56,11 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
5456
: []),
5557
// node:diagnostics_channel.tracingChannel exists on Deno 1.44.3+.
5658
...(TRACING_CHANNEL_SUPPORTED ? [denoRedisIntegration()] : []),
59+
// orchestrion-based instrumentations.
60+
// It's possible that the orchestrion channels will be injected AFTER
61+
// (or in parallel to) loading the SDK, so we only gate on whether the
62+
// feature is possible. If they're never loaded, it'll just be a no-op.
63+
...(MODULE_REGISTER_HOOKS_SUPPORTED ? [denoMysqlIntegration()] : []),
5764
contextLinesIntegration(),
5865
normalizePathsIntegration(),
5966
globalHandlersIntegration(),

packages/deno/test/__snapshots__/mod.test.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ snapshot[`captureException 1`] = `
115115
"DenoServe",
116116
"DenoHttp",
117117
"DenoRedis",
118+
"DenoMysql",
118119
"ContextLines",
119120
"NormalizePaths",
120121
"GlobalHandlers",
@@ -190,6 +191,7 @@ snapshot[`captureMessage 1`] = `
190191
"DenoServe",
191192
"DenoHttp",
192193
"DenoRedis",
194+
"DenoMysql",
193195
"ContextLines",
194196
"NormalizePaths",
195197
"GlobalHandlers",
@@ -272,6 +274,7 @@ snapshot[`captureMessage twice 1`] = `
272274
"DenoServe",
273275
"DenoHttp",
274276
"DenoRedis",
277+
"DenoMysql",
275278
"ContextLines",
276279
"NormalizePaths",
277280
"GlobalHandlers",
@@ -361,6 +364,7 @@ snapshot[`captureMessage twice 2`] = `
361364
"DenoServe",
362365
"DenoHttp",
363366
"DenoRedis",
367+
"DenoMysql",
364368
"ContextLines",
365369
"NormalizePaths",
366370
"GlobalHandlers",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
import type { DenoClient } from '../build/esm/index.js';
6+
import { getCurrentScope, getGlobalScope, getIsolationScope, init } from '../build/esm/index.js';
7+
8+
function resetGlobals(): void {
9+
getCurrentScope().clear();
10+
getCurrentScope().setClient(undefined);
11+
getIsolationScope().clear();
12+
getGlobalScope().clear();
13+
}
14+
15+
Deno.test('denoMysqlIntegration: included in default integrations (Deno 2.8.0+)', () => {
16+
resetGlobals();
17+
const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
18+
const names = client.getOptions().integrations.map(i => i.name);
19+
assert(names.includes('DenoMysql'), `DenoMysql should be in defaults, got ${names.join(', ')}`);
20+
});
21+
22+
// The orchestrion runtime hook (`@sentry/deno/import`) only works as a FIRST
23+
// import inside the entry graph in Deno 2.8.0 through 2.8.2.
24+
// TODO: revisit a `--import` or `--preload` approach once Deno 2.8.3 ships.
25+
Deno.test('@sentry/deno/import: transforms mysql so it publishes the orchestrion channel', async () => {
26+
const scenario = new URL('./orchestrion-mysql/scenario.mjs', import.meta.url);
27+
28+
// packages/deno — where node_modules resolves
29+
const cwd = new URL('../', import.meta.url);
30+
31+
const command = new Deno.Command('deno', {
32+
args: ['run', '--allow-all', scenario.pathname],
33+
cwd: cwd.pathname,
34+
stdout: 'piped',
35+
stderr: 'piped',
36+
});
37+
38+
const { code, stdout, stderr } = await command.output();
39+
const out = new TextDecoder().decode(stdout);
40+
const err = new TextDecoder().decode(stderr);
41+
42+
assertEquals(code, 0, `scenario exited ${code}\nstdout:\n${out}\nstderr:\n${err}`);
43+
44+
const line = out.split('\n').find(l => l.startsWith('SCENARIO')) ?? '';
45+
assert(line, `no SCENARIO line in output:\n${out}\nstderr:\n${err}`);
46+
// The injected channel fired on `connection.query()`
47+
// proves mysql was transformed...
48+
assert(line.includes('events=start'), `expected channel 'start' event, got: ${line}`);
49+
// ...with the real SQL forwarded through the channel context.
50+
assert(line.includes('statement=SELECT 1 AS solution'), `expected forwarded SQL, got: ${line}`);
51+
// The runtime hook set its detection marker at boot.
52+
assert(line.includes('"runtime":true'), `expected runtime marker, got: ${line}`);
53+
});

0 commit comments

Comments
 (0)