Skip to content

Commit 455c1c5

Browse files
committed
add web vital support
1 parent 6a87239 commit 455c1c5

5 files changed

Lines changed: 63 additions & 116 deletions

File tree

packages/browser-utils/src/metrics/cls.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ import {
55
getCurrentScope,
66
htmlTreeAsString,
77
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
8-
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
9-
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
108
SEMANTIC_ATTRIBUTE_SENTRY_OP,
119
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
10+
startInactiveSpan,
1211
timestampInSeconds,
1312
} from '@sentry/core';
1413
import { DEBUG_BUILD } from '../debug-build';
14+
import { WINDOW } from '../types';
1515
import { addClsInstrumentationHandler } from './instrument';
1616
import type { WebVitalReportEvent } from './utils';
17-
import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils';
17+
import { listenForWebVitalReportEvents, msToSec, supportsWebVital } from './utils';
1818

1919
/**
2020
* Starts tracking the Cumulative Layout Shift on the current page and collects the value once
@@ -72,6 +72,17 @@ export function _sendStandaloneClsSpan(
7272
'sentry.pageload.span_id': pageloadSpanId,
7373
// describes what triggered the web vital to be reported
7474
'sentry.report_event': reportEvent,
75+
76+
// TODO: Relay currently expects 'cls', but we should consider 'cls.value'
77+
'cls': clsValue,
78+
'cls.value': clsValue,
79+
80+
transaction: routeName,
81+
82+
// Web vital score calculation relies on the user agent to account for different
83+
// browsers setting different thresholds for what is considered a good/meh/bad value.
84+
// For example: Chrome vs. Chrome Mobile
85+
'user_agent.original': WINDOW.navigator?.userAgent,
7586
};
7687

7788
// Add CLS sources as span attributes to help with debugging layout shifts
@@ -82,21 +93,11 @@ export function _sendStandaloneClsSpan(
8293
});
8394
}
8495

85-
const span = startStandaloneWebVitalSpan({
96+
// LayoutShift performance entries always have a duration of 0, so we don't need to add `entry.duration` here
97+
// see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/duration
98+
startInactiveSpan({
8699
name,
87-
transaction: routeName,
88100
attributes,
89101
startTime,
90-
});
91-
92-
if (span) {
93-
span.addEvent('cls', {
94-
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: '',
95-
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: clsValue,
96-
});
97-
98-
// LayoutShift performance entries always have a duration of 0, so we don't need to add `entry.duration` here
99-
// see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/duration
100-
span.end(startTime);
101-
}
102+
})?.end(startTime);
102103
}

packages/browser-utils/src/metrics/inp.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ import {
77
htmlTreeAsString,
88
isBrowser,
99
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
10-
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
11-
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
1210
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1311
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1412
spanToJSON,
13+
startInactiveSpan,
1514
} from '@sentry/core';
1615
import { WINDOW } from '../types';
1716
import type { InstrumentationHandlerCallback } from './instrument';
@@ -20,7 +19,7 @@ import {
2019
addPerformanceInstrumentationHandler,
2120
isPerformanceEventTiming,
2221
} from './instrument';
23-
import { getBrowserPerformanceAPI, msToSec, startStandaloneWebVitalSpan } from './utils';
22+
import { getBrowserPerformanceAPI, msToSec } from './utils';
2423

2524
interface InteractionContext {
2625
span: Span | undefined;
@@ -136,23 +135,24 @@ export const _onInp: InstrumentationHandlerCallback = ({ metric }) => {
136135
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.inp',
137136
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `ui.interaction.${interactionType}`,
138137
[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry.duration,
138+
139+
// TODO: Relay currently expects 'inp', but we should consider 'inp.value'
140+
'inp': metric.value,
141+
'inp.value': metric.value,
142+
143+
transaction: routeName,
144+
145+
// Web vital score calculation relies on the user agent to account for different
146+
// browsers setting different thresholds for what is considered a good/meh/bad value.
147+
// For example: Chrome vs. Chrome Mobile
148+
'user_agent.original': WINDOW.navigator?.userAgent,
139149
};
140150

141-
const span = startStandaloneWebVitalSpan({
151+
startInactiveSpan({
142152
name,
143-
transaction: routeName,
144153
attributes,
145154
startTime,
146-
});
147-
148-
if (span) {
149-
span.addEvent('inp', {
150-
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond',
151-
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value,
152-
});
153-
154-
span.end(startTime + duration);
155-
}
155+
})?.end(startTime + duration);
156156
};
157157

158158
/**

packages/browser-utils/src/metrics/lcp.ts

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import {
55
getCurrentScope,
66
htmlTreeAsString,
77
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
8-
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
9-
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
108
SEMANTIC_ATTRIBUTE_SENTRY_OP,
119
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
10+
startInactiveSpan,
1211
} from '@sentry/core';
1312
import { DEBUG_BUILD } from '../debug-build';
13+
import { WINDOW } from '../types';
1414
import { addLcpInstrumentationHandler } from './instrument';
1515
import type { WebVitalReportEvent } from './utils';
16-
import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils';
16+
import { listenForWebVitalReportEvents, msToSec, supportsWebVital } from './utils';
1717

1818
/**
1919
* Starts tracking the Largest Contentful Paint on the current page and collects the value once
@@ -71,39 +71,29 @@ export function _sendStandaloneLcpSpan(
7171
'sentry.pageload.span_id': pageloadSpanId,
7272
// describes what triggered the web vital to be reported
7373
'sentry.report_event': reportEvent,
74-
};
75-
76-
if (entry) {
77-
entry.element && (attributes['lcp.element'] = htmlTreeAsString(entry.element));
78-
entry.id && (attributes['lcp.id'] = entry.id);
7974

80-
entry.url && (attributes['lcp.url'] = entry.url);
75+
// TODO: Relay currently expects 'lcp', but we should consider 'lcp.value'
76+
'lcp': lcpValue,
77+
'lcp.value': lcpValue,
8178

82-
// loadTime is the time of LCP that's related to receiving the LCP element response..
83-
entry.loadTime != null && (attributes['lcp.loadTime'] = entry.loadTime);
79+
'lcp.element': entry?.element ? htmlTreeAsString(entry.element) : undefined,
80+
'lcp.id': entry?.id,
81+
'lcp.url': entry?.url,
82+
'lcp.loadTime': entry?.loadTime,
83+
'lcp.renderTime': entry?.renderTime,
84+
'lcp.size': entry?.size,
8485

85-
// renderTime is loadTime + rendering time
86-
// it's 0 if the LCP element is loaded from a 3rd party origin that doesn't send the
87-
// `Timing-Allow-Origin` header.
88-
entry.renderTime != null && (attributes['lcp.renderTime'] = entry.renderTime);
86+
transaction: routeName,
8987

90-
entry.size != null && (attributes['lcp.size'] = entry.size);
91-
}
88+
// Web vital score calculation relies on the user agent to account for different
89+
// browsers setting different thresholds for what is considered a good/meh/bad value.
90+
// For example: Chrome vs. Chrome Mobile
91+
'user_agent.original': WINDOW.navigator?.userAgent,
92+
};
9293

93-
const span = startStandaloneWebVitalSpan({
94+
startInactiveSpan({
9495
name,
95-
transaction: routeName,
9696
attributes,
9797
startTime,
98-
});
99-
100-
if (span) {
101-
span.addEvent('lcp', {
102-
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond',
103-
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: lcpValue,
104-
});
105-
106-
// LCP is a point-in-time metric, so we end the span immediately
107-
span.end(startTime);
108-
}
98+
})?.end(startTime);
10999
}

packages/browser-utils/src/metrics/utils.ts

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -78,49 +78,11 @@ interface StandaloneWebVitalSpanOptions {
7878
* @returns an inactive, standalone and NOT YET ended span
7979
*/
8080
export function startStandaloneWebVitalSpan(options: StandaloneWebVitalSpanOptions): Span | undefined {
81-
const client = getClient();
82-
if (!client) {
83-
return;
84-
}
85-
86-
const { name, transaction, attributes: passedAttributes, startTime } = options;
87-
88-
const { release, environment, sendDefaultPii } = client.getOptions();
89-
// We need to get the replay, user, and activeTransaction from the current scope
90-
// so that we can associate replay id, profile id, and a user display to the span
91-
const replay = client.getIntegrationByName<Integration & { getReplayId: () => string }>('Replay');
92-
const replayId = replay?.getReplayId();
9381

94-
const scope = getCurrentScope();
82+
const { name, attributes: passedAttributes, startTime } = options;
9583

96-
const user = scope.getUser();
97-
const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined;
98-
99-
let profileId: string | undefined;
100-
try {
101-
// @ts-expect-error skip optional chaining to save bundle size with try catch
102-
profileId = scope.getScopeData().contexts.profile.profile_id;
103-
} catch {
104-
// do nothing
105-
}
10684

10785
const attributes: SpanAttributes = {
108-
release,
109-
environment,
110-
111-
user: userDisplay || undefined,
112-
profile_id: profileId || undefined,
113-
replay_id: replayId || undefined,
114-
115-
transaction,
116-
117-
// Web vital score calculation relies on the user agent to account for different
118-
// browsers setting different thresholds for what is considered a good/meh/bad value.
119-
// For example: Chrome vs. Chrome Mobile
120-
'user_agent.original': WINDOW.navigator?.userAgent,
121-
122-
// This tells Sentry to infer the IP address from the request
123-
'client.address': sendDefaultPii ? '{{auto}}' : undefined,
12486

12587
...passedAttributes,
12688
};
@@ -129,9 +91,6 @@ export function startStandaloneWebVitalSpan(options: StandaloneWebVitalSpanOptio
12991
name,
13092
attributes,
13193
startTime,
132-
experimental: {
133-
standalone: true,
134-
},
13594
});
13695
}
13796

