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://public@dsn.ingest.sentry.io/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://public@dsn.ingest.sentry.io/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://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy