Skip to content

meta(changelog): Update changelog for 9.38.0 #16931

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 12 commits into from
Jul 11, 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
3 changes: 3 additions & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ targets:
- name: npm
id: '@sentry/profiling-node'
includeNames: /^sentry-profiling-node-\d.*\.tgz$/
- name: npm
id: '@sentry/node-native'
includeNames: /^sentry-node-native-\d.*\.tgz$/

## 3 Browser-based Packages
- name: npm
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@

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

## 9.38.0

### Important Changes

- **chore: Add craft entry for @sentry/node-native ([#16907](https://github.com/getsentry/sentry-javascript/pull/16907))**

This release publishes the `@sentry/node-native` SDK.

### Other Changes

- feat(core): Introduce `debug` to replace `logger` ([#16906](https://github.com/getsentry/sentry-javascript/pull/16906))
- fix(browser): Guard `nextHopProtocol` when adding resource spans ([#16900](https://github.com/getsentry/sentry-javascript/pull/16900))

## 9.37.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains
sentryTest.skip();
}




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

const clientReportPromise = waitForClientReportRequest(page);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ sentryTest('captures a "GOOD" CLS vital with its source as a standalone span', a
'sentry.exclusive_time': 0,
'sentry.op': 'ui.webvital.cls',
'sentry.origin': 'auto.http.browser.cls',
'sentry.report_event': 'pagehide',
transaction: expect.stringContaining('index.html'),
'user_agent.original': expect.stringContaining('Chrome'),
'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
Expand Down Expand Up @@ -134,6 +135,7 @@ sentryTest('captures a "MEH" CLS vital with its source as a standalone span', as
'sentry.exclusive_time': 0,
'sentry.op': 'ui.webvital.cls',
'sentry.origin': 'auto.http.browser.cls',
'sentry.report_event': 'pagehide',
transaction: expect.stringContaining('index.html'),
'user_agent.original': expect.stringContaining('Chrome'),
'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
Expand Down Expand Up @@ -201,6 +203,7 @@ sentryTest('captures a "POOR" CLS vital with its source as a standalone span.',
'sentry.exclusive_time': 0,
'sentry.op': 'ui.webvital.cls',
'sentry.origin': 'auto.http.browser.cls',
'sentry.report_event': 'pagehide',
transaction: expect.stringContaining('index.html'),
'user_agent.original': expect.stringContaining('Chrome'),
'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
Expand Down Expand Up @@ -269,6 +272,7 @@ sentryTest(
'sentry.exclusive_time': 0,
'sentry.op': 'ui.webvital.cls',
'sentry.origin': 'auto.http.browser.cls',
'sentry.report_event': 'pagehide',
transaction: expect.stringContaining('index.html'),
'user_agent.original': expect.stringContaining('Chrome'),
'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
Expand Down Expand Up @@ -342,6 +346,8 @@ sentryTest(
// Ensure the CLS span is connected to the pageload span and trace
expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadSpanId);
expect(spanEnvelopeItem.trace_id).toEqual(pageloadTraceId);

expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('pagehide');
},
);

Expand Down Expand Up @@ -374,6 +380,8 @@ sentryTest('sends CLS of the initial page when soft-navigating to a new page', a
expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.15);
expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadEventData.contexts?.trace?.span_id);
expect(spanEnvelopeItem.trace_id).toEqual(pageloadTraceId);

expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('navigation');
});

sentryTest("doesn't send further CLS after the first navigation", async ({ getLocalTestUrl, page }) => {
Expand All @@ -398,6 +406,7 @@ sentryTest("doesn't send further CLS after the first navigation", async ({ getLo
const spanEnvelope = (await spanEnvelopePromise)[0];
const spanEnvelopeItem = spanEnvelope[1][0][1];
expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0);
expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('navigation');

getMultipleSentryEnvelopeRequests<SpanEnvelope>(page, 1, { envelopeType: 'span' }, () => {
throw new Error('Unexpected span - This should not happen!');
Expand Down Expand Up @@ -442,6 +451,7 @@ sentryTest("doesn't send further CLS after the first page hide", async ({ getLoc
const spanEnvelope = (await spanEnvelopePromise)[0];
const spanEnvelopeItem = spanEnvelope[1][0][1];
expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0);
expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('pagehide');

getMultipleSentryEnvelopeRequests<SpanEnvelope>(page, 1, { envelopeType: 'span' }, () => {
throw new Error('Unexpected span - This should not happen!');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl,
'sentry.exclusive_time': 0,
'sentry.op': 'ui.webvital.lcp',
'sentry.origin': 'auto.http.browser.lcp',
'sentry.report_event': 'pagehide',
transaction: expect.stringContaining('index.html'),
'user_agent.original': expect.stringContaining('Chrome'),
'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
Expand Down Expand Up @@ -181,6 +182,7 @@ sentryTest('sends LCP of the initial page when soft-navigating to a new page', a

expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0);
expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadEventData.contexts?.trace?.span_id);
expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('navigation');
expect(spanEnvelopeItem.trace_id).toBe(pageloadEventData.contexts?.trace?.trace_id);
});

Expand All @@ -194,10 +196,10 @@ sentryTest("doesn't send further LCP after the first navigation", async ({ getLo

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

const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
const pageloadEventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);

expect(eventData.type).toBe('transaction');
expect(eventData.contexts?.trace?.op).toBe('pageload');
expect(pageloadEventData.type).toBe('transaction');
expect(pageloadEventData.contexts?.trace?.op).toBe('pageload');

const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
page,
Expand All @@ -214,6 +216,8 @@ sentryTest("doesn't send further LCP after the first navigation", async ({ getLo
const spanEnvelope = (await spanEnvelopePromise)[0];
const spanEnvelopeItem = spanEnvelope[1][0][1];
expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0);
expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('navigation');
expect(spanEnvelopeItem.trace_id).toBe(pageloadEventData.contexts?.trace?.trace_id);

getMultipleSentryEnvelopeRequests<SpanEnvelope>(page, 1, { envelopeType: 'span' }, () => {
throw new Error('Unexpected span - This should not happen!');
Expand Down Expand Up @@ -246,10 +250,10 @@ sentryTest("doesn't send further LCP after the first page hide", async ({ getLoc

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

const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
const pageloadEventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);

expect(eventData.type).toBe('transaction');
expect(eventData.contexts?.trace?.op).toBe('pageload');
expect(pageloadEventData.type).toBe('transaction');
expect(pageloadEventData.contexts?.trace?.op).toBe('pageload');

const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
page,
Expand All @@ -266,6 +270,8 @@ sentryTest("doesn't send further LCP after the first page hide", async ({ getLoc
const spanEnvelope = (await spanEnvelopePromise)[0];
const spanEnvelopeItem = spanEnvelope[1][0][1];
expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0);
expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('pagehide');
expect(spanEnvelopeItem.trace_id).toBe(pageloadEventData.contexts?.trace?.trace_id);

getMultipleSentryEnvelopeRequests<SpanEnvelope>(page, 1, { envelopeType: 'span' }, () => {
throw new Error('Unexpected span - This should not happen!');
Expand Down
10 changes: 7 additions & 3 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,9 +715,13 @@ export function _addResourceSpans(

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);

const { name, version } = extractNetworkProtocol(entry.nextHopProtocol);
attributes['network.protocol.name'] = name;
attributes['network.protocol.version'] = version;
// Checking for only `undefined` and `null` is intentional because it's
// valid for `nextHopProtocol` to be an empty string.
if (entry.nextHopProtocol != null) {
const { name, version } = extractNetworkProtocol(entry.nextHopProtocol);
attributes['network.protocol.name'] = name;
attributes['network.protocol.version'] = version;
}

const startTimestamp = timeOrigin + startTime;
const endTimestamp = startTimestamp + duration;
Expand Down
73 changes: 14 additions & 59 deletions packages/browser-utils/src/metrics/cls.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import type { SpanAttributes } from '@sentry/core';
import {
browserPerformanceTimeOrigin,
getActiveSpan,
getClient,
getCurrentScope,
getRootSpan,
htmlTreeAsString,
logger,
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
} from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';
import { addClsInstrumentationHandler } from './instrument';
import { msToSec, startStandaloneWebVitalSpan } from './utils';
import { onHidden } from './web-vitals/lib/onHidden';
import type { WebVitalReportEvent } from './utils';
import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils';

/**
* Starts tracking the Cumulative Layout Shift on the current page and collects the value once
Expand All @@ -31,24 +27,11 @@ import { onHidden } from './web-vitals/lib/onHidden';
export function trackClsAsStandaloneSpan(): void {
let standaloneCLsValue = 0;
let standaloneClsEntry: LayoutShift | undefined;
let pageloadSpanId: string | undefined;

if (!supportsLayoutShift()) {
if (!supportsWebVital('layout-shift')) {
return;
}

let sentSpan = false;
function _collectClsOnce() {
if (sentSpan) {
return;
}
sentSpan = true;
if (pageloadSpanId) {
sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry, pageloadSpanId);
}
cleanupClsHandler();
}

const cleanupClsHandler = addClsInstrumentationHandler(({ metric }) => {
const entry = metric.entries[metric.entries.length - 1] as LayoutShift | undefined;
if (!entry) {
Expand All @@ -58,40 +41,18 @@ export function trackClsAsStandaloneSpan(): void {
standaloneClsEntry = entry;
}, true);

onHidden(() => {
_collectClsOnce();
listenForWebVitalReportEvents((reportEvent, pageloadSpanId) => {
sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry, pageloadSpanId, reportEvent);
cleanupClsHandler();
});

// Since the call chain of this function is synchronous and evaluates before the SDK client is created,
// we need to wait with subscribing to a client hook until the client is created. Therefore, we defer
// to the next tick after the SDK setup.
setTimeout(() => {
const client = getClient();

if (!client) {
return;
}

const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
if (!options?.isRedirect) {
_collectClsOnce();
unsubscribeStartNavigation?.();
}
});

const activeSpan = getActiveSpan();
if (activeSpan) {
const rootSpan = getRootSpan(activeSpan);
const spanJSON = spanToJSON(rootSpan);
if (spanJSON.op === 'pageload') {
pageloadSpanId = rootSpan.spanContext().spanId;
}
}
}, 0);
}

function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined, pageloadSpanId: string) {
function sendStandaloneClsSpan(
clsValue: number,
entry: LayoutShift | undefined,
pageloadSpanId: string,
reportEvent: WebVitalReportEvent,
) {
DEBUG_BUILD && logger.log(`Sending CLS span (${clsValue})`);

const startTime = msToSec((browserPerformanceTimeOrigin() || 0) + (entry?.startTime || 0));
Expand All @@ -105,6 +66,8 @@ function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined,
[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry?.duration || 0,
// attach the pageload span id to the CLS span so that we can link them in the UI
'sentry.pageload.span_id': pageloadSpanId,
// describes what triggered the web vital to be reported
'sentry.report_event': reportEvent,
};

// Add CLS sources as span attributes to help with debugging layout shifts
Expand Down Expand Up @@ -133,11 +96,3 @@ function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined,
span.end(startTime);
}
}

function supportsLayoutShift(): boolean {
try {
return PerformanceObserver.supportedEntryTypes.includes('layout-shift');
} catch {
return false;
}
}
Loading
Loading