Skip to content

meta(changelog): Update changelog for 9.35.0 #16811

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 17 commits into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
12 changes: 6 additions & 6 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration'),
gzip: true,
limit: '40 KB',
limit: '40.7 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay)',
Expand Down Expand Up @@ -75,7 +75,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
gzip: true,
limit: '82 KB',
limit: '83 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
Expand Down Expand Up @@ -120,7 +120,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '41 KB',
limit: '42 KB',
},
// Vue SDK (ESM)
{
Expand All @@ -135,7 +135,7 @@ module.exports = [
path: 'packages/vue/build/esm/index.js',
import: createImport('init', 'browserTracingIntegration'),
gzip: true,
limit: '41 KB',
limit: '42 KB',
},
// Svelte SDK (ESM)
{
Expand Down Expand Up @@ -206,7 +206,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
limit: '43 KB',
limit: '44 KB',
},
// SvelteKit SDK (ESM)
{
Expand All @@ -215,7 +215,7 @@ module.exports = [
import: createImport('init'),
ignore: ['$app/stores'],
gzip: true,
limit: '40 KB',
limit: '41 KB',
},
// Node SDK (ESM)
{
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@

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

## 9.35.0

- feat(browser): Add ElementTiming instrumentation and spans ([#16589](https://github.com/getsentry/sentry-javascript/pull/16589))
- feat(browser): Export `Context` and `Contexts` types ([#16763](https://github.com/getsentry/sentry-javascript/pull/16763))
- feat(cloudflare): Add user agent to cloudflare spans ([#16793](https://github.com/getsentry/sentry-javascript/pull/16793))
- feat(node): Add `eventLoopBlockIntegration` ([#16709](https://github.com/getsentry/sentry-javascript/pull/16709))
- feat(node): Export server-side feature flag integration shims ([#16735](https://github.com/getsentry/sentry-javascript/pull/16735))
- feat(node): Update vercel ai integration attributes ([#16721](https://github.com/getsentry/sentry-javascript/pull/16721))
- fix(astro): Handle errors in middlewares better ([#16693](https://github.com/getsentry/sentry-javascript/pull/16693))
- fix(browser): Ensure explicit `parentSpan` is considered ([#16776](https://github.com/getsentry/sentry-javascript/pull/16776))
- fix(node): Avoid using dynamic `require` for fastify integration ([#16789](https://github.com/getsentry/sentry-javascript/pull/16789))
- fix(nuxt): Add `@sentry/cloudflare` as optional peerDependency ([#16782](https://github.com/getsentry/sentry-javascript/pull/16782))
- fix(nuxt): Ensure order of plugins is consistent ([#16798](https://github.com/getsentry/sentry-javascript/pull/16798))
- fix(nestjs): Fix exception handling in `@Cron` decorated tasks ([#16792](https://github.com/getsentry/sentry-javascript/pull/16792))

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

## 9.34.0

### Important Changes
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
debug: true,
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const lazyDiv = document.getElementById('content-lazy');
const navigationButton = document.getElementById('button1');
const navigationDiv = document.getElementById('content-navigation');
const clickButton = document.getElementById('button2');
const clickDiv = document.getElementById('content-click');

navigationButton.addEventListener('click', () => {
window.history.pushState({}, '', '/some-other-path');
navigationDiv.innerHTML = `
<img src="https://sentry-test-site.example/path/to/image-navigation.png" elementtiming="navigation-image" />
<p elementtiming="navigation-text">This is navigation content</p>
`;
});

setTimeout(() => {
lazyDiv.innerHTML = `
<img src="https://sentry-test-site.example/path/to/image-lazy.png" elementtiming="lazy-image" />
<p elementtiming="lazy-text">This is lazy loaded content</p>
`;
}, 1000);

clickButton.addEventListener('click', () => {
clickDiv.innerHTML = `
<img src="https://sentry-test-site.example/path/to/image-click.png" elementtiming="click-image" />
<p elementtiming="click-text">This is click loaded content</p>
`;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<!-- eagerly loaded image (image-paint) with fast load time -->
<img src="https://sentry-test-site.example/path/to/image-fast.png" elementtiming="image-fast" id="image-fast-id"/>

<!-- eagerly rendered text (text-paint) -->
<p elementtiming="text1" id="text1-id">
This is some text content
<pan>with another nested span</pan>
<small>and a small text</small>
</p>

<!--
eagerly rendered div with an eagerly loaded nested image with slow load time (image-paint)
Although the div has an elementtiming attribute, it will not emit an entry because it's
neither a text nor an image
-->
<div elementtiming="div1">
<h1>Header with element timing</h1>
<img src="https://sentry-test-site.example/path/to/image-slow.png" elementtiming="image-nested-slow" />
</div>

<!-- eagerly loaded image (image-paint) with slow load time -->
<img src="https://sentry-test-site.example/path/to/image-slow.png" elementtiming="image-slow" />

<!-- lazily loaded content (image-paint and text-paint) with slow load time -->
<div id="content-lazy">
<p>This div will be populated lazily</p>
</div>

<!-- content loaded after navigation (image-paint and text-paint) -->
<div id="content-navigation">
<p>This div will be populated after a navigation</p>
</div>

<!-- content loaded after navigation (image-paint and text-paint) -->
<div id="content-click">
<p>This div will be populated on click</p>
</div>

<!-- eagerly rendered buttons-->
<button id="button1" elementtiming="button1">Navigate</button>
<button id="button2" elementtiming="button2">Populate on Click</button>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import type { Page, Route } from '@playwright/test';
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';

sentryTest(
'adds element timing spans to pageload span tree for elements rendered during pageload',
async ({ getLocalTestUrl, page, browserName }) => {
if (shouldSkipTracingTest() || browserName === 'webkit') {
sentryTest.skip();
}

const pageloadEventPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');

serveAssets(page);

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

await page.goto(url);

const eventData = envelopeRequestParser(await pageloadEventPromise);

const elementTimingSpans = eventData.spans?.filter(({ op }) => op === 'ui.elementtiming');

expect(elementTimingSpans?.length).toEqual(8);

// Check image-fast span (this is served with a 100ms delay)
const imageFastSpan = elementTimingSpans?.find(({ description }) => description === 'element[image-fast]');
const imageFastRenderTime = imageFastSpan?.data['element.render_time'];
const imageFastLoadTime = imageFastSpan?.data['element.load_time'];
const duration = imageFastSpan!.timestamp! - imageFastSpan!.start_timestamp!;

expect(imageFastSpan).toBeDefined();
expect(imageFastSpan?.data).toEqual({
'sentry.op': 'ui.elementtiming',
'sentry.origin': 'auto.ui.browser.elementtiming',
'sentry.source': 'component',
'sentry.span_start_time_source': 'load-time',
'element.id': 'image-fast-id',
'element.identifier': 'image-fast',
'element.type': 'img',
'element.size': '600x179',
'element.url': 'https://sentry-test-site.example/path/to/image-fast.png',
'element.render_time': expect.any(Number),
'element.load_time': expect.any(Number),
'element.paint_type': 'image-paint',
'sentry.transaction_name': '/index.html',
});
expect(imageFastRenderTime).toBeGreaterThan(90);
expect(imageFastRenderTime).toBeLessThan(400);
expect(imageFastLoadTime).toBeGreaterThan(90);
expect(imageFastLoadTime).toBeLessThan(400);
expect(imageFastRenderTime).toBeGreaterThan(imageFastLoadTime as number);
expect(duration).toBeGreaterThan(0);
expect(duration).toBeLessThan(20);

// Check text1 span
const text1Span = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'text1');
const text1RenderTime = text1Span?.data['element.render_time'];
const text1LoadTime = text1Span?.data['element.load_time'];
const text1Duration = text1Span!.timestamp! - text1Span!.start_timestamp!;
expect(text1Span).toBeDefined();
expect(text1Span?.data).toEqual({
'sentry.op': 'ui.elementtiming',
'sentry.origin': 'auto.ui.browser.elementtiming',
'sentry.source': 'component',
'sentry.span_start_time_source': 'render-time',
'element.id': 'text1-id',
'element.identifier': 'text1',
'element.type': 'p',
'element.render_time': expect.any(Number),
'element.load_time': expect.any(Number),
'element.paint_type': 'text-paint',
'sentry.transaction_name': '/index.html',
});
expect(text1RenderTime).toBeGreaterThan(0);
expect(text1RenderTime).toBeLessThan(300);
expect(text1LoadTime).toBe(0);
expect(text1RenderTime).toBeGreaterThan(text1LoadTime as number);
expect(text1Duration).toBe(0);

// Check button1 span (no need for a full assertion)
const button1Span = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'button1');
expect(button1Span).toBeDefined();
expect(button1Span?.data).toMatchObject({
'element.identifier': 'button1',
'element.type': 'button',
'element.paint_type': 'text-paint',
'sentry.transaction_name': '/index.html',
});

// Check image-slow span
const imageSlowSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'image-slow');
expect(imageSlowSpan).toBeDefined();
expect(imageSlowSpan?.data).toEqual({
'element.id': '',
'element.identifier': 'image-slow',
'element.type': 'img',
'element.size': '600x179',
'element.url': 'https://sentry-test-site.example/path/to/image-slow.png',
'element.paint_type': 'image-paint',
'element.render_time': expect.any(Number),
'element.load_time': expect.any(Number),
'sentry.op': 'ui.elementtiming',
'sentry.origin': 'auto.ui.browser.elementtiming',
'sentry.source': 'component',
'sentry.span_start_time_source': 'load-time',
'sentry.transaction_name': '/index.html',
});
const imageSlowRenderTime = imageSlowSpan?.data['element.render_time'];
const imageSlowLoadTime = imageSlowSpan?.data['element.load_time'];
const imageSlowDuration = imageSlowSpan!.timestamp! - imageSlowSpan!.start_timestamp!;
expect(imageSlowRenderTime).toBeGreaterThan(1400);
expect(imageSlowRenderTime).toBeLessThan(2000);
expect(imageSlowLoadTime).toBeGreaterThan(1400);
expect(imageSlowLoadTime).toBeLessThan(2000);
expect(imageSlowDuration).toBeGreaterThan(0);
expect(imageSlowDuration).toBeLessThan(20);

// Check lazy-image span
const lazyImageSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'lazy-image');
expect(lazyImageSpan).toBeDefined();
expect(lazyImageSpan?.data).toEqual({
'element.id': '',
'element.identifier': 'lazy-image',
'element.type': 'img',
'element.size': '600x179',
'element.url': 'https://sentry-test-site.example/path/to/image-lazy.png',
'element.paint_type': 'image-paint',
'element.render_time': expect.any(Number),
'element.load_time': expect.any(Number),
'sentry.op': 'ui.elementtiming',
'sentry.origin': 'auto.ui.browser.elementtiming',
'sentry.source': 'component',
'sentry.span_start_time_source': 'load-time',
'sentry.transaction_name': '/index.html',
});
const lazyImageRenderTime = lazyImageSpan?.data['element.render_time'];
const lazyImageLoadTime = lazyImageSpan?.data['element.load_time'];
const lazyImageDuration = lazyImageSpan!.timestamp! - lazyImageSpan!.start_timestamp!;
expect(lazyImageRenderTime).toBeGreaterThan(1000);
expect(lazyImageRenderTime).toBeLessThan(1500);
expect(lazyImageLoadTime).toBeGreaterThan(1000);
expect(lazyImageLoadTime).toBeLessThan(1500);
expect(lazyImageDuration).toBeGreaterThan(0);
expect(lazyImageDuration).toBeLessThan(20);

// Check lazy-text span
const lazyTextSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'lazy-text');
expect(lazyTextSpan?.data).toMatchObject({
'element.id': '',
'element.identifier': 'lazy-text',
'element.type': 'p',
'sentry.transaction_name': '/index.html',
});
const lazyTextRenderTime = lazyTextSpan?.data['element.render_time'];
const lazyTextLoadTime = lazyTextSpan?.data['element.load_time'];
const lazyTextDuration = lazyTextSpan!.timestamp! - lazyTextSpan!.start_timestamp!;
expect(lazyTextRenderTime).toBeGreaterThan(1000);
expect(lazyTextRenderTime).toBeLessThan(1500);
expect(lazyTextLoadTime).toBe(0);
expect(lazyTextDuration).toBe(0);

// the div1 entry does not emit an elementTiming entry because it's neither a text nor an image
expect(elementTimingSpans?.find(({ description }) => description === 'element[div1]')).toBeUndefined();
},
);

sentryTest('emits element timing spans on navigation', async ({ getLocalTestUrl, page, browserName }) => {
if (shouldSkipTracingTest() || browserName === 'webkit') {
sentryTest.skip();
}

serveAssets(page);

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

await page.goto(url);

const pageloadEventPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');

const navigationEventPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');

await pageloadEventPromise;

await page.locator('#button1').click();

const navigationTransactionEvent = envelopeRequestParser(await navigationEventPromise);
const pageloadTransactionEvent = envelopeRequestParser(await pageloadEventPromise);

const navigationElementTimingSpans = navigationTransactionEvent.spans?.filter(({ op }) => op === 'ui.elementtiming');

expect(navigationElementTimingSpans?.length).toEqual(2);

const navigationStartTime = navigationTransactionEvent.start_timestamp!;
const pageloadStartTime = pageloadTransactionEvent.start_timestamp!;

const imageSpan = navigationElementTimingSpans?.find(
({ description }) => description === 'element[navigation-image]',
);
const textSpan = navigationElementTimingSpans?.find(({ description }) => description === 'element[navigation-text]');

// Image started loading after navigation, but render-time and load-time still start from the time origin
// of the pageload. This is somewhat a limitation (though by design according to the ElementTiming spec)
expect((imageSpan!.data['element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan(
navigationStartTime,
);
expect((imageSpan!.data['element.load_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan(
navigationStartTime,
);

expect(textSpan?.data['element.load_time']).toBe(0);
expect((textSpan!.data['element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan(
navigationStartTime,
);
});

function serveAssets(page: Page) {
page.route(/image-(fast|lazy|navigation|click)\.png/, async (route: Route) => {
await new Promise(resolve => setTimeout(resolve, 100));
return route.fulfill({
path: `${__dirname}/assets/sentry-logo-600x179.png`,
});
});

page.route('**/image-slow.png', async (route: Route) => {
await new Promise(resolve => setTimeout(resolve, 1500));
return route.fulfill({
path: `${__dirname}/assets/sentry-logo-600x179.png`,
});
});
}
Loading
Loading