@logbrew/sdk 0.1.36 → 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core.cjs +44 -213
- package/issue-breadcrumbs.cjs +143 -0
- package/issue-diagnostics.cjs +94 -582
- package/issue-evidence.cjs +256 -0
- package/issue-exception.cjs +263 -0
- package/issue-stack-frames.cjs +220 -0
- package/issue-stack-text.cjs +101 -0
- package/issue-stack.cjs +188 -225
- package/issue-text.cjs +72 -0
- package/log-context.cjs +87 -67
- package/package.json +11 -1
- package/sdk-error.cjs +36 -0
- package/support-ticket.cjs +232 -122
- package/telemetry-context.cjs +343 -251
- package/trace-context.cjs +376 -179
- package/transport-error.cjs +23 -0
- package/validation.cjs +279 -0
- package/winston.cjs +6 -7
package/telemetry-context.cjs
CHANGED
|
@@ -1,298 +1,390 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
4
|
+
const { hasUnicodeControlCharacter, optionalEntry, rejectUnknownKeys, requireObject } = require("./validation.cjs");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @import { TelemetryContext, TelemetryResource, TelemetrySessionContext, TelemetrySubjectContext, TelemetryTraceContext } from "./index.cjs"
|
|
8
|
+
* @typedef {{ readonly pattern: Readonly<RegExp>, readonly width: number, readonly zeroValue: string }} HexIdFormat
|
|
9
|
+
* @typedef {{ readonly [Kind in keyof TelemetryResource]?: Readonly<NonNullable<TelemetryResource[Kind]>> }} ReadonlyResource
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* readonly schemaVersion: 1,
|
|
12
|
+
* readonly resource?: ReadonlyResource,
|
|
13
|
+
* readonly trace?: Readonly<TelemetryTraceContext>,
|
|
14
|
+
* readonly session?: Readonly<TelemetrySessionContext>,
|
|
15
|
+
* readonly subject?: Readonly<TelemetrySubjectContext>,
|
|
16
|
+
* readonly tags?: Readonly<Record<string, string>>
|
|
17
|
+
* }} ReadonlyTelemetryContext
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @type {1} */
|
|
3
21
|
const CONTEXT_SCHEMA_VERSION = 1;
|
|
4
22
|
const MAX_CONTEXT_ID_LENGTH = 200;
|
|
5
23
|
const MAX_CONTEXT_STRING_LENGTH = 256;
|
|
6
24
|
const MAX_CONTEXT_TAGS = 32;
|
|
7
25
|
const MAX_CONTEXT_TAG_KEY_LENGTH = 64;
|
|
8
26
|
const MAX_CONTEXT_TAG_VALUE_LENGTH = 256;
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const ZERO_TRACE_ID = "00000000000000000000000000000000";
|
|
12
|
-
const ZERO_SPAN_ID = "0000000000000000";
|
|
27
|
+
const NO_ENTRIES = 0;
|
|
28
|
+
const ONLY_SCHEMA_VERSION = 1;
|
|
13
29
|
const TAG_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u;
|
|
14
30
|
const CONTEXT_KEYS = new Set(["schemaVersion", "resource", "trace", "session", "subject", "tags"]);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
];
|
|
31
|
+
/** @type {ReadonlySet<keyof TelemetryResource>} */
|
|
32
|
+
const RESOURCE_KEYS = new Set(["service", "deployment", "runtime", "framework", "operatingSystem", "device", "application"]);
|
|
33
|
+
const TRACE_KEYS = new Set(["traceId", "spanId", "parentSpanId", "sampled"]);
|
|
34
|
+
const SESSION_KEYS = new Set(["id", "previousId"]);
|
|
35
|
+
const SUBJECT_KEYS = new Set(["id", "kind"]);
|
|
36
|
+
/** @type {readonly ["name", "version"]} */
|
|
37
|
+
const NAME_VERSION_FIELDS = ["name", "version"];
|
|
38
|
+
/** @type {readonly ["name", "version", "build"]} */
|
|
39
|
+
const NAME_VERSION_BUILD_FIELDS = ["name", "version", "build"];
|
|
40
|
+
/** @type {readonly ["environment", "release"]} */
|
|
41
|
+
const DEPLOYMENT_FIELDS = ["environment", "release"];
|
|
42
|
+
/** @type {readonly ["family", "model", "architecture"]} */
|
|
43
|
+
const DEVICE_FIELDS = ["family", "model", "architecture"];
|
|
44
|
+
/** @type {HexIdFormat} */
|
|
45
|
+
const TRACE_ID_FORMAT = { pattern: /^[0-9a-f]{32}$/iu, width: 32, zeroValue: "00000000000000000000000000000000" };
|
|
46
|
+
/** @type {HexIdFormat} */
|
|
47
|
+
const SPAN_ID_FORMAT = { pattern: /^[0-9a-f]{16}$/iu, width: 16, zeroValue: "0000000000000000" };
|
|
24
48
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (source.schemaVersion !== CONTEXT_SCHEMA_VERSION) {
|
|
33
|
-
throw invalid(`${label} schemaVersion must be ${CONTEXT_SCHEMA_VERSION}`);
|
|
34
|
-
}
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} message
|
|
51
|
+
* @returns {Error}
|
|
52
|
+
*/
|
|
53
|
+
function invalid(message) {
|
|
54
|
+
return new SdkError("validation_error", message);
|
|
55
|
+
}
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
if (trace !== undefined) {
|
|
46
|
-
normalized.trace = trace;
|
|
47
|
-
}
|
|
48
|
-
if (session !== undefined) {
|
|
49
|
-
normalized.session = session;
|
|
50
|
-
}
|
|
51
|
-
if (subject !== undefined) {
|
|
52
|
-
normalized.subject = subject;
|
|
53
|
-
}
|
|
54
|
-
if (tags !== undefined) {
|
|
55
|
-
normalized.tags = tags;
|
|
56
|
-
}
|
|
57
|
-
if (Object.keys(normalized).length === 1) {
|
|
58
|
-
throw invalid(`${label} must include resource, trace, session, subject, or tags`);
|
|
59
|
-
}
|
|
60
|
-
return normalized;
|
|
57
|
+
/**
|
|
58
|
+
* @param {unknown} value
|
|
59
|
+
* @param {string} label
|
|
60
|
+
* @param {number} [maxLength]
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function boundedString(value, label, maxLength = MAX_CONTEXT_STRING_LENGTH) {
|
|
64
|
+
if (typeof value !== "string") {
|
|
65
|
+
throw invalid(`${label} must be a string`);
|
|
61
66
|
}
|
|
67
|
+
const normalized = value.trim();
|
|
68
|
+
if (normalized === "" || [...normalized].length > maxLength || hasUnicodeControlCharacter(normalized)) {
|
|
69
|
+
throw invalid(`${label} is invalid`);
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
merged.trace = { ...(normalizedOverride.trace ?? normalizedBase.trace) };
|
|
80
|
-
}
|
|
81
|
-
if (normalizedOverride.session ?? normalizedBase.session) {
|
|
82
|
-
merged.session = { ...(normalizedOverride.session ?? normalizedBase.session) };
|
|
83
|
-
}
|
|
84
|
-
if (normalizedOverride.subject ?? normalizedBase.subject) {
|
|
85
|
-
merged.subject = { ...(normalizedOverride.subject ?? normalizedBase.subject) };
|
|
74
|
+
/**
|
|
75
|
+
* @template {string} Field
|
|
76
|
+
* @param {unknown} value
|
|
77
|
+
* @param {readonly Field[]} fields
|
|
78
|
+
* @param {string} label
|
|
79
|
+
* @returns {{ [Key in Field]?: string }}
|
|
80
|
+
*/
|
|
81
|
+
function sectionFields(value, fields, label) {
|
|
82
|
+
const section = requireObject(label, value);
|
|
83
|
+
rejectUnknownKeys(label, section, new Set(fields));
|
|
84
|
+
/** @type {{ [Key in Field]?: string }} */
|
|
85
|
+
const normalized = {};
|
|
86
|
+
for (const field of fields) {
|
|
87
|
+
const fieldValue = section[field];
|
|
88
|
+
if (fieldValue !== undefined) {
|
|
89
|
+
normalized[field] = boundedString(fieldValue, `${label} ${field}`);
|
|
86
90
|
}
|
|
87
|
-
const mergedTagValues = normalizedBase.tags !== undefined || normalizedOverride.tags !== undefined
|
|
88
|
-
? { ...(normalizedBase.tags ?? {}), ...(normalizedOverride.tags ?? {}) }
|
|
89
|
-
: undefined;
|
|
90
|
-
const tags = normalizeTags(mergedTagValues, "merged telemetry context tags");
|
|
91
|
-
if (tags !== undefined) {
|
|
92
|
-
merged.tags = tags;
|
|
93
|
-
}
|
|
94
|
-
return validateTelemetryContext(merged, "merged telemetry context");
|
|
95
91
|
}
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
/**
|
|
96
|
+
* @template {string} Field
|
|
97
|
+
* @param {unknown} value
|
|
98
|
+
* @param {readonly ["name", ...Field[]]} fields
|
|
99
|
+
* @param {string} label
|
|
100
|
+
* @returns {({ name: string } & { [Key in Field]?: string }) | undefined}
|
|
101
|
+
*/
|
|
102
|
+
function namedSection(value, fields, label) {
|
|
103
|
+
if (value === undefined) {
|
|
104
|
+
return undefined;
|
|
99
105
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
const { name, ...rest } = sectionFields(value, fields, label);
|
|
107
|
+
if (name === undefined) {
|
|
108
|
+
throw invalid(`${label} name is required`);
|
|
103
109
|
}
|
|
110
|
+
return { name, ...rest };
|
|
111
|
+
}
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
/**
|
|
114
|
+
* @template {string} Field
|
|
115
|
+
* @param {unknown} value
|
|
116
|
+
* @param {readonly Field[]} fields
|
|
117
|
+
* @param {string} label
|
|
118
|
+
* @returns {{ [Key in Field]?: string } | undefined}
|
|
119
|
+
*/
|
|
120
|
+
function nonEmptySection(value, fields, label) {
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
return undefined;
|
|
110
123
|
}
|
|
124
|
+
const normalized = sectionFields(value, fields, label);
|
|
125
|
+
if (Object.keys(normalized).length === NO_ENTRIES) {
|
|
126
|
+
throw invalid(`${label} must not be empty`);
|
|
127
|
+
}
|
|
128
|
+
return normalized;
|
|
129
|
+
}
|
|
111
130
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
/**
|
|
132
|
+
* @param {unknown} value
|
|
133
|
+
* @param {string} label
|
|
134
|
+
* @returns {TelemetryResource | undefined}
|
|
135
|
+
*/
|
|
136
|
+
function normalizeResource(value, label) {
|
|
137
|
+
if (value === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const resource = requireObject(label, value);
|
|
141
|
+
rejectUnknownKeys(label, resource, RESOURCE_KEYS);
|
|
142
|
+
const normalized = {
|
|
143
|
+
...optionalEntry("service", namedSection(resource["service"], NAME_VERSION_FIELDS, `${label} service`)),
|
|
144
|
+
...optionalEntry("deployment", nonEmptySection(resource["deployment"], DEPLOYMENT_FIELDS, `${label} deployment`)),
|
|
145
|
+
...optionalEntry("runtime", namedSection(resource["runtime"], NAME_VERSION_FIELDS, `${label} runtime`)),
|
|
146
|
+
...optionalEntry("framework", namedSection(resource["framework"], NAME_VERSION_FIELDS, `${label} framework`)),
|
|
147
|
+
...optionalEntry(
|
|
148
|
+
"operatingSystem",
|
|
149
|
+
namedSection(resource["operatingSystem"], NAME_VERSION_BUILD_FIELDS, `${label} operatingSystem`)
|
|
150
|
+
),
|
|
151
|
+
...optionalEntry("device", nonEmptySection(resource["device"], DEVICE_FIELDS, `${label} device`)),
|
|
152
|
+
...optionalEntry("application", nonEmptySection(resource["application"], NAME_VERSION_BUILD_FIELDS, `${label} application`))
|
|
153
|
+
};
|
|
154
|
+
if (Object.keys(normalized).length === NO_ENTRIES) {
|
|
155
|
+
throw invalid(`${label} must not be empty`);
|
|
117
156
|
}
|
|
157
|
+
return normalized;
|
|
158
|
+
}
|
|
118
159
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
normalized[key] = normalizeResourceSection(key, resource[key], `${label} ${key}`);
|
|
131
|
-
}
|
|
132
|
-
if (Object.keys(normalized).length === 0) {
|
|
133
|
-
throw invalid(`${label} must not be empty`);
|
|
134
|
-
}
|
|
135
|
-
return normalized;
|
|
160
|
+
/**
|
|
161
|
+
* @param {unknown} value
|
|
162
|
+
* @param {string} label
|
|
163
|
+
* @param {HexIdFormat} format
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
function requireHexId(value, label, format) {
|
|
167
|
+
if (typeof value !== "string" || !format.pattern.test(value) || value.toLowerCase() === format.zeroValue) {
|
|
168
|
+
throw invalid(`${label} must be ${format.width} non-zero hex characters`);
|
|
136
169
|
}
|
|
170
|
+
return value.toLowerCase();
|
|
171
|
+
}
|
|
137
172
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
173
|
+
/**
|
|
174
|
+
* @param {unknown} value
|
|
175
|
+
* @param {string} label
|
|
176
|
+
* @param {HexIdFormat} format
|
|
177
|
+
* @returns {string | undefined}
|
|
178
|
+
*/
|
|
179
|
+
function optionalHexId(value, label, format) {
|
|
180
|
+
return value === undefined ? undefined : requireHexId(value, label, format);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {unknown} value
|
|
185
|
+
* @param {string} label
|
|
186
|
+
* @returns {boolean | undefined}
|
|
187
|
+
*/
|
|
188
|
+
function optionalBoolean(value, label) {
|
|
189
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
190
|
+
throw invalid(`${label} must be a boolean`);
|
|
155
191
|
}
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
156
194
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
case "operatingSystem":
|
|
166
|
-
return ["name", "version", "build"];
|
|
167
|
-
case "device":
|
|
168
|
-
return ["family", "model", "architecture"];
|
|
169
|
-
case "application":
|
|
170
|
-
return ["name", "version", "build"];
|
|
171
|
-
default:
|
|
172
|
-
return [];
|
|
173
|
-
}
|
|
195
|
+
/**
|
|
196
|
+
* @param {unknown} value
|
|
197
|
+
* @param {string} label
|
|
198
|
+
* @returns {TelemetryTraceContext | undefined}
|
|
199
|
+
*/
|
|
200
|
+
function normalizeTrace(value, label) {
|
|
201
|
+
if (value === undefined) {
|
|
202
|
+
return undefined;
|
|
174
203
|
}
|
|
204
|
+
const trace = requireObject(label, value);
|
|
205
|
+
rejectUnknownKeys(label, trace, TRACE_KEYS);
|
|
206
|
+
const traceId = requireHexId(trace["traceId"], `${label} traceId`, TRACE_ID_FORMAT);
|
|
207
|
+
const spanId = optionalHexId(trace["spanId"], `${label} spanId`, SPAN_ID_FORMAT);
|
|
208
|
+
const parentSpanId = optionalHexId(trace["parentSpanId"], `${label} parentSpanId`, SPAN_ID_FORMAT);
|
|
209
|
+
const sampled = optionalBoolean(trace["sampled"], `${label} sampled`);
|
|
210
|
+
return {
|
|
211
|
+
traceId,
|
|
212
|
+
...optionalEntry("spanId", spanId),
|
|
213
|
+
...optionalEntry("parentSpanId", parentSpanId),
|
|
214
|
+
...optionalEntry("sampled", sampled)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
175
217
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
trace.parentSpanId,
|
|
190
|
-
SPAN_ID_PATTERN,
|
|
191
|
-
ZERO_SPAN_ID,
|
|
192
|
-
`${label} parentSpanId`,
|
|
193
|
-
16
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
if (trace.sampled !== undefined) {
|
|
197
|
-
if (typeof trace.sampled !== "boolean") {
|
|
198
|
-
throw invalid(`${label} sampled must be a boolean`);
|
|
199
|
-
}
|
|
200
|
-
normalized.sampled = trace.sampled;
|
|
201
|
-
}
|
|
202
|
-
return normalized;
|
|
218
|
+
/**
|
|
219
|
+
* @param {unknown} value
|
|
220
|
+
* @param {string} id
|
|
221
|
+
* @param {string} label
|
|
222
|
+
* @returns {string | undefined}
|
|
223
|
+
*/
|
|
224
|
+
function previousSessionId(value, id, label) {
|
|
225
|
+
if (value === undefined) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
const previousId = boundedString(value, `${label} previousId`, MAX_CONTEXT_ID_LENGTH);
|
|
229
|
+
if (previousId === id) {
|
|
230
|
+
throw invalid(`${label} previousId must differ from id`);
|
|
203
231
|
}
|
|
232
|
+
return previousId;
|
|
233
|
+
}
|
|
204
234
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (session.previousId !== undefined) {
|
|
214
|
-
normalized.previousId = boundedString(session.previousId, `${label} previousId`, MAX_CONTEXT_ID_LENGTH);
|
|
215
|
-
if (normalized.previousId === id) {
|
|
216
|
-
throw invalid(`${label} previousId must differ from id`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return normalized;
|
|
235
|
+
/**
|
|
236
|
+
* @param {unknown} value
|
|
237
|
+
* @param {string} label
|
|
238
|
+
* @returns {TelemetrySessionContext | undefined}
|
|
239
|
+
*/
|
|
240
|
+
function normalizeSession(value, label) {
|
|
241
|
+
if (value === undefined) {
|
|
242
|
+
return undefined;
|
|
220
243
|
}
|
|
244
|
+
const session = requireObject(label, value);
|
|
245
|
+
rejectUnknownKeys(label, session, SESSION_KEYS);
|
|
246
|
+
const id = boundedString(session["id"], `${label} id`, MAX_CONTEXT_ID_LENGTH);
|
|
247
|
+
const previousId = previousSessionId(session["previousId"], id, label);
|
|
248
|
+
return { id, ...optionalEntry("previousId", previousId) };
|
|
249
|
+
}
|
|
221
250
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
/**
|
|
252
|
+
* @param {unknown} value
|
|
253
|
+
* @param {string} label
|
|
254
|
+
* @returns {TelemetrySubjectContext | undefined}
|
|
255
|
+
*/
|
|
256
|
+
function normalizeSubject(value, label) {
|
|
257
|
+
if (value === undefined) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const subject = requireObject(label, value);
|
|
261
|
+
rejectUnknownKeys(label, subject, SUBJECT_KEYS);
|
|
262
|
+
const id = boundedString(subject["id"], `${label} id`, MAX_CONTEXT_ID_LENGTH);
|
|
263
|
+
const { kind } = subject;
|
|
264
|
+
if (kind !== "anonymous" && kind !== "user") {
|
|
265
|
+
throw invalid(`${label} kind must be anonymous or user`);
|
|
233
266
|
}
|
|
267
|
+
return { id, kind };
|
|
268
|
+
}
|
|
234
269
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
270
|
+
/**
|
|
271
|
+
* @param {unknown} value
|
|
272
|
+
* @param {string} label
|
|
273
|
+
* @returns {Record<string, string> | undefined}
|
|
274
|
+
*/
|
|
275
|
+
function normalizeTags(value, label) {
|
|
276
|
+
if (value === undefined) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
const tags = requireObject(label, value);
|
|
280
|
+
const keys = Object.keys(tags);
|
|
281
|
+
keys.sort();
|
|
282
|
+
if (keys.length === NO_ENTRIES || keys.length > MAX_CONTEXT_TAGS) {
|
|
283
|
+
throw invalid(`${label} must contain 1-${MAX_CONTEXT_TAGS} entries`);
|
|
284
|
+
}
|
|
285
|
+
return Object.fromEntries(keys.map((key) => {
|
|
286
|
+
if (key.length > MAX_CONTEXT_TAG_KEY_LENGTH || !TAG_KEY_PATTERN.test(key)) {
|
|
287
|
+
throw invalid(`${label} key is invalid`);
|
|
250
288
|
}
|
|
251
|
-
return
|
|
289
|
+
return [key, boundedString(tags[key], `${label} value for ${key}`, MAX_CONTEXT_TAG_VALUE_LENGTH)];
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @param {Readonly<Record<string, unknown>>} source
|
|
295
|
+
* @param {string} label
|
|
296
|
+
* @returns {TelemetryContext}
|
|
297
|
+
*/
|
|
298
|
+
function normalizeContextSections(source, label) {
|
|
299
|
+
const resource = normalizeResource(source["resource"], `${label} resource`);
|
|
300
|
+
const trace = normalizeTrace(source["trace"], `${label} trace`);
|
|
301
|
+
const session = normalizeSession(source["session"], `${label} session`);
|
|
302
|
+
const subject = normalizeSubject(source["subject"], `${label} subject`);
|
|
303
|
+
const tags = normalizeTags(source["tags"], `${label} tags`);
|
|
304
|
+
const normalized = {
|
|
305
|
+
schemaVersion: CONTEXT_SCHEMA_VERSION,
|
|
306
|
+
...optionalEntry("resource", resource),
|
|
307
|
+
...optionalEntry("trace", trace),
|
|
308
|
+
...optionalEntry("session", session),
|
|
309
|
+
...optionalEntry("subject", subject),
|
|
310
|
+
...optionalEntry("tags", tags)
|
|
311
|
+
};
|
|
312
|
+
if (Object.keys(normalized).length === ONLY_SCHEMA_VERSION) {
|
|
313
|
+
throw invalid(`${label} must include resource, trace, session, subject, or tags`);
|
|
252
314
|
}
|
|
315
|
+
return normalized;
|
|
316
|
+
}
|
|
253
317
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
return normalized;
|
|
318
|
+
/**
|
|
319
|
+
* @param {unknown} context
|
|
320
|
+
* @param {string} [label]
|
|
321
|
+
* @returns {TelemetryContext | undefined}
|
|
322
|
+
*/
|
|
323
|
+
function validateTelemetryContext(context, label = "telemetry context") {
|
|
324
|
+
if (context === undefined) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
const source = requireObject(label, context);
|
|
328
|
+
rejectUnknownKeys(label, source, CONTEXT_KEYS);
|
|
329
|
+
if (source["schemaVersion"] !== CONTEXT_SCHEMA_VERSION) {
|
|
330
|
+
throw invalid(`${label} schemaVersion must be ${CONTEXT_SCHEMA_VERSION}`);
|
|
268
331
|
}
|
|
332
|
+
return normalizeContextSections(source, label);
|
|
333
|
+
}
|
|
269
334
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
335
|
+
/**
|
|
336
|
+
* @param {ReadonlyResource | undefined} base
|
|
337
|
+
* @param {ReadonlyResource | undefined} override
|
|
338
|
+
* @returns {Readonly<Record<string, unknown>> | undefined}
|
|
339
|
+
*/
|
|
340
|
+
function mergeResources(base, override) {
|
|
341
|
+
if (base === undefined || override === undefined) {
|
|
342
|
+
return base ?? override;
|
|
275
343
|
}
|
|
344
|
+
return Object.fromEntries([...RESOURCE_KEYS].flatMap((key) => {
|
|
345
|
+
const baseSection = base[key];
|
|
346
|
+
const overrideSection = override[key];
|
|
347
|
+
return baseSection === undefined && overrideSection === undefined ? [] : [[key, { ...baseSection, ...overrideSection }]];
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
276
350
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
351
|
+
/**
|
|
352
|
+
* @param {ReadonlyTelemetryContext} base
|
|
353
|
+
* @param {ReadonlyTelemetryContext} override
|
|
354
|
+
* @returns {Readonly<Record<string, unknown>>}
|
|
355
|
+
*/
|
|
356
|
+
function mergeDefinedContexts(base, override) {
|
|
357
|
+
const tags = base.tags === undefined && override.tags === undefined ? undefined : { ...base.tags, ...override.tags };
|
|
358
|
+
return {
|
|
359
|
+
schemaVersion: CONTEXT_SCHEMA_VERSION,
|
|
360
|
+
...optionalEntry("resource", mergeResources(base.resource, override.resource)),
|
|
361
|
+
...optionalEntry("trace", override.trace ?? base.trace),
|
|
362
|
+
...optionalEntry("session", override.session ?? base.session),
|
|
363
|
+
...optionalEntry("subject", override.subject ?? base.subject),
|
|
364
|
+
...optionalEntry("tags", normalizeTags(tags, "merged telemetry context tags"))
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @param {unknown} base
|
|
370
|
+
* @param {unknown} override
|
|
371
|
+
* @returns {TelemetryContext | undefined}
|
|
372
|
+
*/
|
|
373
|
+
function mergeTelemetryContexts(base, override) {
|
|
374
|
+
const normalizedBase = validateTelemetryContext(base, "client telemetry context");
|
|
375
|
+
const normalizedOverride = validateTelemetryContext(override, "event telemetry context");
|
|
376
|
+
if (normalizedBase === undefined || normalizedOverride === undefined) {
|
|
377
|
+
return normalizedOverride ?? normalizedBase;
|
|
293
378
|
}
|
|
379
|
+
return validateTelemetryContext(mergeDefinedContexts(normalizedBase, normalizedOverride), "merged telemetry context");
|
|
380
|
+
}
|
|
294
381
|
|
|
295
|
-
|
|
382
|
+
/**
|
|
383
|
+
* @param {unknown} context
|
|
384
|
+
* @returns {TelemetryContext | undefined}
|
|
385
|
+
*/
|
|
386
|
+
function cloneTelemetryContext(context) {
|
|
387
|
+
return validateTelemetryContext(context);
|
|
296
388
|
}
|
|
297
389
|
|
|
298
|
-
module.exports = {
|
|
390
|
+
module.exports = { cloneTelemetryContext, mergeTelemetryContexts, validateTelemetryContext };
|