Skip to content

Commit

Permalink
ref(browser): Create standalone INP spans via startInactiveSpan (#1…
Browse files Browse the repository at this point in the history
…1788)

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
  • Loading branch information
Lms24 committed Apr 25, 2024
1 parent 4b6cb4a commit a0415aa
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { expect } from '@playwright/test';
import type { Event as SentryEvent, SpanJSON } from '@sentry/types';
import type { Event as SentryEvent, SpanEnvelope, SpanJSON } from '@sentry/types';

import { sentryTest } from '../../../../utils/fixtures';
import {
getFirstSentryEnvelopeRequest,
getMultipleSentryEnvelopeRequests,
properFullEnvelopeRequestParser,
shouldSkipTracingTest,
} from '../../../../utils/helpers';

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

const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<SpanJSON>(page, 1, {
envelopeType: 'span',
});
const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
page,
1,
{ envelopeType: 'span' },
properFullEnvelopeRequestParser,
);

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

// Get the INP span envelope
const spanEnvelopes = await spanEnvelopesPromise;

expect(spanEnvelopes).toHaveLength(1);
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
expect(spanEnvelopes[0].description).toBe('body > NormalButton');
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0);
expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(0);
expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond');
const spanEnvelope = (await spanEnvelopePromise)[0];

const spanEnvelopeHeaders = spanEnvelope[0];
const spanEnvelopeItem = spanEnvelope[1][0][1];

const traceId = spanEnvelopeHeaders.trace!.trace_id;
expect(traceId).toMatch(/[a-f0-9]{32}/);

expect(spanEnvelopeHeaders).toEqual({
sent_at: expect.any(String),
trace: {
environment: 'production',
public_key: 'public',
sample_rate: '1',
sampled: 'true',
trace_id: traceId,
},
});

const inpValue = spanEnvelopeItem.measurements?.inp.value;
expect(inpValue).toBeGreaterThan(0);

expect(spanEnvelopeItem).toEqual({
data: {
'sentry.exclusive_time': inpValue,
'sentry.op': 'ui.interaction.click',
'sentry.origin': 'manual',
'sentry.sample_rate': 1,
'sentry.source': 'custom',
},
measurements: {
inp: {
unit: 'millisecond',
value: inpValue,
},
},
description: 'body > NormalButton',
exclusive_time: inpValue,
op: 'ui.interaction.click',
origin: 'manual',
is_segment: true,
segment_id: spanEnvelopeItem.span_id,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: traceId,
});
});

sentryTest(
Expand Down Expand Up @@ -85,7 +128,7 @@ sentryTest(

await page.waitForTimeout(500);

const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<SpanJSON>(page, 1, {
const spanPromise = getMultipleSentryEnvelopeRequests<SpanJSON>(page, 1, {
envelopeType: 'span',
});

Expand All @@ -95,13 +138,12 @@ sentryTest(
});

// Get the INP span envelope
const spanEnvelopes = await spanEnvelopesPromise;

expect(spanEnvelopes).toHaveLength(1);
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
expect(spanEnvelopes[0].description).toBe('body > SlowButton');
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(400);
expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(400);
expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond');
const span = (await spanPromise)[0];

expect(span.op).toBe('ui.interaction.click');
expect(span.description).toBe('body > SlowButton');
expect(span.exclusive_time).toBeGreaterThan(400);
expect(span.measurements?.inp.value).toBeGreaterThan(400);
expect(span.measurements?.inp.unit).toBe('millisecond');
},
);
42 changes: 8 additions & 34 deletions packages/browser-utils/src/metrics/inp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@ import {
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
SentrySpan,
createSpanEnvelope,
getActiveSpan,
getClient,
getCurrentScope,
getRootSpan,
sampleSpan,
spanIsSampled,
spanToJSON,
startInactiveSpan,
} from '@sentry/core';
import type { Integration, SpanAttributes } from '@sentry/types';
import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString, logger } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString } from '@sentry/utils';
import { addInpInstrumentationHandler } from './instrument';
import { getBrowserPerformanceAPI, msToSec } from './utils';

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

const name = htmlTreeAsString(entry.target);
const parentSampled = activeSpan ? spanIsSampled(activeSpan) : undefined;
const attributes: SpanAttributes = dropUndefinedKeys({
release: options.release,
environment: options.environment,
Expand All @@ -111,42 +106,21 @@ function _trackINP(): () => void {
replay_id: replayId || undefined,
});

/** Check to see if the span should be sampled */
const [sampled] = sampleSpan(options, {
const span = startInactiveSpan({
name,
parentSampled,
attributes,
transactionContext: {
name,
parentSampled,
},
});

// Nothing to do
if (!sampled) {
return;
}

const span = new SentrySpan({
startTimestamp: startTime,
endTimestamp: startTime + duration,
op: `ui.interaction.${interactionType}`,
name,
attributes,
startTime: startTime,
experimental: {
standalone: true,
},
});

span.addEvent('inp', {
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond',
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value,
});

const envelope = createSpanEnvelope([span]);
const transport = client && client.getTransport();
if (transport) {
transport.send(envelope).then(null, reason => {
DEBUG_BUILD && logger.error('Error while sending interaction:', reason);
});
}
return;
span.end(startTime + duration);
});
}

0 comments on commit a0415aa

Please sign in to comment.