diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index 7c9302092965..9db79820a5e8 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -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": { diff --git a/.size-limit.js b/.size-limit.js index becbd8285041..cad73f5f5ceb 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -8,7 +8,7 @@ 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'], }, { @@ -16,7 +16,7 @@ module.exports = [ 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'); @@ -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'); @@ -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'], }, { diff --git a/packages/browser-utils/src/performance/utils.ts b/packages/browser-utils/src/performance/utils.ts index 6a089c762fd2..6ab5e011672c 100644 --- a/packages/browser-utils/src/performance/utils.ts +++ b/packages/browser-utils/src/performance/utils.ts @@ -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'; /** diff --git a/packages/browser-utils/src/web-vitals/spans.ts b/packages/browser-utils/src/web-vitals/spans.ts index f7849a1b81b0..d219db12fe43 100644 --- a/packages/browser-utils/src/web-vitals/spans.ts +++ b/packages/browser-utils/src/web-vitals/spans.ts @@ -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'; diff --git a/packages/browser-utils/test/web-vitals/spans.test.ts b/packages/browser-utils/test/web-vitals/spans.test.ts index a1e20352f0f3..802add7e1c7b 100644 --- a/packages/browser-utils/test/web-vitals/spans.test.ts +++ b/packages/browser-utils/test/web-vitals/spans.test.ts @@ -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'; @@ -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(), })); @@ -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); @@ -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', @@ -98,7 +107,7 @@ describe('_emitWebVitalSpan', () => { }); // No standalone flag - expect(SentryCore.startInactiveSpan).not.toHaveBeenCalledWith( + expect(SentryCoreBrowser.startInactiveSpan).not.toHaveBeenCalledWith( expect.objectContaining({ experimental: expect.anything() }), ); @@ -120,7 +129,7 @@ describe('_emitWebVitalSpan', () => { parentSpan, }); - expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith( + expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith( expect.objectContaining({ attributes: expect.objectContaining({ 'sentry.segment.name': 'Pageload', @@ -141,7 +150,7 @@ describe('_emitWebVitalSpan', () => { standalone: true, }); - expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith( + expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith( expect.objectContaining({ experimental: { standalone: true } }), ); }); @@ -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', @@ -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 }), }), @@ -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(); }); @@ -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', @@ -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(), @@ -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', @@ -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', @@ -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({ @@ -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', @@ -362,7 +371,7 @@ describe('_sendLcpSpan', () => { _sendLcpSpan(250, mockEntry, mockPageloadSpan as any, 'pagehide'); - expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith( + expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith( expect.objectContaining({ name: '', attributes: expect.objectContaining({ @@ -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 @@ -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(); }); }); @@ -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', @@ -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: '
', attributes: expect.objectContaining({ @@ -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, @@ -508,7 +517,7 @@ describe('_sendInpSpan', () => { vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any); vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000); vi.mocked(htmlTreeAsString).mockReturnValue('