Repository navigation
feat(core): Add __SENTRY_EXCLUDE_STATIC_LIFECYCLE__ flag to tree-shake the static span lifecycle
#26144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
feat(core): Add __SENTRY_EXCLUDE_STATIC_LIFECYCLE__ flag to tree-shake the static span lifecycle
#26144
Changes from all commits
e6aa1e9
dc60544
479b891
c0ce932
d83094c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,9 @@ import { maybeWarnAboutIgnoredTransactionOptions } from './utils/warnAboutIgnore | |
| import { resolveDataCollectionOptions } from './utils/data-collection/resolveDataCollectionOptions'; | ||
| import { applyBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; | ||
|
|
||
| // Treeshakable guard to always stream spans and drop the code paths that build and process transactions | ||
| declare const __SENTRY_EXCLUDE_STATIC_LIFECYCLE__: boolean | undefined; | ||
|
|
||
| const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; | ||
| const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing or non-string release'; | ||
|
|
||
|
|
@@ -261,11 +264,31 @@ export abstract class Client<O extends ClientOptions = ClientOptions> { | |
| protected constructor(options: O) { | ||
| // Any value other than `'static'` normalizes to the `'stream'` default, so that `traceLifecycle` | ||
| // is always one of the two known values for the rest of the SDK. | ||
| // With the static lifecycle excluded from the bundle, the SDK can only stream spans. | ||
| this._options = { | ||
| attachStacktrace: true, | ||
| ...options, | ||
| traceLifecycle: options.traceLifecycle === 'static' ? 'static' : 'stream', | ||
| traceLifecycle: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A user with The other warning, Suggestion: when |
||
| (typeof __SENTRY_EXCLUDE_STATIC_LIFECYCLE__ === 'boolean' && __SENTRY_EXCLUDE_STATIC_LIFECYCLE__) || | ||
| options.traceLifecycle !== 'static' | ||
| ? 'stream' | ||
| : 'static', | ||
| }; | ||
|
|
||
| // Not debug-gated: builds that set the flag usually strip debug logging too, and the user's | ||
| // static callbacks (often PII scrubbers) would otherwise stop running without any message. | ||
| if ( | ||
| typeof __SENTRY_EXCLUDE_STATIC_LIFECYCLE__ === 'boolean' && | ||
| __SENTRY_EXCLUDE_STATIC_LIFECYCLE__ && | ||
| options.traceLifecycle === 'static' | ||
| ) { | ||
| consoleSandbox(() => { | ||
| // oxlint-disable-next-line no-console | ||
| console.warn( | ||
| "[Sentry] `traceLifecycle: 'static'` is not available in this build (`__SENTRY_EXCLUDE_STATIC_LIFECYCLE__`). `beforeSendTransaction`, `ignoreTransactions` and `withStaticSpan` callbacks are ignored.", | ||
| ); | ||
| }); | ||
| } | ||
| this._integrations = {}; | ||
| this._numProcessing = 0; | ||
| this._outcomes = {}; | ||
|
|
@@ -607,7 +630,11 @@ export abstract class Client<O extends ClientOptions = ClientOptions> { | |
|
|
||
| // Extract gen_ai spans from transaction and convert to span v2 format. | ||
| // This mutates event.spans to remove the extracted spans. | ||
| const genAiSpanItem = extractGenAiSpansFromEvent(event, this); | ||
| // Manually captured transaction events keep their gen_ai spans inline under the flag. | ||
| const genAiSpanItem = | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. low: gen_ai extraction is skipped for manual transaction events. Same root as the comment on |
||
| typeof __SENTRY_EXCLUDE_STATIC_LIFECYCLE__ === 'boolean' && __SENTRY_EXCLUDE_STATIC_LIFECYCLE__ | ||
| ? undefined | ||
| : extractGenAiSpansFromEvent(event, this); | ||
|
|
||
| let env = createEventEnvelope(event, this._dsn, this._options._metadata, this._options.tunnel); | ||
|
|
||
|
|
@@ -1799,8 +1826,12 @@ function processBeforeSend( | |
| } | ||
|
|
||
| if (isTransactionEvent(processedEvent)) { | ||
| // Avoid processing if we don't have to | ||
| if (beforeSendSpan || ignoreSpans) { | ||
| // Only manually captured transaction events get here under the flag. They skip span processing, | ||
| // but still run `beforeSendTransaction`, which users often rely on to scrub PII. | ||
| if ( | ||
| !(typeof __SENTRY_EXCLUDE_STATIC_LIFECYCLE__ === 'boolean' && __SENTRY_EXCLUDE_STATIC_LIFECYCLE__) && | ||
| (beforeSendSpan || ignoreSpans) | ||
| ) { | ||
| // 1. Process root span | ||
| const rootSpanJson = convertTransactionEventToSpanJson(processedEvent); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
| import { getCurrentScope } from '../../../src/currentScopes'; | ||
| import { setCurrentClient } from '../../../src/sdk'; | ||
| import { ServerRuntimeClient } from '../../../src/server-runtime-client'; | ||
| import { SentrySpan } from '../../../src/tracing/sentrySpan'; | ||
| import { createTransport } from '../../../src/transports/base'; | ||
| import type { TransactionEvent } from '../../../src/types/event'; | ||
| import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; | ||
|
|
||
| describe('__SENTRY_EXCLUDE_STATIC_LIFECYCLE__', () => { | ||
| beforeEach(() => { | ||
| vi.stubGlobal('__SENTRY_EXCLUDE_STATIC_LIFECYCLE__', true); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it('ignores `traceLifecycle: "static"` and warns about it', () => { | ||
| const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); | ||
|
|
||
| const client = new TestClient(getDefaultTestClientOptions({ traceLifecycle: 'static' })); | ||
|
|
||
| expect(client.getOptions().traceLifecycle).toBe('stream'); | ||
| expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("`traceLifecycle: 'static'` is not available")); | ||
| }); | ||
|
|
||
| it('does not warn with `traceLifecycle: "stream"`', () => { | ||
| const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); | ||
|
|
||
| new TestClient(getDefaultTestClientOptions({ traceLifecycle: 'stream' })); | ||
|
|
||
| expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining("`traceLifecycle: 'static'` is not available")); | ||
| }); | ||
|
|
||
| it('hands segment spans to span streaming instead of capturing a transaction', () => { | ||
| const client = new TestClient(getDefaultTestClientOptions({ traceLifecycle: 'static', tracesSampleRate: 1 })); | ||
| setCurrentClient(client); | ||
| const onSegmentSpanEnd = vi.fn(); | ||
| client.on('afterSegmentSpanEnd', onSegmentSpanEnd); | ||
| const captureEventSpy = vi.spyOn(getCurrentScope(), 'captureEvent'); | ||
|
|
||
| const segmentSpan = new SentrySpan({ name: 'segment', sampled: true, startTimestamp: 1 }); | ||
| segmentSpan.end(2); | ||
|
|
||
| expect(onSegmentSpanEnd).toHaveBeenCalledWith(segmentSpan); | ||
| expect(captureEventSpy).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('still runs `beforeSendTransaction` on manually captured transaction events', async () => { | ||
| const beforeSendTransaction = vi.fn(event => ({ ...event, transaction: 'scrubbed' })); | ||
| const client = new TestClient( | ||
| getDefaultTestClientOptions({ dsn: 'https://username@domain/123', beforeSendTransaction }), | ||
| ); | ||
| const sendEventSpy = vi.spyOn(client, 'sendEvent'); | ||
| const event: TransactionEvent = { type: 'transaction', transaction: 'test', spans: [] }; | ||
|
|
||
| client.captureEvent(event); | ||
| await client.flush(); | ||
|
|
||
| expect(beforeSendTransaction).toHaveBeenCalledTimes(1); | ||
| expect(sendEventSpy).toHaveBeenCalledWith(expect.objectContaining({ transaction: 'scrubbed' }), expect.anything()); | ||
| }); | ||
|
|
||
| it('adds the span streaming integration to server clients with `traceLifecycle: "static"`', () => { | ||
| const client = new ServerRuntimeClient({ | ||
| integrations: [], | ||
| transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), | ||
| stackParser: () => [], | ||
| traceLifecycle: 'static', | ||
| }); | ||
|
|
||
| expect(client.getOptions().integrations.map(integration => integration.name)).toContain('SpanStreaming'); | ||
| }); | ||
| }); | ||
|
sentry-junior[bot] marked this conversation as resolved.
Resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.