Skip to content

Commit 4de3e7b

Browse files
committed
feat(core): Add Scope::SetAttribute(s) APIs
1 parent 02625ef commit 4de3e7b

File tree

3 files changed

+301
-2
lines changed

3 files changed

+301
-2
lines changed

packages/core/src/scope.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable max-lines */
2+
import { Attributes, attributeValueToTypedAttributeValue, AttributeValueType, TypedAttributeValue } from './attributes';
23
import type { Client } from './client';
34
import { DEBUG_BUILD } from './debug-build';
45
import { updateSession } from './session';
@@ -46,6 +47,7 @@ export interface ScopeContext {
4647
extra: Extras;
4748
contexts: Contexts;
4849
tags: { [key: string]: Primitive };
50+
attributes?: Attributes;
4951
fingerprint: string[];
5052
propagationContext: PropagationContext;
5153
}
@@ -71,6 +73,8 @@ export interface ScopeData {
7173
breadcrumbs: Breadcrumb[];
7274
user: User;
7375
tags: { [key: string]: Primitive };
76+
// TODO(v11): Make this a required field (could be subtly breaking if we did it today)
77+
attributes?: Attributes;
7478
extra: Extras;
7579
contexts: Contexts;
7680
attachments: Attachment[];
@@ -104,6 +108,9 @@ export class Scope {
104108
/** Tags */
105109
protected _tags: { [key: string]: Primitive };
106110

111+
/** Attributes */
112+
protected _attributes: Attributes;
113+
107114
/** Extra */
108115
protected _extra: Extras;
109116

@@ -155,6 +162,7 @@ export class Scope {
155162
this._attachments = [];
156163
this._user = {};
157164
this._tags = {};
165+
this._attributes = {};
158166
this._extra = {};
159167
this._contexts = {};
160168
this._sdkProcessingMetadata = {};
@@ -171,6 +179,7 @@ export class Scope {
171179
const newScope = new Scope();
172180
newScope._breadcrumbs = [...this._breadcrumbs];
173181
newScope._tags = { ...this._tags };
182+
newScope._attributes = { ...this._attributes };
174183
newScope._extra = { ...this._extra };
175184
newScope._contexts = { ...this._contexts };
176185
if (this._contexts.flags) {
@@ -296,6 +305,59 @@ export class Scope {
296305
return this;
297306
}
298307

308+
/**
309+
* Sets attributes onto the scope.
310+
*
311+
* TODO:
312+
* Currently, these attributes are not applied to any telemetry data but they will be in the future.
313+
*
314+
* @param newAttributes - The attributes to set on the scope. You can either pass in key-value pairs, or
315+
* an object with a concrete type declaration and an optional unit (if applicable to your attribute).
316+
* You can only pass in primitive values or arrays of primitive values.
317+
*
318+
* @example
319+
* ```typescript
320+
* scope.setAttributes({
321+
* is_admin: true,
322+
* payment_selection: 'credit_card',
323+
* clicked_products: [130, 554, 292],
324+
* render_duration: { value: 'render_duration', type: 'float', unit: 'ms' },
325+
* });
326+
* ```
327+
*/
328+
public setAttributes(newAttributes: Record<string, AttributeValueType | TypedAttributeValue>): this {
329+
Object.entries(newAttributes).forEach(([key, value]) => {
330+
if (typeof value === 'object' && !Array.isArray(value)) {
331+
this._attributes[key] = value;
332+
} else {
333+
this._attributes[key] = attributeValueToTypedAttributeValue(value);
334+
}
335+
});
336+
this._notifyScopeListeners();
337+
return this;
338+
}
339+
340+
/**
341+
* Sets an attribute onto the scope.
342+
*
343+
* TODO:
344+
* Currently, these attributes are not applied to any telemetry data but they will be in the future.
345+
*
346+
* @param key - The attribute key.
347+
* @param value - the attribute value. You can either pass in a raw value (primitive or array of primitives), or
348+
* a typed attribute value object with a concrete type declaration and an optional unit (if applicable to your attribute).
349+
*
350+
* @example
351+
* ```typescript
352+
* scope.setAttribute('is_admin', true);
353+
* scope.setAttribute('clicked_products', [130, 554, 292]);
354+
* scope.setAttribute('render_duration', { value: 'render_duration', type: 'float', unit: 'ms' });
355+
* ```
356+
*/
357+
public setAttribute(key: string, value: AttributeValueType | TypedAttributeValue): this {
358+
return this.setAttributes({ [key]: value });
359+
}
360+
299361
/**
300362
* Set an object that will be merged into existing extra on the scope,
301363
* and will be sent as extra data with the event.
@@ -411,9 +473,19 @@ export class Scope {
411473
? (captureContext as ScopeContext)
412474
: undefined;
413475

414-
const { tags, extra, user, contexts, level, fingerprint = [], propagationContext } = scopeInstance || {};
476+
const {
477+
tags,
478+
attributes,
479+
extra,
480+
user,
481+
contexts,
482+
level,
483+
fingerprint = [],
484+
propagationContext,
485+
} = scopeInstance || {};
415486

416487
this._tags = { ...this._tags, ...tags };
488+
this._attributes = { ...this._attributes, ...attributes };
417489
this._extra = { ...this._extra, ...extra };
418490
this._contexts = { ...this._contexts, ...contexts };
419491

@@ -444,6 +516,7 @@ export class Scope {
444516
// client is not cleared here on purpose!
445517
this._breadcrumbs = [];
446518
this._tags = {};
519+
this._attributes = {};
447520
this._extra = {};
448521
this._user = {};
449522
this._contexts = {};
@@ -530,6 +603,7 @@ export class Scope {
530603
attachments: this._attachments,
531604
contexts: this._contexts,
532605
tags: this._tags,
606+
attributes: this._attributes,
533607
extra: this._extra,
534608
user: this._user,
535609
level: this._level,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { attributeValueToTypedAttributeValue } from '../../src/attributes';
3+
4+
describe('attributeValueToTypedAttributeValue', () => {
5+
describe('primitive values', () => {
6+
it('converts a string value to a typed attribute value', () => {
7+
const result = attributeValueToTypedAttributeValue('test');
8+
expect(result).toEqual({
9+
value: 'test',
10+
type: 'string',
11+
});
12+
});
13+
14+
it('converts an interger number value to a typed attribute value', () => {
15+
const result = attributeValueToTypedAttributeValue(42);
16+
expect(result).toEqual({
17+
value: 42,
18+
type: 'integer',
19+
});
20+
});
21+
22+
it('converts a double number value to a typed attribute value', () => {
23+
const result = attributeValueToTypedAttributeValue(42.34);
24+
expect(result).toEqual({
25+
value: 42.34,
26+
type: 'double',
27+
});
28+
});
29+
30+
it('converts a boolean value to a typed attribute value', () => {
31+
const result = attributeValueToTypedAttributeValue(true);
32+
expect(result).toEqual({
33+
value: true,
34+
type: 'boolean',
35+
});
36+
});
37+
});
38+
39+
describe('arrays', () => {
40+
it('converts an array of strings to a typed attribute value', () => {
41+
const result = attributeValueToTypedAttributeValue(['foo', 'bar']);
42+
expect(result).toEqual({
43+
value: ['foo', 'bar'],
44+
type: 'string[]',
45+
});
46+
});
47+
48+
it('converts an array of integer numbers to a typed attribute value', () => {
49+
const result = attributeValueToTypedAttributeValue([1, 2, 3]);
50+
expect(result).toEqual({
51+
value: [1, 2, 3],
52+
type: 'integer[]',
53+
});
54+
});
55+
56+
it('converts an array of double numbers to a typed attribute value', () => {
57+
const result = attributeValueToTypedAttributeValue([1.1, 2.2, 3.3]);
58+
expect(result).toEqual({
59+
value: [1.1, 2.2, 3.3],
60+
type: 'double[]',
61+
});
62+
});
63+
64+
it('converts an array of booleans to a typed attribute value', () => {
65+
const result = attributeValueToTypedAttributeValue([true, false, true]);
66+
expect(result).toEqual({
67+
value: [true, false, true],
68+
type: 'boolean[]',
69+
});
70+
});
71+
});
72+
73+
describe('disallowed value types', () => {
74+
it('stringifies mixed float and integer numbers to a string attribute value', () => {
75+
const result = attributeValueToTypedAttributeValue([1, 2.2, 3]);
76+
expect(result).toEqual({
77+
value: '[1,2.2,3]',
78+
type: 'string',
79+
});
80+
});
81+
82+
it('stringifies an array of mixed types to a string attribute value', () => {
83+
// @ts-expect-error - this is not allowed by types but we still test fallback behaviour
84+
const result = attributeValueToTypedAttributeValue([1, 'foo', true]);
85+
expect(result).toEqual({
86+
value: '[1,"foo",true]',
87+
type: 'string',
88+
});
89+
});
90+
91+
it('stringifies an object value to a string attribute value', () => {
92+
// @ts-expect-error - this is not allowed by types but we still test fallback behaviour
93+
const result = attributeValueToTypedAttributeValue({ foo: 'bar' });
94+
expect(result).toEqual({
95+
value: '{"foo":"bar"}',
96+
type: 'string',
97+
});
98+
});
99+
});
100+
});

0 commit comments

Comments
 (0)