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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ module.exports = [
path: createCDNPath('bundle.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '100 KB',
limit: '101 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ Uses **Git Flow** (see `docs/gitflow.md`).
- Never expose secrets or keys
- When modifying files, cover all occurrences (including `src/` and `test/`)
- Comments explain **why**, never **what** — never add a comment that restates what the code does or describes the change being made; only comment when the reasoning isn't obvious from the code itself
- Do not use `expect(someSpy.mock.calls[0]?.[0])` or similar constructs to check what a spy was called with.
Instead use `expect(someSpy).toHaveBeenCalledWith(...)` or derivatives for a more readable and less brittle test assertion.

## Lazy Loading Is a Last Resort

Expand Down
2 changes: 2 additions & 0 deletions packages/bundler-plugins/src/core/sentry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export function createSentryInstance(
return event;
},

// Deprecated, but still applied because this client runs on the static trace lifecycle.
// oxlint-disable-next-line typescript/no-deprecated
beforeSendTransaction: event => {
delete event.server_name; // Server name might contain PII
return event;
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span
import { showSpanDropWarning } from './utils/spanUtils';
import { safeUnref } from './utils/timer';
import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent';
import { maybeWarnAboutIgnoredTransactionOptions } from './utils/warnAboutIgnoredTransactionOptions';
import { resolveDataCollectionOptions } from './utils/data-collection/resolveDataCollectionOptions';

const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
Expand Down Expand Up @@ -507,6 +508,8 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
this._options.integrations.some(({ name }) => name.startsWith('Spotlight'))
) {
this._setupIntegrations();

maybeWarnAboutIgnoredTransactionOptions(this._options);
}
}

Comment thread
sentry[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -1653,7 +1656,12 @@ function processBeforeSend(
event: Event,
hint: EventHint,
): PromiseLike<Event | null> | Event | null {
const { beforeSend, beforeSendTransaction, ignoreSpans } = options;
const {
beforeSend,
ignoreSpans,
// oxlint-disable-next-line typescript/no-deprecated
beforeSendTransaction,
} = options;
const beforeSendSpan = !isStreamedBeforeSendSpanCallback(options.beforeSendSpan) && options.beforeSendSpan;

let processedEvent = event;
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,19 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
* A pattern for transaction names which should not be sent to Sentry.
* By default, all transactions will be sent.
*
* Important: This option is ignored by default! It only has an effect if {@link traceLifecycle} is set to `'static'`.
* Use {@link ignoreSpans} instead, which works with both trace lifecycles.
*
* Behavior of the `ignoreTransactions` option is controlled by the `Sentry.eventFiltersIntegration` integration.
* If the event filters integration is not installed, the `ignoreTransactions` option will not have any effect.
*
* @default []
*
* @deprecated This option only has an effect if {@link traceLifecycle} is set to `'static'`. With span streaming
* (`traceLifecycle: 'stream'`, the default), the SDK ignores it. Use {@link ignoreSpans} instead, which works with both
* trace lifecycles. `ignoreTransactions` will be removed in v12.
*
* @see {@link ClientOptions.ignoreSpans}
*/
ignoreTransactions?: Array<string | RegExp>;

Expand Down Expand Up @@ -643,12 +652,20 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
* An event-processing callback for transaction events, guaranteed to be invoked after all other event
* processors. This allows an event to be modified or dropped before it's sent.
*
* Important: This callback is ignored by default! It only runs if {@link traceLifecycle} is set to `'static'`.
*
* Note that you must return a valid event from this callback. If you do not wish to modify the event, simply return
* it at the end. Returning `null` will cause the event to be dropped.
*
* @param event The error or message event generated by the SDK.
* @param event The transaction event generated by the SDK.
* @param hint Event metadata useful for processing.
* @returns A new event that will be sent | null.
*
* @deprecated This option only has an effect if {@link traceLifecycle} is set to `'static'`. With span streaming
* (`traceLifecycle: 'stream'`, the default), the SDK ignores it. Use {@link beforeSendSpan} instead, which works with both
* trace lifecycles. `beforeSendTransaction` will be removed in v12 of the SDK.
*
* @see {@link ClientOptions.beforeSendSpan}
*/
beforeSendTransaction?: (
event: TransactionEvent,
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/utils/warnAboutIgnoredTransactionOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ClientOptions } from '../types/options';
import { consoleSandbox } from './debug-logger';

/**
* Warns about `beforeSendTransaction` and `ignoreTransactions` being ignored because span streaming is enabled.
*
* Both options are tied to the transaction event that span streaming no longer produces, so they silently
* stop taking effect when users upgrade. Since that's easy to miss, this warning bypasses the `debug` logger
* (which is opt-in and stripped from non-debug bundles) and writes to the console directly.
*/
export function maybeWarnAboutIgnoredTransactionOptions(options: ClientOptions): void {
if (
options.traceLifecycle !== 'stream' ||
// oxlint-disable-next-line typescript/no-deprecated
!(options.beforeSendTransaction || options.ignoreTransactions?.length)
) {
return;
}

consoleSandbox(() => {
// oxlint-disable-next-line no-console
console.warn(
"[Sentry] `beforeSendTransaction` and `ignoreTransactions` are ignored with `traceLifecycle: 'stream'` (enabled by default). Use `beforeSendSpan` and `ignoreSpans` instead, or set `traceLifecycle: 'static'`.",
);
});
}
Comment thread
cursor[bot] marked this conversation as resolved.
87 changes: 87 additions & 0 deletions packages/core/test/lib/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,93 @@ describe('Client', () => {
});
});

describe('init() / transaction option warnings', () => {
test('warns about transaction options ignored by span streaming', () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);

const options = getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
traceLifecycle: 'stream',
beforeSendTransaction: event => event,
ignoreTransactions: ['/healthcheck'],
});
new TestClient(options).init();

expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining(
"`beforeSendTransaction` and `ignoreTransactions` are ignored with `traceLifecycle: 'stream'` (enabled by default).",
),
);
Comment thread
cursor[bot] marked this conversation as resolved.
consoleWarnSpy.mockRestore();
});