packages/browser/src/integrations/spanstreaming.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import type { Client, IntegrationFn, Span, SpanV2JSON, SpanV2JSONWithSegmentRef } from '@sentry/core';
1+
import type { IntegrationFn } from '@sentry/core';
22
import {
33
captureSpan,
4-
createSpanV2Envelope,
54
debug,
65
defineIntegration,
7-
getDynamicSamplingContextFromSpan,
86
isV2BeforeSendSpanCallback,
7+
safeSetSpanJSONAttributes,
98
SpanBuffer,
109
} from '@sentry/core';
1110
import { DEBUG_BUILD } from '../debug-build';
12-
import { WINDOW } from '../helpers';
1311

1412
export interface SpanStreamingOptions {
1513
batchLimit: number;
@@ -25,14 +23,6 @@ export const spanStreamingIntegration = defineIntegration(((userOptions?: Partia
2523
debug.warn('SpanStreaming batchLimit must be between 1 and 1000, defaulting to 1000');
2624
}
2725

28-
const options: SpanStreamingOptions = {
29-
...userOptions,
30-
batchLimit:
31-
userOptions?.batchLimit && userOptions.batchLimit <= 1000 && userOptions.batchLimit >= 1
32-
? userOptions.batchLimit
33-
: 1000,
34-
};
35-
3626
return {
3727
name: 'SpanStreaming',
3828
setup(client) {
@@ -62,6 +52,13 @@ export const spanStreamingIntegration = defineIntegration(((userOptions?: Partia
6252
captureSpan(span, client);
6353
});
6454

55+
client.on('processSpan', (spanJSON) => {
56+
safeSetSpanJSONAttributes(spanJSON, {
57+
// browser-only: tell Sentry to infer the IP address from the request
58+
'client.address': client.getOptions().sendDefaultPii ? '{{auto}}' : undefined,
59+
});
60+
});
61+
6562
// in addition to capturing the span, we also flush the trace when the segment
6663
// span ends to ensure things are sent timely. We never know when the browser
6764
// is closed, users navigate away, etc.

0 commit comments

Comments
 (0)