@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.
@@ -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 TRACE_ID_PATTERN = /^[0-9a-f]{32}$/iu;
10
- const SPAN_ID_PATTERN = /^[0-9a-f]{16}$/iu;
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
- const RESOURCE_KEYS = [
16
- "service",
17
- "deployment",
18
- "runtime",
19
- "framework",
20
- "operatingSystem",
21
- "device",
22
- "application"
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
- function buildTelemetryContextHelpers({ SdkError }) {
26
- function validateTelemetryContext(context, label = "telemetry context") {
27
- if (context === undefined) {
28
- return undefined;
29
- }
30
- const source = requireRecord(context, label);
31
- rejectUnknownFields(source, CONTEXT_KEYS, label);
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
- const normalized = { schemaVersion: CONTEXT_SCHEMA_VERSION };
37
- const resource = normalizeResource(source.resource, `${label} resource`);
38
- const trace = normalizeTrace(source.trace, `${label} trace`);
39
- const session = normalizeSession(source.session, `${label} session`);
40
- const subject = normalizeSubject(source.subject, `${label} subject`);
41
- const tags = normalizeTags(source.tags, `${label} tags`);
42
- if (resource !== undefined) {
43
- normalized.resource = resource;
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
- function mergeTelemetryContexts(base, override) {
64
- const normalizedBase = validateTelemetryContext(base, "client telemetry context");
65
- const normalizedOverride = validateTelemetryContext(override, "event telemetry context");
66
- if (normalizedBase === undefined) {
67
- return normalizedOverride;
68
- }
69
- if (normalizedOverride === undefined) {
70
- return normalizedBase;
71
- }
72
-
73
- const merged = { schemaVersion: CONTEXT_SCHEMA_VERSION };
74
- const resource = mergeResources(normalizedBase.resource, normalizedOverride.resource);
75
- if (resource !== undefined) {
76
- merged.resource = resource;
77
- }
78
- if (normalizedOverride.trace ?? normalizedBase.trace) {
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
- function cloneTelemetryContext(context) {
98
- return validateTelemetryContext(context);
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
- function invalid(message) {
102
- return new SdkError("validation_error", message);
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
- function requireRecord(value, label) {
106
- if (!value || Array.isArray(value) || typeof value !== "object") {
107
- throw invalid(`${label} must be an object`);
108
- }
109
- return value;
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
- function rejectUnknownFields(value, allowed, label) {
113
- const unknown = Object.keys(value).filter((key) => !allowed.has(key)).sort();
114
- if (unknown.length > 0) {
115
- throw invalid(`${label} has unsupported fields: ${unknown.join(", ")}`);
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
- function normalizeResource(value, label) {
120
- if (value === undefined) {
121
- return undefined;
122
- }
123
- const resource = requireRecord(value, label);
124
- rejectUnknownFields(resource, new Set(RESOURCE_KEYS), label);
125
- const normalized = {};
126
- for (const key of RESOURCE_KEYS) {
127
- if (resource[key] === undefined) {
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
- function normalizeResourceSection(kind, value, label) {
139
- const section = requireRecord(value, label);
140
- const fields = resourceFields(kind);
141
- rejectUnknownFields(section, new Set(fields), label);
142
- const normalized = {};
143
- for (const field of fields) {
144
- if (section[field] !== undefined) {
145
- normalized[field] = boundedString(section[field], `${label} ${field}`);
146
- }
147
- }
148
- if (["service", "runtime", "framework", "operatingSystem"].includes(kind) && normalized.name === undefined) {
149
- throw invalid(`${label} name is required`);
150
- }
151
- if (Object.keys(normalized).length === 0) {
152
- throw invalid(`${label} must not be empty`);
153
- }
154
- return normalized;
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
- function resourceFields(kind) {
158
- switch (kind) {
159
- case "service":
160
- case "runtime":
161
- case "framework":
162
- return ["name", "version"];
163
- case "deployment":
164
- return ["environment", "release"];
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
- function normalizeTrace(value, label) {
177
- if (value === undefined) {
178
- return undefined;
179
- }
180
- const trace = requireRecord(value, label);
181
- rejectUnknownFields(trace, new Set(["traceId", "spanId", "parentSpanId", "sampled"]), label);
182
- const traceId = normalizedHexId(trace.traceId, TRACE_ID_PATTERN, ZERO_TRACE_ID, `${label} traceId`, 32);
183
- const normalized = { traceId };
184
- if (trace.spanId !== undefined) {
185
- normalized.spanId = normalizedHexId(trace.spanId, SPAN_ID_PATTERN, ZERO_SPAN_ID, `${label} spanId`, 16);
186
- }
187
- if (trace.parentSpanId !== undefined) {
188
- normalized.parentSpanId = normalizedHexId(
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
- function normalizeSession(value, label) {
206
- if (value === undefined) {
207
- return undefined;
208
- }
209
- const session = requireRecord(value, label);
210
- rejectUnknownFields(session, new Set(["id", "previousId"]), label);
211
- const id = boundedString(session.id, `${label} id`, MAX_CONTEXT_ID_LENGTH);
212
- const normalized = { id };
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
- function normalizeSubject(value, label) {
223
- if (value === undefined) {
224
- return undefined;
225
- }
226
- const subject = requireRecord(value, label);
227
- rejectUnknownFields(subject, new Set(["id", "kind"]), label);
228
- const id = boundedString(subject.id, `${label} id`, MAX_CONTEXT_ID_LENGTH);
229
- if (subject.kind !== "anonymous" && subject.kind !== "user") {
230
- throw invalid(`${label} kind must be anonymous or user`);
231
- }
232
- return { id, kind: subject.kind };
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
- function normalizeTags(value, label) {
236
- if (value === undefined) {
237
- return undefined;
238
- }
239
- const tags = requireRecord(value, label);
240
- const entries = Object.entries(tags).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
241
- if (entries.length === 0 || entries.length > MAX_CONTEXT_TAGS) {
242
- throw invalid(`${label} must contain 1-${MAX_CONTEXT_TAGS} entries`);
243
- }
244
- const normalized = {};
245
- for (const [key, tagValue] of entries) {
246
- if (key.length > MAX_CONTEXT_TAG_KEY_LENGTH || !TAG_KEY_PATTERN.test(key)) {
247
- throw invalid(`${label} key is invalid`);
248
- }
249
- normalized[key] = boundedString(tagValue, `${label} value for ${key}`, MAX_CONTEXT_TAG_VALUE_LENGTH);
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 normalized;
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
- function boundedString(value, label, maxLength = MAX_CONTEXT_STRING_LENGTH) {
255
- if (typeof value !== "string") {
256
- throw invalid(`${label} must be a string`);
257
- }
258
- const normalized = value.trim();
259
- if (!normalized
260
- || Array.from(normalized).length > maxLength
261
- || Array.from(normalized).some((character) => {
262
- const code = character.codePointAt(0);
263
- return code !== undefined && (code <= 31 || (code >= 127 && code <= 159));
264
- })) {
265
- throw invalid(`${label} is invalid`);
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
- function normalizedHexId(value, pattern, zeroValue, label, width) {
271
- if (typeof value !== "string" || !pattern.test(value) || value.toLowerCase() === zeroValue) {
272
- throw invalid(`${label} must be ${width} non-zero hex characters`);
273
- }
274
- return value.toLowerCase();
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
- function mergeResources(base, override) {
278
- if (base === undefined) {
279
- return override;
280
- }
281
- if (override === undefined) {
282
- return base;
283
- }
284
- const merged = {};
285
- for (const key of RESOURCE_KEYS) {
286
- const baseSection = base[key];
287
- const overrideSection = override[key];
288
- if (baseSection !== undefined || overrideSection !== undefined) {
289
- merged[key] = { ...(baseSection ?? {}), ...(overrideSection ?? {}) };
290
- }
291
- }
292
- return merged;
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
- return { cloneTelemetryContext, mergeTelemetryContexts, validateTelemetryContext };
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 = { buildTelemetryContextHelpers };
390
+ module.exports = { cloneTelemetryContext, mergeTelemetryContexts, validateTelemetryContext };