From 8d70d2924c928dd26dbeb9b1d90c8337a8c39a40 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:28:08 +0000 Subject: [PATCH] ref(core): Use DEBUG_BUILD ternaries so esbuild drops debug-only code esbuild inlines the imported DEBUG_BUILD constant after its dead-code pass, so early-return guards and guarded blocks with declarations stayed in non-debug bundles. Also adds an esbuild size-limit entry. Closes #26140 Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- .size-limit.js | 12 +++ packages/core/src/tracing/idleSpan.ts | 11 ++- packages/core/src/tracing/logSpans.ts | 93 ++++++++++--------- packages/core/src/utils/dsn.ts | 67 +++++++------ .../core/test/lib/tracing/logSpans.test.ts | 48 ++++++++++ packages/core/test/lib/utils/dsn.test.ts | 9 +- 6 files changed, 154 insertions(+), 86 deletions(-) create mode 100644 packages/core/test/lib/tracing/logSpans.test.ts diff --git a/.size-limit.js b/.size-limit.js index b841667d9d89..64bc2ab99d1d 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -64,6 +64,18 @@ module.exports = [ gzip: true, disablePlugins: ['@size-limit/esbuild'], }, + // esbuild drops dead code differently from webpack + terser, so measure one bundle with it too. + { + name: '@sentry/browser (incl. Tracing) - esbuild with treeshaking flags', + path: 'packages/browser/build/npm/esm/prod/index.js', + import: createImport('init', 'browserTracingIntegration'), + gzip: true, + disablePlugins: ['@size-limit/webpack'], + modifyEsbuildConfig: function (config) { + config.define = { __SENTRY_DEBUG__: 'false' }; + return config; + }, + }, { name: '@sentry/browser (incl. Tracing + Span Streaming)', path: 'packages/browser/build/npm/esm/prod/index.js', diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index f9d163a1de02..5c8b2180ae68 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -339,11 +339,16 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti const spanEndedBeforeFinalTimeout = childEndTimestamp - childStartTimestamp <= timeoutWithMarginOfError; if (DEBUG_BUILD) { - const stringifiedSpan = JSON.stringify(childSpan, undefined, 2); if (!spanStartedBeforeIdleSpanEnd) { - debug.log('[Tracing] Discarding span since it happened after idle span was finished', stringifiedSpan); + debug.log( + '[Tracing] Discarding span since it happened after idle span was finished', + JSON.stringify(childSpan, undefined, 2), + ); } else if (!spanEndedBeforeFinalTimeout) { - debug.log('[Tracing] Discarding span since it finished after idle span final timeout', stringifiedSpan); + debug.log( + '[Tracing] Discarding span since it finished after idle span final timeout', + JSON.stringify(childSpan, undefined, 2), + ); } } diff --git a/packages/core/src/tracing/logSpans.ts b/packages/core/src/tracing/logSpans.ts index f5120e519bc9..5017fe98c0da 100644 --- a/packages/core/src/tracing/logSpans.ts +++ b/packages/core/src/tracing/logSpans.ts @@ -3,57 +3,58 @@ import type { Span } from '../types/span'; import { debug } from '../utils/debug-logger'; import { getRootSpan, spanIsSampled, spanToStaticSpanJSON } from '../utils/spanUtils'; +// A ternary instead of an early return, so that esbuild can drop the debug-only body in non-debug builds. /** * Print a log message for a started span. */ -export function logSpanStart(span: Span): void { - if (!DEBUG_BUILD) return; - - const { - description = '< unknown name >', - op = '< unknown op >', - parent_span_id: parentSpanId, - } = spanToStaticSpanJSON(span); - const { spanId } = span.spanContext(); - - const sampled = spanIsSampled(span); - const rootSpan = getRootSpan(span); - const isRootSpan = rootSpan === span; - - const header = `[Tracing] Starting ${sampled ? 'sampled' : 'unsampled'} ${isRootSpan ? 'root ' : ''}span`; - - const infoParts: string[] = [`op: ${op}`, `name: ${description}`, `ID: ${spanId}`]; - - if (parentSpanId) { - infoParts.push(`parent ID: ${parentSpanId}`); - } - - if (!isRootSpan) { - const { op, description } = spanToStaticSpanJSON(rootSpan); - infoParts.push(`root ID: ${rootSpan.spanContext().spanId}`); - if (op) { - infoParts.push(`root op: ${op}`); - } - if (description) { - infoParts.push(`root description: ${description}`); - } - } - - debug.log(`${header} +export const logSpanStart: (span: Span) => void = DEBUG_BUILD + ? span => { + const { + description = '< unknown name >', + op = '< unknown op >', + parent_span_id: parentSpanId, + } = spanToStaticSpanJSON(span); + const { spanId } = span.spanContext(); + + const sampled = spanIsSampled(span); + const rootSpan = getRootSpan(span); + const isRootSpan = rootSpan === span; + + const header = `[Tracing] Starting ${sampled ? 'sampled' : 'unsampled'} ${isRootSpan ? 'root ' : ''}span`; + + const infoParts: string[] = [`op: ${op}`, `name: ${description}`, `ID: ${spanId}`]; + + if (parentSpanId) { + infoParts.push(`parent ID: ${parentSpanId}`); + } + + if (!isRootSpan) { + const { op, description } = spanToStaticSpanJSON(rootSpan); + infoParts.push(`root ID: ${rootSpan.spanContext().spanId}`); + if (op) { + infoParts.push(`root op: ${op}`); + } + if (description) { + infoParts.push(`root description: ${description}`); + } + } + + debug.log(`${header} ${infoParts.join('\n ')}`); -} + } + : () => {}; /** * Print a log message for an ended span. */ -export function logSpanEnd(span: Span): void { - if (!DEBUG_BUILD) return; - - const { description = '< unknown name >', op = '< unknown op >' } = spanToStaticSpanJSON(span); - const { spanId } = span.spanContext(); - const rootSpan = getRootSpan(span); - const isRootSpan = rootSpan === span; - - const msg = `[Tracing] Finishing "${op}" ${isRootSpan ? 'root ' : ''}span "${description}" with ID ${spanId}`; - debug.log(msg); -} +export const logSpanEnd: (span: Span) => void = DEBUG_BUILD + ? span => { + const { description = '< unknown name >', op = '< unknown op >' } = spanToStaticSpanJSON(span); + const { spanId } = span.spanContext(); + const rootSpan = getRootSpan(span); + const isRootSpan = rootSpan === span; + + const msg = `[Tracing] Finishing "${op}" ${isRootSpan ? 'root ' : ''}span "${description}" with ID ${spanId}`; + debug.log(msg); + } + : () => {}; diff --git a/packages/core/src/utils/dsn.ts b/packages/core/src/utils/dsn.ts index 205b135e7cc5..7aa8a68301ee 100644 --- a/packages/core/src/utils/dsn.ts +++ b/packages/core/src/utils/dsn.ts @@ -80,43 +80,42 @@ function dsnFromComponents(components: DsnComponents): DsnComponents { }; } -function validateDsn(dsn: DsnComponents): boolean { - if (!DEBUG_BUILD) { - return true; - } - - const { port, projectId, protocol } = dsn; +// A ternary instead of an early return, so that esbuild can drop the debug-only body in non-debug builds. +const validateDsn: (dsn: DsnComponents) => boolean = DEBUG_BUILD + ? dsn => { + const { port, projectId, protocol } = dsn; + + const requiredComponents: ReadonlyArray = ['protocol', 'publicKey', 'host', 'projectId']; + const hasMissingRequiredComponent = requiredComponents.find(component => { + if (!dsn[component]) { + debug.error(`Invalid Sentry Dsn: ${component} missing`); + return true; + } + return false; + }); + + if (hasMissingRequiredComponent) { + return false; + } + + if (!projectId.match(/^\d+$/)) { + debug.error(`Invalid Sentry Dsn: Invalid projectId ${projectId}`); + return false; + } + + if (!isValidProtocol(protocol)) { + debug.error(`Invalid Sentry Dsn: Invalid protocol ${protocol}`); + return false; + } + + if (port && isNaN(parseInt(port, 10))) { + debug.error(`Invalid Sentry Dsn: Invalid port ${port}`); + return false; + } - const requiredComponents: ReadonlyArray = ['protocol', 'publicKey', 'host', 'projectId']; - const hasMissingRequiredComponent = requiredComponents.find(component => { - if (!dsn[component]) { - debug.error(`Invalid Sentry Dsn: ${component} missing`); return true; } - return false; - }); - - if (hasMissingRequiredComponent) { - return false; - } - - if (!projectId.match(/^\d+$/)) { - debug.error(`Invalid Sentry Dsn: Invalid projectId ${projectId}`); - return false; - } - - if (!isValidProtocol(protocol)) { - debug.error(`Invalid Sentry Dsn: Invalid protocol ${protocol}`); - return false; - } - - if (port && isNaN(parseInt(port, 10))) { - debug.error(`Invalid Sentry Dsn: Invalid port ${port}`); - return false; - } - - return true; -} + : () => true; /** * Extract the org ID from a DSN host. diff --git a/packages/core/test/lib/tracing/logSpans.test.ts b/packages/core/test/lib/tracing/logSpans.test.ts new file mode 100644 index 000000000000..bc8cd1c088b4 --- /dev/null +++ b/packages/core/test/lib/tracing/logSpans.test.ts @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { SentrySpan } from '../../../src/tracing/sentrySpan'; + +// must be var, because vi.mock hoists +var debugBuild: boolean = true; +vi.mock(import('../../../src/debug-build'), () => ({ + get DEBUG_BUILD() { + return debugBuild ?? true; + }, +})); + +describe('logSpans', () => { + afterEach(() => { + debugBuild = true; + vi.restoreAllMocks(); + vi.resetModules(); + }); + + it('logs span start and end in debug builds', async () => { + const { logSpanEnd, logSpanStart } = await import('../../../src/tracing/logSpans'); + const { debug } = await import('../../../src/utils/debug-logger'); + const logSpy = vi.spyOn(debug, 'log').mockImplementation(() => {}); + const span = new SentrySpan({ name: 'test span', op: 'test.op', sampled: true }); + + logSpanStart(span); + logSpanEnd(span); + + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('[Tracing] Starting sampled root span')); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining( + `[Tracing] Finishing "test.op" root span "test span" with ID ${span.spanContext().spanId}`, + ), + ); + }); + + it('is a no-op in non-debug builds', async () => { + debugBuild = false; + const { logSpanEnd, logSpanStart } = await import('../../../src/tracing/logSpans'); + const { debug } = await import('../../../src/utils/debug-logger'); + const logSpy = vi.spyOn(debug, 'log').mockImplementation(() => {}); + const span = new SentrySpan({ name: 'test span', op: 'test.op', sampled: true }); + + logSpanStart(span); + logSpanEnd(span); + + expect(logSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/test/lib/utils/dsn.test.ts b/packages/core/test/lib/utils/dsn.test.ts index 29c6dc12884c..f2d41abe0bbc 100644 --- a/packages/core/test/lib/utils/dsn.test.ts +++ b/packages/core/test/lib/utils/dsn.test.ts @@ -3,11 +3,12 @@ import { debug } from '../../../src/utils/debug-logger'; import { dsnToString, extractOrgIdFromClient, extractOrgIdFromDsnHost, makeDsn } from '../../../src/utils/dsn'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; -let mockDebugBuild = true; +// must be var, because vi.mock hoists +var mockDebugBuild: boolean = true; vi.mock('../../../src/debug-build', () => ({ get DEBUG_BUILD() { - return mockDebugBuild; + return mockDebugBuild ?? true; }, })); @@ -204,8 +205,10 @@ describe('Dsn', () => { expect(dsn?.projectId).toBe('123'); }); - test('skips validation for non-debug builds', () => { + test('skips validation for non-debug builds', async () => { + vi.resetModules(); mockDebugBuild = false; + const { makeDsn } = await import('../../../src/utils/dsn'); const dsn = makeDsn('httx://abc@192.168.1.1/123'); expect(dsn?.protocol).toBe('httx'); expect(dsn?.publicKey).toBe('abc');