Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
480217f
feat(node): Allow selective tracking of `pino` loggers (#17933)
timfish Oct 15, 2025
7bc1380
chore(solid): Remove unnecessary import from README (#17947)
andreiborza Oct 16, 2025
b7a340a
Merge branch 'develop' into cg-sync-master-develop
chargome Oct 16, 2025
6230aed
fix(cloudflare): copy execution context in durable objects and handle…
0xbad0c0d3 Oct 16, 2025
a680e0c
feat(browser): Add `onRequestSpanEnd` hook to browser tracing integra…
logaretm Oct 16, 2025
a38eed1
test(nextjs): Skip webpack dev test for next 16
chargome Oct 16, 2025
a34d0bf
Merge pull request #17948 from getsentry/cg-sync-master-develop
chargome Oct 16, 2025
ee16e35
chore(ci): Fix external contributor action when multiple contribution…
andreiborza Oct 16, 2025
4dc6c7b
chore: Add external contributor to CHANGELOG.md (#17949)
HazAT Oct 16, 2025
8e3afe2
feat(nuxt): Instrument storage API (#17858)
logaretm Oct 16, 2025
66dc9a2
feat(nuxt): Instrument server cache API (#17886)
logaretm Oct 17, 2025
af83b87
fix(nextjs): Inconsistent transaction naming for i18n routing (#17927)
logaretm Oct 17, 2025
55f03e0
build: Update to typescript 5.8.0 (#17710)
mydea Oct 17, 2025
2e652f3
feat(nextjs): Support Next.js proxy files (#17926)
chargome Oct 17, 2025
f94b203
feat(nuxt): Instrument Database (#17899)
logaretm Oct 17, 2025
24ecd3a
feat(replay): Record outcome when event buffer size exceeded (#17946)
billyvg Oct 17, 2025
9742f9e
test(nextjs): Fix proxy/middleware test (#17970)
chargome Oct 20, 2025
f664505
chore(build): Upgrade nodemon to 3.1.10 (#17956)
AbhiPrasad Oct 20, 2025
910b40b
fix(nextjs): Update bundler detection (#17976)
chargome Oct 20, 2025
063ad99
fix(nextjs): Don't set experimental instrumentation hook flag for nex…
chargome Oct 21, 2025
1bd76c0
fix(ember): Use updated version for `clean-css` (#17979)
s1gr1d Oct 21, 2025
d551d23
feat(browserProfiling): Add `trace` lifecycle mode for UI profiling (…
s1gr1d Oct 21, 2025
75f68c7
fix(core): Fix and add missing cache attributes in Vercel AI (#17982)
RulaKhaled Oct 21, 2025
40bcc3d
fix(core): Improve uuid performance (#17938)
timfish Oct 21, 2025
5bc35a7
meta(changelog): Update changelog for 10.21.0
JPeer264 Oct 21, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ packages/gatsby/gatsby-node.d.ts
# intellij
*.iml
/**/.wrangler/*

#junit reports
packages/**/*.junit.xml
11 changes: 9 additions & 2 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ module.exports = [
gzip: true,
limit: '41 KB',
},
{
name: '@sentry/browser (incl. Tracing, Profiling)',
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'browserProfilingIntegration'),
gzip: true,
limit: '48 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay)',
path: 'packages/browser/build/npm/esm/index.js',
Expand Down Expand Up @@ -75,7 +82,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
gzip: true,
limit: '84 KB',
limit: '85 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
Expand Down Expand Up @@ -206,7 +213,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
limit: '45 KB',
limit: '46 KB',
},
// SvelteKit SDK (ESM)
{
Expand Down
49 changes: 48 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@

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

## 10.21.0

### Important Changes

- **feat(browserProfiling): Add `trace` lifecycle mode for UI profiling ([#17619](https://github.com/getsentry/sentry-javascript/pull/17619))**

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.

- **feat(nuxt): Instrument Database ([#17899](https://github.com/getsentry/sentry-javascript/pull/17899))**

Adds instrumentation for Nuxt database operations, enabling better performance tracking of database queries.

- **feat(nuxt): Instrument server cache API ([#17886](https://github.com/getsentry/sentry-javascript/pull/17886))**

Adds instrumentation for Nuxt's server cache API, providing visibility into cache operations.

- **feat(nuxt): Instrument storage API ([#17858](https://github.com/getsentry/sentry-javascript/pull/17858))**

Adds instrumentation for Nuxt's storage API, enabling tracking of storage operations.

### Other Changes

- feat(browser): Add `onRequestSpanEnd` hook to browser tracing integration ([#17884](https://github.com/getsentry/sentry-javascript/pull/17884))
- feat(nextjs): Support Next.js proxy files ([#17926](https://github.com/getsentry/sentry-javascript/pull/17926))
- feat(replay): Record outcome when event buffer size exceeded ([#17946](https://github.com/getsentry/sentry-javascript/pull/17946))
- fix(cloudflare): copy execution context in durable objects and handlers ([#17786](https://github.com/getsentry/sentry-javascript/pull/17786))
- fix(core): Fix and add missing cache attributes in Vercel AI ([#17982](https://github.com/getsentry/sentry-javascript/pull/17982))
- fix(core): Improve uuid performance ([#17938](https://github.com/getsentry/sentry-javascript/pull/17938))
- fix(ember): Use updated version for `clean-css` ([#17979](https://github.com/getsentry/sentry-javascript/pull/17979))
- fix(nextjs): Don't set experimental instrumentation hook flag for next 16 ([#17978](https://github.com/getsentry/sentry-javascript/pull/17978))
- fix(nextjs): Inconsistent transaction naming for i18n routing ([#17927](https://github.com/getsentry/sentry-javascript/pull/17927))
- fix(nextjs): Update bundler detection ([#17976](https://github.com/getsentry/sentry-javascript/pull/17976))

<details>
<summary> <strong>Internal Changes</strong> </summary>

- build: Update to typescript 5.8.0 ([#17710](https://github.com/getsentry/sentry-javascript/pull/17710))
- chore: Add external contributor to CHANGELOG.md ([#17949](https://github.com/getsentry/sentry-javascript/pull/17949))
- chore(build): Upgrade nodemon to 3.1.10 ([#17956](https://github.com/getsentry/sentry-javascript/pull/17956))
- chore(ci): Fix external contributor action when multiple contributions existed ([#17950](https://github.com/getsentry/sentry-javascript/pull/17950))
- chore(solid): Remove unnecessary import from README ([#17947](https://github.com/getsentry/sentry-javascript/pull/17947))
- test(nextjs): Fix proxy/middleware test ([#17970](https://github.com/getsentry/sentry-javascript/pull/17970))

</details>

Work in this release was contributed by @0xbad0c0d3. Thank you for your contribution!

## 10.20.0

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

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

## 10.19.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function fibonacci(n) {
return fibonacci(n - 1) + fibonacci(n - 2);
}

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

// Timeout to prevent flaky tests. Integration samples every 20ms, if function is too fast it might not get sampled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,16 @@ sentryTest('sends profile envelope in legacy mode', async ({ page, getLocalTestU
expect(profile.frames.length).toBeGreaterThan(0);
for (const frame of profile.frames) {
expect(frame).toHaveProperty('function');
expect(frame).toHaveProperty('abs_path');
expect(frame).toHaveProperty('lineno');
expect(frame).toHaveProperty('colno');

expect(typeof frame.function).toBe('string');
expect(typeof frame.abs_path).toBe('string');
expect(typeof frame.lineno).toBe('number');
expect(typeof frame.colno).toBe('number');

if (frame.function !== 'fetch' && frame.function !== 'setTimeout') {
expect(frame).toHaveProperty('abs_path');
expect(frame).toHaveProperty('lineno');
expect(frame).toHaveProperty('colno');
expect(typeof frame.abs_path).toBe('string');
expect(typeof frame.lineno).toBe('number');
expect(typeof frame.colno).toBe('number');
}
}

const functionNames = profile.frames.map(frame => frame.function).filter(name => name !== '');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/browser';
import { browserProfilingIntegration } from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [browserProfilingIntegration()],
tracesSampleRate: 1,
profileSessionSampleRate: 1,
profileLifecycle: 'trace',
});

function largeSum(amount = 1000000) {
let sum = 0;
for (let i = 0; i < amount; i++) {
sum += Math.sqrt(i) * Math.sin(i);
}
}

function fibonacci(n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}

// Create two NON-overlapping root spans so that the profiler stops and emits a chunk
// after each span (since active root span count returns to 0 between them).
await Sentry.startSpanManual({ name: 'root-fibonacci-1', parentSpan: null, forceTransaction: true }, async span => {
fibonacci(40);
// Ensure we cross the sampling interval to avoid flakes
await new Promise(resolve => setTimeout(resolve, 25));
span.end();
});

// Small delay to ensure the first chunk is collected and sent
await new Promise(r => setTimeout(r, 25));

await Sentry.startSpanManual({ name: 'root-largeSum-2', parentSpan: null, forceTransaction: true }, async span => {
largeSum();
// Ensure we cross the sampling interval to avoid flakes
await new Promise(resolve => setTimeout(resolve, 25));
span.end();
});

const client = Sentry.getClient();
await client?.flush(5000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { expect } from '@playwright/test';
import type { ProfileChunkEnvelope } from '@sentry/core';
import { sentryTest } from '../../../utils/fixtures';
import {
countEnvelopes,
getMultipleSentryEnvelopeRequests,
properFullEnvelopeRequestParser,
shouldSkipTracingTest,
} from '../../../utils/helpers';

sentryTest(
'does not send profile envelope when document-policy is not set',
async ({ page, getLocalTestUrl, browserName }) => {
if (shouldSkipTracingTest() || browserName !== 'chromium') {
// Profiling only works when tracing is enabled
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

// Assert that no profile_chunk envelope is sent without policy header
const chunkCount = await countEnvelopes(page, { url, envelopeType: 'profile_chunk', timeout: 1500 });
expect(chunkCount).toBe(0);
},
);

sentryTest(
'sends profile_chunk envelopes in trace mode (multiple chunks)',
async ({ page, getLocalTestUrl, browserName }) => {
if (shouldSkipTracingTest() || browserName !== 'chromium') {
// Profiling only works when tracing is enabled
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname, responseHeaders: { 'Document-Policy': 'js-profiling' } });

// Expect at least 2 chunks because subject creates two separate root spans,
// causing the profiler to stop and emit a chunk after each root span ends.
const profileChunkEnvelopes = await getMultipleSentryEnvelopeRequests<ProfileChunkEnvelope>(
page,
2,
{ url, envelopeType: 'profile_chunk', timeout: 5000 },
properFullEnvelopeRequestParser,
);

expect(profileChunkEnvelopes.length).toBeGreaterThanOrEqual(2);

// Validate the first chunk thoroughly
const profileChunkEnvelopeItem = profileChunkEnvelopes[0][1][0];
const envelopeItemHeader = profileChunkEnvelopeItem[0];
const envelopeItemPayload1 = profileChunkEnvelopeItem[1];

expect(envelopeItemHeader).toHaveProperty('type', 'profile_chunk');

expect(envelopeItemPayload1.profile).toBeDefined();
expect(envelopeItemPayload1.version).toBe('2');
expect(envelopeItemPayload1.platform).toBe('javascript');

// Required profile metadata (Sample Format V2)
expect(typeof envelopeItemPayload1.profiler_id).toBe('string');
expect(envelopeItemPayload1.profiler_id).toMatch(/^[a-f0-9]{32}$/);
expect(typeof envelopeItemPayload1.chunk_id).toBe('string');
expect(envelopeItemPayload1.chunk_id).toMatch(/^[a-f0-9]{32}$/);
expect(envelopeItemPayload1.client_sdk).toBeDefined();
expect(typeof envelopeItemPayload1.client_sdk.name).toBe('string');
expect(typeof envelopeItemPayload1.client_sdk.version).toBe('string');
expect(typeof envelopeItemPayload1.release).toBe('string');
expect(envelopeItemPayload1.debug_meta).toBeDefined();
expect(Array.isArray(envelopeItemPayload1?.debug_meta?.images)).toBe(true);

const profile1 = envelopeItemPayload1.profile;

expect(profile1.samples).toBeDefined();
expect(profile1.stacks).toBeDefined();
expect(profile1.frames).toBeDefined();
expect(profile1.thread_metadata).toBeDefined();

// Samples
expect(profile1.samples.length).toBeGreaterThanOrEqual(2);
let previousTimestamp = Number.NEGATIVE_INFINITY;
for (const sample of profile1.samples) {
expect(typeof sample.stack_id).toBe('number');
expect(sample.stack_id).toBeGreaterThanOrEqual(0);
expect(sample.stack_id).toBeLessThan(profile1.stacks.length);

// In trace lifecycle mode, samples carry a numeric timestamp (ms since epoch or similar clock)
expect(typeof (sample as any).timestamp).toBe('number');
const ts = (sample as any).timestamp as number;
expect(Number.isFinite(ts)).toBe(true);
expect(ts).toBeGreaterThan(0);
// Monotonic non-decreasing timestamps
expect(ts).toBeGreaterThanOrEqual(previousTimestamp);
previousTimestamp = ts;

expect(sample.thread_id).toBe('0'); // Should be main thread
}

// Stacks
expect(profile1.stacks.length).toBeGreaterThan(0);
for (const stack of profile1.stacks) {
expect(Array.isArray(stack)).toBe(true);
for (const frameIndex of stack) {
expect(typeof frameIndex).toBe('number');
expect(frameIndex).toBeGreaterThanOrEqual(0);
expect(frameIndex).toBeLessThan(profile1.frames.length);
}
}

// Frames
expect(profile1.frames.length).toBeGreaterThan(0);
for (const frame of profile1.frames) {
expect(frame).toHaveProperty('function');
expect(typeof frame.function).toBe('string');

if (frame.function !== 'fetch' && frame.function !== 'setTimeout') {
expect(frame).toHaveProperty('abs_path');
expect(frame).toHaveProperty('lineno');
expect(frame).toHaveProperty('colno');
expect(typeof frame.abs_path).toBe('string');
expect(typeof frame.lineno).toBe('number');
expect(typeof frame.colno).toBe('number');
}
}

const functionNames = profile1.frames.map(frame => frame.function).filter(name => name !== '');

if ((process.env.PW_BUNDLE || '').endsWith('min')) {
// In bundled mode, function names are minified
expect(functionNames.length).toBeGreaterThan(0);
expect((functionNames as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings
} else {
expect(functionNames).toEqual(
expect.arrayContaining([
'_startRootSpan',
'withScope',
'createChildOrRootSpan',
'startSpanManual',
'startJSSelfProfile',

// first function is captured (other one is in other chunk)
'fibonacci',
]),
);
}

expect(profile1.thread_metadata).toHaveProperty('0');
expect(profile1.thread_metadata['0']).toHaveProperty('name');
expect(profile1.thread_metadata['0'].name).toBe('main');

// Test that profile duration makes sense (should be > 20ms based on test setup)
const startTimeSec = (profile1.samples[0] as any).timestamp as number;
const endTimeSec = (profile1.samples[profile1.samples.length - 1] as any).timestamp as number;
const durationSec = endTimeSec - startTimeSec;

// Should be at least 20ms based on our setTimeout(21) in the test
expect(durationSec).toBeGreaterThan(0.2);

// === PROFILE CHUNK 2 ===

const profileChunkEnvelopeItem2 = profileChunkEnvelopes[1][1][0];
const envelopeItemHeader2 = profileChunkEnvelopeItem2[0];
const envelopeItemPayload2 = profileChunkEnvelopeItem2[1];

// Basic sanity on the second chunk: has correct envelope type and structure
expect(envelopeItemHeader2).toHaveProperty('type', 'profile_chunk');
expect(envelopeItemPayload2.profile).toBeDefined();
expect(envelopeItemPayload2.version).toBe('2');
expect(envelopeItemPayload2.platform).toBe('javascript');

// Required profile metadata (Sample Format V2)
// https://develop.sentry.dev/sdk/telemetry/profiles/sample-format-v2/
expect(typeof envelopeItemPayload2.profiler_id).toBe('string');
expect(envelopeItemPayload2.profiler_id).toMatch(/^[a-f0-9]{32}$/);
expect(typeof envelopeItemPayload2.chunk_id).toBe('string');
expect(envelopeItemPayload2.chunk_id).toMatch(/^[a-f0-9]{32}$/);
expect(envelopeItemPayload2.client_sdk).toBeDefined();
expect(typeof envelopeItemPayload2.client_sdk.name).toBe('string');
expect(typeof envelopeItemPayload2.client_sdk.version).toBe('string');
expect(typeof envelopeItemPayload2.release).toBe('string');
expect(envelopeItemPayload2.debug_meta).toBeDefined();
expect(Array.isArray(envelopeItemPayload2?.debug_meta?.images)).toBe(true);

const profile2 = envelopeItemPayload2.profile;

const functionNames2 = profile2.frames.map(frame => frame.function).filter(name => name !== '');

if ((process.env.PW_BUNDLE || '').endsWith('min')) {
// In bundled mode, function names are minified
expect(functionNames2.length).toBeGreaterThan(0);
expect((functionNames2 as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings
} else {
expect(functionNames2).toEqual(
expect.arrayContaining([
'_startRootSpan',
'withScope',
'createChildOrRootSpan',
'startSpanManual',
'startJSSelfProfile',

// second function is captured (other one is in other chunk)
'largeSum',
]),
);
}
},
);
Loading
Loading