Skip to content

Commit 009acbd

Browse files
authored
Merge pull request #16592 from getsentry/prepare-release/9.30.0
2 parents afe49dd + 7464721 commit 009acbd

File tree

33 files changed

+1173
-106
lines changed

33 files changed

+1173
-106
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
## 9.30.0
8+
9+
- feat(nextjs): Add URL to tags of server components and generation functions issues ([#16500](https://github.com/getsentry/sentry-javascript/pull/16500))
10+
- feat(nextjs): Ensure all packages we auto-instrument are externalized ([#16552](https://github.com/getsentry/sentry-javascript/pull/16552))
11+
- feat(node): Automatically enable `vercelAiIntegration` when `ai` module is detected ([#16565](https://github.com/getsentry/sentry-javascript/pull/16565))
12+
- feat(node): Ensure `modulesIntegration` works in more environments ([#16566](https://github.com/getsentry/sentry-javascript/pull/16566))
13+
- feat(core): Don't gate user on logs with `sendDefaultPii` ([#16527](https://github.com/getsentry/sentry-javascript/pull/16527))
14+
- feat(browser): Add detail to measure spans and add regression tests ([#16557](https://github.com/getsentry/sentry-javascript/pull/16557))
15+
- feat(node): Update Vercel AI span attributes ([#16580](https://github.com/getsentry/sentry-javascript/pull/16580))
16+
- fix(opentelemetry): Ensure only orphaned spans of sent spans are sent ([#16590](https://github.com/getsentry/sentry-javascript/pull/16590))
17+
718
## 9.29.0
819

920
### Important Changes
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
// Create measures BEFORE SDK initializes
4+
5+
// Create a measure with detail
6+
const measure = performance.measure('restricted-test-measure', {
7+
start: performance.now(),
8+
end: performance.now() + 1,
9+
detail: { test: 'initial-value' },
10+
});
11+
12+
// Simulate Firefox's permission denial by overriding the detail getter
13+
// This mimics the actual Firefox behavior where accessing detail throws
14+
Object.defineProperty(measure, 'detail', {
15+
get() {
16+
throw new DOMException('Permission denied to access object', 'SecurityError');
17+
},
18+
configurable: false,
19+
enumerable: true,
20+
});
21+
22+
window.Sentry = Sentry;
23+
24+
Sentry.init({
25+
dsn: 'https://[email protected]/1337',
26+
integrations: [
27+
Sentry.browserTracingIntegration({
28+
idleTimeout: 9000,
29+
}),
30+
],
31+
tracesSampleRate: 1,
32+
});
33+
34+
// Also create a normal measure to ensure SDK still works
35+
performance.measure('normal-measure', {
36+
start: performance.now(),
37+
end: performance.now() + 50,
38+
detail: 'this-should-work',
39+
});
40+
41+
// Create a measure with complex detail object
42+
performance.measure('complex-detail-measure', {
43+
start: performance.now(),
44+
end: performance.now() + 25,
45+
detail: {
46+
nested: {
47+
array: [1, 2, 3],
48+
object: {
49+
key: 'value',
50+
},
51+
},
52+
metadata: {
53+
type: 'test',
54+
version: '1.0',
55+
tags: ['complex', 'nested', 'object'],
56+
},
57+
},
58+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/core';
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
5+
6+
// This is a regression test for https://github.com/getsentry/sentry-javascript/issues/16347
7+
8+
sentryTest(
9+
'should handle permission denial gracefully and still create measure spans',
10+
async ({ getLocalTestUrl, page, browserName }) => {
11+
// Skip test on webkit because we can't validate the detail in the browser
12+
if (shouldSkipTracingTest() || browserName === 'webkit') {
13+
sentryTest.skip();
14+
}
15+
16+
const url = await getLocalTestUrl({ testDir: __dirname });
17+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
18+
19+
// Find all measure spans
20+
const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure');
21+
expect(measureSpans?.length).toBe(3); // All three measures should create spans
22+
23+
// Test 1: Verify the restricted-test-measure span exists but has no detail
24+
const restrictedMeasure = measureSpans?.find(span => span.description === 'restricted-test-measure');
25+
expect(restrictedMeasure).toBeDefined();
26+
expect(restrictedMeasure?.data).toMatchObject({
27+
'sentry.op': 'measure',
28+
'sentry.origin': 'auto.resource.browser.metrics',
29+
});
30+
31+
// Verify no detail attributes were added due to the permission error
32+
const restrictedDataKeys = Object.keys(restrictedMeasure?.data || {});
33+
const restrictedDetailKeys = restrictedDataKeys.filter(key => key.includes('detail'));
34+
expect(restrictedDetailKeys).toHaveLength(0);
35+
36+
// Test 2: Verify the normal measure still captures detail correctly
37+
const normalMeasure = measureSpans?.find(span => span.description === 'normal-measure');
38+
expect(normalMeasure).toBeDefined();
39+
expect(normalMeasure?.data).toMatchObject({
40+
'sentry.browser.measure.detail': 'this-should-work',
41+
'sentry.op': 'measure',
42+
'sentry.origin': 'auto.resource.browser.metrics',
43+
});
44+
45+
// Test 3: Verify the complex detail object is captured correctly
46+
const complexMeasure = measureSpans?.find(span => span.description === 'complex-detail-measure');
47+
expect(complexMeasure).toBeDefined();
48+
expect(complexMeasure?.data).toMatchObject({
49+
'sentry.op': 'measure',
50+
'sentry.origin': 'auto.resource.browser.metrics',
51+
// The entire nested object is stringified as a single value
52+
'sentry.browser.measure.detail.nested': JSON.stringify({
53+
array: [1, 2, 3],
54+
object: {
55+
key: 'value',
56+
},
57+
}),
58+
'sentry.browser.measure.detail.metadata': JSON.stringify({
59+
type: 'test',
60+
version: '1.0',
61+
tags: ['complex', 'nested', 'object'],
62+
}),
63+
});
64+
},
65+
);

dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ performance.measure('Next.js-before-hydration', {
1010
window.Sentry = Sentry;
1111

1212
Sentry.init({
13-
debug: true,
1413
dsn: 'https://[email protected]/1337',
1514
integrations: [
1615
Sentry.browserTracingIntegration({
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { generateText } from 'ai';
2+
import { MockLanguageModelV1 } from 'ai/test';
3+
import { z } from 'zod';
4+
import * as Sentry from '@sentry/nextjs';
5+
6+
export const dynamic = 'force-dynamic';
7+
8+
async function runAITest() {
9+
// First span - telemetry should be enabled automatically but no input/output recorded when sendDefaultPii: true
10+
const result1 = await generateText({
11+
model: new MockLanguageModelV1({
12+
doGenerate: async () => ({
13+
rawCall: { rawPrompt: null, rawSettings: {} },
14+
finishReason: 'stop',
15+
usage: { promptTokens: 10, completionTokens: 20 },
16+
text: 'First span here!',
17+
}),
18+
}),
19+
prompt: 'Where is the first span?',
20+
});
21+
22+
// Second span - explicitly enabled telemetry, should record inputs/outputs
23+
const result2 = await generateText({
24+
experimental_telemetry: { isEnabled: true },
25+
model: new MockLanguageModelV1({
26+
doGenerate: async () => ({
27+
rawCall: { rawPrompt: null, rawSettings: {} },
28+
finishReason: 'stop',
29+
usage: { promptTokens: 10, completionTokens: 20 },
30+
text: 'Second span here!',
31+
}),
32+
}),
33+
prompt: 'Where is the second span?',
34+
});
35+
36+
// Third span - with tool calls and tool results
37+
const result3 = await generateText({
38+
model: new MockLanguageModelV1({
39+
doGenerate: async () => ({
40+
rawCall: { rawPrompt: null, rawSettings: {} },
41+
finishReason: 'tool-calls',
42+
usage: { promptTokens: 15, completionTokens: 25 },
43+
text: 'Tool call completed!',
44+
toolCalls: [
45+
{
46+
toolCallType: 'function',
47+
toolCallId: 'call-1',
48+
toolName: 'getWeather',
49+
args: '{ "location": "San Francisco" }',
50+
},
51+
],
52+
}),
53+
}),
54+
tools: {
55+
getWeather: {
56+
parameters: z.object({ location: z.string() }),
57+
execute: async (args) => {
58+
return `Weather in ${args.location}: Sunny, 72°F`;
59+
},
60+
},
61+
},
62+
prompt: 'What is the weather in San Francisco?',
63+
});
64+
65+
// Fourth span - explicitly disabled telemetry, should not be captured
66+
const result4 = await generateText({
67+
experimental_telemetry: { isEnabled: false },
68+
model: new MockLanguageModelV1({
69+
doGenerate: async () => ({
70+
rawCall: { rawPrompt: null, rawSettings: {} },
71+
finishReason: 'stop',
72+
usage: { promptTokens: 10, completionTokens: 20 },
73+
text: 'Third span here!',
74+
}),
75+
}),
76+
prompt: 'Where is the third span?',
77+
});
78+
79+
return {
80+
result1: result1.text,
81+
result2: result2.text,
82+
result3: result3.text,
83+
result4: result4.text,
84+
};
85+
}
86+
87+
export default async function Page() {
88+
const results = await Sentry.startSpan(
89+
{ op: 'function', name: 'ai-test' },
90+
async () => {
91+
return await runAITest();
92+
}
93+
);
94+
95+
return (
96+
<div>
97+
<h1>AI Test Results</h1>
98+
<pre id="ai-results">{JSON.stringify(results, null, 2)}</pre>
99+
</div>
100+
);
101+
}

dev-packages/e2e-tests/test-applications/nextjs-15/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
"@types/node": "^18.19.1",
1919
"@types/react": "18.0.26",
2020
"@types/react-dom": "18.0.9",
21+
"ai": "^3.0.0",
2122
"next": "15.3.0-canary.33",
2223
"react": "beta",
2324
"react-dom": "beta",
24-
"typescript": "~5.0.0"
25+
"typescript": "~5.0.0",
26+
"zod": "^3.22.4"
2527
},
2628
"devDependencies": {
2729
"@playwright/test": "~1.50.0",

dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ Sentry.init({
1010
// We are doing a lot of events at once in this test
1111
bufferSize: 1000,
1212
},
13+
integrations: [
14+
Sentry.vercelAIIntegration(),
15+
],
1316
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('should create AI spans with correct attributes', async ({ page }) => {
5+
const aiTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
6+
return transactionEvent.transaction === 'GET /ai-test';
7+
});
8+
9+
await page.goto('/ai-test');
10+
11+
const aiTransaction = await aiTransactionPromise;
12+
13+
expect(aiTransaction).toBeDefined();
14+
expect(aiTransaction.transaction).toBe('GET /ai-test');
15+
16+
const spans = aiTransaction.spans || [];
17+
18+
// We expect spans for the first 3 AI calls (4th is disabled)
19+
// Each generateText call should create 2 spans: one for the pipeline and one for doGenerate
20+
// Plus a span for the tool call
21+
// TODO: For now, this is sadly not fully working - the monkey patching of the ai package is not working
22+
// because of this, only spans that are manually opted-in at call time will be captured
23+
// this may be fixed by https://github.com/vercel/ai/pull/6716 in the future
24+
const aiPipelineSpans = spans.filter(span => span.op === 'ai.pipeline.generate_text');
25+
const aiGenerateSpans = spans.filter(span => span.op === 'gen_ai.generate_text');
26+
const toolCallSpans = spans.filter(span => span.op === 'gen_ai.execute_tool');
27+
28+
expect(aiPipelineSpans.length).toBeGreaterThanOrEqual(1);
29+
expect(aiGenerateSpans.length).toBeGreaterThanOrEqual(1);
30+
expect(toolCallSpans.length).toBeGreaterThanOrEqual(0);
31+
32+
// First AI call - should have telemetry enabled and record inputs/outputs (sendDefaultPii: true)
33+
/* const firstPipelineSpan = aiPipelineSpans[0];
34+
expect(firstPipelineSpan?.data?.['ai.model.id']).toBe('mock-model-id');
35+
expect(firstPipelineSpan?.data?.['ai.model.provider']).toBe('mock-provider');
36+
expect(firstPipelineSpan?.data?.['ai.prompt']).toContain('Where is the first span?');
37+
expect(firstPipelineSpan?.data?.['gen_ai.response.text']).toBe('First span here!');
38+
expect(firstPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(10);
39+
expect(firstPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(20); */
40+
41+
// Second AI call - explicitly enabled telemetry
42+
const secondPipelineSpan = aiPipelineSpans[0];
43+
expect(secondPipelineSpan?.data?.['ai.prompt']).toContain('Where is the second span?');
44+
expect(secondPipelineSpan?.data?.['gen_ai.response.text']).toContain('Second span here!');
45+
46+
// Third AI call - with tool calls
47+
/* const thirdPipelineSpan = aiPipelineSpans[2];
48+
expect(thirdPipelineSpan?.data?.['ai.response.finishReason']).toBe('tool-calls');
49+
expect(thirdPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(15);
50+
expect(thirdPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(25); */
51+
52+
// Tool call span
53+
/* const toolSpan = toolCallSpans[0];
54+
expect(toolSpan?.data?.['ai.toolCall.name']).toBe('getWeather');
55+
expect(toolSpan?.data?.['ai.toolCall.id']).toBe('call-1');
56+
expect(toolSpan?.data?.['ai.toolCall.args']).toContain('San Francisco');
57+
expect(toolSpan?.data?.['ai.toolCall.result']).toContain('Sunny, 72°F'); */
58+
59+
// Verify the fourth call was not captured (telemetry disabled)
60+
const promptsInSpans = spans
61+
.map(span => span.data?.['ai.prompt'])
62+
.filter((prompt): prompt is string => prompt !== undefined);
63+
const hasDisabledPrompt = promptsInSpans.some(prompt => prompt.includes('Where is the third span?'));
64+
expect(hasDisabledPrompt).toBe(false);
65+
66+
// Verify results are displayed on the page
67+
const resultsText = await page.locator('#ai-results').textContent();
68+
expect(resultsText).toContain('First span here!');
69+
expect(resultsText).toContain('Second span here!');
70+
expect(resultsText).toContain('Tool call completed!');
71+
expect(resultsText).toContain('Third span here!');
72+
});

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ test('Sends a transaction for a request to app router', async ({ page }) => {
3939
headers: expect.objectContaining({
4040
'user-agent': expect.any(String),
4141
}),
42+
url: expect.stringContaining('/server-component/parameter/1337/42'),
4243
});
4344

4445
// The transaction should not contain any spans with the same name as the transaction
@@ -123,4 +124,12 @@ test('Should capture an error and transaction for a app router page', async ({ p
123124
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
124125
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
125126
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
127+
128+
// Modules are set for Next.js
129+
expect(errorEvent.modules).toEqual(
130+
expect.objectContaining({
131+
'@sentry/nextjs': expect.any(String),
132+
'@playwright/test': expect.any(String),
133+
}),
134+
);
126135
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
});

0 commit comments

Comments
 (0)