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
9 changes: 9 additions & 0 deletions .oxlintrc.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@
"sdk/no-unfiltered-url-attributes": "error"
}
},
{
// The rule itself decides which files are browser-facing (see `no-unguarded-span-apis.js`);
// oxlint resolves `files` globs relative to each package's own config, so scoping it to
// individual packages here is not possible.
"files": ["**/src/**/*.ts", "**/src/**/*.tsx", "**/addon/**/*.ts"],
"rules": {
"sdk/no-unguarded-span-apis": "error"
}
},
{
"files": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
"rules": {
Expand Down
8 changes: 4 additions & 4 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init'),
gzip: true,
limit: '36 KB',
limit: '30 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
name: '@sentry/browser - with treeshaking flags',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init'),
gzip: true,
limit: '34 KB',
limit: '28 KB',
disablePlugins: ['@size-limit/esbuild'],
modifyWebpackConfig: function (config) {
const webpack = require('webpack');
Expand All @@ -40,7 +40,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init'),
gzip: true,
limit: '32 KB',
limit: '28 KB',
disablePlugins: ['@size-limit/esbuild'],
modifyWebpackConfig: function (config) {
const webpack = require('webpack');
Expand Down Expand Up @@ -230,7 +230,7 @@ module.exports = [
name: 'CDN Bundle',
path: createCDNPath('bundle.min.js'),
gzip: true,
limit: '37 KB',
limit: '32 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/performance/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SentrySpan, Span, SpanTimeInput, StartSpanOptions } from '@sentry/core';
import { spanToStaticSpanJSON, startInactiveSpan, withActiveSpan } from '@sentry/core';
import { spanToStaticSpanJSON, withActiveSpan, startInactiveSpan } from '@sentry/core/browser';
import { WINDOW } from '../types';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/web-vitals/spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
startInactiveSpan,
timestampInSeconds,
} from '@sentry/core';
import { startInactiveSpan } from '@sentry/core/browser';
import { DEBUG_BUILD } from '../debug-build';
import { htmlTreeAsString } from '../htmlTreeAsString';
import { WINDOW } from '../types';
Expand Down
71 changes: 40 additions & 31 deletions packages/browser-utils/test/web-vitals/spans.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as SentryCore from '@sentry/core';
import * as SentryCoreBrowser from '@sentry/core/browser';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { htmlTreeAsString } from '../../src/htmlTreeAsString';
import * as inpModule from '../../src/web-vitals/inp';
Expand All @@ -20,13 +21,21 @@ vi.mock('@sentry/core', async () => {
timestampInSeconds: vi.fn(),
getCurrentScope: vi.fn(),
getClient: vi.fn(),
startInactiveSpan: vi.fn(),
getActiveSpan: vi.fn(),
getRootSpan: vi.fn(),
spanToJSON: vi.fn(),
};
});

// `startInactiveSpan` comes from `@sentry/core/browser`, not the root entry - see `browserSpanApi.ts`.
vi.mock('@sentry/core/browser', async () => {
const actual = await vi.importActual('@sentry/core/browser');
return {
...actual,
startInactiveSpan: vi.fn(),
};
});

vi.mock('../../src/htmlTreeAsString', () => ({
htmlTreeAsString: vi.fn(),
}));
Expand Down Expand Up @@ -62,7 +71,7 @@ describe('_emitWebVitalSpan', () => {

beforeEach(() => {
vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any);
vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(mockSpan as any);
vi.mocked(SentryCoreBrowser.startInactiveSpan).mockReturnValue(mockSpan as any);
vi.mocked(SentryCore.spanToJSON).mockReturnValue({ attributes: {} } as any);
// A root span is its own root, which is what the web vital spans are parented to.
vi.mocked(SentryCore.getRootSpan).mockImplementation(span => span);
Expand All @@ -83,7 +92,7 @@ describe('_emitWebVitalSpan', () => {
startTime: 1.5,
});

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith({
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith({
name: 'Test Vital',
attributes: {
'sentry.origin': 'auto.http.browser.lcp',
Expand All @@ -98,7 +107,7 @@ describe('_emitWebVitalSpan', () => {
});

// No standalone flag
expect(SentryCore.startInactiveSpan).not.toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).not.toHaveBeenCalledWith(
expect.objectContaining({ experimental: expect.anything() }),
);

Expand All @@ -120,7 +129,7 @@ describe('_emitWebVitalSpan', () => {
parentSpan,
});

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'sentry.segment.name': 'Pageload',
Expand All @@ -141,7 +150,7 @@ describe('_emitWebVitalSpan', () => {
standalone: true,
});

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({ experimental: { standalone: true } }),
);
});
Expand All @@ -161,7 +170,7 @@ describe('_emitWebVitalSpan', () => {
standalone: true,
});

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'sentry.replay_id': 'replay-123',
Expand All @@ -186,7 +195,7 @@ describe('_emitWebVitalSpan', () => {
standalone: true,
});

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({ 'sentry._internal.replay_is_buffering': true }),
}),
Expand All @@ -207,7 +216,7 @@ describe('_emitWebVitalSpan', () => {
startTime: 1.5,
});

const attributes = vi.mocked(SentryCore.startInactiveSpan).mock.calls[0]![0].attributes!;
const attributes = vi.mocked(SentryCoreBrowser.startInactiveSpan).mock.calls[0]![0].attributes!;
expect(attributes['sentry.replay_id']).toBeUndefined();
});

Expand All @@ -229,7 +238,7 @@ describe('_emitWebVitalSpan', () => {
startTime: 1.0,
});

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'sentry.pageload.span_id': 'abc123',
Expand All @@ -255,7 +264,7 @@ describe('_emitWebVitalSpan', () => {
startTime: 1.0,
});

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.not.objectContaining({
'sentry.pageload.span_id': expect.anything(),
Expand All @@ -275,7 +284,7 @@ describe('_emitWebVitalSpan', () => {
startTime: 1.0,
});

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'browser.web_vital.cls.report_event': 'pagehide',
Expand All @@ -295,7 +304,7 @@ describe('_emitWebVitalSpan', () => {
startTime: 1.0,
});

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'custom.attr': 'value',
Expand All @@ -305,7 +314,7 @@ describe('_emitWebVitalSpan', () => {
});

it('handles when startInactiveSpan returns undefined', () => {
vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(undefined as any);
vi.mocked(SentryCoreBrowser.startInactiveSpan).mockReturnValue(undefined as any);

expect(() => {
_emitWebVitalSpan({
Expand Down Expand Up @@ -335,7 +344,7 @@ describe('_sendLcpSpan', () => {
vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any);
vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000);
vi.mocked(htmlTreeAsString).mockImplementation((node: any) => `<${node?.tagName || 'div'}>`);
vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(mockSpan as any);
vi.mocked(SentryCoreBrowser.startInactiveSpan).mockReturnValue(mockSpan as any);
vi.mocked(SentryCore.spanToJSON).mockReturnValue({
// The web vital span takes its segment name off the pageload span it is parented to.
name: 'test-route',
Expand All @@ -362,7 +371,7 @@ describe('_sendLcpSpan', () => {

_sendLcpSpan(250, mockEntry, mockPageloadSpan as any, 'pagehide');

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
name: '<img>',
attributes: expect.objectContaining({
Expand Down Expand Up @@ -392,7 +401,7 @@ describe('_sendLcpSpan', () => {
it('sends a streamed LCP span without entry data', () => {
_sendLcpSpan(250, undefined);

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
name: 'Largest contentful paint',
startTime: 1, // timeOrigin: 1000 / 1000
Expand All @@ -404,7 +413,7 @@ describe('_sendLcpSpan', () => {
_sendLcpSpan(0, undefined);
_sendLcpSpan(MAX_PLAUSIBLE_LCP_DURATION + 1, undefined);

expect(SentryCore.startInactiveSpan).not.toHaveBeenCalled();
expect(SentryCoreBrowser.startInactiveSpan).not.toHaveBeenCalled();
});
});

Expand All @@ -424,7 +433,7 @@ describe('_sendClsSpan', () => {
vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000);
vi.mocked(SentryCore.timestampInSeconds).mockReturnValue(1.5);
vi.mocked(htmlTreeAsString).mockImplementation((node: any) => `<${node?.tagName || 'div'}>`);
vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(mockSpan as any);
vi.mocked(SentryCoreBrowser.startInactiveSpan).mockReturnValue(mockSpan as any);
vi.mocked(SentryCore.spanToJSON).mockReturnValue({
// The web vital span takes its segment name off the pageload span it is parented to.
name: 'test-route',
Expand Down Expand Up @@ -462,7 +471,7 @@ describe('_sendClsSpan', () => {

_sendClsSpan(0.1, mockEntry, mockPageloadSpan as any, 'navigation');

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
name: '<div>',
attributes: expect.objectContaining({
Expand All @@ -484,7 +493,7 @@ describe('_sendClsSpan', () => {
_sendClsSpan(0, undefined);

expect(SentryCore.timestampInSeconds).toHaveBeenCalled();
expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
name: 'Layout shift',
startTime: 1.5,
Expand All @@ -508,7 +517,7 @@ describe('_sendInpSpan', () => {
vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any);
vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000);
vi.mocked(htmlTreeAsString).mockReturnValue('<button>');
vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(mockSpan as any);
vi.mocked(SentryCoreBrowser.startInactiveSpan).mockReturnValue(mockSpan as any);
vi.mocked(SentryCore.getActiveSpan).mockReturnValue(undefined);
vi.mocked(SentryCore.spanToJSON).mockReturnValue({ attributes: {} } as any);
// A root span is its own root, which is what the web vital spans are parented to.
Expand All @@ -533,7 +542,7 @@ describe('_sendInpSpan', () => {
_sendInpSpan(120, mockEntry);

// startTime = (1000 + 500) / 1000 = 1.5
expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
name: '<button>',
startTime: 1.5,
Expand Down Expand Up @@ -564,7 +573,7 @@ describe('_sendInpSpan', () => {

_sendInpSpan(80, mockEntry);

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'sentry.op': 'ui.interaction.press',
Expand Down Expand Up @@ -596,7 +605,7 @@ describe('_sendInpSpan', () => {

expect(inpModule.getCachedInteractionContext).toHaveBeenCalledWith(42);

expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
name: 'body > CachedButton',
attributes: expect.objectContaining({
Expand Down Expand Up @@ -628,7 +637,7 @@ describe('trackInpAsSpan', () => {
vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000);
vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any);
vi.mocked(SentryCore.getActiveSpan).mockReturnValue(undefined);
vi.mocked(SentryCore.startInactiveSpan).mockReturnValue({ end: vi.fn() } as any);
vi.mocked(SentryCoreBrowser.startInactiveSpan).mockReturnValue({ end: vi.fn() } as any);
vi.mocked(SentryCore.spanToJSON).mockReturnValue({ attributes: {} } as any);
// A root span is its own root, which is what the web vital spans are parented to.
vi.mocked(SentryCore.getRootSpan).mockImplementation(span => span);
Expand All @@ -648,7 +657,7 @@ describe('trackInpAsSpan', () => {
trackInpAsSpan(streamingClient);
inpCallback({ metric: validMetric });

const call = vi.mocked(SentryCore.startInactiveSpan).mock.calls[0]![0];
const call = vi.mocked(SentryCoreBrowser.startInactiveSpan).mock.calls[0]![0];
expect(call.experimental).toBeUndefined();
expect(call.attributes?.['sentry.op']).toBe('ui.interaction.click');
});
Expand All @@ -658,25 +667,25 @@ describe('trackInpAsSpan', () => {
trackInpAsSpan(staticClient);
inpCallback({ metric: validMetric });

const call = vi.mocked(SentryCore.startInactiveSpan).mock.calls[0]![0];
const call = vi.mocked(SentryCoreBrowser.startInactiveSpan).mock.calls[0]![0];
expect(call.experimental).toEqual({ standalone: true });
});

it('ignores INP metrics without a value', () => {
trackInpAsSpan(streamingClient);
inpCallback({ metric: { value: null, entries: [] } });
expect(SentryCore.startInactiveSpan).not.toHaveBeenCalled();
expect(SentryCoreBrowser.startInactiveSpan).not.toHaveBeenCalled();
});

it('ignores implausibly long INP durations', () => {
trackInpAsSpan(streamingClient);
inpCallback({ metric: { value: (inpModule.MAX_PLAUSIBLE_INP_DURATION + 1) * 1000, entries: validMetric.entries } });
expect(SentryCore.startInactiveSpan).not.toHaveBeenCalled();
expect(SentryCoreBrowser.startInactiveSpan).not.toHaveBeenCalled();
});

it('ignores INP metrics without a matching interaction entry', () => {
trackInpAsSpan(streamingClient);
inpCallback({ metric: { value: 120, entries: [{ name: 'scroll', duration: 120 }] } });
expect(SentryCore.startInactiveSpan).not.toHaveBeenCalled();
expect(SentryCoreBrowser.startInactiveSpan).not.toHaveBeenCalled();
});
});
4 changes: 2 additions & 2 deletions packages/browser/src/integrations/fetchStreamPerformance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
parseStringToURLObject,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
stripDataUrlContent,
filterCollectedUrl,
} from '@sentry/core';
startInactiveSpan,
} from '@sentry/core/browser';

const responseToStreamSpan = new WeakMap<object, Span>();
const responseToFallbackTimeout = new WeakMap<object, ReturnType<typeof setTimeout>>();
Expand Down
Loading
Loading