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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { execFileSync } from 'node:child_process';
import { expect, test } from '@playwright/test';
import { isDevMode } from './isDevMode';

// Nitro v3 bundles every dependency, including the SDK's copy of `@sentry/server-runtime-injection`.
// On Node versions without `Module.registerHooks` (< 24.13), that bundled copy registers its ESM
// loader hook through the `@sentry/server-runtime-injection/hook` specifier, which has to resolve
// from the server output. The runner's Node takes the `registerHooks` path, so assert the
// resolution directly instead of the missing warning.
test('keeps the runtime injection loader hook resolvable from the server output', () => {
test.skip(isDevMode, 'nuxt dev has no server output');

const resolved = execFileSync(
'node',
['--input-type=module', '-e', "console.log(import.meta.resolve('@sentry/server-runtime-injection/hook'))"],
{ cwd: '.output/server', encoding: 'utf8' },
);

expect(resolved.trim()).toMatch(/\/\.output\/server\/node_modules\/@sentry\/server-runtime-injection\/.*hook\.js$/);
});
3 changes: 2 additions & 1 deletion packages/nuxt/src/vite/nitroServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { addDatabaseInstrumentation } from './databaseConfig';
import { addMiddlewareImports, addMiddlewareInstrumentation } from './middlewareConfig';
import { addStorageInstrumentation } from './storageConfig';
import type { ServerApi } from './utils';
import { addRuntimeInjectionTraceDep, type ServerApi } from './utils';

type NitroModuleOptions = Omit<SentryNuxtModuleOptions, 'experimental_entrypointWrappedFunctions'> &
Required<Pick<SentryNuxtModuleOptions, 'experimental_entrypointWrappedFunctions'>>;
Expand Down Expand Up @@ -61,6 +61,7 @@ export function setupNitroServer(
addMiddlewareImports();
addStorageInstrumentation(nuxt, !isNitroV3);
addDatabaseInstrumentation(nuxt.options.nitro, !isNitroV3, moduleOptions);
addRuntimeInjectionTraceDep(nuxt, isNitroV3);

nuxt.hooks.hook('nitro:init', nitro => {
if (nuxt.options?._prepare) {
Expand Down
16 changes: 16 additions & 0 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,19 @@ export function toResolvablePath(source: string): { path: string; wasFileUrl: bo
return undefined;
}
}

/**
* Nitro v3 bundles every dependency by default, but `@sentry/server-runtime-injection` must load
* from `node_modules`: on Node versions without `Module.registerHooks` it registers its ESM loader
* hook by file, and that file does not exist next to a bundled copy. Nitro v2 already keeps
* dependencies external.
*/
export function addRuntimeInjectionTraceDep(nuxt: Nuxt, isNitroV3: boolean): void {
if (!isNitroV3) {
return;
}

// Nuxt types `nitro` against Nitro v2, which has no `traceDeps`.
const nitroOptions = nuxt.options.nitro as { traceDeps?: (string | RegExp)[] };
nitroOptions.traceDeps = [...(nitroOptions.traceDeps ?? []), '@sentry/server-runtime-injection'];
}
19 changes: 19 additions & 0 deletions packages/nuxt/test/vite/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
addRuntimeInjectionTraceDep,
constructFunctionReExport,
constructWrappedFunctionExportQuery,
extractFunctionReexportQueryParameters,
Expand Down Expand Up @@ -427,3 +428,21 @@ export { foo_sentryWrapped as foo };
expect(result).toBe('');
});
});

describe('addRuntimeInjectionTraceDep', () => {
it('keeps `@sentry/server-runtime-injection` out of the Nitro v3 bundle', () => {
const nuxtMock = { options: { nitro: { traceDeps: ['sharp'] } } } as unknown as Nuxt;

addRuntimeInjectionTraceDep(nuxtMock, true);

expect(nuxtMock.options.nitro).toEqual({ traceDeps: ['sharp', '@sentry/server-runtime-injection'] });
});

it('does not touch the Nitro v2 config', () => {
const nuxtMock = { options: { nitro: {} } } as unknown as Nuxt;

addRuntimeInjectionTraceDep(nuxtMock, false);

expect(nuxtMock.options.nitro).toEqual({});
});
});
3 changes: 2 additions & 1 deletion packages/server-runtime-injection/src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,9 @@ export function registerDiagnosticsChannelInjection(): void {
}
} catch (error) {
marker.runtimeUnavailable = true;
const reason = error instanceof Error ? error.message : String(error);
warnRuntimeUnavailable(
'Failed to register diagnostics-channel injection hooks, so channel-based integrations will not record spans.',
`Failed to register diagnostics-channel injection hooks (${reason}), so channel-based integrations will not record spans.`,
);
debug.warn('Diagnostics-channel injection registration error:', error);
return;
Expand Down
19 changes: 19 additions & 0 deletions packages/server-runtime-injection/test/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,25 @@ describe('registerDiagnosticsChannelInjection - bundled/tree-shaken detection',
warnSpy.mockRestore();
});

it('includes the underlying error when registering the hooks fails', () => {
// Deno always takes the `Module.registerHooks` path, independent of the Node version running the tests.
(globalThis as { Deno?: unknown }).Deno = { version: { deno: '2.8.3' } };
registerHooksMock.mockImplementationOnce(() => {
throw new Error('boom');
});

try {
registerDiagnosticsChannelInjection();
} finally {
delete (globalThis as { Deno?: unknown }).Deno;
}

expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to register diagnostics-channel injection hooks (boom)'),
);
expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.runtimeUnavailable).toBe(true);
});

it('warns once when a module fails to transform because the transformer was tree-shaken', () => {
registerDiagnosticsChannelInjection();
const onDiagnostics = diagnosticsCallback();
Expand Down
Loading