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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import codeTransformerRollup from '@apm-js-collab/code-transformer-bundler-plugins/rollup';
import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config';

// Tells the SDK the orchestrion bundler transform ran, so `detectOrchestrionSetup()`
// no-ops the runtime diagnostics-channel hook. Injected via `codeTransformerRollup`'s
// `injectDiagnostics` option (sourcemap-safe) instead of a hand-rolled plugin.
const orchestrionBundlerMarker = [
'globalThis.__SENTRY_ORCHESTRION__ = (globalThis.__SENTRY_ORCHESTRION__ || {});',
'globalThis.__SENTRY_ORCHESTRION__.bundler = true;',
'',
].join('\n');

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },

modules: ['@sentry/nuxt/module', './modules/sentry-server-init'],
modules: ['@sentry/nuxt/module'],

sentry: {
_experimental: {
useDiagnosticsChannelInjection: true,
},
autoInjectServerSentry: 'top-level-import',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Is the plan to make this the default then?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 👍

},

runtimeConfig: {
public: {
Expand All @@ -24,29 +19,4 @@ export default defineNuxtConfig({
},
},
},

nitro: {
// Nuxt's server is built by Nitro (Rollup), not Vite — so the orchestrion
// code transform has to run as a Nitro Rollup plugin to reach `server/api/*`
// routes. Force-bundle the instrumented deps via `externals.inline`;
// externalized deps are `require()`d from `node_modules` at runtime and never
// pass through the transform.
//
// `standard-as-callback` is ioredis' CJS `export default` helper used by
// `connect()`. Left external, Rollup's interop resolves its `.default` to a
// non-function in the bundle; inlining it alongside ioredis links the
// interop consistently.
externals: {
inline: [...INSTRUMENTED_MODULE_NAMES, 'standard-as-callback'],
},
rollupConfig: {
plugins: [
codeTransformerRollup({
instrumentations: SENTRY_INSTRUMENTATIONS,
injectDiagnostics: () => orchestrionBundlerMarker,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
],
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"generate": "nuxt generate",
"preview": "nuxt preview",
"start": "node .output/server/index.mjs",
"start:import": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs",
"clean": "npx nuxi cleanup",
"test": "playwright test",
"test:prod": "TEST_ENV=production playwright test",
Expand All @@ -19,15 +18,13 @@
"//": "Need to use ioredis 5.10.1 because that's the last version before they support tracing channels",
"dependencies": {
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
"@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz",
"ioredis": "5.10.1",
"mysql": "^2.18.1",
"nuxt": "^4.4.8",
"vue": "^3.5.38",
"vue-router": "^5.1.0"
},
"devDependencies": {
"@apm-js-collab/code-transformer-bundler-plugins": "^0.7.1",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as Sentry from '@sentry/nuxt';

// Opt into diagnostics-channel-based auto-instrumentation. This registers the
// channel subscribers (e.g. for `mysql`) that turn the diagnostics-channel
// events — injected at build time by the orchestrion Rollup plugin (see
// `nuxt.config.ts`) — into Sentry spans. Must run before `Sentry.init()`.
// The Nuxt module transforms supported Nitro dependencies when enabled in
// `nuxt.config.ts`. In v10, register Node's matching channel subscribers before
// initializing the SDK so those events become spans.
Sentry.experimentalUseDiagnosticsChannelInjection();

Sentry.init({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@ const connection = mysql.createConnection({
});

export default defineEventHandler(() => {
return new Promise(resolve => {
connection.query('SELECT 1 + 1 AS solution', () => {
connection.query('SELECT NOW()', ['1', '2'], () => {
return new Promise((resolve, reject) => {
connection.query('SELECT 1 + 1 AS solution', error => {
if (error) {
reject(error);
return;
}

connection.query('SELECT NOW()', ['1', '2'], nestedError => {
if (nestedError) {
reject(nestedError);
return;
}

resolve({ status: 'ok' });
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';
import { expect, test } from '@playwright/test';

function readServerBundle(): string {
const serverDir = path.join(process.cwd(), '.output/server');
return readdirSync(serverDir, { recursive: true })
.filter(file => typeof file === 'string' && file.endsWith('.mjs'))
.map(file => readFileSync(path.join(serverDir, file), 'utf8'))
.join('\n');
}

function readClientBundle(): string {
const serverDir = path.join(process.cwd(), '.output/public');
return readdirSync(serverDir, { recursive: true })
.filter(file => typeof file === 'string' && file.endsWith('.js'))
.map(file => readFileSync(path.join(serverDir, file), 'utf8'))
.join('\n');
}

test.describe('Orchestrion build-time injection', () => {
const serverBundle = readServerBundle();
const clientBundle = readClientBundle();

test('force-bundles instrumented dependencies', () => {
expect(serverBundle).not.toMatch(/(from\s*["']mysql["']|require\(["']mysql["']\))/);
expect(serverBundle).not.toMatch(/(from\s*["']ioredis["']|require\(["']ioredis["']\))/);
expect(serverBundle).not.toMatch(/(from\s*["']standard-as-callback["']|require\(["']standard-as-callback["']\))/);
});

test('injects diagnostics-channel publishers into the server build', () => {
expect(serverBundle).toContain('__SENTRY_ORCHESTRION__');
expect(serverBundle).toMatch(/tracingChannel\(["']orchestrion:mysql:query["']\)/);
expect(serverBundle).toMatch(/tracingChannel\(["']orchestrion:ioredis:command["']\)/);
expect(serverBundle).toMatch(/tracingChannel\(["']orchestrion:ioredis:connect["']\)/);
});

test('does not inject diagnostics-channel publishers into the client build', () => {
expect(clientBundle).not.toContain('__SENTRY_ORCHESTRION__');
expect(clientBundle).not.toMatch(/orchestrion:/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ test('Instruments ioredis automatically', async ({ baseURL }) => {
);
});

await fetch(`${baseURL}/api/db-ioredis`);
const response = await fetch(`${baseURL}/api/db-ioredis`);
expect(response.status).toBe(200);
expect(await response.text()).toBe('test-value');

const transactionEvent = await transactionEventPromise;

Expand Down Expand Up @@ -50,7 +52,9 @@ test('Instruments mysql automatically', async ({ baseURL }) => {
);
});

await fetch(`${baseURL}/api/db-mysql`);
const response = await fetch(`${baseURL}/api/db-mysql`);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ status: 'ok' });

const transactionEvent = await transactionEventPromise;

Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@sentry/node": "10.66.0",
"@sentry/node-core": "10.66.0",
"@sentry/rollup-plugin": "^5.3.0",
"@sentry/server-utils": "10.66.0",
"@sentry/vite-plugin": "^5.3.0",
"@sentry/vue": "10.66.0",
"local-pkg": "^1.1.2"
Expand Down
18 changes: 18 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,24 @@ export type SentryNuxtModuleOptions = BuildTimeOptionsBase & {
*/
enabled?: boolean;

// todo(v11): Update this JSDoc (and remove from experimental)
/**
* Experimental build-time options that may change or be removed without notice.
*/
_experimental?: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: this will go away again in v11?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

/**
* Enables build-time diagnostics-channel instrumentation for supported dependencies bundled into the Nitro server.
*
* 1. Call `Sentry.experimentalUseDiagnosticsChannelInjection()` just before `Sentry.init()`
* 2. Remove `--import ./.output/server/sentry.server.config.mjs` from your `start`script
* 3. Add `sentry.autoInjectServerSentry: 'top-level-import'` in `nuxt.config.ts` so Sentry's server configuration is automatically imported
*
* @default false
* @experimental May change or be removed in any release.
*/
useDiagnosticsChannelInjection?: boolean;
};

/**
* Options for the Sentry Vite plugin to customize the source maps upload process.
*
Expand Down
5 changes: 5 additions & 0 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { SentryNuxtModuleOptions } from './common/types';
import { addDynamicImportEntryFileWrapper, addSentryTopImport, addServerConfigToBuild } from './vite/addServerConfig';
import { addDatabaseInstrumentation } from './vite/databaseConfig';
import { addMiddlewareImports, addMiddlewareInstrumentation } from './vite/middlewareConfig';
import { setupOrchestrion } from './vite/orchestrion';
import { setupSourceMaps } from './vite/sourceMaps';
import { addStorageInstrumentation } from './vite/storageConfig';
import { addOTelCommonJSImportAlias, findDefaultSdkInitFile, getNitroMajorVersion } from './vite/utils';
Expand Down Expand Up @@ -84,6 +85,10 @@ export default defineNuxtModule<ModuleOptions>({
const isMinNuxtV4 = nuxtMajor >= 4;

if (serverConfigFile) {
if (moduleOptions._experimental?.useDiagnosticsChannelInjection) {
setupOrchestrion(nuxt);
}

if (isNitroV3) {
addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/handler.server'));
addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/update-route-name.server'));
Expand Down
35 changes: 35 additions & 0 deletions packages/nuxt/src/vite/orchestrion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Nuxt } from '@nuxt/schema';
import { INSTRUMENTED_MODULE_NAMES } from '@sentry/server-utils/orchestrion/config';
import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/rollup';
import type { NitroConfig } from 'nitropack';

// ioredis requires this CommonJS helper to be bundled with it. Leaving it
// external makes Nitro resolve the default export as a namespace object.
const IORedisDependencies = ['standard-as-callback'];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: This will likely fail for all nitro based frameworks I guess? Maybe we can move this to server utils at some point so we can make this reusable if we run into more of these things.


/**
* Configures Nitro to bundle and transform dependencies that publish tracing
* events through diagnostics channels.
*/
export function setupOrchestrion(nuxt: Nuxt): void {
nuxt.hook('nitro:config', (nitroConfig: NitroConfig) => {
if (nuxt.options?._prepare) {
return;
}

nitroConfig.rollupConfig ??= {};

if (nitroConfig.rollupConfig.plugins === null || nitroConfig.rollupConfig.plugins === undefined) {
nitroConfig.rollupConfig.plugins = [];
} else if (!Array.isArray(nitroConfig.rollupConfig.plugins)) {
nitroConfig.rollupConfig.plugins = [nitroConfig.rollupConfig.plugins];
}

nitroConfig.rollupConfig.plugins.push(sentryOrchestrionPlugin());

const externals = (nitroConfig.externals ||= {});
const inline = externals.inline;
const existingInline = Array.isArray(inline) ? inline : inline ? [inline] : [];
externals.inline = [...new Set([...existingInline, ...INSTRUMENTED_MODULE_NAMES, ...IORedisDependencies])];
});
}
4 changes: 1 addition & 3 deletions packages/nuxt/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ export function setupSourceMaps(

nuxt.hook('nitro:config', (nitroConfig: NitroConfig) => {
if (sourceMapsEnabled && !nitroConfig.dev && !nuxt.options?._prepare) {
if (!nitroConfig.rollupConfig) {
nitroConfig.rollupConfig = {};
}
nitroConfig.rollupConfig ??= {};

if (nitroConfig.rollupConfig.plugins === null || nitroConfig.rollupConfig.plugins === undefined) {
nitroConfig.rollupConfig.plugins = [];
Expand Down
3 changes: 3 additions & 0 deletions packages/nuxt/test/vite/buildOptions.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ describe('Sentry Nuxt build-time options type', () => {

// --- SentryNuxtModuleOptions specific options ---
enabled: true,
_experimental: {
useDiagnosticsChannelInjection: true,
},
autoInjectServerSentry: 'experimental_dynamic-import',
configDir: '~/custom-config',
experimental_entrypointWrappedFunctions: ['default', 'handler', 'server', 'customExport'],
Expand Down
Loading
Loading