diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts
index 4786fe1e4aa9..6cfccfbd2714 100644
--- a/packages/nuxt/src/module.ts
+++ b/packages/nuxt/src/module.ts
@@ -60,7 +60,13 @@ function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined {
const cwd = process.cwd();
const filePath = possibleFileExtensions
- .map(e => path.resolve(path.join(cwd, `sentry.${type}.config.${e}`)))
+ .map(e =>
+ path.resolve(
+ type === 'server'
+ ? path.join(cwd, 'public', `instrument.${type}.${e}`)
+ : path.join(cwd, `sentry.${type}.config.${e}`),
+ ),
+ )
.find(filename => fs.existsSync(filename));
return filePath ? path.basename(filePath) : undefined;
diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts
index f0a815375ec8..476037ac980b 100644
--- a/packages/nuxt/src/runtime/plugins/sentry.server.ts
+++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts
@@ -1,7 +1,8 @@
import { captureException } from '@sentry/node';
import { H3Error } from 'h3';
import { defineNitroPlugin } from 'nitropack/runtime';
-import { extractErrorContext } from '../utils';
+import type { NuxtRenderHTMLContext } from 'nuxt/app';
+import { addSentryTracingMetaTags, extractErrorContext } from '../utils';
export default defineNitroPlugin(nitroApp => {
nitroApp.hooks.hook('error', (error, errorContext) => {
@@ -20,4 +21,9 @@ export default defineNitroPlugin(nitroApp => {
mechanism: { handled: false },
});
});
+
+ // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context
+ nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext) => {
+ addSentryTracingMetaTags(html.head);
+ });
});
diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts
index 07294806a546..b7c038ddd505 100644
--- a/packages/nuxt/src/runtime/utils.ts
+++ b/packages/nuxt/src/runtime/utils.ts
@@ -1,6 +1,10 @@
+import { getActiveSpan, getRootSpan, spanToTraceHeader } from '@sentry/core';
+import { getDynamicSamplingContextFromSpan } from '@sentry/opentelemetry';
import type { Context } from '@sentry/types';
import { dropUndefinedKeys } from '@sentry/utils';
+import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
import type { CapturedErrorContext } from 'nitropack';
+import type { NuxtRenderHTMLContext } from 'nuxt/app';
/**
* Extracts the relevant context information from the error context (H3Event in Nitro Error)
@@ -26,3 +30,23 @@ export function extractErrorContext(errorContext: CapturedErrorContext): Context
return dropUndefinedKeys(structuredContext);
}
+
+/**
+ * Adds Sentry tracing tags to the returned html page.
+ *
+ * Exported only for testing
+ */
+export function addSentryTracingMetaTags(head: NuxtRenderHTMLContext['head']): void {
+ const activeSpan = getActiveSpan();
+ const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
+
+ if (rootSpan) {
+ const traceParentData = spanToTraceHeader(rootSpan);
+ const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader(
+ getDynamicSamplingContextFromSpan(rootSpan),
+ );
+
+ head.push(``);
+ head.push(``);
+ }
+}
diff --git a/packages/nuxt/test/server/runtime/plugin.test.ts b/packages/nuxt/test/server/runtime/plugin.test.ts
new file mode 100644
index 000000000000..518b20026cbd
--- /dev/null
+++ b/packages/nuxt/test/server/runtime/plugin.test.ts
@@ -0,0 +1,68 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { addSentryTracingMetaTags } from '../../../src/runtime/utils';
+
+const mockReturns = vi.hoisted(() => {
+ return {
+ traceHeader: 'trace-header',
+ baggageHeader: 'baggage-header',
+ };
+});
+
+vi.mock('@sentry/core', async () => {
+ const actual = await vi.importActual('@sentry/core');
+
+ return {
+ ...actual,
+ getActiveSpan: vi.fn().mockReturnValue({ spanId: '123' }),
+ getRootSpan: vi.fn().mockReturnValue({ spanId: 'root123' }),
+ spanToTraceHeader: vi.fn(() => mockReturns.traceHeader),
+ };
+});
+
+vi.mock('@sentry/opentelemetry', async () => {
+ const actual = await vi.importActual('@sentry/opentelemetry');
+
+ return {
+ ...actual,
+ getDynamicSamplingContextFromSpan: vi.fn().mockReturnValue('contextValue'),
+ };
+});
+
+vi.mock('@sentry/utils', async () => {
+ const actual = await vi.importActual('@sentry/utils');
+
+ return {
+ ...actual,
+ dynamicSamplingContextToSentryBaggageHeader: vi.fn().mockReturnValue(mockReturns.baggageHeader),
+ };
+});
+
+describe('addSentryTracingMetaTags', () => {
+ afterEach(() => {
+ vi.resetAllMocks();
+ });
+
+ it('should add meta tags when there is an active root span', () => {
+ const head: string[] = [];
+ addSentryTracingMetaTags(head);
+
+ expect(head).toContain(``);
+ expect(head).toContain(``);
+ });
+
+ it('should not add meta tags when there is no active root span', () => {
+ vi.doMock('@sentry/core', async () => {
+ const actual = await vi.importActual('@sentry/core');
+
+ return {
+ ...actual,
+ getActiveSpan: vi.fn().mockReturnValue(undefined),
+ };
+ });
+
+ const head: string[] = [];
+ addSentryTracingMetaTags(head);
+
+ expect(head).toHaveLength(0);
+ });
+});