@logbrew/sdk 0.1.38 → 0.1.39

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.
Files changed (90) hide show
  1. package/automatic-delivery.cjs +271 -0
  2. package/batch-sender.cjs +164 -0
  3. package/bounded-number.cjs +34 -0
  4. package/browser-breadcrumbs.cjs +346 -136
  5. package/capture-support.cjs +100 -0
  6. package/client-options.cjs +292 -0
  7. package/client-report.cjs +232 -0
  8. package/client.cjs +447 -0
  9. package/console-arguments.cjs +151 -0
  10. package/console-capture.cjs +194 -0
  11. package/core.cjs +30 -2810
  12. package/delivery-health.cjs +133 -0
  13. package/error-causes.cjs +130 -0
  14. package/error-details.cjs +124 -0
  15. package/error-issue.cjs +187 -0
  16. package/event-clone.cjs +55 -0
  17. package/event-metadata.cjs +40 -0
  18. package/event-pipeline.cjs +325 -0
  19. package/event-queue.cjs +306 -0
  20. package/event-store.cjs +237 -0
  21. package/event-validation.cjs +159 -0
  22. package/examples/agent-timeline.cjs +57 -46
  23. package/examples/agent-timeline.mjs +41 -35
  24. package/examples/index.mjs +122 -94
  25. package/examples/readme-example.cjs +36 -27
  26. package/examples/readme-example.mjs +21 -17
  27. package/examples/real-user-smoke.cjs +41 -32
  28. package/examples/real-user-smoke.mjs +26 -22
  29. package/exception-chain.cjs +205 -0
  30. package/index.d.ts +144 -1076
  31. package/index.js +1 -1
  32. package/issue-exception.cjs +13 -8
  33. package/metric-validation.cjs +95 -0
  34. package/network-milestone.cjs +155 -0
  35. package/null-value.cjs +4 -0
  36. package/opentelemetry-bridge.cjs +330 -0
  37. package/opentelemetry-context.cjs +245 -0
  38. package/opentelemetry-exceptions.cjs +69 -0
  39. package/opentelemetry-keys.cjs +183 -0
  40. package/opentelemetry-span.cjs +286 -0
  41. package/opentelemetry-summary.cjs +213 -0
  42. package/opentelemetry-time.cjs +131 -0
  43. package/opentelemetry.cjs +99 -1068
  44. package/package.json +55 -1
  45. package/pino-destination.cjs +108 -0
  46. package/pino-record.cjs +237 -0
  47. package/product-action.cjs +163 -0
  48. package/provided-breadcrumbs.cjs +117 -0
  49. package/random-hex.cjs +29 -0
  50. package/react-native.js +1 -1
  51. package/recording-transport.cjs +68 -0
  52. package/release-artifacts-build.cjs +263 -84
  53. package/release-artifacts-common.js +347 -66
  54. package/release-artifacts-debug-id.js +178 -0
  55. package/release-artifacts-inspect.js +213 -0
  56. package/release-artifacts-issue-event.js +99 -0
  57. package/release-artifacts-manifest-artifact.js +107 -0
  58. package/release-artifacts-manifest.js +77 -0
  59. package/release-artifacts-mappings.js +265 -0
  60. package/release-artifacts-prepare.js +297 -0
  61. package/release-artifacts-source-context.js +210 -0
  62. package/release-artifacts-symbolication.js +272 -421
  63. package/release-artifacts-upload-files.js +122 -0
  64. package/release-artifacts-upload-request.js +246 -0
  65. package/release-artifacts-upload.js +155 -300
  66. package/release-artifacts-urls.js +49 -0
  67. package/release-artifacts.js +189 -662
  68. package/retry-delay.cjs +32 -0
  69. package/span-validation.cjs +191 -0
  70. package/stored-events.cjs +243 -0
  71. package/types/client.d.ts +67 -0
  72. package/types/delivery.d.ts +125 -0
  73. package/types/issues.d.ts +249 -0
  74. package/types/logging.d.ts +175 -0
  75. package/types/opentelemetry.d.ts +205 -0
  76. package/types/product.d.ts +128 -0
  77. package/types/recording-transport.d.ts +15 -0
  78. package/types/sdk-error.d.ts +9 -0
  79. package/types/telemetry.d.ts +171 -0
  80. package/types/trace-context.d.ts +110 -0
  81. package/types/transport-error.d.ts +10 -0
  82. package/utf8.cjs +37 -0
  83. package/validation.cjs +49 -1
  84. package/vite-release-artifacts-config.cjs +120 -0
  85. package/vite-release-artifacts.cjs +199 -99
  86. package/vite-release-artifacts.d.cts +65 -40
  87. package/vite-release-artifacts.d.ts +65 -40
  88. package/vite-release-artifacts.js +2 -2
  89. package/winston-info.cjs +205 -0
  90. package/winston.cjs +189 -289
