Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Work in this release was contributed by @psh4607, @thijsw, @trinitiwowka, @nehap

- feat(browser)!: `browser.navigation.type` on web vital and bfcache navigation spans now carries the navigation type exactly as web-vitals reports it. `bfcache` is now `back-forward-cache`, and a back/forward navigation that missed the bfcache (`back-forward`) or a discarded-tab restore (`restore`) is no longer folded into `navigate`. Update any dashboards or alerts filtering on `bfcache`.
- feat(browser): The pageload span now carries `browser.navigation.type`, the navigation type web-vitals reported TTFB and FCP for. Those vitals, and FP, are attributes of the pageload span rather than spans of their own, so until now there was no navigation type to read them against.
- feat(browser): INP spans now carry `browser.web_vital.inp.target`, the element the user interacted with, and `browser.web_vital.inp.interaction_type` (`click`, `hover`, `drag` or `press`). Both were only available as the span's name and op, which always have a value. The attributes are left out when there is nothing to describe: an INP that web-vitals reports without an interaction gets neither, and an element that could not be resolved gets no target.
- feat(core): Add `createFetchIntegration`, the shared implementation behind the global-`fetch` integrations in `@sentry/bun`, `@sentry/cloudflare`, `@sentry/deno` and `@sentry/vercel-edge`. Those four packages carried four copies of it; they now share one. Two changes come out of that:
- All four gain a `tracePropagation` option (default `true`). Turn it off to stop injecting `sentry-trace` and `baggage` without also turning off spans. To scope propagation to specific URLs, keep using `tracePropagationTargets` in the client options.
- Integration options now follow the client. Previously a second `Sentry.init()` in the same process silently reused the options of the first one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ sentryTest(
'sentry.op': { value: 'ui.interaction.click', type: 'string' },
'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.target': { value: 'body > NormalButton', type: 'string' },
'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' },
'browser.navigation.type': { value: 'navigate', type: 'string' },
'sentry.transaction': { value: 'test-url', type: 'string' },
'sentry.segment.name': { value: 'test-url', type: 'string' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ sentryTest(
'sentry.op': { value: 'ui.interaction.click', type: 'string' },
'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.target': { value: 'body > nav#navigation > NavigationLink', type: 'string' },
'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' },
'browser.navigation.type': { value: 'navigate', type: 'string' },
'sentry.transaction': { value: 'test-url', type: 'string' },
'sentry.segment.name': { value: 'test-url', type: 'string' },
Expand Down Expand Up @@ -163,6 +165,8 @@ sentryTest(
'sentry.op': { value: 'ui.interaction.click', type: 'string' },
'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.target': { value: 'body > nav#navigation > NavigationLink', type: 'string' },
'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' },
'browser.navigation.type': { value: 'navigate', type: 'string' },
'sentry.transaction': { value: 'test-url', type: 'string' },
'sentry.segment.name': { value: 'test-url', type: 'string' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ sentryTest(
'sentry.op': { value: 'ui.interaction.click', type: 'string' },
'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.target': { value: 'body > NormalButton', type: 'string' },
'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' },
'browser.navigation.type': { value: 'navigate', type: 'string' },
// the parametrized route name flows onto the INP span
'sentry.transaction': { value: 'test-route', type: 'string' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ sentryTest(
'sentry.op': { value: 'ui.interaction.click', type: 'string' },
'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.target': { value: 'body > NormalButton', type: 'string' },
'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' },
'browser.navigation.type': { value: 'navigate', type: 'string' },
// the parametrized route name flows onto the INP span
'sentry.transaction': { value: 'test-route', type: 'string' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ sentryTest(
'sentry.op': { value: 'ui.interaction.click', type: 'string' },
'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.target': { value: 'body > NormalButton', type: 'string' },
'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' },
'browser.navigation.type': { value: 'navigate', type: 'string' },
'sentry.transaction': { value: 'test-url', type: 'string' },
'sentry.segment.name': { value: 'test-url', type: 'string' },
Expand Down Expand Up @@ -148,6 +150,8 @@ sentryTest(
'sentry.op': { value: 'ui.interaction.click', type: 'string' },
'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) },
'browser.web_vital.inp.target': { value: 'body > SlowButton', type: 'string' },
'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' },
'browser.navigation.type': { value: 'navigate', type: 'string' },
'sentry.transaction': { value: 'test-url', type: 'string' },
'sentry.segment.name': { value: 'test-url', type: 'string' },
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/web-vitals/inp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const INTERACTIONS_SPAN_MAP = new Map<number, InteractionContext>();

// Map to store element names by timestamp, since we get the DOM event before the PerformanceObserver entry
/** What `htmlTreeAsString` returns when it cannot describe the target. */
const UNKNOWN_ELEMENT_NAME = '<unknown>';
export const UNKNOWN_ELEMENT_NAME = '<unknown>';

const ELEMENT_NAME_TIMESTAMP_MAP = new Map<number, string>();

Expand Down
20 changes: 15 additions & 5 deletions packages/browser-utils/src/web-vitals/spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { DEBUG_BUILD } from '../debug-build';
import { htmlTreeAsString } from '../htmlTreeAsString';
import type { InteractionType } from './inp';
import { getCachedInteractionContext, INP_ENTRY_MAP, MAX_PLAUSIBLE_INP_DURATION } from './inp';
import { getCachedInteractionContext, INP_ENTRY_MAP, MAX_PLAUSIBLE_INP_DURATION, UNKNOWN_ELEMENT_NAME } from './inp';
import type { InstrumentationHandlerCallback, MetricNavigationType } from '../instrumentation/performanceObserver';
import {
addClsInstrumentationHandler,
Expand Down Expand Up @@ -381,7 +381,8 @@ export function _sendInpSpan(
// `ui.interaction.*` family, because falling outside it would hide exactly the fast navigations
// that web-vitals synthesizes these values for (GoogleChrome/web-vitals#724), reintroducing the
// reporting bias they were added to remove.
const interactionType = (entry && INP_ENTRY_MAP[entry.name]) || 'click';
const entryInteractionType = entry && INP_ENTRY_MAP[entry.name];
const interactionType = entryInteractionType || 'click';

const cachedContext = entry && getCachedInteractionContext(entry.interactionId);
const activeSpan = getActiveSpan();
Expand All @@ -392,15 +393,24 @@ export function _sendInpSpan(
const spanToUse = attributedSpan || cachedContext?.span || rootSpan;
const name = cachedContext?.elementName || (entry ? htmlTreeAsString(entry.target) : 'Interaction to next paint');

const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry?.duration ?? inpValue,
};

// The span's name and op always have a value, even for an INP without an entry, so they can't
// say whether there was an interaction to describe. These attributes can: they are only set for
// what was actually observed.
// TODO: use the `@sentry/conventions` constants once getsentry/sentry-conventions#641 is released.
entry && name !== UNKNOWN_ELEMENT_NAME && (attributes['browser.web_vital.inp.target'] = name);
entryInteractionType && (attributes['browser.web_vital.inp.interaction_type'] = entryInteractionType);

_emitWebVitalSpan({
name,
op: INTERACTION_TYPE_TO_SPAN_OP[interactionType],
origin: 'auto.http.browser.inp',
metricName: 'inp',
value: inpValue,
attributes: {
[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry?.duration ?? inpValue,
},
attributes,
startTime,
endTime: startTime + duration,
navigationType: metric?.navigationType,
Expand Down
28 changes: 28 additions & 0 deletions packages/browser-utils/test/web-vitals/spans.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,8 @@ describe('_sendInpSpan', () => {
'sentry.exclusive_time': 120,
'sentry.transaction': 'test-route',
'sentry.segment.name': 'test-route',
'browser.web_vital.inp.target': '<button>',
'browser.web_vital.inp.interaction_type': 'click',
}),
}),
);
Expand Down Expand Up @@ -691,6 +693,32 @@ describe('_sendInpSpan', () => {
}),
);
});

it('leaves out the target when the element could not be resolved', () => {
vi.spyOn(inpModule, 'getCachedInteractionContext').mockReturnValue(undefined);
vi.mocked(htmlTreeAsString).mockReturnValue('<unknown>');

_sendInpSpan(80, { name: 'keydown', startTime: 600, duration: 80, interactionId: 2, target: null } as any);

const attributes = vi.mocked(SentryCoreBrowser.startInactiveSpan).mock.calls[0]![0].attributes!;
expect(attributes).not.toHaveProperty('browser.web_vital.inp.target');
expect(attributes['browser.web_vital.inp.interaction_type']).toBe('press');
});

it('leaves out the target and interaction type for an INP without an entry', () => {
_sendInpSpan(40, undefined);

// The name and op still have a value, which is why they can't stand in for these attributes.
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
name: 'Interaction to next paint',
attributes: expect.objectContaining({ 'sentry.op': 'ui.interaction.click' }),
}),
);
const attributes = vi.mocked(SentryCoreBrowser.startInactiveSpan).mock.calls[0]![0].attributes!;
expect(attributes).not.toHaveProperty('browser.web_vital.inp.target');
expect(attributes).not.toHaveProperty('browser.web_vital.inp.interaction_type');
});
});

describe('trackInpAsSpan', () => {
Expand Down
Loading