@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,267 @@
1
+ "use strict";
2
+
3
+ const MAX_ISSUE_BREADCRUMBS = 64;
4
+ const MAX_EXCEPTION_TYPE_LENGTH = 256;
5
+ const MAX_MECHANISM_TYPE_LENGTH = 64;
6
+ const MAX_BREADCRUMB_NAME_LENGTH = 64;
7
+ const MAX_BREADCRUMB_MESSAGE_LENGTH = 512;
8
+ const MAX_BREADCRUMB_DATA_FIELDS = 8;
9
+ const MAX_BREADCRUMB_DATA_STRING_LENGTH = 256;
10
+ const MACHINE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/u;
11
+ const DATA_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u;
12
+ const BREADCRUMB_LEVEL_ALIASES = new Map([
13
+ ["trace", "debug"],
14
+ ["debug", "debug"],
15
+ ["info", "info"],
16
+ ["log", "info"],
17
+ ["warn", "warning"],
18
+ ["warning", "warning"],
19
+ ["error", "error"],
20
+ ["fatal", "critical"],
21
+ ["critical", "critical"]
22
+ ]);
23
+
24
+ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp }) {
25
+ function validationError(message) {
26
+ return new SdkError("validation_error", message);
27
+ }
28
+
29
+ function validateIssueException(exception) {
30
+ if (exception === undefined) {
31
+ return undefined;
32
+ }
33
+ requireObject("issue exception", exception);
34
+ rejectUnknownKeys("issue exception", exception, new Set(["type", "mechanism"]));
35
+ const type = boundedText("issue exception type", exception.type, MAX_EXCEPTION_TYPE_LENGTH, {
36
+ rejectLocationText: true
37
+ });
38
+ const mechanism = validateIssueExceptionMechanism(exception.mechanism);
39
+ return {
40
+ type,
41
+ ...(mechanism === undefined ? {} : { mechanism })
42
+ };
43
+ }
44
+
45
+ function validateIssueExceptionMechanism(mechanism) {
46
+ if (mechanism === undefined) {
47
+ return undefined;
48
+ }
49
+ requireObject("issue exception mechanism", mechanism);
50
+ rejectUnknownKeys(
51
+ "issue exception mechanism",
52
+ mechanism,
53
+ new Set(["type", "handled"])
54
+ );
55
+ if (typeof mechanism.type !== "string" || !MACHINE_NAME_PATTERN.test(mechanism.type)) {
56
+ throw validationError(
57
+ `issue exception mechanism type must match ${MACHINE_NAME_PATTERN}`
58
+ );
59
+ }
60
+ if (Array.from(mechanism.type).length > MAX_MECHANISM_TYPE_LENGTH) {
61
+ throw validationError(
62
+ `issue exception mechanism type must be at most ${MAX_MECHANISM_TYPE_LENGTH} characters`
63
+ );
64
+ }
65
+ if (typeof mechanism.handled !== "boolean") {
66
+ throw validationError("issue exception mechanism handled must be a boolean");
67
+ }
68
+ return { type: mechanism.type, handled: mechanism.handled };
69
+ }
70
+
71
+ function createIssueException(type, mechanism, handled) {
72
+ return validateIssueException({
73
+ type,
74
+ mechanism: { type: mechanism, handled }
75
+ });
76
+ }
77
+
78
+ function validateIssueBreadcrumb(breadcrumb, defaultTimestamp) {
79
+ requireObject("issue breadcrumb", breadcrumb);
80
+ rejectUnknownKeys(
81
+ "issue breadcrumb",
82
+ breadcrumb,
83
+ new Set(["timestamp", "type", "category", "level", "message", "data"])
84
+ );
85
+ const timestamp = breadcrumb.timestamp ?? defaultTimestamp;
86
+ requireTimestamp(timestamp);
87
+ if (typeof breadcrumb.category !== "string" || !MACHINE_NAME_PATTERN.test(breadcrumb.category)) {
88
+ throw validationError(`issue breadcrumb category must match ${MACHINE_NAME_PATTERN}`);
89
+ }
90
+ const type = optionalMachineName("issue breadcrumb type", breadcrumb.type);
91
+ const level = optionalBreadcrumbLevel(breadcrumb.level);
92
+ const message = breadcrumb.message === undefined
93
+ ? undefined
94
+ : boundedText(
95
+ "issue breadcrumb message",
96
+ breadcrumb.message,
97
+ MAX_BREADCRUMB_MESSAGE_LENGTH
98
+ );
99
+ const data = validateBreadcrumbData(breadcrumb.data);
100
+ return {
101
+ timestamp,
102
+ ...(type === undefined ? {} : { type }),
103
+ category: breadcrumb.category,
104
+ ...(level === undefined ? {} : { level }),
105
+ ...(message === undefined ? {} : { message }),
106
+ ...(data === undefined ? {} : { data })
107
+ };
108
+ }
109
+
110
+ function validateIssueBreadcrumbs(breadcrumbs) {
111
+ if (breadcrumbs === undefined) {
112
+ return undefined;
113
+ }
114
+ if (!Array.isArray(breadcrumbs) || breadcrumbs.length < 1 || breadcrumbs.length > MAX_ISSUE_BREADCRUMBS) {
115
+ throw validationError(
116
+ `issue breadcrumbs must contain 1-${MAX_ISSUE_BREADCRUMBS} entries`
117
+ );
118
+ }
119
+ return breadcrumbs.map((breadcrumb) => validateIssueBreadcrumb(breadcrumb));
120
+ }
121
+
122
+ function validateIssueDiagnostics(attributes) {
123
+ const exception = validateIssueException(attributes.exception);
124
+ const breadcrumbs = validateIssueBreadcrumbs(attributes.breadcrumbs);
125
+ if (
126
+ attributes.breadcrumbsTruncated !== undefined
127
+ && typeof attributes.breadcrumbsTruncated !== "boolean"
128
+ ) {
129
+ throw validationError("issue breadcrumbsTruncated must be a boolean");
130
+ }
131
+ return {
132
+ ...(exception === undefined ? {} : { exception }),
133
+ ...(breadcrumbs === undefined ? {} : { breadcrumbs }),
134
+ ...(attributes.breadcrumbsTruncated === true ? { breadcrumbsTruncated: true } : {})
135
+ };
136
+ }
137
+
138
+ function cloneIssueDiagnostics(attributes) {
139
+ const diagnostics = {};
140
+ if (attributes.exception !== undefined) {
141
+ diagnostics.exception = {
142
+ ...attributes.exception,
143
+ ...(attributes.exception.mechanism === undefined
144
+ ? {}
145
+ : { mechanism: { ...attributes.exception.mechanism } })
146
+ };
147
+ }
148
+ if (Array.isArray(attributes.breadcrumbs)) {
149
+ diagnostics.breadcrumbs = attributes.breadcrumbs.map((breadcrumb) => ({
150
+ ...breadcrumb,
151
+ ...(breadcrumb.data === undefined ? {} : { data: { ...breadcrumb.data } })
152
+ }));
153
+ }
154
+ if (attributes.breadcrumbsTruncated === true) {
155
+ diagnostics.breadcrumbsTruncated = true;
156
+ }
157
+ return diagnostics;
158
+ }
159
+
160
+ function validateBreadcrumbData(data) {
161
+ if (data === undefined) {
162
+ return undefined;
163
+ }
164
+ requireObject("issue breadcrumb data", data);
165
+ const entries = Object.entries(data);
166
+ if (entries.length > MAX_BREADCRUMB_DATA_FIELDS) {
167
+ throw validationError(
168
+ `issue breadcrumb data must contain at most ${MAX_BREADCRUMB_DATA_FIELDS} fields`
169
+ );
170
+ }
171
+ const validated = {};
172
+ for (const [key, value] of entries) {
173
+ if (!DATA_KEY_PATTERN.test(key)) {
174
+ throw validationError(`issue breadcrumb data key must match ${DATA_KEY_PATTERN}`);
175
+ }
176
+ if (typeof value === "string") {
177
+ validated[key] = boundedText(
178
+ `issue breadcrumb data value for ${key}`,
179
+ value,
180
+ MAX_BREADCRUMB_DATA_STRING_LENGTH
181
+ );
182
+ } else if (typeof value === "number") {
183
+ if (!Number.isFinite(value)) {
184
+ throw validationError(`issue breadcrumb data value for ${key} must be finite`);
185
+ }
186
+ validated[key] = value;
187
+ } else if (typeof value === "boolean" || value === null) {
188
+ validated[key] = value;
189
+ } else {
190
+ throw validationError(
191
+ `issue breadcrumb data value for ${key} must be a string, number, boolean, or null`
192
+ );
193
+ }
194
+ }
195
+ return validated;
196
+ }
197
+
198
+ function optionalMachineName(label, value) {
199
+ if (value === undefined) {
200
+ return undefined;
201
+ }
202
+ if (typeof value !== "string" || !MACHINE_NAME_PATTERN.test(value)) {
203
+ throw validationError(`${label} must match ${MACHINE_NAME_PATTERN}`);
204
+ }
205
+ if (Array.from(value).length > MAX_BREADCRUMB_NAME_LENGTH) {
206
+ throw validationError(`${label} must be at most ${MAX_BREADCRUMB_NAME_LENGTH} characters`);
207
+ }
208
+ return value;
209
+ }
210
+
211
+ function optionalBreadcrumbLevel(value) {
212
+ if (value === undefined) {
213
+ return undefined;
214
+ }
215
+ const normalized = typeof value === "string" ? BREADCRUMB_LEVEL_ALIASES.get(value) : undefined;
216
+ if (normalized === undefined) {
217
+ throw validationError(
218
+ `issue breadcrumb level must be one of: ${Array.from(BREADCRUMB_LEVEL_ALIASES.keys()).join(", ")}`
219
+ );
220
+ }
221
+ return normalized;
222
+ }
223
+
224
+ function boundedText(label, value, maxLength, { rejectLocationText = false } = {}) {
225
+ if (typeof value !== "string" || value.trim() === "") {
226
+ throw validationError(`${label} must be non-empty`);
227
+ }
228
+ if (
229
+ Array.from(value).length > maxLength
230
+ || hasControlCharacter(value)
231
+ || (rejectLocationText && /[?#]/u.test(value))
232
+ ) {
233
+ throw validationError(`${label} is invalid or exceeds ${maxLength} characters`);
234
+ }
235
+ return value;
236
+ }
237
+
238
+ function requireObject(label, value) {
239
+ if (!value || Array.isArray(value) || typeof value !== "object") {
240
+ throw validationError(`${label} must be an object`);
241
+ }
242
+ }
243
+
244
+ function rejectUnknownKeys(label, value, allowed) {
245
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
246
+ if (unknown.length > 0) {
247
+ throw validationError(`${label} has unsupported fields: ${unknown.sort().join(", ")}`);
248
+ }
249
+ }
250
+
251
+ function hasControlCharacter(value) {
252
+ return Array.from(value).some((character) => {
253
+ const code = character.codePointAt(0);
254
+ return code !== undefined && (code <= 31 || (code >= 127 && code <= 159));
255
+ });
256
+ }
257
+
258
+ return {
259
+ MAX_ISSUE_BREADCRUMBS,
260
+ cloneIssueDiagnostics,
261
+ createIssueException,
262
+ validateIssueBreadcrumb,
263
+ validateIssueDiagnostics
264
+ };
265
+ }
266
+
267
+ module.exports = { buildIssueDiagnosticsHelpers };
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.6",
3
+ "version": "0.1.8",
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,