-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathattributes.ts
More file actions
141 lines (123 loc) · 4.87 KB
/
attributes.ts
File metadata and controls
141 lines (123 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import { DEBUG_BUILD } from './debug-build';
import type { DurationUnit, FractionUnit, InformationUnit } from './types-hoist/measurement';
import { debug } from './utils/debug-logger';
export type RawAttributes<T> = T & ValidatedAttributes<T>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type RawAttribute<T> = T extends { value: any } | { unit: any } ? AttributeObject : T;
export type Attributes = Record<string, TypedAttributeValue>;
export type AttributeValueType = string | number | boolean | Array<string> | Array<number> | Array<boolean>;
type AttributeTypeMap = {
string: string;
integer: number;
double: number;
boolean: boolean;
'string[]': Array<string>;
'integer[]': Array<number>;
'double[]': Array<number>;
'boolean[]': Array<boolean>;
};
/* Generates a type from the AttributeTypeMap like:
| { value: string; type: 'string' }
| { value: number; type: 'integer' }
| { value: number; type: 'double' }
*/
type AttributeUnion = {
[K in keyof AttributeTypeMap]: {
value: AttributeTypeMap[K];
type: K;
};
}[keyof AttributeTypeMap];
export type TypedAttributeValue = AttributeUnion & { unit?: AttributeUnit };
export type AttributeObject = {
value: unknown;
unit?: AttributeUnit;
};
// Unfortunately, we loose type safety if we did something like Exclude<MeasurementUnit, string>
// so therefore we unionize between the three supported unit categories.
type AttributeUnit = DurationUnit | InformationUnit | FractionUnit;
/* If an attribute has either a 'value' or 'unit' property, we use the ValidAttributeObject type. */
export type ValidatedAttributes<T> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[K in keyof T]: T[K] extends { value: any } | { unit: any } ? AttributeObject : unknown;
};
/**
* Type-guard: The attribute object has the shape the official attribute object (value, type, unit).
* https://develop.sentry.dev/sdk/telemetry/scopes/#setting-attributes
*/
export function isAttributeObject(maybeObj: unknown): maybeObj is AttributeObject {
return (
typeof maybeObj === 'object' &&
maybeObj != null &&
!Array.isArray(maybeObj) &&
Object.keys(maybeObj).includes('value')
);
}
/**
* Converts an attribute value to a typed attribute value.
*
* Does not allow mixed arrays. In case of a mixed array, the value is stringified and the type is 'string'.
* All values besides the supported attribute types (see {@link AttributeTypeMap}) are stringified to a string attribute value.
*
* @param value - The value of the passed attribute.
* @returns The typed attribute.
*/
export function attributeValueToTypedAttributeValue(rawValue: unknown): TypedAttributeValue {
const { value, unit } = isAttributeObject(rawValue) ? rawValue : { value: rawValue, unit: undefined };
return { ...getTypedAttributeValue(value), ...(unit && typeof unit === 'string' ? { unit } : {}) };
}
// Only allow string, boolean, or number types
const getPrimitiveType: (
item: unknown,
) => keyof Pick<AttributeTypeMap, 'string' | 'integer' | 'double' | 'boolean'> | null = item =>
typeof item === 'string'
? 'string'
: typeof item === 'boolean'
? 'boolean'
: typeof item === 'number' && !Number.isNaN(item)
? Number.isInteger(item)
? 'integer'
: 'double'
: null;
function getTypedAttributeValue(value: unknown): TypedAttributeValue {
const primitiveType = getPrimitiveType(value);
if (primitiveType) {
// @ts-expect-error - TS complains because {@link TypedAttributeValue} is strictly typed to
// avoid setting the wrong `type` on the attribute value.
// In this case, getPrimitiveType already does the check but TS doesn't know that.
// The "clean" alternative is to return an object per `typeof value` case
// but that would require more bundle size
// Therefore, we ignore it.
return { value, type: primitiveType };
}
if (Array.isArray(value)) {
const coherentArrayType = value.reduce((acc: 'string' | 'boolean' | 'integer' | 'double' | null, item) => {
if (!acc || getPrimitiveType(item) !== acc) {
return null;
}
return acc;
}, getPrimitiveType(value[0]));
if (coherentArrayType) {
return { value, type: `${coherentArrayType}[]` };
}
}
// Fallback: stringify the passed value
let fallbackValue = '';
try {
fallbackValue = JSON.stringify(value) ?? String(value);
} catch {
try {
fallbackValue = String(value);
} catch {
DEBUG_BUILD && debug.warn('Failed to stringify attribute value', value);
// ignore
}
}
// This is quite a low-quality message but we cannot safely log the original `value`
// here due to String() or JSON.stringify() potentially throwing.
DEBUG_BUILD &&
debug.log(`Stringified attribute value to ${fallbackValue} because it's not a supported attribute value type`);
return {
value: fallbackValue,
type: 'string',
};
}