@@ -0,0 +1,122 @@
1
+ import {
2
+ byteSize,
3
+ isFile,
4
+ isJsonObject,
5
+ mapInOrder,
6
+ normalizeProjectId,
7
+ pathExists,
8
+ safeResolve,
9
+ sha256File
10
+ } from "./release-artifacts-common.js";
11
+ import path from "node:path";
12
+
13
+ /**
14
+ * @import { JsonObject } from "./release-artifacts-common.js"
15
+ * @import { UploadFile } from "./release-artifacts-upload-request.js"
16
+ * @typedef {{ readonly artifacts: readonly unknown[], readonly manifest: JsonObject }} ReadyManifest
17
+ */
18
+
19
+ const REQUIRED_FILE_FIELDS = ["path", "artifactSha256", "byteSize"];
20
+ const NO_ARTIFACTS = 0;
21
+
22
+ /**
23
+ * @param {JsonObject} manifest
24
+ * @param {boolean} requireProjectId
25
+ * @returns {ReadyManifest}
26
+ */
27
+ function requireReadyJavaScriptManifest(manifest, requireProjectId) {
28
+ const { artifacts, validation } = manifest;
29
+ if (manifest["artifactType"] !== "javascript_source_map_manifest") {
30
+ throw new Error("only javascript_source_map_manifest uploads are supported by this verifier");
31
+ }
32
+ if ((isJsonObject(validation) ? validation["status"] : undefined) !== "ready") {
33
+ throw new Error("manifest validation status must be ready before upload");
34
+ }
35
+ if (!Array.isArray(artifacts) || artifacts.length === NO_ARTIFACTS) {
36
+ throw new Error("manifest must contain at least one JavaScript release artifact");
37
+ }
38
+ normalizeProjectId(
39
+ manifest["projectId"],
40
+ requireProjectId
41
+ ? "hosted release artifact uploads require manifest projectId as a UUID"
42
+ : "manifest projectId must be a UUID when provided",
43
+ requireProjectId
44
+ );
45
+ return { artifacts, manifest };
46
+ }
47
+
48
+ /**
49
+ * @param {JsonObject} artifact
50
+ * @param {string} section
51
+ * @returns {JsonObject}
52
+ */
53
+ function requireFileMetadata(artifact, section) {
54
+ const payload = artifact[section];
55
+ if (!isJsonObject(payload)) {
56
+ throw new Error(`artifact is missing ${section}`);
57
+ }
58
+ const missingField = REQUIRED_FILE_FIELDS.find((field) => payload[field] === undefined || payload[field] === "");
59
+ if (missingField !== undefined) {
60
+ throw new Error(`${section} is missing ${missingField}`);
61
+ }
62
+ return payload;
63
+ }
64
+
65
+ /**
66
+ * @param {string | undefined} filePath
67
+ * @param {JsonObject} payload
68
+ * @param {string} section
69
+ * @returns {Promise<string>}
70
+ */
71
+ async function requireUnchangedFile(filePath, payload, section) {
72
+ const recordedPath = String(payload["path"]);
73
+ if (filePath === undefined || !(await pathExists(filePath)) || !(await isFile(filePath))) {
74
+ throw new Error(`${section} file is missing: ${recordedPath}`);
75
+ }
76
+ const size = await byteSize(filePath);
77
+ if (size !== Number(payload["byteSize"])) {
78
+ throw new Error(`${section} byte size changed after manifest creation: ${recordedPath}`);
79
+ }
80
+ const digest = await sha256File(filePath);
81
+ if (digest !== String(payload["artifactSha256"])) {
82
+ throw new Error(`${section} sha256 changed after manifest creation: ${recordedPath}`);
83
+ }
84
+ return filePath;
85
+ }
86
+
87
+ /**
88
+ * @param {JsonObject} artifact
89
+ * @param {string} buildDir
90
+ * @param {string} section
91
+ * @returns {Promise<string>}
92
+ */
93
+ async function requireArtifactFile(artifact, buildDir, section) {
94
+ const payload = requireFileMetadata(artifact, section);
95
+ const filePath = await safeResolve(path.join(buildDir, String(payload["path"])), buildDir);
96
+ return requireUnchangedFile(filePath, payload, section);
97
+ }
98
+
99
+ /**
100
+ * @param {JsonObject} manifest
101
+ * @param {string} buildDir
102
+ * @param {boolean} requireProjectId
103
+ * @returns {Promise<{ readonly artifactCount: number, readonly files: readonly UploadFile[] }>}
104
+ */
105
+ async function collectUploadFiles(manifest, buildDir, requireProjectId) {
106
+ const { artifacts } = requireReadyJavaScriptManifest(manifest, requireProjectId);
107
+ const files = await mapInOrder([...artifacts.keys()], async (index) => {
108
+ const artifact = artifacts[index];
109
+ if (!isJsonObject(artifact)) {
110
+ throw new Error("artifact entries must be JSON objects");
111
+ }
112
+ /** @type {UploadFile[]} */
113
+ const pair = [
114
+ [`minified_source_${String(index)}`, await requireArtifactFile(artifact, buildDir, "minifiedSource")],
115
+ [`source_map_${String(index)}`, await requireArtifactFile(artifact, buildDir, "sourceMap")]
116
+ ];
117
+ return pair;
118
+ });
119
+ return { artifactCount: artifacts.length, files: files.flat() };
120
+ }
121
+
122
+ export { collectUploadFiles };
@@ -0,0 +1,246 @@
1
+ import { errorMessage, mapInOrder, stableJson } from "./release-artifacts-common.js";
2
+ import { Buffer } from "node:buffer";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import net from "node:net";
5
+ import path from "node:path";
6
+ import { randomUUID } from "node:crypto";
7
+ import { readFile } from "node:fs/promises";
8
+
9
+ /**
10
+ * @import { JsonObject } from "./release-artifacts-common.js"
11
+ * @typedef {"auth_failed" | "retryable_error" | "upload_failed" | "uploaded" | "validation_failed"} UploadResult
12
+ * @typedef {{ readonly attempt: number, readonly httpStatus?: number, readonly result: UploadResult }} UploadAttempt
13
+ * @typedef {readonly [name: string, filePath: string]} UploadFile
14
+ * @typedef {{
15
+ * readonly body: Buffer,
16
+ * readonly boundary: string,
17
+ * readonly endpoint: string,
18
+ * readonly maxRetries: number,
19
+ * readonly retryDelaySeconds: number,
20
+ * readonly timeoutSeconds: number,
21
+ * readonly token: string
22
+ * }} UploadRequest
23
+ */
24
+
25
+ const HOSTED_UPLOAD_ENDPOINT = "https://api.logbrew.co/api/release-artifacts";
26
+ const HTTP_BAD_REQUEST = 400;
27
+ const HTTP_UNAUTHORIZED = 401;
28
+ const HTTP_FORBIDDEN = 403;
29
+ const HTTP_REQUEST_TIMEOUT = 408;
30
+ const HTTP_PAYLOAD_TOO_LARGE = 413;
31
+ const HTTP_TOO_MANY_REQUESTS = 429;
32
+ const AUTH_FAILED_STATUSES = new Set([HTTP_UNAUTHORIZED, HTTP_FORBIDDEN]);
33
+ const NON_RETRYABLE_UPLOAD_STATUSES = new Set([HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_PAYLOAD_TOO_LARGE]);
34
+ const RETRYABLE_UPLOAD_STATUSES = new Set([HTTP_REQUEST_TIMEOUT, HTTP_TOO_MANY_REQUESTS]);
35
+ const SUCCESS_STATUS_MIN = 200;
36
+ const SUCCESS_STATUS_LIMIT = 300;
37
+ const SERVER_ERROR_STATUS_MIN = 500;
38
+ const MILLISECONDS_PER_SECOND = 1000;
39
+ const FIRST_ATTEMPT = 1;
40
+ const NO_RETRY_DELAY = 0;
41
+ const NOT_AN_IP = 0;
42
+ const SCRIPT_VERSION = "0.1.0";
43
+
44
+ /**
45
+ * @param {string} endpoint
46
+ * @returns {string}
47
+ */
48
+ function endpointWithoutQuery(endpoint) {
49
+ const { host, pathname, protocol } = new URL(endpoint);
50
+ return `${protocol}//${host}${pathname === "" ? "/" : pathname}`;
51
+ }
52
+
53
+ /**
54
+ * @param {string} endpoint
55
+ * @returns {URL}
56
+ */
57
+ function parseEndpoint(endpoint) {
58
+ try {
59
+ const parsed = new URL(endpoint);
60
+ if (["http:", "https:"].includes(parsed.protocol)) {
61
+ return parsed;
62
+ }
63
+ } catch (error) {
64
+ throw new Error(`upload endpoint is not a valid URL: ${errorMessage(error)}`, { cause: error });
65
+ }
66
+ throw new Error("release artifact upload proof endpoint must use http or https");
67
+ }
68
+
69
+ /**
70
+ * @param {string} endpointHostname
71
+ * @returns {boolean}
72
+ */
73
+ function isLoopbackEndpoint(endpointHostname) {
74
+ const hostname = endpointHostname.toLowerCase();
75
+ return (
76
+ hostname === "localhost" ||
77
+ hostname === "[::1]" ||
78
+ hostname === "::1" ||
79
+ (net.isIP(hostname) !== NOT_AN_IP && (hostname.startsWith("127.") || hostname === "::1"))
80
+ );
81
+ }
82
+
83
+ /**
84
+ * @param {string} endpoint
85
+ * @returns {boolean}
86
+ */
87
+ function isLoopbackUploadEndpoint(endpoint) {
88
+ return isLoopbackEndpoint(parseEndpoint(endpoint).hostname);
89
+ }
90
+
91
+ /**
92
+ * @param {string} endpoint
93
+ * @param {boolean} allowHosted
94
+ * @returns {boolean}
95
+ */
96
+ function requireUploadEndpoint(endpoint, allowHosted) {
97
+ if (isLoopbackUploadEndpoint(endpoint)) {
98
+ return true;
99
+ }
100
+ if (!allowHosted) {
101
+ throw new Error("release artifact hosted upload requires explicit --allow-hosted; use loopback endpoints for local proof");
102
+ }
103
+ if (endpoint !== HOSTED_UPLOAD_ENDPOINT) {
104
+ throw new Error(`hosted release artifact uploads must use ${HOSTED_UPLOAD_ENDPOINT}`);
105
+ }
106
+ return false;
107
+ }
108
+
109
+ /**
110
+ * @param {string} value
111
+ * @returns {string}
112
+ */
113
+ function quoteMultipartValue(value) {
114
+ return value.replaceAll("\\", String.raw`\\`).replaceAll("\"", String.raw`\"`);
115
+ }
116
+
117
+ /**
118
+ * @param {string} boundary
119
+ * @param {{ readonly contentType: string, readonly filename: string, readonly name: string }} part
120
+ * @param {Buffer} bytes
121
+ * @returns {Buffer[]}
122
+ */
123
+ function multipartPart(boundary, { contentType, filename, name }, bytes) {
124
+ return [
125
+ Buffer.from(`--${boundary}\r\n`, "ascii"),
126
+ Buffer.from(
127
+ `Content-Disposition: form-data; name="${quoteMultipartValue(name)}"; filename="${quoteMultipartValue(filename)}"\r\n`,
128
+ "utf8"
129
+ ),
130
+ Buffer.from(`Content-Type: ${contentType}\r\n\r\n`, "ascii"),
131
+ bytes,
132
+ Buffer.from("\r\n", "ascii")
133
+ ];
134
+ }
135
+
136
+ /**
137
+ * @param {JsonObject} manifest
138
+ * @param {readonly UploadFile[]} files
139
+ * @returns {Promise<{ readonly body: Buffer, readonly boundary: string }>}
140
+ */
141
+ async function encodeMultipart(manifest, files) {
142
+ const boundary = `logbrew-${randomUUID().replaceAll("-", "")}`;
143
+ const manifestPart = multipartPart(
144
+ boundary,
145
+ { contentType: "application/json", filename: "manifest.json", name: "manifest" },
146
+ Buffer.from(stableJson(manifest), "utf8")
147
+ );
148
+ const fileParts = await mapInOrder(files, async ([name, filePath]) =>
149
+ multipartPart(boundary, { contentType: "application/octet-stream", filename: path.basename(filePath), name }, await readFile(filePath)));
150
+ return {
151
+ body: Buffer.concat([...manifestPart, ...fileParts.flat(), Buffer.from(`--${boundary}--\r\n`, "ascii")]),
152
+ boundary
153
+ };
154
+ }
155
+
156
+ /**
157
+ * @param {number} status
158
+ * @returns {UploadResult}
159
+ */
160
+ function classifyUploadStatus(status) {
161
+ if (status >= SUCCESS_STATUS_MIN && status < SUCCESS_STATUS_LIMIT) {
162
+ return "uploaded";
163
+ }
164
+ if (AUTH_FAILED_STATUSES.has(status)) {
165
+ return "auth_failed";
166
+ }
167
+ if (NON_RETRYABLE_UPLOAD_STATUSES.has(status)) {
168
+ return "validation_failed";
169
+ }
170
+ if (RETRYABLE_UPLOAD_STATUSES.has(status) || status >= SERVER_ERROR_STATUS_MIN) {
171
+ return "retryable_error";
172
+ }
173
+ return "upload_failed";
174
+ }
175
+
176
+ /**
177
+ * @param {UploadRequest} request
178
+ * @returns {Promise<number>}
179
+ */
180
+ async function postMultipart({ body, boundary, endpoint, timeoutSeconds, token }) {
181
+ const controller = new AbortController();
182
+ const timeout = setTimeout(() => {
183
+ controller.abort();
184
+ }, timeoutSeconds * MILLISECONDS_PER_SECOND);
185
+ try {
186
+ const response = await fetch(endpoint, {
187
+ body,
188
+ headers: {
189
+ Authorization: `Bearer ${token}`,
190
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
191
+ "User-Agent": `logbrew-release-artifact-verifier/${SCRIPT_VERSION}`
192
+ },
193
+ method: "POST",
194
+ signal: controller.signal
195
+ });
196
+ await response.arrayBuffer();
197
+ return response.status;
198
+ } finally {
199
+ clearTimeout(timeout);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * @param {UploadRequest} request
205
+ * @param {number} attempt
206
+ * @returns {Promise<UploadAttempt>}
207
+ */
208
+ async function attemptUpload(request, attempt) {
209
+ try {
210
+ const httpStatus = await postMultipart(request);
211
+ return { attempt, httpStatus, result: classifyUploadStatus(httpStatus) };
212
+ } catch {
213
+ return { attempt, result: "retryable_error" };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * @param {UploadRequest} request
219
+ * @returns {Promise<{ readonly attempts: readonly UploadAttempt[], readonly retryCount: number, readonly status: UploadResult }>}
220
+ */
221
+ async function uploadWithRetries(request) {
222
+ /** @type {UploadAttempt[]} */
223
+ const attempts = [];
224
+ /**
225
+ * @param {number} attempt
226
+ * @returns {Promise<void>}
227
+ */
228
+ async function attemptFrom(attempt) {
229
+ const outcome = await attemptUpload(request, attempt);
230
+ attempts.push(outcome);
231
+ if (outcome.result === "retryable_error" && attempt <= request.maxRetries) {
232
+ if (request.retryDelaySeconds > NO_RETRY_DELAY) {
233
+ await delay(request.retryDelaySeconds * MILLISECONDS_PER_SECOND);
234
+ }
235
+ await attemptFrom(attempt + FIRST_ATTEMPT);
236
+ }
237
+ }
238
+ await attemptFrom(FIRST_ATTEMPT);
239
+ return {
240
+ attempts,
241
+ retryCount: Math.max(NO_RETRY_DELAY, attempts.length - FIRST_ATTEMPT),
242
+ status: attempts.at(-FIRST_ATTEMPT)?.result ?? "upload_failed"
243
+ };
244
+ }
245
+
246
+ export { encodeMultipart, endpointWithoutQuery, isLoopbackUploadEndpoint, requireUploadEndpoint, uploadWithRetries };