Skip to content

Commit b271bc8

Browse files
authored
feat(core): Update & deprecate undefined option handling (#14450)
This PR updates & streamlines our handling of certain `undefined` options: First, we do not want to differentiate anymore between options being unset and set to `undefined` - the resulting behavior should be the same. This especially applies to the tracing options `tracesSampleRate`, `tracesSampler` and `enableTracing` - if any of those is set to `undefined`, `hasEnabledTracing()` will be true and certain things will happen. In v9, we want to change this so that `undefined` will also result in `hasEnabledTracing() === false`. We'll warn if we encounter such a scenario. Another related thing this PR does is streamline how we handle falsy values for `environment`, `release` and `dist` on an event. Today, we go out of our way to check if the properties are set and only update them accordingly. However, fasly values do not make sense for these fields anyhow, so we can streamline this a bit and simply check for truthiness to determine if we want to use event, option, or default values. Closes #14261
1 parent 1a31ec4 commit b271bc8

File tree

7 files changed

+247
-16
lines changed

7 files changed

+247
-16
lines changed

.size-limit.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ module.exports = [
4040
path: 'packages/browser/build/npm/esm/index.js',
4141
import: createImport('init', 'browserTracingIntegration'),
4242
gzip: true,
43-
limit: '36.5 KB',
43+
limit: '37.5 KB',
4444
},
4545
{
4646
name: '@sentry/browser (incl. Tracing, Replay)',
@@ -124,7 +124,7 @@ module.exports = [
124124
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
125125
ignore: ['react/jsx-runtime'],
126126
gzip: true,
127-
limit: '39.5 KB',
127+
limit: '40.5 KB',
128128
},
129129
// Vue SDK (ESM)
130130
{
@@ -139,7 +139,7 @@ module.exports = [
139139
path: 'packages/vue/build/esm/index.js',
140140
import: createImport('init', 'browserTracingIntegration'),
141141
gzip: true,
142-
limit: '38.5 KB',
142+
limit: '39.5 KB',
143143
},
144144
// Svelte SDK (ESM)
145145
{
@@ -219,7 +219,7 @@ module.exports = [
219219
import: createImport('init'),
220220
ignore: ['$app/stores'],
221221
gzip: true,
222-
limit: '37 KB',
222+
limit: '38 KB',
223223
},
224224
// Node SDK (ESM)
225225
{

docs/migration/draft-v9-migration-guide.md

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
# Deprecations
44

5+
## General
6+
7+
- **Passing `undefined` to `tracesSampleRate` / `tracesSampler` / `enableTracing` will be handled differently in v9**
8+
9+
In v8, a setup like the following:
10+
11+
```ts
12+
Sentry.init({
13+
tracesSampleRate: undefined,
14+
});
15+
```
16+
17+
Will result in tracing being _enabled_, although no spans will be generated.
18+
In v9, we will streamline this behavior so that passing `undefined` will result in tracing being disabled, the same as not passing the option at all.
19+
If you are relying on `undefined` being passed in and having tracing enabled because of this, you should update your config to set e.g. `tracesSampleRate: 0` instead, which will also enable tracing in v9.
20+
521
## `@sentry/utils`
622

723
- **The `@sentry/utils` package has been deprecated. Import everything from `@sentry/core` instead.**

packages/core/src/baseclient.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { dsnToString, makeDsn } from './utils-hoist/dsn';
4646
import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils-hoist/envelope';
4747
import { SentryError } from './utils-hoist/error';
4848
import { isParameterizedString, isPlainObject, isPrimitive, isThenable } from './utils-hoist/is';
49-
import { logger } from './utils-hoist/logger';
49+
import { consoleSandbox, logger } from './utils-hoist/logger';
5050
import { checkOrSetAlreadyCaught, uuid4 } from './utils-hoist/misc';
5151
import { dropUndefinedKeys } from './utils-hoist/object';
5252
import { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils-hoist/syncpromise';
@@ -142,6 +142,18 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
142142
url,
143143
});
144144
}
145+
146+
// TODO(v9): Remove this deprecation warning
147+
const tracingOptions = ['enableTracing', 'tracesSampleRate', 'tracesSampler'] as const;
148+
const undefinedOption = tracingOptions.find(option => option in options && options[option] == undefined);
149+
if (undefinedOption) {
150+
consoleSandbox(() => {
151+
// eslint-disable-next-line no-console
152+
console.warn(
153+
`[Sentry] Deprecation warning: \`${undefinedOption}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`,
154+
);
155+
});
156+
}
145157
}
146158

147159
/**

packages/core/src/utils/prepareEvent.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -129,23 +129,26 @@ export function prepareEvent(
129129
}
130130

131131
/**
132-
* Enhances event using the client configuration.
133-
* It takes care of all "static" values like environment, release and `dist`,
134-
* as well as truncating overly long values.
132+
* Enhances event using the client configuration.
133+
* It takes care of all "static" values like environment, release and `dist`,
134+
* as well as truncating overly long values.
135+
*
136+
* Only exported for tests.
137+
*
135138
* @param event event instance to be enhanced
136139
*/
137-
function applyClientOptions(event: Event, options: ClientOptions): void {
140+
export function applyClientOptions(event: Event, options: ClientOptions): void {
138141
const { environment, release, dist, maxValueLength = 250 } = options;
139142

140-
if (!('environment' in event)) {
141-
event.environment = 'environment' in options ? environment : DEFAULT_ENVIRONMENT;
142-
}
143+
// empty strings do not make sense for environment, release, and dist
144+
// so we handle them the same as if they were not provided
145+
event.environment = event.environment || environment || DEFAULT_ENVIRONMENT;
143146

144-
if (event.release === undefined && release !== undefined) {
147+
if (!event.release && release) {
145148
event.release = release;
146149
}
147150

148-
if (event.dist === undefined && dist !== undefined) {
151+
if (!event.dist && dist) {
149152
event.dist = dist;
150153
}
151154

packages/core/test/lib/base.test.ts renamed to packages/core/test/lib/baseclient.test.ts

+60-2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,56 @@ describe('BaseClient', () => {
7777
});
7878
});
7979

80+
describe('constructor() / warnings', () => {
81+
test('does not warn for defaults', () => {
82+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
83+
84+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
85+
new TestClient(options);
86+
87+
expect(consoleWarnSpy).toHaveBeenCalledTimes(0);
88+
consoleWarnSpy.mockRestore();
89+
});
90+
91+
describe.each(['tracesSampleRate', 'tracesSampler', 'enableTracing'])('%s', key => {
92+
it('warns when set to undefined', () => {
93+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
94+
95+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: undefined });
96+
new TestClient(options);
97+
98+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
99+
expect(consoleWarnSpy).toBeCalledWith(
100+
`[Sentry] Deprecation warning: \`${key}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`,
101+
);
102+
consoleWarnSpy.mockRestore();
103+
});
104+
105+
it('warns when set to null', () => {
106+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
107+
108+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: null });
109+
new TestClient(options);
110+
111+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
112+
expect(consoleWarnSpy).toBeCalledWith(
113+
`[Sentry] Deprecation warning: \`${key}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`,
114+
);
115+
consoleWarnSpy.mockRestore();
116+
});
117+
118+
it('does not warn when set to 0', () => {
119+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
120+
121+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: 0 });
122+
new TestClient(options);
123+
124+
expect(consoleWarnSpy).toHaveBeenCalledTimes(0);
125+
consoleWarnSpy.mockRestore();
126+
});
127+
});
128+
});
129+
80130
describe('getOptions()', () => {
81131
test('returns the options', () => {
82132
expect.assertions(1);
@@ -552,7 +602,7 @@ describe('BaseClient', () => {
552602
);
553603
});
554604

555-
test('allows for environment to be explicitly set to falsy value', () => {
605+
test('uses default environment when set to falsy value', () => {
556606
expect.assertions(1);
557607

558608
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, environment: undefined });
@@ -563,7 +613,7 @@ describe('BaseClient', () => {
563613

564614
expect(TestClient.instance!.event!).toEqual(
565615
expect.objectContaining({
566-
environment: undefined,
616+
environment: 'production',
567617
event_id: '42',
568618
message: 'message',
569619
timestamp: 2020,
@@ -1122,6 +1172,8 @@ describe('BaseClient', () => {
11221172
});
11231173

11241174
test('calls `beforeSendSpan` and discards the span', () => {
1175+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
1176+
11251177
const beforeSendSpan = jest.fn(() => null);
11261178
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan });
11271179
const client = new TestClient(options);
@@ -1150,6 +1202,12 @@ describe('BaseClient', () => {
11501202
const capturedEvent = TestClient.instance!.event!;
11511203
expect(capturedEvent.spans).toHaveLength(0);
11521204
expect(client['_outcomes']).toEqual({ 'before_send:span': 2 });
1205+
1206+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
1207+
expect(consoleWarnSpy).toBeCalledWith(
1208+
'[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.',
1209+
);
1210+
consoleWarnSpy.mockRestore();
11531211
});
11541212

11551213
test('calls `beforeSend` and logs info about invalid return value', () => {

packages/core/test/lib/prepareEvent.test.ts

+134
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { GLOBAL_OBJ, createStackParser, getGlobalScope, getIsolationScope } from
1212

1313
import { Scope } from '../../src/scope';
1414
import {
15+
applyClientOptions,
1516
applyDebugIds,
1617
applyDebugMeta,
1718
parseEventHintOrCaptureContext,
@@ -518,3 +519,136 @@ describe('prepareEvent', () => {
518519
});
519520
});
520521
});
522+
523+
describe('applyClientOptions', () => {
524+
it('works with defaults', () => {
525+
const event: Event = {};
526+
const options = {} as ClientOptions;
527+
528+
applyClientOptions(event, options);
529+
530+
expect(event).toEqual({
531+
environment: 'production',
532+
});
533+
534+
// These should not be set at all on the event
535+
expect('release' in event).toBe(false);
536+
expect('dist' in event).toBe(false);
537+
});
538+
539+
it('works with event data and no options', () => {
540+
const event: Event = {
541+
environment: 'blub',
542+
release: 'blab',
543+
dist: 'blib',
544+
};
545+
const options = {} as ClientOptions;
546+
547+
applyClientOptions(event, options);
548+
549+
expect(event).toEqual({
550+
environment: 'blub',
551+
release: 'blab',
552+
dist: 'blib',
553+
});
554+
});
555+
556+
it('event data has precedence over options', () => {
557+
const event: Event = {
558+
environment: 'blub',
559+
release: 'blab',
560+
dist: 'blib',
561+
};
562+
const options = {
563+
environment: 'blub2',
564+
release: 'blab2',
565+
dist: 'blib2',
566+
} as ClientOptions;
567+
568+
applyClientOptions(event, options);
569+
570+
expect(event).toEqual({
571+
environment: 'blub',
572+
release: 'blab',
573+
dist: 'blib',
574+
});
575+
});
576+
577+
it('option data is used if no event data exists', () => {
578+
const event: Event = {};
579+
const options = {
580+
environment: 'blub2',
581+
release: 'blab2',
582+
dist: 'blib2',
583+
} as ClientOptions;
584+
585+
applyClientOptions(event, options);
586+
587+
expect(event).toEqual({
588+
environment: 'blub2',
589+
release: 'blab2',
590+
dist: 'blib2',
591+
});
592+
});
593+
594+
it('option data is ignored if empty string', () => {
595+
const event: Event = {};
596+
const options = {
597+
environment: '',
598+
release: '',
599+
dist: '',
600+
} as ClientOptions;
601+
602+
applyClientOptions(event, options);
603+
604+
expect(event).toEqual({
605+
environment: 'production',
606+
});
607+
608+
// These should not be set at all on the event
609+
expect('release' in event).toBe(false);
610+
expect('dist' in event).toBe(false);
611+
});
612+
613+
it('option data is used if event data is undefined', () => {
614+
const event: Event = {
615+
environment: undefined,
616+
release: undefined,
617+
dist: undefined,
618+
};
619+
const options = {
620+
environment: 'blub2',
621+
release: 'blab2',
622+
dist: 'blib2',
623+
} as ClientOptions;
624+
625+
applyClientOptions(event, options);
626+
627+
expect(event).toEqual({
628+
environment: 'blub2',
629+
release: 'blab2',
630+
dist: 'blib2',
631+
});
632+
});
633+
634+
it('option data is used if event data is empty string', () => {
635+
const event: Event = {
636+
environment: '',
637+
release: '',
638+
dist: '',
639+
};
640+
const options = {
641+
environment: 'blub2',
642+
release: 'blab2',
643+
dist: 'blib2',
644+
} as ClientOptions;
645+
646+
applyClientOptions(event, options);
647+
648+
expect(event).toEqual({
649+
environment: 'blub2',
650+
release: 'blab2',
651+
dist: 'blib2',
652+
});
653+
});
654+
});

packages/core/test/lib/tracing/sentrySpan.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ describe('SentrySpan', () => {
161161
});
162162

163163
test('does not send the span if `beforeSendSpan` drops the span', () => {
164+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
165+
164166
const beforeSendSpan = jest.fn(() => null);
165167
const client = new TestClient(
166168
getDefaultTestClientOptions({
@@ -185,6 +187,12 @@ describe('SentrySpan', () => {
185187

186188
expect(mockSend).not.toHaveBeenCalled();
187189
expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'span');
190+
191+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
192+
expect(consoleWarnSpy).toBeCalledWith(
193+
'[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.',
194+
);
195+
consoleWarnSpy.mockRestore();
188196
});
189197
});
190198

0 commit comments

Comments
 (0)