Skip to content

meta(changelog): Update changelog for 9.30.0 #16592

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 17, 2025
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

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

## 9.30.0

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

## 9.29.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as Sentry from '@sentry/browser';

// Create measures BEFORE SDK initializes

// Create a measure with detail
const measure = performance.measure('restricted-test-measure', {
start: performance.now(),
end: performance.now() + 1,
detail: { test: 'initial-value' },
});

// Simulate Firefox's permission denial by overriding the detail getter
// This mimics the actual Firefox behavior where accessing detail throws
Object.defineProperty(measure, 'detail', {
get() {
throw new DOMException('Permission denied to access object', 'SecurityError');
},
configurable: false,
enumerable: true,
});

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [
Sentry.browserTracingIntegration({
idleTimeout: 9000,
}),
],
tracesSampleRate: 1,
});

// Also create a normal measure to ensure SDK still works
performance.measure('normal-measure', {
start: performance.now(),
end: performance.now() + 50,
detail: 'this-should-work',
});

// Create a measure with complex detail object
performance.measure('complex-detail-measure', {
start: performance.now(),
end: performance.now() + 25,
detail: {
nested: {
array: [1, 2, 3],
object: {
key: 'value',
},
},
metadata: {
type: 'test',
version: '1.0',
tags: ['complex', 'nested', 'object'],
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';

// This is a regression test for https://github.com/getsentry/sentry-javascript/issues/16347

sentryTest(
'should handle permission denial gracefully and still create measure spans',
async ({ getLocalTestUrl, page, browserName }) => {
// Skip test on webkit because we can't validate the detail in the browser
if (shouldSkipTracingTest() || browserName === 'webkit') {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

// Find all measure spans
const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure');
expect(measureSpans?.length).toBe(3); // All three measures should create spans

// Test 1: Verify the restricted-test-measure span exists but has no detail
const restrictedMeasure = measureSpans?.find(span => span.description === 'restricted-test-measure');
expect(restrictedMeasure).toBeDefined();
expect(restrictedMeasure?.data).toMatchObject({
'sentry.op': 'measure',
'sentry.origin': 'auto.resource.browser.metrics',
});

// Verify no detail attributes were added due to the permission error
const restrictedDataKeys = Object.keys(restrictedMeasure?.data || {});
const restrictedDetailKeys = restrictedDataKeys.filter(key => key.includes('detail'));
expect(restrictedDetailKeys).toHaveLength(0);

// Test 2: Verify the normal measure still captures detail correctly
const normalMeasure = measureSpans?.find(span => span.description === 'normal-measure');
expect(normalMeasure).toBeDefined();
expect(normalMeasure?.data).toMatchObject({
'sentry.browser.measure.detail': 'this-should-work',
'sentry.op': 'measure',
'sentry.origin': 'auto.resource.browser.metrics',
});

// Test 3: Verify the complex detail object is captured correctly
const complexMeasure = measureSpans?.find(span => span.description === 'complex-detail-measure');
expect(complexMeasure).toBeDefined();
expect(complexMeasure?.data).toMatchObject({
'sentry.op': 'measure',
'sentry.origin': 'auto.resource.browser.metrics',
// The entire nested object is stringified as a single value
'sentry.browser.measure.detail.nested': JSON.stringify({
array: [1, 2, 3],
object: {
key: 'value',
},
}),
'sentry.browser.measure.detail.metadata': JSON.stringify({
type: 'test',
version: '1.0',
tags: ['complex', 'nested', 'object'],
}),
});
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ performance.measure('Next.js-before-hydration', {
window.Sentry = Sentry;

Sentry.init({
debug: true,
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [
Sentry.browserTracingIntegration({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';
import * as Sentry from '@sentry/nextjs';

export const dynamic = 'force-dynamic';

async function runAITest() {
// First span - telemetry should be enabled automatically but no input/output recorded when sendDefaultPii: true
const result1 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'First span here!',
}),
}),
prompt: 'Where is the first span?',
});

// Second span - explicitly enabled telemetry, should record inputs/outputs
const result2 = await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Second span here!',
}),
}),
prompt: 'Where is the second span?',
});

// Third span - with tool calls and tool results
const result3 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (args) => {
return `Weather in ${args.location}: Sunny, 72°F`;
},
},
},
prompt: 'What is the weather in San Francisco?',
});

// Fourth span - explicitly disabled telemetry, should not be captured
const result4 = await generateText({
experimental_telemetry: { isEnabled: false },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Third span here!',
}),
}),
prompt: 'Where is the third span?',
});

return {
result1: result1.text,
result2: result2.text,
result3: result3.text,
result4: result4.text,
};
}