test('stays silent when the client is disabled because no DSN was provided', () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);

const options = getDefaultTestClientOptions({
traceLifecycle: 'stream',
beforeSendTransaction: event => event,
ignoreTransactions: ['/healthcheck'],
});
new TestClient(options).init();

expect(consoleWarnSpy).not.toHaveBeenCalled();
consoleWarnSpy.mockRestore();
});

test('stays silent when the client is disabled via `enabled: false`', () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);

const options = getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
enabled: false,
traceLifecycle: 'stream',
beforeSendTransaction: event => event,
});
new TestClient(options).init();

expect(consoleWarnSpy).not.toHaveBeenCalled();
consoleWarnSpy.mockRestore();
});

test('warns without a DSN when Spotlight is enabled, since spans are still sent', () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);

const options = getDefaultTestClientOptions({
traceLifecycle: 'stream',
beforeSendTransaction: event => event,
integrations: [{ name: 'SpotlightBrowser' }],
});
new TestClient(options).init();

expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
consoleWarnSpy.mockRestore();
});

test('stays silent when an integration falls back to the static trace lifecycle during setup', () => {
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);

const options = getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
traceLifecycle: 'stream',
beforeSendTransaction: event => event,
integrations: [
{
name: 'FallsBackToStatic',
setup: client => {
client.getOptions().traceLifecycle = 'static';
},
},
],
});
new TestClient(options).init();

expect(consoleWarnSpy).not.toHaveBeenCalled();
consoleWarnSpy.mockRestore();
});
});

describe('getOptions()', () => {
test('returns the options', () => {
expect.assertions(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ClientOptions } from '../../../src/types/options';
import * as debugLoggerModule from '../../../src/utils/debug-logger';
import { maybeWarnAboutIgnoredTransactionOptions } from '../../../src/utils/warnAboutIgnoredTransactionOptions';

describe('maybeWarnAboutIgnoredTransactionOptions', () => {
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
vi.spyOn(debugLoggerModule, 'consoleSandbox').mockImplementation(cb => cb());
});

afterEach(() => {
vi.restoreAllMocks();
});

function options(overrides: Partial<ClientOptions>): ClientOptions {
return { integrations: [], transport: () => ({}) as never, stackParser: () => [], ...overrides } as ClientOptions;
}

it('warns when `beforeSendTransaction` is set and span streaming is enabled', () => {
maybeWarnAboutIgnoredTransactionOptions(
options({ traceLifecycle: 'stream', beforeSendTransaction: event => event }),
);

expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy.mock.calls[0]?.[0]).toContain('`beforeSendTransaction` and `ignoreTransactions`');
});

it('warns when `ignoreTransactions` is set and span streaming is enabled', () => {
maybeWarnAboutIgnoredTransactionOptions(
options({ traceLifecycle: 'stream', ignoreTransactions: ['/healthcheck'] }),
);

expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy.mock.calls[0]?.[0]).toContain('`beforeSendTransaction` and `ignoreTransactions`');
});

it('warns only once when both options are set', () => {
maybeWarnAboutIgnoredTransactionOptions(
options({
traceLifecycle: 'stream',
beforeSendTransaction: event => event,
ignoreTransactions: ['/healthcheck'],
}),
);

expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
});

it('points at the replacement options and the opt-out', () => {
maybeWarnAboutIgnoredTransactionOptions(
options({ traceLifecycle: 'stream', beforeSendTransaction: event => event }),
);

const message = consoleWarnSpy.mock.calls[0]?.[0];
expect(message).toContain('`beforeSendSpan` and `ignoreSpans`');
expect(message).toContain("`traceLifecycle: 'static'`");
});

it('does not warn when the trace lifecycle is static', () => {
maybeWarnAboutIgnoredTransactionOptions(
options({
traceLifecycle: 'static',
beforeSendTransaction: event => event,
ignoreTransactions: ['/healthcheck'],
}),
);

expect(consoleWarnSpy).not.toHaveBeenCalled();
});

it('does not warn when neither option is set', () => {
maybeWarnAboutIgnoredTransactionOptions(options({ traceLifecycle: 'stream' }));

expect(consoleWarnSpy).not.toHaveBeenCalled();
});

it('does not warn for an empty `ignoreTransactions` array', () => {
maybeWarnAboutIgnoredTransactionOptions(options({ traceLifecycle: 'stream', ignoreTransactions: [] }));

expect(consoleWarnSpy).not.toHaveBeenCalled();
});
});
Loading