Skip to content

Commit 7dc6a25

Browse files
authored
feat(nuxt): Add connected tracing meta tags (#13098)
Also changes the `findDefaultSdkInitFile` function as the backend-related file is located and named differently than the client config file (`sentry.client.config.ts` and `public/instrument.server.mjs`). closes #13097
1 parent 8f037ac commit 7dc6a25

File tree

4 files changed

+106
-2
lines changed

4 files changed

+106
-2
lines changed

packages/nuxt/src/module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,13 @@ function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined {
6060

6161
const cwd = process.cwd();
6262
const filePath = possibleFileExtensions
63-
.map(e => path.resolve(path.join(cwd, `sentry.${type}.config.${e}`)))
63+
.map(e =>
64+
path.resolve(
65+
type === 'server'
66+
? path.join(cwd, 'public', `instrument.${type}.${e}`)
67+
: path.join(cwd, `sentry.${type}.config.${e}`),
68+
),
69+
)
6470
.find(filename => fs.existsSync(filename));
6571

6672
return filePath ? path.basename(filePath) : undefined;

packages/nuxt/src/runtime/plugins/sentry.server.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { captureException } from '@sentry/node';
22
import { H3Error } from 'h3';
33
import { defineNitroPlugin } from 'nitropack/runtime';
4-
import { extractErrorContext } from '../utils';
4+
import type { NuxtRenderHTMLContext } from 'nuxt/app';
5+
import { addSentryTracingMetaTags, extractErrorContext } from '../utils';
56

67
export default defineNitroPlugin(nitroApp => {
78
nitroApp.hooks.hook('error', (error, errorContext) => {
@@ -20,4 +21,9 @@ export default defineNitroPlugin(nitroApp => {
2021
mechanism: { handled: false },
2122
});
2223
});
24+
25+
// @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context
26+
nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext) => {
27+
addSentryTracingMetaTags(html.head);
28+
});
2329
});

packages/nuxt/src/runtime/utils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { getActiveSpan, getRootSpan, spanToTraceHeader } from '@sentry/core';
2+
import { getDynamicSamplingContextFromSpan } from '@sentry/opentelemetry';
13
import type { Context } from '@sentry/types';
24
import { dropUndefinedKeys } from '@sentry/utils';
5+
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
36
import type { CapturedErrorContext } from 'nitropack';
7+
import type { NuxtRenderHTMLContext } from 'nuxt/app';
48

59
/**
610
* Extracts the relevant context information from the error context (H3Event in Nitro Error)
@@ -26,3 +30,23 @@ export function extractErrorContext(errorContext: CapturedErrorContext): Context
2630

2731
return dropUndefinedKeys(structuredContext);
2832
}
33+
34+
/**
35+
* Adds Sentry tracing <meta> tags to the returned html page.
36+
*
37+
* Exported only for testing
38+
*/
39+
export function addSentryTracingMetaTags(head: NuxtRenderHTMLContext['head']): void {
40+
const activeSpan = getActiveSpan();
41+
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
42+
43+
if (rootSpan) {
44+
const traceParentData = spanToTraceHeader(rootSpan);
45+
const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader(
46+
getDynamicSamplingContextFromSpan(rootSpan),
47+
);
48+
49+
head.push(`<meta name="sentry-trace" content="${traceParentData}"/>`);
50+
head.push(`<meta name="baggage" content="${dynamicSamplingContext}"/>`);
51+
}
52+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
import { addSentryTracingMetaTags } from '../../../src/runtime/utils';
3+
4+
const mockReturns = vi.hoisted(() => {
5+
return {
6+
traceHeader: 'trace-header',
7+
baggageHeader: 'baggage-header',
8+
};
9+
});
10+
11+
vi.mock('@sentry/core', async () => {
12+
const actual = await vi.importActual('@sentry/core');
13+
14+
return {
15+
...actual,
16+
getActiveSpan: vi.fn().mockReturnValue({ spanId: '123' }),
17+
getRootSpan: vi.fn().mockReturnValue({ spanId: 'root123' }),
18+
spanToTraceHeader: vi.fn(() => mockReturns.traceHeader),
19+
};
20+
});
21+
22+
vi.mock('@sentry/opentelemetry', async () => {
23+
const actual = await vi.importActual('@sentry/opentelemetry');
24+
25+
return {
26+
...actual,
27+
getDynamicSamplingContextFromSpan: vi.fn().mockReturnValue('contextValue'),
28+
};
29+
});
30+
31+
vi.mock('@sentry/utils', async () => {
32+
const actual = await vi.importActual('@sentry/utils');
33+
34+
return {
35+
...actual,
36+
dynamicSamplingContextToSentryBaggageHeader: vi.fn().mockReturnValue(mockReturns.baggageHeader),
37+
};
38+
});
39+
40+
describe('addSentryTracingMetaTags', () => {
41+
afterEach(() => {
42+
vi.resetAllMocks();
43+
});
44+
45+
it('should add meta tags when there is an active root span', () => {
46+
const head: string[] = [];
47+
addSentryTracingMetaTags(head);
48+
49+
expect(head).toContain(`<meta name="sentry-trace" content="${mockReturns.traceHeader}"/>`);
50+
expect(head).toContain(`<meta name="baggage" content="${mockReturns.baggageHeader}"/>`);
51+
});
52+
53+
it('should not add meta tags when there is no active root span', () => {
54+
vi.doMock('@sentry/core', async () => {
55+
const actual = await vi.importActual('@sentry/core');
56+
57+
return {
58+
...actual,
59+
getActiveSpan: vi.fn().mockReturnValue(undefined),
60+
};
61+
});
62+
63+
const head: string[] = [];
64+
addSentryTracingMetaTags(head);
65+
66+
expect(head).toHaveLength(0);
67+
});
68+
});

0 commit comments

Comments
 (0)