@logbrew/sdk 0.1.6 → 0.1.8

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.
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+
3
+ const CONTEXT_SCHEMA_VERSION = 1;
4
+ const MAX_CONTEXT_ID_LENGTH = 200;
5
+ const MAX_CONTEXT_STRING_LENGTH = 256;
6
+ const MAX_CONTEXT_TAGS = 32;
7
+ const MAX_CONTEXT_TAG_KEY_LENGTH = 64;
8
+ 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";
13
+ const TAG_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u;
14
+ 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
+ ];
24
+
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
+ }
35
+
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;
61
+ }
62
+
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) };
86
+ }
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
+ }
96
+
97
+ function cloneTelemetryContext(context) {
98
+ return validateTelemetryContext(context);
99
+ }
100
+
101
+ function invalid(message) {
102
+ return new SdkError("validation_error", message);
103
+ }
104
+
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;
110
+ }
111
+
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
+ }
117
+ }
118
+
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;
136
+ }
137
+
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;
155
+ }
156
+
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
+ }
174
+ }
175
+
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;
203
+ }
204
+
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;
220
+ }
221
+
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 };
233
+ }
234
+
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);
250
+ }
251
+ return normalized;
252
+ }
253
+
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;
268
+ }
269
+
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();
275
+ }
276
+
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;
293
+ }
294
+
295
+ return { cloneTelemetryContext, mergeTelemetryContexts, validateTelemetryContext };
296
+ }
297
+
298
+ module.exports = { buildTelemetryContextHelpers };