Skip to content

Commit 1ac0dd0

Browse files
committed
feat(core): Add Scope::SetAttribute(s) APIs
1 parent 9482a02 commit 1ac0dd0

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) {
@@ -294,6 +303,59 @@ export class Scope {
294303
return this.setTags({ [key]: value });
295304
}
296305

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

412-
const { tags, extra, user, contexts, level, fingerprint = [], propagationContext } = scopeInstance || {};
474+
const {
475+
tags,
476+
attributes,
477+
extra,
478+
user,
479+
contexts,
480+
level,
481+
fingerprint = [],
482+
propagationContext,
483+
} = scopeInstance || {};
413484

414485
this._tags = { ...this._tags, ...tags };
486+
this._attributes = { ...this._attributes, ...attributes };
415487
this._extra = { ...this._extra, ...extra };
416488
this._contexts = { ...this._contexts, ...contexts };
417489

@@ -442,6 +514,7 @@ export class Scope {
442514
// client is not cleared here on purpose!
443515
this._breadcrumbs = [];
444516
this._tags = {};
517+
this._attributes = {};
445518
this._extra = {};
446519
this._user = {};
447520
this._contexts = {};
@@ -528,6 +601,7 @@ export class Scope {
528601
attachments: this._attachments,
529602
contexts: this._contexts,
530603
tags: this._tags,
604+
attributes: this._attributes,
531605
extra: this._extra,
532606
user: this._user,
533607
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)