Skip to content

Commit dc3b94c

Browse files
authored
Merge pull request #16811 from getsentry/prepare-release/9.35.0
meta(changelog): Update changelog for 9.35.0
2 parents 9f5900a + 1a1c19d commit dc3b94c

File tree

78 files changed

+2430
-275
lines changed

Some content is hidden

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

78 files changed

+2430
-275
lines changed

.size-limit.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ module.exports = [
3838
path: 'packages/browser/build/npm/esm/index.js',
3939
import: createImport('init', 'browserTracingIntegration'),
4040
gzip: true,
41-
limit: '40 KB',
41+
limit: '40.7 KB',
4242
},
4343
{
4444
name: '@sentry/browser (incl. Tracing, Replay)',
@@ -75,7 +75,7 @@ module.exports = [
7575
path: 'packages/browser/build/npm/esm/index.js',
7676
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
7777
gzip: true,
78-
limit: '82 KB',
78+
limit: '83 KB',
7979
},
8080
{
8181
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
@@ -120,7 +120,7 @@ module.exports = [
120120
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
121121
ignore: ['react/jsx-runtime'],
122122
gzip: true,
123-
limit: '41 KB',
123+
limit: '42 KB',
124124
},
125125
// Vue SDK (ESM)
126126
{
@@ -135,7 +135,7 @@ module.exports = [
135135
path: 'packages/vue/build/esm/index.js',
136136
import: createImport('init', 'browserTracingIntegration'),
137137
gzip: true,
138-
limit: '41 KB',
138+
limit: '42 KB',
139139
},
140140
// Svelte SDK (ESM)
141141
{
@@ -206,7 +206,7 @@ module.exports = [
206206
import: createImport('init'),
207207
ignore: ['next/router', 'next/constants'],
208208
gzip: true,
209-
limit: '43 KB',
209+
limit: '44 KB',
210210
},
211211
// SvelteKit SDK (ESM)
212212
{
@@ -215,7 +215,7 @@ module.exports = [
215215
import: createImport('init'),
216216
ignore: ['$app/stores'],
217217
gzip: true,
218-
limit: '40 KB',
218+
limit: '41 KB',
219219
},
220220
// Node SDK (ESM)
221221
{

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@
44

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

7+
## 9.35.0
8+
9+
- feat(browser): Add ElementTiming instrumentation and spans ([#16589](https://github.com/getsentry/sentry-javascript/pull/16589))
10+
- feat(browser): Export `Context` and `Contexts` types ([#16763](https://github.com/getsentry/sentry-javascript/pull/16763))
11+
- feat(cloudflare): Add user agent to cloudflare spans ([#16793](https://github.com/getsentry/sentry-javascript/pull/16793))
12+
- feat(node): Add `eventLoopBlockIntegration` ([#16709](https://github.com/getsentry/sentry-javascript/pull/16709))
13+
- feat(node): Export server-side feature flag integration shims ([#16735](https://github.com/getsentry/sentry-javascript/pull/16735))
14+
- feat(node): Update vercel ai integration attributes ([#16721](https://github.com/getsentry/sentry-javascript/pull/16721))
15+
- fix(astro): Handle errors in middlewares better ([#16693](https://github.com/getsentry/sentry-javascript/pull/16693))
16+
- fix(browser): Ensure explicit `parentSpan` is considered ([#16776](https://github.com/getsentry/sentry-javascript/pull/16776))
17+
- fix(node): Avoid using dynamic `require` for fastify integration ([#16789](https://github.com/getsentry/sentry-javascript/pull/16789))
18+
- fix(nuxt): Add `@sentry/cloudflare` as optional peerDependency ([#16782](https://github.com/getsentry/sentry-javascript/pull/16782))
19+
- fix(nuxt): Ensure order of plugins is consistent ([#16798](https://github.com/getsentry/sentry-javascript/pull/16798))
20+
- fix(nestjs): Fix exception handling in `@Cron` decorated tasks ([#16792](https://github.com/getsentry/sentry-javascript/pull/16792))
21+
22+
Work in this release was contributed by @0xbad0c0d3 and @alSergey. Thank you for your contributions!
23+
724
## 9.34.0
825

926
### Important Changes
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
debug: true,
7+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8+
integrations: [Sentry.browserTracingIntegration()],
9+
tracesSampleRate: 1,
10+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const lazyDiv = document.getElementById('content-lazy');
2+
const navigationButton = document.getElementById('button1');
3+
const navigationDiv = document.getElementById('content-navigation');
4+
const clickButton = document.getElementById('button2');
5+
const clickDiv = document.getElementById('content-click');
6+
7+
navigationButton.addEventListener('click', () => {
8+
window.history.pushState({}, '', '/some-other-path');
9+
navigationDiv.innerHTML = `
10+
<img src="https://sentry-test-site.example/path/to/image-navigation.png" elementtiming="navigation-image" />
11+
<p elementtiming="navigation-text">This is navigation content</p>
12+
`;
13+
});
14+
15+
setTimeout(() => {
16+
lazyDiv.innerHTML = `
17+
<img src="https://sentry-test-site.example/path/to/image-lazy.png" elementtiming="lazy-image" />
18+
<p elementtiming="lazy-text">This is lazy loaded content</p>
19+
`;
20+
}, 1000);
21+
22+
clickButton.addEventListener('click', () => {
23+
clickDiv.innerHTML = `
24+
<img src="https://sentry-test-site.example/path/to/image-click.png" elementtiming="click-image" />
25+
<p elementtiming="click-text">This is click loaded content</p>
26+
`;
27+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<!-- eagerly loaded image (image-paint) with fast load time -->
8+
<img src="https://sentry-test-site.example/path/to/image-fast.png" elementtiming="image-fast" id="image-fast-id"/>
9+
10+
<!-- eagerly rendered text (text-paint) -->
11+
<p elementtiming="text1" id="text1-id">
12+
This is some text content
13+
<pan>with another nested span</pan>
14+
<small>and a small text</small>
15+
</p>
16+
17+
<!--
18+
eagerly rendered div with an eagerly loaded nested image with slow load time (image-paint)
19+
Although the div has an elementtiming attribute, it will not emit an entry because it's
20+
neither a text nor an image
21+
-->
22+
<div elementtiming="div1">
23+
<h1>Header with element timing</h1>
24+
<img src="https://sentry-test-site.example/path/to/image-slow.png" elementtiming="image-nested-slow" />
25+
</div>
26+
27+
<!-- eagerly loaded image (image-paint) with slow load time -->
28+
<img src="https://sentry-test-site.example/path/to/image-slow.png" elementtiming="image-slow" />
29+
30+
<!-- lazily loaded content (image-paint and text-paint) with slow load time -->
31+
<div id="content-lazy">
32+
<p>This div will be populated lazily</p>
33+
</div>
34+
35+
<!-- content loaded after navigation (image-paint and text-paint) -->
36+
<div id="content-navigation">
37+
<p>This div will be populated after a navigation</p>
38+
</div>
39+
40+
<!-- content loaded after navigation (image-paint and text-paint) -->
41+
<div id="content-click">
42+
<p>This div will be populated on click</p>
43+
</div>
44+
45+
<!-- eagerly rendered buttons-->
46+
<button id="button1" elementtiming="button1">Navigate</button>
47+
<button id="button2" elementtiming="button2">Populate on Click</button>
48+
</body>
49+
</html>
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import type { Page, Route } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';
5+
6+
sentryTest(
7+
'adds element timing spans to pageload span tree for elements rendered during pageload',
8+
async ({ getLocalTestUrl, page, browserName }) => {
9+
if (shouldSkipTracingTest() || browserName === 'webkit') {
10+
sentryTest.skip();
11+
}
12+
13+
const pageloadEventPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
14+
15+
serveAssets(page);
16+
17+
const url = await getLocalTestUrl({ testDir: __dirname });
18+
19+
await page.goto(url);
20+
21+
const eventData = envelopeRequestParser(await pageloadEventPromise);
22+
23+
const elementTimingSpans = eventData.spans?.filter(({ op }) => op === 'ui.elementtiming');
24+
25+
expect(elementTimingSpans?.length).toEqual(8);
26+
27+
// Check image-fast span (this is served with a 100ms delay)
28+
const imageFastSpan = elementTimingSpans?.find(({ description }) => description === 'element[image-fast]');
29+
const imageFastRenderTime = imageFastSpan?.data['element.render_time'];
30+
const imageFastLoadTime = imageFastSpan?.data['element.load_time'];
31+
const duration = imageFastSpan!.timestamp! - imageFastSpan!.start_timestamp!;
32+
33+
expect(imageFastSpan).toBeDefined();
34+
expect(imageFastSpan?.data).toEqual({
35+
'sentry.op': 'ui.elementtiming',
36+
'sentry.origin': 'auto.ui.browser.elementtiming',
37+
'sentry.source': 'component',
38+
'sentry.span_start_time_source': 'load-time',
39+
'element.id': 'image-fast-id',
40+
'element.identifier': 'image-fast',
41+
'element.type': 'img',
42+
'element.size': '600x179',
43+
'element.url': 'https://sentry-test-site.example/path/to/image-fast.png',
44+
'element.render_time': expect.any(Number),
45+
'element.load_time': expect.any(Number),
46+
'element.paint_type': 'image-paint',
47+
'sentry.transaction_name': '/index.html',
48+
});
49+
expect(imageFastRenderTime).toBeGreaterThan(90);
50+
expect(imageFastRenderTime).toBeLessThan(400);
51+
expect(imageFastLoadTime).toBeGreaterThan(90);
52+
expect(imageFastLoadTime).toBeLessThan(400);
53+
expect(imageFastRenderTime).toBeGreaterThan(imageFastLoadTime as number);
54+
expect(duration).toBeGreaterThan(0);
55+
expect(duration).toBeLessThan(20);
56+
57+
// Check text1 span
58+
const text1Span = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'text1');
59+
const text1RenderTime = text1Span?.data['element.render_time'];
60+
const text1LoadTime = text1Span?.data['element.load_time'];
61+
const text1Duration = text1Span!.timestamp! - text1Span!.start_timestamp!;
62+
expect(text1Span).toBeDefined();
63+
expect(text1Span?.data).toEqual({
64+
'sentry.op': 'ui.elementtiming',
65+
'sentry.origin': 'auto.ui.browser.elementtiming',
66+
'sentry.source': 'component',
67+
'sentry.span_start_time_source': 'render-time',
68+
'element.id': 'text1-id',
69+
'element.identifier': 'text1',
70+
'element.type': 'p',
71+
'element.render_time': expect.any(Number),
72+
'element.load_time': expect.any(Number),
73+
'element.paint_type': 'text-paint',
74+
'sentry.transaction_name': '/index.html',
75+
});
76+
expect(text1RenderTime).toBeGreaterThan(0);
77+
expect(text1RenderTime).toBeLessThan(300);
78+
expect(text1LoadTime).toBe(0);
79+
expect(text1RenderTime).toBeGreaterThan(text1LoadTime as number);
80+
expect(text1Duration).toBe(0);
81+
82+
// Check button1 span (no need for a full assertion)
83+
const button1Span = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'button1');
84+
expect(button1Span).toBeDefined();
85+
expect(button1Span?.data).toMatchObject({
86+
'element.identifier': 'button1',
87+
'element.type': 'button',
88+
'element.paint_type': 'text-paint',
89+
'sentry.transaction_name': '/index.html',
90+
});
91+
92+
// Check image-slow span
93+
const imageSlowSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'image-slow');
94+
expect(imageSlowSpan).toBeDefined();
95+
expect(imageSlowSpan?.data).toEqual({
96+
'element.id': '',
97+
'element.identifier': 'image-slow',
98+
'element.type': 'img',
99+
'element.size': '600x179',
100+
'element.url': 'https://sentry-test-site.example/path/to/image-slow.png',
101+
'element.paint_type': 'image-paint',
102+
'element.render_time': expect.any(Number),
103+
'element.load_time': expect.any(Number),
104+
'sentry.op': 'ui.elementtiming',
105+
'sentry.origin': 'auto.ui.browser.elementtiming',
106+
'sentry.source': 'component',
107+
'sentry.span_start_time_source': 'load-time',
108+
'sentry.transaction_name': '/index.html',
109+
});
110+
const imageSlowRenderTime = imageSlowSpan?.data['element.render_time'];
111+
const imageSlowLoadTime = imageSlowSpan?.data['element.load_time'];
112+
const imageSlowDuration = imageSlowSpan!.timestamp! - imageSlowSpan!.start_timestamp!;
113+
expect(imageSlowRenderTime).toBeGreaterThan(1400);
114+
expect(imageSlowRenderTime).toBeLessThan(2000);
115+
expect(imageSlowLoadTime).toBeGreaterThan(1400);
116+
expect(imageSlowLoadTime).toBeLessThan(2000);
117+
expect(imageSlowDuration).toBeGreaterThan(0);
118+
expect(imageSlowDuration).toBeLessThan(20);
119+
120+
// Check lazy-image span
121+
const lazyImageSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'lazy-image');
122+
expect(lazyImageSpan).toBeDefined();
123+
expect(lazyImageSpan?.data).toEqual({
124+
'element.id': '',
125+
'element.identifier': 'lazy-image',
126+
'element.type': 'img',
127+
'element.size': '600x179',
128+
'element.url': 'https://sentry-test-site.example/path/to/image-lazy.png',
129+
'element.paint_type': 'image-paint',
130+
'element.render_time': expect.any(Number),
131+
'element.load_time': expect.any(Number),
132+
'sentry.op': 'ui.elementtiming',
133+
'sentry.origin': 'auto.ui.browser.elementtiming',
134+
'sentry.source': 'component',
135+
'sentry.span_start_time_source': 'load-time',
136+
'sentry.transaction_name': '/index.html',
137+
});
138+
const lazyImageRenderTime = lazyImageSpan?.data['element.render_time'];
139+
const lazyImageLoadTime = lazyImageSpan?.data['element.load_time'];
140+
const lazyImageDuration = lazyImageSpan!.timestamp! - lazyImageSpan!.start_timestamp!;
141+
expect(lazyImageRenderTime).toBeGreaterThan(1000);
142+
expect(lazyImageRenderTime).toBeLessThan(1500);
143+
expect(lazyImageLoadTime).toBeGreaterThan(1000);
144+
expect(lazyImageLoadTime).toBeLessThan(1500);
145+
expect(lazyImageDuration).toBeGreaterThan(0);
146+
expect(lazyImageDuration).toBeLessThan(20);
147+
148+
// Check lazy-text span
149+
const lazyTextSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'lazy-text');
150+
expect(lazyTextSpan?.data).toMatchObject({
151+
'element.id': '',
152+
'element.identifier': 'lazy-text',
153+
'element.type': 'p',
154+
'sentry.transaction_name': '/index.html',
155+
});
156+
const lazyTextRenderTime = lazyTextSpan?.data['element.render_time'];
157+
const lazyTextLoadTime = lazyTextSpan?.data['element.load_time'];
158+
const lazyTextDuration = lazyTextSpan!.timestamp! - lazyTextSpan!.start_timestamp!;
159+
expect(lazyTextRenderTime).toBeGreaterThan(1000);
160+
expect(lazyTextRenderTime).toBeLessThan(1500);
161+
expect(lazyTextLoadTime).toBe(0);
162+
expect(lazyTextDuration).toBe(0);
163+
164+
// the div1 entry does not emit an elementTiming entry because it's neither a text nor an image
165+
expect(elementTimingSpans?.find(({ description }) => description === 'element[div1]')).toBeUndefined();
166+
},
167+
);
168+
169+
sentryTest('emits element timing spans on navigation', async ({ getLocalTestUrl, page, browserName }) => {
170+
if (shouldSkipTracingTest() || browserName === 'webkit') {
171+
sentryTest.skip();
172+
}
173+
174+
serveAssets(page);
175+
176+
const url = await getLocalTestUrl({ testDir: __dirname });
177+
178+
await page.goto(url);
179+
180+
const pageloadEventPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
181+
182+
const navigationEventPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
183+
184+
await pageloadEventPromise;
185+
186+
await page.locator('#button1').click();
187+
188+
const navigationTransactionEvent = envelopeRequestParser(await navigationEventPromise);
189+
const pageloadTransactionEvent = envelopeRequestParser(await pageloadEventPromise);
190+
191+
const navigationElementTimingSpans = navigationTransactionEvent.spans?.filter(({ op }) => op === 'ui.elementtiming');
192+
193+
expect(navigationElementTimingSpans?.length).toEqual(2);
194+
195+
const navigationStartTime = navigationTransactionEvent.start_timestamp!;
196+
const pageloadStartTime = pageloadTransactionEvent.start_timestamp!;
197+
198+
const imageSpan = navigationElementTimingSpans?.find(
199+
({ description }) => description === 'element[navigation-image]',
200+
);
201+
const textSpan = navigationElementTimingSpans?.find(({ description }) => description === 'element[navigation-text]');
202+
203+
// Image started loading after navigation, but render-time and load-time still start from the time origin
204+
// of the pageload. This is somewhat a limitation (though by design according to the ElementTiming spec)
205+
expect((imageSpan!.data['element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan(
206+
navigationStartTime,
207+
);
208+
expect((imageSpan!.data['element.load_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan(
209+
navigationStartTime,
210+
);
211+
212+
expect(textSpan?.data['element.load_time']).toBe(0);
213+
expect((textSpan!.data['element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan(
214+
navigationStartTime,
215+
);
216+
});
217+
218+
function serveAssets(page: Page) {
219+
page.route(/image-(fast|lazy|navigation|click)\.png/, async (route: Route) => {
220+
await new Promise(resolve => setTimeout(resolve, 100));
221+
return route.fulfill({
222+
path: `${__dirname}/assets/sentry-logo-600x179.png`,
223+
});
224+
});
225+
226+
page.route('**/image-slow.png', async (route: Route) => {
227+
await new Promise(resolve => setTimeout(resolve, 1500));
228+
return route.fulfill({
229+
path: `${__dirname}/assets/sentry-logo-600x179.png`,
230+
});
231+
});
232+
}

0 commit comments

Comments
 (0)