export default async function Page() {
const results = await Sentry.startSpan(
{ op: 'function', name: 'ai-test' },
async () => {
return await runAITest();
}
);

return (
<div>
<h1>AI Test Results</h1>
<pre id="ai-results">{JSON.stringify(results, null, 2)}</pre>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
"ai": "^3.0.0",
"next": "15.3.0-canary.33",
"react": "beta",
"react-dom": "beta",
"typescript": "~5.0.0"
"typescript": "~5.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@playwright/test": "~1.50.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ Sentry.init({
// We are doing a lot of events at once in this test
bufferSize: 1000,
},
integrations: [
Sentry.vercelAIIntegration(),
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('should create AI spans with correct attributes', async ({ page }) => {
const aiTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
return transactionEvent.transaction === 'GET /ai-test';
});

await page.goto('/ai-test');

const aiTransaction = await aiTransactionPromise;

expect(aiTransaction).toBeDefined();
expect(aiTransaction.transaction).toBe('GET /ai-test');

const spans = aiTransaction.spans || [];

// We expect spans for the first 3 AI calls (4th is disabled)
// Each generateText call should create 2 spans: one for the pipeline and one for doGenerate
// Plus a span for the tool call
// TODO: For now, this is sadly not fully working - the monkey patching of the ai package is not working
// because of this, only spans that are manually opted-in at call time will be captured
// this may be fixed by https://github.com/vercel/ai/pull/6716 in the future
const aiPipelineSpans = spans.filter(span => span.op === 'ai.pipeline.generate_text');
const aiGenerateSpans = spans.filter(span => span.op === 'gen_ai.generate_text');
const toolCallSpans = spans.filter(span => span.op === 'gen_ai.execute_tool');

expect(aiPipelineSpans.length).toBeGreaterThanOrEqual(1);
expect(aiGenerateSpans.length).toBeGreaterThanOrEqual(1);
expect(toolCallSpans.length).toBeGreaterThanOrEqual(0);

// First AI call - should have telemetry enabled and record inputs/outputs (sendDefaultPii: true)
/* const firstPipelineSpan = aiPipelineSpans[0];
expect(firstPipelineSpan?.data?.['ai.model.id']).toBe('mock-model-id');
expect(firstPipelineSpan?.data?.['ai.model.provider']).toBe('mock-provider');
expect(firstPipelineSpan?.data?.['ai.prompt']).toContain('Where is the first span?');
expect(firstPipelineSpan?.data?.['gen_ai.response.text']).toBe('First span here!');
expect(firstPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(10);
expect(firstPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(20); */

// Second AI call - explicitly enabled telemetry
const secondPipelineSpan = aiPipelineSpans[0];
expect(secondPipelineSpan?.data?.['ai.prompt']).toContain('Where is the second span?');
expect(secondPipelineSpan?.data?.['gen_ai.response.text']).toContain('Second span here!');

// Third AI call - with tool calls
/* const thirdPipelineSpan = aiPipelineSpans[2];
expect(thirdPipelineSpan?.data?.['ai.response.finishReason']).toBe('tool-calls');
expect(thirdPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(15);
expect(thirdPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(25); */

// Tool call span
/* const toolSpan = toolCallSpans[0];
expect(toolSpan?.data?.['ai.toolCall.name']).toBe('getWeather');
expect(toolSpan?.data?.['ai.toolCall.id']).toBe('call-1');
expect(toolSpan?.data?.['ai.toolCall.args']).toContain('San Francisco');
expect(toolSpan?.data?.['ai.toolCall.result']).toContain('Sunny, 72°F'); */

// Verify the fourth call was not captured (telemetry disabled)
const promptsInSpans = spans
.map(span => span.data?.['ai.prompt'])
.filter((prompt): prompt is string => prompt !== undefined);
const hasDisabledPrompt = promptsInSpans.some(prompt => prompt.includes('Where is the third span?'));
expect(hasDisabledPrompt).toBe(false);

// Verify results are displayed on the page
const resultsText = await page.locator('#ai-results').textContent();
expect(resultsText).toContain('First span here!');
expect(resultsText).toContain('Second span here!');
expect(resultsText).toContain('Tool call completed!');
expect(resultsText).toContain('Third span here!');
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ test('Sends a transaction for a request to app router', async ({ page }) => {
headers: expect.objectContaining({
'user-agent': expect.any(String),
}),
url: expect.stringContaining('/server-component/parameter/1337/42'),
});

// The transaction should not contain any spans with the same name as the transaction
Expand Down Expand Up @@ -123,4 +124,12 @@ test('Should capture an error and transaction for a app router page', async ({ p
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();

// Modules are set for Next.js
expect(errorEvent.modules).toEqual(
expect.objectContaining({
'@sentry/nextjs': expect.any(String),
'@playwright/test': expect.any(String),
}),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
transport: loggingTransport,
});
Loading
Loading