Skip to content

Commit a0415aa

Browse files
authored
ref(browser): Create standalone INP spans via startInactiveSpan (#11788)
Refactors how we create INP spans. Previously we'd directly initialize a `SentrySpan` instance with the necessary properties and manually sample the span. Ironically, we heavily discourage directly initializing a `Span` class. With this change we leverage the `standalone: true` flag introduced in #11696. This way, we can also use the built-in sampling functionality and ensure that we only have one logic for creating standalone span envelopes. Also bundle size should decrease slightly 🤞 Adjusted the integration tests to more thoroughly test the INP span envelopes
1 parent 4b6cb4a commit a0415aa

File tree

2 files changed

+71
-55
lines changed
  • dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp
  • packages/browser-utils/src/metrics

2 files changed

+71
-55
lines changed

dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts

+63-21
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { expect } from '@playwright/test';
2-
import type { Event as SentryEvent, SpanJSON } from '@sentry/types';
2+
import type { Event as SentryEvent, SpanEnvelope, SpanJSON } from '@sentry/types';
33

44
import { sentryTest } from '../../../../utils/fixtures';
55
import {
66
getFirstSentryEnvelopeRequest,
77
getMultipleSentryEnvelopeRequests,
8+
properFullEnvelopeRequestParser,
89
shouldSkipTracingTest,
910
} from '../../../../utils/helpers';
1011

@@ -28,9 +29,12 @@ sentryTest('should capture an INP click event span.', async ({ browserName, getL
2829
await page.goto(url);
2930
await getFirstSentryEnvelopeRequest<SentryEvent>(page); // wait for page load
3031

31-
const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<SpanJSON>(page, 1, {
32-
envelopeType: 'span',
33-
});
32+
const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
33+
page,
34+
1,
35+
{ envelopeType: 'span' },
36+
properFullEnvelopeRequestParser,
37+
);
3438

3539
await page.locator('[data-test-id=normal-button]').click();
3640
await page.locator('.clicked[data-test-id=normal-button]').isVisible();
@@ -43,14 +47,53 @@ sentryTest('should capture an INP click event span.', async ({ browserName, getL
4347
});
4448

4549
// Get the INP span envelope
46-
const spanEnvelopes = await spanEnvelopesPromise;
47-
48-
expect(spanEnvelopes).toHaveLength(1);
49-
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
50-
expect(spanEnvelopes[0].description).toBe('body > NormalButton');
51-
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0);
52-
expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(0);
53-
expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond');
50+
const spanEnvelope = (await spanEnvelopePromise)[0];
51+
52+
const spanEnvelopeHeaders = spanEnvelope[0];
53+
const spanEnvelopeItem = spanEnvelope[1][0][1];
54+
55+
const traceId = spanEnvelopeHeaders.trace!.trace_id;
56+
expect(traceId).toMatch(/[a-f0-9]{32}/);
57+
58+
expect(spanEnvelopeHeaders).toEqual({
59+
sent_at: expect.any(String),
60+
trace: {
61+
environment: 'production',
62+
public_key: 'public',
63+
sample_rate: '1',
64+
sampled: 'true',
65+
trace_id: traceId,
66+
},
67+
});
68+
69+
const inpValue = spanEnvelopeItem.measurements?.inp.value;
70+
expect(inpValue).toBeGreaterThan(0);
71+
72+
expect(spanEnvelopeItem).toEqual({
73+
data: {
74+
'sentry.exclusive_time': inpValue,
75+
'sentry.op': 'ui.interaction.click',
76+
'sentry.origin': 'manual',
77+
'sentry.sample_rate': 1,
78+
'sentry.source': 'custom',
79+
},
80+
measurements: {
81+
inp: {
82+
unit: 'millisecond',
83+
value: inpValue,
84+
},
85+
},
86+
description: 'body > NormalButton',
87+
exclusive_time: inpValue,
88+
op: 'ui.interaction.click',
89+
origin: 'manual',
90+
is_segment: true,
91+
segment_id: spanEnvelopeItem.span_id,
92+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
93+
start_timestamp: expect.any(Number),
94+
timestamp: expect.any(Number),
95+
trace_id: traceId,
96+
});
5497
});
5598

5699
sentryTest(
@@ -85,7 +128,7 @@ sentryTest(
85128

86129
await page.waitForTimeout(500);
87130

88-
const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<SpanJSON>(page, 1, {
131+
const spanPromise = getMultipleSentryEnvelopeRequests<SpanJSON>(page, 1, {
89132
envelopeType: 'span',
90133
});
91134

@@ -95,13 +138,12 @@ sentryTest(
95138
});
96139

97140
// Get the INP span envelope
98-
const spanEnvelopes = await spanEnvelopesPromise;
99-
100-
expect(spanEnvelopes).toHaveLength(1);
101-
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
102-
expect(spanEnvelopes[0].description).toBe('body > SlowButton');
103-
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(400);
104-
expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(400);
105-
expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond');
141+
const span = (await spanPromise)[0];
142+
143+
expect(span.op).toBe('ui.interaction.click');
144+
expect(span.description).toBe('body > SlowButton');
145+
expect(span.exclusive_time).toBeGreaterThan(400);
146+
expect(span.measurements?.inp.value).toBeGreaterThan(400);
147+
expect(span.measurements?.inp.unit).toBe('millisecond');
106148
},
107149
);

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

+8-34
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,15 @@ import {
22
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
33
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
44
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
5-
SentrySpan,
6-
createSpanEnvelope,
75
getActiveSpan,
86
getClient,
97
getCurrentScope,
108
getRootSpan,
11-
sampleSpan,
12-
spanIsSampled,
139
spanToJSON,
10+
startInactiveSpan,
1411
} from '@sentry/core';
1512
import type { Integration, SpanAttributes } from '@sentry/types';
16-
import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString, logger } from '@sentry/utils';
17-
import { DEBUG_BUILD } from '../debug-build';
13+
import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString } from '@sentry/utils';
1814
import { addInpInstrumentationHandler } from './instrument';
1915
import { getBrowserPerformanceAPI, msToSec } from './utils';
2016

