Skip to content

Commit 300dce1

Browse files
authored
Merge pull request #17984 from getsentry/prepare-release/10.21.0
meta(changelog): Update changelog for 10.21.0
2 parents cedef2e + 5bc35a7 commit 300dce1

File tree

162 files changed

+7158
-998
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

162 files changed

+7158
-998
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,6 @@ packages/gatsby/gatsby-node.d.ts
6161
# intellij
6262
*.iml
6363
/**/.wrangler/*
64+
65+
#junit reports
66+
packages/**/*.junit.xml

.size-limit.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ module.exports = [
4040
gzip: true,
4141
limit: '41 KB',
4242
},
43+
{
44+
name: '@sentry/browser (incl. Tracing, Profiling)',
45+
path: 'packages/browser/build/npm/esm/index.js',
46+
import: createImport('init', 'browserTracingIntegration', 'browserProfilingIntegration'),
47+
gzip: true,
48+
limit: '48 KB',
49+
},
4350
{
4451
name: '@sentry/browser (incl. Tracing, Replay)',
4552
path: 'packages/browser/build/npm/esm/index.js',
@@ -75,7 +82,7 @@ module.exports = [
7582
path: 'packages/browser/build/npm/esm/index.js',
7683
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
7784
gzip: true,
78-
limit: '84 KB',
85+
limit: '85 KB',
7986
},
8087
{
8188
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
@@ -206,7 +213,7 @@ module.exports = [
206213
import: createImport('init'),
207214
ignore: ['next/router', 'next/constants'],
208215
gzip: true,
209-
limit: '45 KB',
216+
limit: '46 KB',
210217
},
211218
// SvelteKit SDK (ESM)
212219
{

CHANGELOG.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,53 @@
44

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

7+
## 10.21.0
8+
9+
### Important Changes
10+
11+
- **feat(browserProfiling): Add `trace` lifecycle mode for UI profiling ([#17619](https://github.com/getsentry/sentry-javascript/pull/17619))**
12+
13+
Adds a new `trace` lifecycle mode for UI profiling, allowing profiles to be captured for the duration of a trace. A `manual` mode will be added in a future release.
14+
15+
- **feat(nuxt): Instrument Database ([#17899](https://github.com/getsentry/sentry-javascript/pull/17899))**
16+
17+
Adds instrumentation for Nuxt database operations, enabling better performance tracking of database queries.
18+
19+
- **feat(nuxt): Instrument server cache API ([#17886](https://github.com/getsentry/sentry-javascript/pull/17886))**
20+
21+
Adds instrumentation for Nuxt's server cache API, providing visibility into cache operations.
22+
23+
- **feat(nuxt): Instrument storage API ([#17858](https://github.com/getsentry/sentry-javascript/pull/17858))**
24+
25+
Adds instrumentation for Nuxt's storage API, enabling tracking of storage operations.
26+
27+
### Other Changes
28+
29+
- feat(browser): Add `onRequestSpanEnd` hook to browser tracing integration ([#17884](https://github.com/getsentry/sentry-javascript/pull/17884))
30+
- feat(nextjs): Support Next.js proxy files ([#17926](https://github.com/getsentry/sentry-javascript/pull/17926))
31+
- feat(replay): Record outcome when event buffer size exceeded ([#17946](https://github.com/getsentry/sentry-javascript/pull/17946))
32+
- fix(cloudflare): copy execution context in durable objects and handlers ([#17786](https://github.com/getsentry/sentry-javascript/pull/17786))
33+
- fix(core): Fix and add missing cache attributes in Vercel AI ([#17982](https://github.com/getsentry/sentry-javascript/pull/17982))
34+
- fix(core): Improve uuid performance ([#17938](https://github.com/getsentry/sentry-javascript/pull/17938))
35+
- fix(ember): Use updated version for `clean-css` ([#17979](https://github.com/getsentry/sentry-javascript/pull/17979))
36+
- fix(nextjs): Don't set experimental instrumentation hook flag for next 16 ([#17978](https://github.com/getsentry/sentry-javascript/pull/17978))
37+
- fix(nextjs): Inconsistent transaction naming for i18n routing ([#17927](https://github.com/getsentry/sentry-javascript/pull/17927))
38+
- fix(nextjs): Update bundler detection ([#17976](https://github.com/getsentry/sentry-javascript/pull/17976))
39+
40+
<details>
41+
<summary> <strong>Internal Changes</strong> </summary>
42+
43+
- build: Update to typescript 5.8.0 ([#17710](https://github.com/getsentry/sentry-javascript/pull/17710))
44+
- chore: Add external contributor to CHANGELOG.md ([#17949](https://github.com/getsentry/sentry-javascript/pull/17949))
45+
- chore(build): Upgrade nodemon to 3.1.10 ([#17956](https://github.com/getsentry/sentry-javascript/pull/17956))
46+
- chore(ci): Fix external contributor action when multiple contributions existed ([#17950](https://github.com/getsentry/sentry-javascript/pull/17950))
47+
- chore(solid): Remove unnecessary import from README ([#17947](https://github.com/getsentry/sentry-javascript/pull/17947))
48+
- test(nextjs): Fix proxy/middleware test ([#17970](https://github.com/getsentry/sentry-javascript/pull/17970))
49+
50+
</details>
51+
52+
Work in this release was contributed by @0xbad0c0d3. Thank you for your contribution!
53+
754
## 10.20.0
855

956
### Important Changes
@@ -42,7 +89,7 @@
4289
- chore: Add external contributor to CHANGELOG.md ([#17940](https://github.com/getsentry/sentry-javascript/pull/17940))
4390
</details>
4491

45-
Work in this release was contributed by @seoyeon9888, @madhuchavva and @thedanchez . Thank you for your contributions!
92+
Work in this release was contributed by @seoyeon9888, @madhuchavva and @thedanchez. Thank you for your contributions!
4693

4794
## 10.19.0
4895

dev-packages/browser-integration-tests/suites/profiling/legacyMode/subject.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function fibonacci(n) {
1717
return fibonacci(n - 1) + fibonacci(n - 2);
1818
}
1919

20-
await Sentry.startSpanManual({ name: 'root-fibonacci-2', parentSpan: null, forceTransaction: true }, async span => {
20+
await Sentry.startSpanManual({ name: 'root-fibonacci', parentSpan: null, forceTransaction: true }, async span => {
2121
fibonacci(30);
2222

2323
// Timeout to prevent flaky tests. Integration samples every 20ms, if function is too fast it might not get sampled

dev-packages/browser-integration-tests/suites/profiling/legacyMode/test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,16 @@ sentryTest('sends profile envelope in legacy mode', async ({ page, getLocalTestU
7373
expect(profile.frames.length).toBeGreaterThan(0);
7474
for (const frame of profile.frames) {
7575
expect(frame).toHaveProperty('function');
76-
expect(frame).toHaveProperty('abs_path');
77-
expect(frame).toHaveProperty('lineno');
78-
expect(frame).toHaveProperty('colno');
79-
8076
expect(typeof frame.function).toBe('string');
81-
expect(typeof frame.abs_path).toBe('string');
82-
expect(typeof frame.lineno).toBe('number');
83-
expect(typeof frame.colno).toBe('number');
77+
78+
if (frame.function !== 'fetch' && frame.function !== 'setTimeout') {
79+
expect(frame).toHaveProperty('abs_path');
80+
expect(frame).toHaveProperty('lineno');
81+
expect(frame).toHaveProperty('colno');
82+
expect(typeof frame.abs_path).toBe('string');
83+
expect(typeof frame.lineno).toBe('number');
84+
expect(typeof frame.colno).toBe('number');
85+
}
8486
}
8587

8688
const functionNames = profile.frames.map(frame => frame.function).filter(name => name !== '');
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { browserProfilingIntegration } from '@sentry/browser';
3+
4+
window.Sentry = Sentry;
5+
6+
Sentry.init({
7+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8+
integrations: [browserProfilingIntegration()],
9+
tracesSampleRate: 1,
10+
profileSessionSampleRate: 1,
11+
profileLifecycle: 'trace',
12+
});
13+
14+
function largeSum(amount = 1000000) {
15+
let sum = 0;
16+
for (let i = 0; i < amount; i++) {
17+
sum += Math.sqrt(i) * Math.sin(i);
18+
}
19+
}
20+
21+
function fibonacci(n) {
22+
if (n <= 1) {
23+
return n;
24+
}
25+
return fibonacci(n - 1) + fibonacci(n - 2);
26+
}
27+
28+
// Create two NON-overlapping root spans so that the profiler stops and emits a chunk
29+
// after each span (since active root span count returns to 0 between them).
30+
await Sentry.startSpanManual({ name: 'root-fibonacci-1', parentSpan: null, forceTransaction: true }, async span => {
31+
fibonacci(40);
32+
// Ensure we cross the sampling interval to avoid flakes
33+
await new Promise(resolve => setTimeout(resolve, 25));
34+
span.end();
35+
});
36+
37+
// Small delay to ensure the first chunk is collected and sent
38+
await new Promise(r => setTimeout(r, 25));
39+
40+
await Sentry.startSpanManual({ name: 'root-largeSum-2', parentSpan: null, forceTransaction: true }, async span => {
41+
largeSum();
42+
// Ensure we cross the sampling interval to avoid flakes
43+
await new Promise(resolve => setTimeout(resolve, 25));
44+
span.end();
45+
});
46+
47+
const client = Sentry.getClient();
48+
await client?.flush(5000);
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { expect } from '@playwright/test';
2+
import type { ProfileChunkEnvelope } from '@sentry/core';
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import {
5+
countEnvelopes,
6+
getMultipleSentryEnvelopeRequests,
7+
properFullEnvelopeRequestParser,
8+
shouldSkipTracingTest,
9+
} from '../../../utils/helpers';
10+
11+
sentryTest(
12+
'does not send profile envelope when document-policy is not set',
13+
async ({ page, getLocalTestUrl, browserName }) => {
14+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
15+
// Profiling only works when tracing is enabled
16+
sentryTest.skip();
17+
}
18+
19+
const url = await getLocalTestUrl({ testDir: __dirname });
20+
21+
// Assert that no profile_chunk envelope is sent without policy header
22+
const chunkCount = await countEnvelopes(page, { url, envelopeType: 'profile_chunk', timeout: 1500 });
23+
expect(chunkCount).toBe(0);
24+
},
25+
);
26+
27+
sentryTest(
28+
'sends profile_chunk envelopes in trace mode (multiple chunks)',
29+
async ({ page, getLocalTestUrl, browserName }) => {
30+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
31+
// Profiling only works when tracing is enabled
32+
sentryTest.skip();
33+
}
34+
35+
const url = await getLocalTestUrl({ testDir: __dirname, responseHeaders: { 'Document-Policy': 'js-profiling' } });
36+
37+
// Expect at least 2 chunks because subject creates two separate root spans,
38+
// causing the profiler to stop and emit a chunk after each root span ends.
39+
const profileChunkEnvelopes = await getMultipleSentryEnvelopeRequests<ProfileChunkEnvelope>(
40+
page,
41+
2,
42+
{ url, envelopeType: 'profile_chunk', timeout: 5000 },
43+
properFullEnvelopeRequestParser,
44+
);
45+
46+
expect(profileChunkEnvelopes.length).toBeGreaterThanOrEqual(2);
47+
48+
// Validate the first chunk thoroughly
49+
const profileChunkEnvelopeItem = profileChunkEnvelopes[0][1][0];
50+
const envelopeItemHeader = profileChunkEnvelopeItem[0];
51+
const envelopeItemPayload1 = profileChunkEnvelopeItem[1];
52+
53+
expect(envelopeItemHeader).toHaveProperty('type', 'profile_chunk');
54+
55+
expect(envelopeItemPayload1.profile).toBeDefined();
56+
expect(envelopeItemPayload1.version).toBe('2');
57+
expect(envelopeItemPayload1.platform).toBe('javascript');
58+
59+
// Required profile metadata (Sample Format V2)
60+
expect(typeof envelopeItemPayload1.profiler_id).toBe('string');
61+
expect(envelopeItemPayload1.profiler_id).toMatch(/^[a-f0-9]{32}$/);
62+
expect(typeof envelopeItemPayload1.chunk_id).toBe('string');
63+
expect(envelopeItemPayload1.chunk_id).toMatch(/^[a-f0-9]{32}$/);
64+
expect(envelopeItemPayload1.client_sdk).toBeDefined();
65+
expect(typeof envelopeItemPayload1.client_sdk.name).toBe('string');
66+
expect(typeof envelopeItemPayload1.client_sdk.version).toBe('string');
67+
expect(typeof envelopeItemPayload1.release).toBe('string');
68+
expect(envelopeItemPayload1.debug_meta).toBeDefined();
69+
expect(Array.isArray(envelopeItemPayload1?.debug_meta?.images)).toBe(true);
70+
71+
const profile1 = envelopeItemPayload1.profile;
72+
73+
expect(profile1.samples).toBeDefined();
74+
expect(profile1.stacks).toBeDefined();
75+
expect(profile1.frames).toBeDefined();
76+
expect(profile1.thread_metadata).toBeDefined();
77+
78+
// Samples
79+
expect(profile1.samples.length).toBeGreaterThanOrEqual(2);
80+
let previousTimestamp = Number.NEGATIVE_INFINITY;
81+
for (const sample of profile1.samples) {
82+
expect(typeof sample.stack_id).toBe('number');
83+
expect(sample.stack_id).toBeGreaterThanOrEqual(0);
84+
expect(sample.stack_id).toBeLessThan(profile1.stacks.length);
85+
86+
// In trace lifecycle mode, samples carry a numeric timestamp (ms since epoch or similar clock)
87+
expect(typeof (sample as any).timestamp).toBe('number');
88+
const ts = (sample as any).timestamp as number;
89+
expect(Number.isFinite(ts)).toBe(true);
90+
expect(ts).toBeGreaterThan(0);
91+
// Monotonic non-decreasing timestamps
92+
expect(ts).toBeGreaterThanOrEqual(previousTimestamp);
93+
previousTimestamp = ts;
94+
95+
expect(sample.thread_id).toBe('0'); // Should be main thread
96+
}
97+
98+
// Stacks
99+
expect(profile1.stacks.length).toBeGreaterThan(0);
100+
for (const stack of profile1.stacks) {
101+
expect(Array.isArray(stack)).toBe(true);
102+
for (const frameIndex of stack) {
103+
expect(typeof frameIndex).toBe('number');
104+
expect(frameIndex).toBeGreaterThanOrEqual(0);
105+
expect(frameIndex).toBeLessThan(profile1.frames.length);
106+
}
107+
}
108+
109+
// Frames
110+
expect(profile1.frames.length).toBeGreaterThan(0);
111+
for (const frame of profile1.frames) {
112+
expect(frame).toHaveProperty('function');
113+
expect(typeof frame.function).toBe('string');
114+
115+
if (frame.function !== 'fetch' && frame.function !== 'setTimeout') {
116+
expect(frame).toHaveProperty('abs_path');
117+
expect(frame).toHaveProperty('lineno');
118+
expect(frame).toHaveProperty('colno');
119+
expect(typeof frame.abs_path).toBe('string');
120+
expect(typeof frame.lineno).toBe('number');
121+
expect(typeof frame.colno).toBe('number');
122+
}
123+
}
124+
125+
const functionNames = profile1.frames.map(frame => frame.function).filter(name => name !== '');
126+
127+
if ((process.env.PW_BUNDLE || '').endsWith('min')) {
128+
// In bundled mode, function names are minified
129+
expect(functionNames.length).toBeGreaterThan(0);
130+
expect((functionNames as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings
131+
} else {
132+
expect(functionNames).toEqual(
133+
expect.arrayContaining([
134+
'_startRootSpan',
135+
'withScope',
136+
'createChildOrRootSpan',
137+
'startSpanManual',
138+
'startJSSelfProfile',
139+
140+
// first function is captured (other one is in other chunk)
141+
'fibonacci',
142+
]),
143+
);
144+
}
145+
146+
expect(profile1.thread_metadata).toHaveProperty('0');
147+
expect(profile1.thread_metadata['0']).toHaveProperty('name');
148+
expect(profile1.thread_metadata['0'].name).toBe('main');
149+
150+
// Test that profile duration makes sense (should be > 20ms based on test setup)
151+
const startTimeSec = (profile1.samples[0] as any).timestamp as number;
152+
const endTimeSec = (profile1.samples[profile1.samples.length - 1] as any).timestamp as number;
153+
const durationSec = endTimeSec - startTimeSec;
154+
155+
// Should be at least 20ms based on our setTimeout(21) in the test
156+
expect(durationSec).toBeGreaterThan(0.2);
157+
158+
// === PROFILE CHUNK 2 ===
159+
160+
const profileChunkEnvelopeItem2 = profileChunkEnvelopes[1][1][0];
161+
const envelopeItemHeader2 = profileChunkEnvelopeItem2[0];
162+
const envelopeItemPayload2 = profileChunkEnvelopeItem2[1];
163+
164+
// Basic sanity on the second chunk: has correct envelope type and structure
165+
expect(envelopeItemHeader2).toHaveProperty('type', 'profile_chunk');
166+
expect(envelopeItemPayload2.profile).toBeDefined();
167+
expect(envelopeItemPayload2.version).toBe('2');
168+
expect(envelopeItemPayload2.platform).toBe('javascript');
169+
170+
// Required profile metadata (Sample Format V2)
171+
// https://develop.sentry.dev/sdk/telemetry/profiles/sample-format-v2/
172+
expect(typeof envelopeItemPayload2.profiler_id).toBe('string');
173+
expect(envelopeItemPayload2.profiler_id).toMatch(/^[a-f0-9]{32}$/);
174+
expect(typeof envelopeItemPayload2.chunk_id).toBe('string');
175+
expect(envelopeItemPayload2.chunk_id).toMatch(/^[a-f0-9]{32}$/);
176+
expect(envelopeItemPayload2.client_sdk).toBeDefined();
177+
expect(typeof envelopeItemPayload2.client_sdk.name).toBe('string');
178+
expect(typeof envelopeItemPayload2.client_sdk.version).toBe('string');
179+
expect(typeof envelopeItemPayload2.release).toBe('string');
180+
expect(envelopeItemPayload2.debug_meta).toBeDefined();
181+
expect(Array.isArray(envelopeItemPayload2?.debug_meta?.images)).toBe(true);
182+
183+
const profile2 = envelopeItemPayload2.profile;
184+
185+
const functionNames2 = profile2.frames.map(frame => frame.function).filter(name => name !== '');
186+
187+
if ((process.env.PW_BUNDLE || '').endsWith('min')) {
188+
// In bundled mode, function names are minified
189+
expect(functionNames2.length).toBeGreaterThan(0);
190+
expect((functionNames2 as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings
191+
} else {
192+
expect(functionNames2).toEqual(
193+
expect.arrayContaining([
194+
'_startRootSpan',
195+
'withScope',
196+
'createChildOrRootSpan',
197+
'startSpanManual',
198+
'startJSSelfProfile',
199+
200+
// second function is captured (other one is in other chunk)
201+
'largeSum',
202+
]),
203+
);
204+
}
205+
},
206+
);

0 commit comments

Comments
 (0)