Skip to content

Commit b174d53

Browse files
Luca Forstnermydea
Luca Forstner
andauthored
feat(core): Write data from setUser, setTags, setExtras, setTag, setExtra, and setContext to isolation scope (#10163)
Co-authored-by: Francesco Novy <[email protected]>
1 parent 99f6f92 commit b174d53

File tree

5 files changed

+143
-83
lines changed

5 files changed

+143
-83
lines changed

packages/core/src/hub.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,14 @@ export class Hub implements HubInterface {
387387
client.emit('beforeAddBreadcrumb', finalBreadcrumb, hint);
388388
}
389389

390+
// TODO(v8): I know this comment doesn't make much sense because the hub will be deprecated but I still wanted to
391+
// write it down. In theory, we would have to add the breadcrumbs to the isolation scope here, however, that would
392+
// duplicate all of the breadcrumbs. There was the possibility of adding breadcrumbs to both, the isolation scope
393+
// and the normal scope, and deduplicating it down the line in the event processing pipeline. However, that would
394+
// have been very fragile, because the breadcrumb objects would have needed to keep their identity all throughout
395+
// the event processing pipeline.
396+
// In the new implementation, the top level `Sentry.addBreadcrumb()` should ONLY write to the isolation scope.
397+
390398
scope.addBreadcrumb(finalBreadcrumb, maxBreadcrumbs);
391399
}
392400

@@ -395,44 +403,59 @@ export class Hub implements HubInterface {
395403
* @deprecated Use `Sentry.setUser()` instead.
396404
*/
397405
public setUser(user: User | null): void {
406+
// TODO(v8): The top level `Sentry.setUser()` function should write ONLY to the isolation scope.
398407
// eslint-disable-next-line deprecation/deprecation
399408
this.getScope().setUser(user);
409+
// eslint-disable-next-line deprecation/deprecation
410+
this.getIsolationScope().setUser(user);
400411
}
401412

402413
/**
403414
* @inheritDoc
404415
* @deprecated Use `Sentry.setTags()` instead.
405416
*/
406417
public setTags(tags: { [key: string]: Primitive }): void {
418+
// TODO(v8): The top level `Sentry.setTags()` function should write ONLY to the isolation scope.
407419
// eslint-disable-next-line deprecation/deprecation
408420
this.getScope().setTags(tags);
421+
// eslint-disable-next-line deprecation/deprecation
422+
this.getIsolationScope().setTags(tags);
409423
}
410424

411425
/**
412426
* @inheritDoc
413427
* @deprecated Use `Sentry.setExtras()` instead.
414428
*/
415429
public setExtras(extras: Extras): void {
430+
// TODO(v8): The top level `Sentry.setExtras()` function should write ONLY to the isolation scope.
416431
// eslint-disable-next-line deprecation/deprecation
417432
this.getScope().setExtras(extras);
433+
// eslint-disable-next-line deprecation/deprecation
434+
this.getIsolationScope().setExtras(extras);
418435
}
419436

420437
/**
421438
* @inheritDoc
422439
* @deprecated Use `Sentry.setTag()` instead.
423440
*/
424441
public setTag(key: string, value: Primitive): void {
442+
// TODO(v8): The top level `Sentry.setTag()` function should write ONLY to the isolation scope.
425443
// eslint-disable-next-line deprecation/deprecation
426444
this.getScope().setTag(key, value);
445+
// eslint-disable-next-line deprecation/deprecation
446+
this.getIsolationScope().setTag(key, value);
427447
}
428448

429449
/**
430450
* @inheritDoc
431451
* @deprecated Use `Sentry.setExtra()` instead.
432452
*/
433453
public setExtra(key: string, extra: Extra): void {
454+
// TODO(v8): The top level `Sentry.setExtra()` function should write ONLY to the isolation scope.
434455
// eslint-disable-next-line deprecation/deprecation
435456
this.getScope().setExtra(key, extra);
457+
// eslint-disable-next-line deprecation/deprecation
458+
this.getIsolationScope().setExtra(key, extra);
436459
}
437460

438461
/**
@@ -441,8 +464,11 @@ export class Hub implements HubInterface {
441464
*/
442465
// eslint-disable-next-line @typescript-eslint/no-explicit-any
443466
public setContext(name: string, context: { [key: string]: any } | null): void {
467+
// TODO(v8): The top level `Sentry.setContext()` function should write ONLY to the isolation scope.
444468
// eslint-disable-next-line deprecation/deprecation
445469
this.getScope().setContext(name, context);
470+
// eslint-disable-next-line deprecation/deprecation
471+
this.getIsolationScope().setContext(name, context);
446472
}
447473

448474
/**

packages/core/src/scope.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,20 @@ export class Scope implements ScopeInterface {
189189
* @inheritDoc
190190
*/
191191
public setUser(user: User | null): this {
192-
this._user = user || {};
192+
// If null is passed we want to unset everything, but still define keys,
193+
// so that later down in the pipeline any existing values are cleared.
194+
this._user = user || {
195+
email: undefined,
196+
id: undefined,
197+
ip_address: undefined,
198+
segment: undefined,
199+
username: undefined,
200+
};
201+
193202
if (this._session) {
194203
updateSession(this._session, { user });
195204
}
205+
196206
this._notifyScopeListeners();
197207
return this;
198208
}

packages/core/src/utils/applyScopeDataToEvent.ts

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Breadcrumb, Event, PropagationContext, ScopeData, Span } from '@sentry/types';
2-
import { arrayify } from '@sentry/utils';
2+
import { arrayify, dropUndefinedKeys } from '@sentry/utils';
33
import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext';
44
import { getRootSpan } from './getRootSpan';
55
import { spanToJSON, spanToTraceContext } from './spanUtils';
@@ -44,11 +44,11 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void {
4444
span,
4545
} = mergeData;
4646