@@ -100,7 +96,6 @@ function _trackINP(): () => void {
10096
const profileId = scope.getScopeData().contexts?.profile?.profile_id as string | undefined;
10197

10298
const name = htmlTreeAsString(entry.target);
103-
const parentSampled = activeSpan ? spanIsSampled(activeSpan) : undefined;
10499
const attributes: SpanAttributes = dropUndefinedKeys({
105100
release: options.release,
106101
environment: options.environment,
@@ -111,42 +106,21 @@ function _trackINP(): () => void {
111106
replay_id: replayId || undefined,
112107
});
113108

114-
/** Check to see if the span should be sampled */
115-
const [sampled] = sampleSpan(options, {
109+
const span = startInactiveSpan({
116110
name,
117-
parentSampled,
118-
attributes,
119-
transactionContext: {
120-
name,
121-
parentSampled,
122-
},
123-
});
124-
125-
// Nothing to do
126-
if (!sampled) {
127-
return;
128-
}
129-
130-
const span = new SentrySpan({
131-
startTimestamp: startTime,
132-
endTimestamp: startTime + duration,
133111
op: `ui.interaction.${interactionType}`,
134-
name,
135112
attributes,
113+
startTime: startTime,
114+
experimental: {
115+
standalone: true,
116+
},
136117
});
137118

138119
span.addEvent('inp', {
139120
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond',
140121
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value,
141122
});
142123

143-
const envelope = createSpanEnvelope([span]);
144-
const transport = client && client.getTransport();
145-
if (transport) {
146-
transport.send(envelope).then(null, reason => {
147-
DEBUG_BUILD && logger.error('Error while sending interaction:', reason);
148-
});
149-
}
150-
return;
124+
span.end(startTime + duration);
151125
});
152126
}

0 commit comments

Comments
 (0)