@logbrew/sdk 0.1.5 → 0.1.7

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/issue-stack.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  const MAX_ISSUE_STACK_FRAMES = 32;
4
+ const MAX_ISSUE_STACK_FUNCTION_LENGTH = 256;
5
+ const MAX_ISSUE_STACK_MODULE_LENGTH = 512;
4
6
  const SAFE_DEBUG_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
5
7
  const LOCAL_ABSOLUTE_PATH_PATTERN = /^(?:\/(?:Users|home|private|tmp|var|Volumes)\/|[A-Za-z]:[\\/])/u;
6
8
 
@@ -54,7 +56,31 @@ function buildIssueStackHelpers({ SdkError }) {
54
56
  if (debugId === null) {
55
57
  throw new SdkError("validation_error", "issue stack frame debugId is invalid");
56
58
  }
57
- return { filename, line, column, ...(debugId ? { debugId } : {}) };
59
+ const functionName = optionalFrameIdentity(frame.function, MAX_ISSUE_STACK_FUNCTION_LENGTH);
60
+ if (functionName === null) {
61
+ throw new SdkError("validation_error", "issue stack frame function is invalid");
62
+ }
63
+ const moduleName = optionalFrameIdentity(frame.module, MAX_ISSUE_STACK_MODULE_LENGTH, true);
64
+ if (moduleName === null) {
65
+ throw new SdkError("validation_error", "issue stack frame module is invalid");
66
+ }
67
+ const inApp = frame.inApp === undefined
68
+ ? undefined
69
+ : typeof frame.inApp === "boolean"
70
+ ? frame.inApp
71
+ : null;
72
+ if (inApp === null) {
73
+ throw new SdkError("validation_error", "issue stack frame inApp is invalid");
74
+ }
75
+ return {
76
+ filename,
77
+ line,
78
+ column,
79
+ ...(functionName ? { function: functionName } : {}),
80
+ ...(moduleName ? { module: moduleName } : {}),
81
+ ...(inApp !== undefined ? { inApp } : {}),
82
+ ...(debugId ? { debugId } : {})
83
+ };
58
84
  });
59
85
  }
60
86
 
@@ -67,13 +93,18 @@ function parseJavaScriptStackFrame(rawLine) {
67
93
  return null;
68
94
  }
69
95
  let location = line;
96
+ let functionName;
70
97
  if (location.startsWith("at ")) {
71
98
  location = location.slice(3).trim();
72
99
  if (location.endsWith(")") && location.includes("(")) {
73
- location = location.slice(location.lastIndexOf("(") + 1, -1);
100
+ const marker = location.lastIndexOf("(");
101
+ functionName = generatedFrameFunction(location.slice(0, marker));
102
+ location = location.slice(marker + 1, -1);
74
103
  }
75
104
  } else if (location.includes("@")) {
76
- location = location.slice(location.lastIndexOf("@") + 1);
105
+ const marker = location.lastIndexOf("@");
106
+ functionName = generatedFrameFunction(location.slice(0, marker));
107
+ location = location.slice(marker + 1);
77
108
  }
78
109
  const parts = location.split(":");
79
110
  if (parts.length < 3) {
@@ -85,7 +116,42 @@ function parseJavaScriptStackFrame(rawLine) {
85
116
  if (!filename || lineNumber === null || column === null) {
86
117
  return null;
87
118
  }
88
- return { filename, line: lineNumber, column };
119
+ return {
120
+ filename,
121
+ line: lineNumber,
122
+ column,
123
+ ...(functionName ? { function: functionName } : {})
124
+ };
125
+ }
126
+
127
+ function optionalFrameIdentity(value, maxLength, rejectLocationText = false) {
128
+ if (value === undefined) {
129
+ return undefined;
130
+ }
131
+ if (typeof value !== "string") {
132
+ return null;
133
+ }
134
+ const identity = value.trim();
135
+ if (!identity
136
+ || Array.from(identity).length > maxLength
137
+ || hasControlCharacter(identity)
138
+ || (rejectLocationText && (identity.includes("?") || identity.includes("#")))) {
139
+ return null;
140
+ }
141
+ return identity;
142
+ }
143
+
144
+ function generatedFrameFunction(value) {
145
+ const functionName = optionalFrameIdentity(value, MAX_ISSUE_STACK_FUNCTION_LENGTH);
146
+ if (!functionName
147
+ || functionName.includes("@")
148
+ || functionName.includes("/")
149
+ || functionName.includes("\\")
150
+ || functionName.includes("?")
151
+ || functionName.includes("#")) {
152
+ return undefined;
153
+ }
154
+ return functionName;
89
155
  }
90
156
 
91
157
  function positiveIntegerFromText(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logbrew/sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Public LogBrew JavaScript SDK for building, validating, and flushing event batches.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",
@@ -43,12 +43,14 @@
43
43
  "index.js",
44
44
  "index.d.ts",
45
45
  "index.d.cts",
46
+ "issue-diagnostics.cjs",
46
47
  "issue-stack.cjs",
47
48
  "log-context.cjs",
48
49
  "opentelemetry.cjs",
49
50
  "react-native.js",
50
51
  "react-native.d.ts",
51
52
  "support-ticket.cjs",
53
+ "telemetry-context.cjs",
52
54
  "trace-context.cjs",
53
55
  "winston.cjs",
54
56
  "release-artifacts-common.js",
package/react-native.d.ts CHANGED
@@ -40,6 +40,7 @@ export type {
40
40
  PinoDestinationConfig,
41
41
  PinoDestinationHandle,
42
42
  PinoLogRecord,
43
+ ProductAnalyticsKind,
43
44
  ProductActionInput,
44
45
  ReleaseAttributes,
45
46
  Severity,
@@ -65,6 +66,8 @@ export type {
65
66
  } from "./index.js";
66
67
 
67
68
  export {
69
+ PRODUCT_ANALYTICS_KINDS,
70
+ PRODUCT_ANALYTICS_SCHEMA_VERSION,
68
71
  createBaggage,
69
72
  createIssueAttributesFromError,
70
73
  createLogBrewOpenTelemetrySpanExporter,
package/react-native.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import sdk from "./core.cjs";
2
2
 
3
3
  export const {
4
+ PRODUCT_ANALYTICS_KINDS,
5
+ PRODUCT_ANALYTICS_SCHEMA_VERSION,
4
6
  createBaggage,
5
7
  createIssueAttributesFromError,
6
8
  createNetworkMilestoneAttributes,
@@ -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 };