47-
mergePropOverwrite(data, 'extra', extra);
48-
mergePropOverwrite(data, 'tags', tags);
49-
mergePropOverwrite(data, 'user', user);
50-
mergePropOverwrite(data, 'contexts', contexts);
51-
mergePropOverwrite(data, 'sdkProcessingMetadata', sdkProcessingMetadata);
47+
mergeAndOverwriteScopeData(data, 'extra', extra);
48+
mergeAndOverwriteScopeData(data, 'tags', tags);
49+
mergeAndOverwriteScopeData(data, 'user', user);
50+
mergeAndOverwriteScopeData(data, 'contexts', contexts);
51+
mergeAndOverwriteScopeData(data, 'sdkProcessingMetadata', sdkProcessingMetadata);
5252

5353
if (level) {
5454
data.level = level;
@@ -83,28 +83,21 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void {
8383
}
8484

8585
/**
86-
* Merge properties, overwriting existing keys.
86+
* Merges certain scope data. Undefined values will overwrite any existing values.
8787
* Exported only for tests.
8888
*/
89-
export function mergePropOverwrite<
89+
export function mergeAndOverwriteScopeData<
9090
Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata',
91-
Data extends ScopeData | Event,
91+
Data extends ScopeData,
9292
>(data: Data, prop: Prop, mergeVal: Data[Prop]): void {
9393
if (mergeVal && Object.keys(mergeVal).length) {
94-
data[prop] = { ...data[prop], ...mergeVal };
95-
}
96-
}
97-
98-
/**
99-
* Merge properties, keeping existing keys.
100-
* Exported only for tests.
101-
*/
102-
export function mergePropKeep<
103-
Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata',
104-
Data extends ScopeData | Event,
105-
>(data: Data, prop: Prop, mergeVal: Data[Prop]): void {
106-
if (mergeVal && Object.keys(mergeVal).length) {
107-
data[prop] = { ...mergeVal, ...data[prop] };
94+
// Clone object
95+
data[prop] = { ...data[prop] };
96+
for (const key in mergeVal) {
97+
if (Object.prototype.hasOwnProperty.call(mergeVal, key)) {
98+
data[prop][key] = mergeVal[key];
99+
}
100+
}
108101
}
109102
}
110103

@@ -136,21 +129,30 @@ function applyDataToEvent(event: Event, data: ScopeData): void {
136129
transactionName,
137130
} = data;
138131

139-
if (extra && Object.keys(extra).length) {
140-
event.extra = { ...extra, ...event.extra };
132+
const cleanedExtra = dropUndefinedKeys(extra);
133+
if (cleanedExtra && Object.keys(cleanedExtra).length) {
134+
event.extra = { ...cleanedExtra, ...event.extra };
141135
}
142-
if (tags && Object.keys(tags).length) {
143-
event.tags = { ...tags, ...event.tags };
136+
137+
const cleanedTags = dropUndefinedKeys(tags);
138+
if (cleanedTags && Object.keys(cleanedTags).length) {
139+
event.tags = { ...cleanedTags, ...event.tags };
144140
}
145-
if (user && Object.keys(user).length) {
146-
event.user = { ...user, ...event.user };
141+
142+
const cleanedUser = dropUndefinedKeys(user);
143+
if (cleanedUser && Object.keys(cleanedUser).length) {
144+
event.user = { ...cleanedUser, ...event.user };
147145
}
148-
if (contexts && Object.keys(contexts).length) {
149-
event.contexts = { ...contexts, ...event.contexts };
146+
147+
const cleanedContexts = dropUndefinedKeys(contexts);
148+
if (cleanedContexts && Object.keys(cleanedContexts).length) {
149+
event.contexts = { ...cleanedContexts, ...event.contexts };
150150
}
151+
151152
if (level) {
152153
event.level = level;
153154
}
155+
154156
if (transactionName) {
155157
event.transaction = transactionName;
156158
}

packages/core/test/lib/exports.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import {
66
getCurrentScope,
77
getIsolationScope,
88
makeMain,
9+
setContext,
10+
setExtra,
11+
setExtras,
12+
setTag,
13+
setTags,
14+
setUser,
915
startSession,
16+
withIsolationScope,
1017
withScope,
1118
} from '../../src';
1219
import { TestClient, getDefaultTestClientOptions } from '../mocks/client';
@@ -249,4 +256,60 @@ describe('session APIs', () => {
249256
expect(getIsolationScope().getSession()).toBe(undefined);
250257
});
251258
});
259+
260+
describe('setUser', () => {
261+
it('should write to the isolation scope', () => {
262+
withIsolationScope(isolationScope => {
263+
setUser({ id: 'foo' });
264+
expect(isolationScope.getScopeData().user.id).toBe('foo');
265+
});
266+
});
267+
});
268+
269+
describe('setTags', () => {
270+
it('should write to the isolation scope', () => {
271+
withIsolationScope(isolationScope => {
272+
setTags({ wee: true, woo: false });
273+
expect(isolationScope.getScopeData().tags['wee']).toBe(true);
274+
expect(isolationScope.getScopeData().tags['woo']).toBe(false);
275+
});
276+
});
277+
});
278+
279+
describe('setTag', () => {
280+
it('should write to the isolation scope', () => {
281+
withIsolationScope(isolationScope => {
282+
setTag('foo', true);
283+
expect(isolationScope.getScopeData().tags['foo']).toBe(true);
284+
});
285+
});
286+
});
287+
288+
describe('setExtras', () => {
289+
it('should write to the isolation scope', () => {
290+
withIsolationScope(isolationScope => {
291+
setExtras({ wee: { foo: 'bar' }, woo: { foo: 'bar' } });
292+
expect(isolationScope.getScopeData().extra?.wee).toEqual({ foo: 'bar' });
293+
expect(isolationScope.getScopeData().extra?.woo).toEqual({ foo: 'bar' });
294+
});
295+
});
296+
});
297+
298+
describe('setExtra', () => {
299+
it('should write to the isolation scope', () => {
300+
withIsolationScope(isolationScope => {
301+
setExtra('foo', { bar: 'baz' });
302+
expect(isolationScope.getScopeData().extra?.foo).toEqual({ bar: 'baz' });
303+
});
304+
});
305+
});
306+
307+
describe('setContext', () => {
308+
it('should write to the isolation scope', () => {
309+
withIsolationScope(isolationScope => {
310+
setContext('foo', { bar: 'baz' });
311+
expect(isolationScope.getScopeData().contexts?.foo?.bar).toBe('baz');
312+
});
313+
});
314+
});
252315
});

packages/core/test/lib/utils/applyScopeDataToEvent.test.ts

Lines changed: 10 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import type { Attachment, Breadcrumb, EventProcessor, ScopeData } from '@sentry/types';
2-
import {
3-
mergeArray,
4-
mergePropKeep,
5-
mergePropOverwrite,
6-
mergeScopeData,
7-
} from '../../../src/utils/applyScopeDataToEvent';
2+
import { mergeAndOverwriteScopeData, mergeArray, mergeScopeData } from '../../../src/utils/applyScopeDataToEvent';
83

94
describe('mergeArray', () => {
105
it.each([
@@ -28,55 +23,19 @@ describe('mergeArray', () => {
2823
});
2924
});
3025

31-
describe('mergePropKeep', () => {
32-
it.each([
33-
[{}, {}, {}],
34-
[{ a: 'aa' }, {}, { a: 'aa' }],
35-
[{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }],
36-
// Does not overwrite existing keys
37-
[{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'aa', b: 'bb' }],
38-
])('works with %s and %s', (a, b, expected) => {
39-
const data = { tags: a } as unknown as ScopeData;
40-
mergePropKeep(data, 'tags', b);
41-
expect(data.tags).toEqual(expected);
42-
});
43-
44-
it('does not deep merge', () => {
45-
const data = {
46-
contexts: {
47-
app: { app_version: 'v1' },
48-
culture: { display_name: 'name1' },
49-
},
50-
} as unknown as ScopeData;
51-
mergePropKeep(data, 'contexts', {
52-
os: { name: 'os1' },
53-
app: { app_name: 'name1' },
54-
});
55-
expect(data.contexts).toEqual({
56-
os: { name: 'os1' },
57-
culture: { display_name: 'name1' },
58-
app: { app_version: 'v1' },
59-
});
60-
});
61-
62-
it('does not mutate the original object if no changes are made', () => {
63-
const tags = { a: 'aa' };
64-
const data = { tags } as unknown as ScopeData;
65-
mergePropKeep(data, 'tags', {});
66-
expect(data.tags).toBe(tags);
67-
});
68-
});
69-
70-
describe('mergePropOverwrite', () => {
26+
describe('mergeAndOverwriteScopeData', () => {
7127
it.each([
7228
[{}, {}, {}],
7329
[{ a: 'aa' }, {}, { a: 'aa' }],
7430
[{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }],
7531
// overwrites existing keys
7632
[{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'cc', b: 'bb' }],
77-
])('works with %s and %s', (a, b, expected) => {
78-
const data = { tags: a } as unknown as ScopeData;
79-
mergePropOverwrite(data, 'tags', b);
33+
// undefined values overwrite existing values
34+
[{ a: 'defined' }, { a: undefined, b: 'defined' }, { a: undefined, b: 'defined' }],
35+
[{ a: 'defined' }, { a: null, b: 'defined' }, { a: null, b: 'defined' }],
36+
])('works with %s and %s', (oldData, newData, expected) => {
37+
const data = { tags: oldData } as unknown as ScopeData;
38+
mergeAndOverwriteScopeData(data, 'tags', newData);
8039
expect(data.tags).toEqual(expected);
8140
});
8241

@@ -87,7 +46,7 @@ describe('mergePropOverwrite', () => {
8746
culture: { display_name: 'name1' },
8847
},
8948
} as unknown as ScopeData;
90-
mergePropOverwrite(data, 'contexts', {
49+
mergeAndOverwriteScopeData(data, 'contexts', {
9150
os: { name: 'os1' },
9251
app: { app_name: 'name1' },
9352
});
@@ -101,7 +60,7 @@ describe('mergePropOverwrite', () => {
10160
it('does not mutate the original object if no changes are made', () => {
10261
const tags = { a: 'aa' };
10362
const data = { tags } as unknown as ScopeData;
104-
mergePropOverwrite(data, 'tags', {});
63+
mergeAndOverwriteScopeData(data, 'tags', {});
10564
expect(data.tags).toBe(tags);
10665
});
10766
});

0 commit comments

Comments
 (0)