@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.
- package/automatic-delivery.cjs +271 -0
- package/batch-sender.cjs +164 -0
- package/bounded-number.cjs +34 -0
- package/browser-breadcrumbs.cjs +346 -136
- package/capture-support.cjs +100 -0
- package/client-options.cjs +292 -0
- package/client-report.cjs +232 -0
- package/client.cjs +447 -0
- package/console-arguments.cjs +151 -0
- package/console-capture.cjs +194 -0
- package/core.cjs +30 -2810
- package/delivery-health.cjs +133 -0
- package/error-causes.cjs +130 -0
- package/error-details.cjs +124 -0
- package/error-issue.cjs +187 -0
- package/event-clone.cjs +55 -0
- package/event-metadata.cjs +40 -0
- package/event-pipeline.cjs +325 -0
- package/event-queue.cjs +306 -0
- package/event-store.cjs +237 -0
- package/event-validation.cjs +159 -0
- package/examples/agent-timeline.cjs +57 -46
- package/examples/agent-timeline.mjs +41 -35
- package/examples/index.mjs +122 -94
- package/examples/readme-example.cjs +36 -27
- package/examples/readme-example.mjs +21 -17
- package/examples/real-user-smoke.cjs +41 -32
- package/examples/real-user-smoke.mjs +26 -22
- package/exception-chain.cjs +205 -0
- package/index.d.ts +144 -1076
- package/index.js +1 -1
- package/issue-exception.cjs +13 -8
- package/metric-validation.cjs +95 -0
- package/network-milestone.cjs +155 -0
- package/null-value.cjs +4 -0
- package/opentelemetry-bridge.cjs +330 -0
- package/opentelemetry-context.cjs +245 -0
- package/opentelemetry-exceptions.cjs +69 -0
- package/opentelemetry-keys.cjs +183 -0
- package/opentelemetry-span.cjs +286 -0
- package/opentelemetry-summary.cjs +213 -0
- package/opentelemetry-time.cjs +131 -0
- package/opentelemetry.cjs +99 -1068
- package/package.json +55 -1
- package/pino-destination.cjs +108 -0
- package/pino-record.cjs +237 -0
- package/product-action.cjs +163 -0
- package/provided-breadcrumbs.cjs +117 -0
- package/random-hex.cjs +29 -0
- package/react-native.js +1 -1
- package/recording-transport.cjs +68 -0
- package/release-artifacts-build.cjs +263 -84
- package/release-artifacts-common.js +347 -66
- package/release-artifacts-debug-id.js +178 -0
- package/release-artifacts-inspect.js +213 -0
- package/release-artifacts-issue-event.js +99 -0
- package/release-artifacts-manifest-artifact.js +107 -0
- package/release-artifacts-manifest.js +77 -0
- package/release-artifacts-mappings.js +265 -0
- package/release-artifacts-prepare.js +297 -0
- package/release-artifacts-source-context.js +210 -0
- package/release-artifacts-symbolication.js +272 -421
- package/release-artifacts-upload-files.js +122 -0
- package/release-artifacts-upload-request.js +246 -0
- package/release-artifacts-upload.js +155 -300
- package/release-artifacts-urls.js +49 -0
- package/release-artifacts.js +189 -662
- package/retry-delay.cjs +32 -0
- package/span-validation.cjs +191 -0
- package/stored-events.cjs +243 -0
- package/types/client.d.ts +67 -0
- package/types/delivery.d.ts +125 -0
- package/types/issues.d.ts +249 -0
- package/types/logging.d.ts +175 -0
- package/types/opentelemetry.d.ts +205 -0
- package/types/product.d.ts +128 -0
- package/types/recording-transport.d.ts +15 -0
- package/types/sdk-error.d.ts +9 -0
- package/types/telemetry.d.ts +171 -0
- package/types/trace-context.d.ts +110 -0
- package/types/transport-error.d.ts +10 -0
- package/utf8.cjs +37 -0
- package/validation.cjs +49 -1
- package/vite-release-artifacts-config.cjs +120 -0
- package/vite-release-artifacts.cjs +199 -99
- package/vite-release-artifacts.d.cts +65 -40
- package/vite-release-artifacts.d.ts +65 -40
- package/vite-release-artifacts.js +2 -2
- package/winston-info.cjs +205 -0
- package/winston.cjs +189 -289
package/core.cjs
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
+
const { installBrowserBreadcrumbs } = require("./browser-breadcrumbs.cjs");
|
|
2
|
+
const { LogBrewClient } = require("./client.cjs");
|
|
3
|
+
const { installLogBrewConsoleCapture } = require("./console-capture.cjs");
|
|
4
|
+
const { logAttributesFromConsoleArgs, logbrewLevelFromConsoleMethod } = require("./console-arguments.cjs");
|
|
5
|
+
const { createIssueAttributesFromError } = require("./error-issue.cjs");
|
|
6
|
+
const { createNetworkMilestoneAttributes } = require("./network-milestone.cjs");
|
|
1
7
|
const {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} = require("./
|
|
9
|
-
const {
|
|
10
|
-
const {
|
|
11
|
-
compactMetadata,
|
|
12
|
-
isMetadataValue,
|
|
13
|
-
normalizeLogTraceContext,
|
|
14
|
-
traceFromProvider,
|
|
15
|
-
traceMetadataFromLogContext
|
|
16
|
-
} = require("./log-context.cjs");
|
|
17
|
-
const { buildOpenTelemetryHelpers } = require("./opentelemetry.cjs");
|
|
8
|
+
createLogBrewOpenTelemetrySpanExporter,
|
|
9
|
+
createLogBrewOpenTelemetrySpanProcessor,
|
|
10
|
+
logbrewTraceContextFromCurrentOpenTelemetrySpan,
|
|
11
|
+
logbrewTraceContextFromOpenTelemetrySpan,
|
|
12
|
+
logbrewTraceContextFromOpenTelemetrySpanContext,
|
|
13
|
+
spanAttributesFromOpenTelemetryReadableSpan
|
|
14
|
+
} = require("./opentelemetry.cjs");
|
|
15
|
+
const { createLogBrewPinoDestination } = require("./pino-destination.cjs");
|
|
16
|
+
const { logAttributesFromPinoRecord } = require("./pino-record.cjs");
|
|
18
17
|
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} = require("./
|
|
23
|
-
const {
|
|
18
|
+
PRODUCT_ANALYTICS_KINDS,
|
|
19
|
+
PRODUCT_ANALYTICS_SCHEMA_VERSION,
|
|
20
|
+
createProductActionAttributes
|
|
21
|
+
} = require("./product-action.cjs");
|
|
22
|
+
const { RecordingTransport } = require("./recording-transport.cjs");
|
|
23
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
24
|
+
const { spanAttributesFromTraceparent } = require("./span-validation.cjs");
|
|
24
25
|
const { createSupportTicketDraft } = require("./support-ticket.cjs");
|
|
25
|
-
const { TransportError } = require("./transport-error.cjs");
|
|
26
26
|
const {
|
|
27
27
|
createBaggage,
|
|
28
28
|
createTraceContextHeaders,
|
|
@@ -33,2803 +33,18 @@ const {
|
|
|
33
33
|
parseTraceparent,
|
|
34
34
|
parseTracestate
|
|
35
35
|
} = require("./trace-context.cjs");
|
|
36
|
-
const {
|
|
37
|
-
requireAllowedValue,
|
|
38
|
-
requireFiniteNumber,
|
|
39
|
-
requireNonEmpty,
|
|
40
|
-
requireNonNegativeInteger,
|
|
41
|
-
requirePositiveInteger,
|
|
42
|
-
requireSpanId,
|
|
43
|
-
requireTimestamp,
|
|
44
|
-
requireTraceId
|
|
45
|
-
} = require("./validation.cjs");
|
|
46
|
-
|
|
47
|
-
const SEVERITY_ALIASES = new Map([
|
|
48
|
-
["trace", "info"],
|
|
49
|
-
["debug", "info"],
|
|
50
|
-
["info", "info"],
|
|
51
|
-
["warn", "warning"],
|
|
52
|
-
["warning", "warning"],
|
|
53
|
-
["error", "error"],
|
|
54
|
-
["fatal", "critical"],
|
|
55
|
-
["critical", "critical"]
|
|
56
|
-
]);
|
|
57
|
-
const SEVERITY_VALUES = new Set(SEVERITY_ALIASES.keys());
|
|
58
|
-
const EVENT_ADMISSIONS = Object.freeze({
|
|
59
|
-
admitted: Object.freeze({ status: "admitted" }),
|
|
60
|
-
filtered: Object.freeze({ status: "filtered" }),
|
|
61
|
-
unavailable: Object.freeze({ status: "unavailable" }),
|
|
62
|
-
event_too_large: Object.freeze({ status: "dropped", reason: "event_too_large" }),
|
|
63
|
-
queue_overflow: Object.freeze({ status: "dropped", reason: "queue_overflow" }),
|
|
64
|
-
queue_bytes_overflow: Object.freeze({ status: "dropped", reason: "queue_bytes_overflow" })
|
|
65
|
-
});
|
|
66
|
-
const SPAN_STATUSES = new Set(["ok", "error"]);
|
|
67
|
-
const ACTION_STATUSES = new Set(["queued", "running", "success", "failure"]);
|
|
68
|
-
const PRODUCT_ANALYTICS_SCHEMA_VERSION = 1;
|
|
69
|
-
const PRODUCT_ANALYTICS_KINDS = Object.freeze(["page_view", "screen_view", "interaction"]);
|
|
70
|
-
const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
|
|
71
|
-
const METRIC_KINDS = new Set(["counter", "gauge", "histogram"]);
|
|
72
|
-
const MAX_METRIC_DESCRIPTION_LENGTH = 1024;
|
|
73
|
-
const NON_NEGATIVE_METRIC_KINDS = new Set(["counter", "histogram"]);
|
|
74
|
-
const METRIC_TEMPORALITIES_BY_KIND = new Map([
|
|
75
|
-
["counter", new Set(["delta", "cumulative"])],
|
|
76
|
-
["gauge", new Set(["instant"])],
|
|
77
|
-
["histogram", new Set(["delta", "cumulative"])]
|
|
78
|
-
]);
|
|
79
|
-
const CONSOLE_METHODS = new Set(["debug", "info", "log", "warn", "error"]);
|
|
80
|
-
const DEFAULT_CONSOLE_LEVELS = ["debug", "info", "log", "warn", "error"];
|
|
81
|
-
const PINO_HOST_FIELD = ["host", "name"].join("");
|
|
82
|
-
const PINO_RESERVED_FIELDS = new Set(["level", "time", "timestamp", "msg", "message", "err", "error", "pid", PINO_HOST_FIELD, "v"]);
|
|
83
|
-
const PINO_SENSITIVE_CONTEXT_FIELDS = new Set([
|
|
84
|
-
"apikey",
|
|
85
|
-
"authorization",
|
|
86
|
-
"baggage",
|
|
87
|
-
"body",
|
|
88
|
-
"cookie",
|
|
89
|
-
"credentials",
|
|
90
|
-
"cwd",
|
|
91
|
-
"directory",
|
|
92
|
-
"dirname",
|
|
93
|
-
"errorstack",
|
|
94
|
-
"file",
|
|
95
|
-
"filename",
|
|
96
|
-
"filepath",
|
|
97
|
-
"headers",
|
|
98
|
-
"href",
|
|
99
|
-
"key",
|
|
100
|
-
"password",
|
|
101
|
-
"payload",
|
|
102
|
-
"proxyauthorization",
|
|
103
|
-
"query",
|
|
104
|
-
"querystring",
|
|
105
|
-
"requestbody",
|
|
106
|
-
"requestheaders",
|
|
107
|
-
"requesturl",
|
|
108
|
-
"responsebody",
|
|
109
|
-
"responseheaders",
|
|
110
|
-
"responseurl",
|
|
111
|
-
"search",
|
|
112
|
-
"secret",
|
|
113
|
-
"setcookie",
|
|
114
|
-
"stack",
|
|
115
|
-
"token",
|
|
116
|
-
"traceparent",
|
|
117
|
-
"tracestate",
|
|
118
|
-
"uri",
|
|
119
|
-
"url"
|
|
120
|
-
]);
|
|
121
|
-
const DEFAULT_MAX_QUEUE_SIZE = 1000;
|
|
122
|
-
const DEFAULT_MAX_QUEUE_BYTES = 4 * 1024 * 1024;
|
|
123
|
-
const DEFAULT_MAX_BATCH_EVENTS = 100;
|
|
124
|
-
const DEFAULT_MAX_BATCH_BYTES = 256 * 1024;
|
|
125
|
-
// One second matches the OpenTelemetry log processor default, so a lone event becomes readable quickly.
|
|
126
|
-
const DEFAULT_DELIVERY_INTERVAL_MS = 1000;
|
|
127
|
-
const MAX_DELIVERY_INTERVAL_MS = 60 * 1000;
|
|
128
|
-
// A rate-limited batch waits from 1 second to 1 hour, plus up to 10 percent jitter, before it is sent again.
|
|
129
|
-
const MIN_RATE_LIMIT_WAIT_MS = 1000;
|
|
130
|
-
const MAX_RATE_LIMIT_WAIT_MS = 60 * 60 * 1000;
|
|
131
|
-
const DEFAULT_DELIVERY_QUEUE_THRESHOLD = 50;
|
|
132
|
-
const MAX_CLIENT_REPORT_REASONS = 16;
|
|
133
|
-
const MAX_CLIENT_REPORT_COUNT = 1_000_000_000;
|
|
134
|
-
const DELIVERY_HEALTH_SCHEMA_VERSION = 1;
|
|
135
|
-
const EVENT_QUEUE_FACTORY = Symbol.for("@logbrew/sdk.eventQueueFactory");
|
|
136
|
-
// Runtimes without an event store (browser storage) persist unreported drop counts through this seam.
|
|
137
|
-
const CLIENT_REPORT_STORE = Symbol.for("@logbrew/sdk.clientReportStore");
|
|
138
|
-
const CLIENT_REPORT_SAVE_INTERVAL_MS = 1_000;
|
|
139
|
-
const CLIENT_REPORT_ID_PATTERN = /^[A-Za-z0-9_-]{16,64}$/u;
|
|
140
|
-
const CLIENT_REPORT_REASON_PATTERN = /^[a-z][a-z0-9_]{0,63}$/u;
|
|
141
|
-
const CONTEXT_PROVIDER = Symbol.for("@logbrew/sdk.contextProvider");
|
|
142
|
-
// Runtime packages supply request-scoped breadcrumbs through this seam; core merges them into issues.
|
|
143
|
-
const BREADCRUMB_PROVIDER = Symbol.for("@logbrew/sdk.breadcrumbProvider");
|
|
144
|
-
const MAX_ERROR_CAUSES = 5;
|
|
145
|
-
const BUILTIN_ERROR_NAMES = new Set([
|
|
146
|
-
"AggregateError",
|
|
147
|
-
"Error",
|
|
148
|
-
"EvalError",
|
|
149
|
-
"RangeError",
|
|
150
|
-
"ReferenceError",
|
|
151
|
-
"SyntaxError",
|
|
152
|
-
"TypeError",
|
|
153
|
-
"URIError"
|
|
154
|
-
]);
|
|
155
|
-
const MAX_SPAN_EVENTS = 8;
|
|
156
|
-
const MAX_SPAN_LINKS = 8;
|
|
157
|
-
const EVENT_VALIDATORS = new Map([
|
|
158
|
-
["release", validateRelease],
|
|
159
|
-
["environment", validateEnvironment],
|
|
160
|
-
["issue", validateIssue],
|
|
161
|
-
["log", validateLog],
|
|
162
|
-
["span", validateSpan],
|
|
163
|
-
["action", validateAction],
|
|
164
|
-
["metric", validateMetric]
|
|
165
|
-
]);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
class RecordingTransport {
|
|
169
|
-
constructor(scriptedResponses = [{ statusCode: 202 }]) {
|
|
170
|
-
this.scriptedResponses = [...scriptedResponses];
|
|
171
|
-
this.sentBodies = [];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
static alwaysAccept() {
|
|
175
|
-
return new RecordingTransport([{ statusCode: 202 }]);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
lastBody() {
|
|
179
|
-
return this.sentBodies.at(-1) ?? null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async send(apiKey, body) {
|
|
183
|
-
requireNonEmpty("apiKey", apiKey);
|
|
184
|
-
this.sentBodies.push(body);
|
|
185
|
-
|
|
186
|
-
const next = this.scriptedResponses.length > 0
|
|
187
|
-
? this.scriptedResponses.shift()
|
|
188
|
-
: { statusCode: 202 };
|
|
189
|
-
|
|
190
|
-
if (next instanceof Error) {
|
|
191
|
-
throw next;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const retryAfterMs = retryAfterMsOrUndefined(next.retryAfterMs);
|
|
195
|
-
return {
|
|
196
|
-
statusCode: next.statusCode,
|
|
197
|
-
attempts: 1,
|
|
198
|
-
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
199
|
-
...(typeof next.errorCode === "string" ? { errorCode: next.errorCode } : {})
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
class LogBrewClient {
|
|
205
|
-
static #issueCapture = LogBrewClient.prototype.issue;
|
|
206
|
-
|
|
207
|
-
static create({
|
|
208
|
-
apiKey,
|
|
209
|
-
sdkName,
|
|
210
|
-
sdkVersion,
|
|
211
|
-
context,
|
|
212
|
-
[CONTEXT_PROVIDER]: contextProvider,
|
|
213
|
-
[BREADCRUMB_PROVIDER]: breadcrumbProvider,
|
|
214
|
-
maxRetries = 2,
|
|
215
|
-
eventFilter,
|
|
216
|
-
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
|
217
|
-
maxQueueBytes = DEFAULT_MAX_QUEUE_BYTES,
|
|
218
|
-
maxBatchEvents = DEFAULT_MAX_BATCH_EVENTS,
|
|
219
|
-
maxBatchBytes = DEFAULT_MAX_BATCH_BYTES,
|
|
220
|
-
onEventDropped,
|
|
221
|
-
eventStore,
|
|
222
|
-
[EVENT_QUEUE_FACTORY]: eventQueueFactory,
|
|
223
|
-
[CLIENT_REPORT_STORE]: clientReportStore,
|
|
224
|
-
transport,
|
|
225
|
-
automaticDelivery = transport !== undefined,
|
|
226
|
-
deliveryIntervalMs = DEFAULT_DELIVERY_INTERVAL_MS,
|
|
227
|
-
deliveryQueueThreshold = Math.min(DEFAULT_DELIVERY_QUEUE_THRESHOLD, maxQueueSize)
|
|
228
|
-
}) {
|
|
229
|
-
requireNonEmpty("apiKey", apiKey);
|
|
230
|
-
requireNonEmpty("sdkName", sdkName);
|
|
231
|
-
requireNonEmpty("sdkVersion", sdkVersion);
|
|
232
|
-
const clientContext = validateTelemetryContext(context, "client telemetry context");
|
|
233
|
-
if (contextProvider !== undefined && typeof contextProvider !== "function") {
|
|
234
|
-
throw new SdkError("validation_error", "context provider must be a function");
|
|
235
|
-
}
|
|
236
|
-
if (breadcrumbProvider !== undefined && typeof breadcrumbProvider !== "function") {
|
|
237
|
-
throw new SdkError("validation_error", "breadcrumb provider must be a function");
|
|
238
|
-
}
|
|
239
|
-
requireNonNegativeInteger("maxRetries", maxRetries);
|
|
240
|
-
if (eventFilter !== undefined && typeof eventFilter !== "function") {
|
|
241
|
-
throw new SdkError("validation_error", "eventFilter must be a function");
|
|
242
|
-
}
|
|
243
|
-
requirePositiveInteger("maxQueueSize", maxQueueSize);
|
|
244
|
-
requirePositiveInteger("maxQueueBytes", maxQueueBytes);
|
|
245
|
-
requirePositiveInteger("maxBatchEvents", maxBatchEvents);
|
|
246
|
-
requirePositiveInteger("maxBatchBytes", maxBatchBytes);
|
|
247
|
-
if (onEventDropped !== undefined && typeof onEventDropped !== "function") {
|
|
248
|
-
throw new SdkError("validation_error", "onEventDropped must be a function");
|
|
249
|
-
}
|
|
250
|
-
validateEventStore(eventStore);
|
|
251
|
-
if (eventQueueFactory !== undefined && typeof eventQueueFactory !== "function") {
|
|
252
|
-
throw new SdkError("validation_error", "event queue factory must be a function");
|
|
253
|
-
}
|
|
254
|
-
if (eventStore !== undefined && eventQueueFactory !== undefined) {
|
|
255
|
-
throw new SdkError("validation_error", "eventStore and event queue factory are mutually exclusive");
|
|
256
|
-
}
|
|
257
|
-
if (clientReportStore !== undefined && !isClientReportStore(clientReportStore)) {
|
|
258
|
-
throw new SdkError("validation_error", "client report store must provide loadClientReport and saveClientReport");
|
|
259
|
-
}
|
|
260
|
-
validateTransport(transport);
|
|
261
|
-
if (typeof automaticDelivery !== "boolean") {
|
|
262
|
-
throw new SdkError("validation_error", "automaticDelivery must be a boolean");
|
|
263
|
-
}
|
|
264
|
-
if (automaticDelivery && transport === undefined) {
|
|
265
|
-
throw new SdkError("validation_error", "automaticDelivery requires transport");
|
|
266
|
-
}
|
|
267
|
-
requirePositiveInteger("deliveryIntervalMs", deliveryIntervalMs);
|
|
268
|
-
if (deliveryIntervalMs > MAX_DELIVERY_INTERVAL_MS) {
|
|
269
|
-
throw new SdkError("validation_error", `deliveryIntervalMs must be at most ${MAX_DELIVERY_INTERVAL_MS}`);
|
|
270
|
-
}
|
|
271
|
-
requirePositiveInteger("deliveryQueueThreshold", deliveryQueueThreshold);
|
|
272
|
-
if (deliveryQueueThreshold > maxQueueSize) {
|
|
273
|
-
throw new SdkError("validation_error", "deliveryQueueThreshold must not exceed maxQueueSize");
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return new LogBrewClient({
|
|
277
|
-
apiKey,
|
|
278
|
-
automaticDelivery,
|
|
279
|
-
deliveryIntervalMs,
|
|
280
|
-
deliveryQueueThreshold,
|
|
281
|
-
eventStore,
|
|
282
|
-
eventQueueFactory,
|
|
283
|
-
clientReportStore,
|
|
284
|
-
eventFilter,
|
|
285
|
-
maxBatchBytes,
|
|
286
|
-
maxBatchEvents,
|
|
287
|
-
maxQueueBytes,
|
|
288
|
-
maxQueueSize,
|
|
289
|
-
onEventDropped,
|
|
290
|
-
context: clientContext,
|
|
291
|
-
contextProvider,
|
|
292
|
-
breadcrumbProvider,
|
|
293
|
-
sdk: {
|
|
294
|
-
name: sdkName,
|
|
295
|
-
language: "javascript",
|
|
296
|
-
version: sdkVersion
|
|
297
|
-
},
|
|
298
|
-
maxRetries,
|
|
299
|
-
transport
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
constructor({
|
|
304
|
-
apiKey,
|
|
305
|
-
sdk,
|
|
306
|
-
maxRetries,
|
|
307
|
-
eventFilter,
|
|
308
|
-
maxBatchBytes,
|
|
309
|
-
maxBatchEvents,
|
|
310
|
-
maxQueueBytes,
|
|
311
|
-
maxQueueSize,
|
|
312
|
-
onEventDropped,
|
|
313
|
-
context,
|
|
314
|
-
contextProvider,
|
|
315
|
-
breadcrumbProvider,
|
|
316
|
-
eventStore,
|
|
317
|
-
eventQueueFactory,
|
|
318
|
-
clientReportStore,
|
|
319
|
-
transport,
|
|
320
|
-
automaticDelivery,
|
|
321
|
-
deliveryIntervalMs,
|
|
322
|
-
deliveryQueueThreshold
|
|
323
|
-
}) {
|
|
324
|
-
this.apiKey = apiKey;
|
|
325
|
-
this.automaticDelivery = automaticDelivery;
|
|
326
|
-
this.deliveryIntervalMs = deliveryIntervalMs;
|
|
327
|
-
this.deliveryQueueThreshold = deliveryQueueThreshold;
|
|
328
|
-
this.eventFilter = eventFilter;
|
|
329
|
-
this.maxBatchBytes = maxBatchBytes;
|
|
330
|
-
this.maxBatchEvents = maxBatchEvents;
|
|
331
|
-
this.maxQueueBytes = maxQueueBytes;
|
|
332
|
-
this.maxQueueSize = maxQueueSize;
|
|
333
|
-
this.onEventDropped = onEventDropped;
|
|
334
|
-
this.context = cloneTelemetryContext(context);
|
|
335
|
-
this.contextProvider = contextProvider;
|
|
336
|
-
this.breadcrumbProvider = breadcrumbProvider;
|
|
337
|
-
this.transport = transport;
|
|
338
|
-
this.sdk = sdk;
|
|
339
|
-
this.maxRetries = maxRetries;
|
|
340
|
-
this.batchPrefix = `{"sdk":${JSON.stringify(this.sdk)},"events":[`;
|
|
341
|
-
this.batchPrefixBytes = utf8ByteLength(this.batchPrefix);
|
|
342
|
-
this.batchSuffix = "]}";
|
|
343
|
-
this.batchSuffixBytes = utf8ByteLength(this.batchSuffix);
|
|
344
|
-
const ownsEventStore = eventQueueFactory !== undefined;
|
|
345
|
-
const resolvedEventStore = ownsEventStore
|
|
346
|
-
? createEventStoreFromQueueFactory(eventQueueFactory, {
|
|
347
|
-
batchPrefixBytes: this.batchPrefixBytes,
|
|
348
|
-
batchSuffixBytes: this.batchSuffixBytes,
|
|
349
|
-
maxBatchBytes,
|
|
350
|
-
maxQueueBytes,
|
|
351
|
-
maxQueueSize
|
|
352
|
-
})
|
|
353
|
-
: eventStore;
|
|
354
|
-
this.eventStore = resolvedEventStore;
|
|
355
|
-
let recovered;
|
|
356
|
-
try {
|
|
357
|
-
recovered = loadStoredEvents(this);
|
|
358
|
-
} catch (error) {
|
|
359
|
-
if (ownsEventStore) {
|
|
360
|
-
try {
|
|
361
|
-
requireSynchronousStoreResult("close", resolvedEventStore.close());
|
|
362
|
-
} catch {
|
|
363
|
-
// Preserve the queue construction or recovery failure.
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
throw error;
|
|
367
|
-
}
|
|
368
|
-
this.events = recovered.events;
|
|
369
|
-
this.serializedEvents = recovered.serializedEvents;
|
|
370
|
-
this.serializedEventBytes = recovered.serializedEventBytes;
|
|
371
|
-
this.queuedEventBytes = recovered.queuedEventBytes;
|
|
372
|
-
this.operationTail = Promise.resolve();
|
|
373
|
-
this.pendingOperations = 0;
|
|
374
|
-
this.closing = false;
|
|
375
|
-
this.closed = false;
|
|
376
|
-
this.droppedEventCount = 0;
|
|
377
|
-
this.droppedEventsByReason = {
|
|
378
|
-
event_too_large: 0,
|
|
379
|
-
evicted_for_fatal: 0,
|
|
380
|
-
queue_bytes_overflow: 0,
|
|
381
|
-
queue_overflow: 0
|
|
382
|
-
};
|
|
383
|
-
// A persistent store keeps counts not yet acknowledged by a 2xx across restarts.
|
|
384
|
-
this.clientReportStore = clientReportStore
|
|
385
|
-
?? (isClientReportStore(resolvedEventStore) ? resolvedEventStore : undefined);
|
|
386
|
-
this.clientReportSavedAtMs = 0;
|
|
387
|
-
const restoredReport = loadClientReport(this.clientReportStore);
|
|
388
|
-
this.unreportedDropsByReason = restoredReport.unreported;
|
|
389
|
-
// Drop counts frozen under one report ID until a 2xx acknowledges a body that carried them.
|
|
390
|
-
this.pendingClientReport = restoredReport.pending;
|
|
391
|
-
this.lastDropReason = "none";
|
|
392
|
-
this.storage = resolvedEventStore ? "persistent" : "memory";
|
|
393
|
-
this.hydratedEventCount = recovered.events.length;
|
|
394
|
-
this.hydratedEventBytes = recovered.queuedEventBytes;
|
|
395
|
-
this.deliveryTimer = undefined;
|
|
396
|
-
this.automaticFlushActive = false;
|
|
397
|
-
this.automaticFlushPending = false;
|
|
398
|
-
this.deliveryInFlight = false;
|
|
399
|
-
this.lastDeliveryOutcome = "idle";
|
|
400
|
-
this.automaticPauseReason = "none";
|
|
401
|
-
this.consecutiveDeliveryFailures = 0;
|
|
402
|
-
this.retryDelayMs = 0;
|
|
403
|
-
this.successfulFlushCount = 0;
|
|
404
|
-
this.failedFlushCount = 0;
|
|
405
|
-
this.deliveryAttemptCount = 0;
|
|
406
|
-
this.acceptedBatchCount = 0;
|
|
407
|
-
this.acceptedEventCount = 0;
|
|
408
|
-
this.lastStatusClass = "none";
|
|
409
|
-
this.lastAttemptAtUnixMs = 0;
|
|
410
|
-
this.lastAcceptedAtUnixMs = 0;
|
|
411
|
-
this.lastDroppedAtUnixMs = 0;
|
|
412
|
-
this.unacknowledgedBatch = undefined;
|
|
413
|
-
this.issueBreadcrumbs = [];
|
|
414
|
-
this.issueBreadcrumbsTruncated = false;
|
|
415
|
-
this.#scheduleAutomaticDelivery();
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
pendingEvents() {
|
|
419
|
-
return this.events.length;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
pendingBytes() {
|
|
423
|
-
return this.queuedEventBytes;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
droppedEvents() {
|
|
427
|
-
return this.droppedEventCount;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
deliveryHealth() {
|
|
431
|
-
const droppedByReason = Object.freeze({ ...this.droppedEventsByReason });
|
|
432
|
-
return Object.freeze({
|
|
433
|
-
schemaVersion: DELIVERY_HEALTH_SCHEMA_VERSION,
|
|
434
|
-
automaticDelivery: this.automaticDelivery,
|
|
435
|
-
lifecycle: this.closed ? "closed" : this.closing ? "shutting_down" : "active",
|
|
436
|
-
deliveryState: this.#deliveryState(),
|
|
437
|
-
storage: this.storage,
|
|
438
|
-
queueEvents: this.events.length,
|
|
439
|
-
queueBytes: this.queuedEventBytes,
|
|
440
|
-
hydratedEvents: this.hydratedEventCount,
|
|
441
|
-
hydratedBytes: this.hydratedEventBytes,
|
|
442
|
-
droppedEvents: this.droppedEventCount,
|
|
443
|
-
droppedByReason,
|
|
444
|
-
lastDropReason: this.lastDropReason,
|
|
445
|
-
scheduled: this.deliveryTimer !== undefined,
|
|
446
|
-
inFlight: this.deliveryInFlight,
|
|
447
|
-
coalesced: this.automaticFlushPending,
|
|
448
|
-
pendingOperations: this.pendingOperations,
|
|
449
|
-
lastOutcome: this.lastDeliveryOutcome,
|
|
450
|
-
lastStatusClass: this.lastStatusClass,
|
|
451
|
-
pausedReason: this.automaticPauseReason,
|
|
452
|
-
consecutiveFailures: this.consecutiveDeliveryFailures,
|
|
453
|
-
retryDelayMs: this.retryDelayMs,
|
|
454
|
-
flushes: this.successfulFlushCount,
|
|
455
|
-
failures: this.failedFlushCount,
|
|
456
|
-
attempts: this.deliveryAttemptCount,
|
|
457
|
-
batches: this.acceptedBatchCount,
|
|
458
|
-
acceptedEvents: this.acceptedEventCount,
|
|
459
|
-
lastAttemptAtUnixMs: this.lastAttemptAtUnixMs,
|
|
460
|
-
lastAcceptedAtUnixMs: this.lastAcceptedAtUnixMs,
|
|
461
|
-
lastDroppedAtUnixMs: this.lastDroppedAtUnixMs
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
#deliveryState() {
|
|
466
|
-
if (this.deliveryInFlight) {
|
|
467
|
-
return "in_flight";
|
|
468
|
-
}
|
|
469
|
-
if (this.retryDelayMs > 0) {
|
|
470
|
-
return "retrying";
|
|
471
|
-
}
|
|
472
|
-
if (this.automaticPauseReason !== "none") {
|
|
473
|
-
return "paused";
|
|
474
|
-
}
|
|
475
|
-
if (this.events.length > 0) {
|
|
476
|
-
return this.deliveryTimer !== undefined ? "scheduled" : "queued";
|
|
477
|
-
}
|
|
478
|
-
if (this.lastDeliveryOutcome === "accepted") {
|
|
479
|
-
return "accepted";
|
|
480
|
-
}
|
|
481
|
-
if (this.lastDeliveryOutcome === "failed") {
|
|
482
|
-
return "failed";
|
|
483
|
-
}
|
|
484
|
-
if (this.droppedEventCount > 0) {
|
|
485
|
-
return "dropped";
|
|
486
|
-
}
|
|
487
|
-
return "idle";
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
previewJson() {
|
|
491
|
-
return JSON.stringify({ sdk: this.sdk, events: this.events }, null, 2);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
purgePendingEvents() {
|
|
495
|
-
if (this.closed) {
|
|
496
|
-
throw new SdkError("shutdown_error", "client is already shut down");
|
|
497
|
-
}
|
|
498
|
-
if (this.closing) {
|
|
499
|
-
throw new SdkError("shutdown_error", "client is shutting down");
|
|
500
|
-
}
|
|
501
|
-
if (this.pendingOperations > 0 || this.automaticFlushActive) {
|
|
502
|
-
throw new SdkError("persistence_error", "cannot purge while a delivery operation is active");
|
|
503
|
-
}
|
|
504
|
-
const purgedEvents = this.events.length;
|
|
505
|
-
if (this.eventStore) {
|
|
506
|
-
requireSynchronousStoreResult("purge", this.eventStore.purge());
|
|
507
|
-
}
|
|
508
|
-
this.#clearDeliveryTimer();
|
|
509
|
-
this.automaticFlushPending = false;
|
|
510
|
-
this.unacknowledgedBatch = undefined;
|
|
511
|
-
this.events.splice(0, this.events.length);
|
|
512
|
-
this.serializedEvents.splice(0, this.serializedEvents.length);
|
|
513
|
-
this.serializedEventBytes.splice(0, this.serializedEventBytes.length);
|
|
514
|
-
this.queuedEventBytes = 0;
|
|
515
|
-
return purgedEvents;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
addBreadcrumb(breadcrumb, timestamp = new Date().toISOString()) {
|
|
519
|
-
if (this.closed) {
|
|
520
|
-
throw new SdkError("shutdown_error", "client is already shut down");
|
|
521
|
-
}
|
|
522
|
-
if (this.closing) {
|
|
523
|
-
throw new SdkError("shutdown_error", "client is shutting down");
|
|
524
|
-
}
|
|
525
|
-
const validated = validateIssueBreadcrumb(breadcrumb, timestamp);
|
|
526
|
-
if (this.issueBreadcrumbs.length === MAX_ISSUE_BREADCRUMBS) {
|
|
527
|
-
this.issueBreadcrumbs.shift();
|
|
528
|
-
this.issueBreadcrumbsTruncated = true;
|
|
529
|
-
}
|
|
530
|
-
this.issueBreadcrumbs.push(validated);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
clearBreadcrumbs() {
|
|
534
|
-
const cleared = this.issueBreadcrumbs.length;
|
|
535
|
-
this.issueBreadcrumbs.splice(0, cleared);
|
|
536
|
-
this.issueBreadcrumbsTruncated = false;
|
|
537
|
-
return cleared;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
release(id, timestamp, attributes) {
|
|
541
|
-
this.#pushEvent("release", id, timestamp, validateRelease(attributes));
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
environment(id, timestamp, attributes) {
|
|
545
|
-
this.#pushEvent("environment", id, timestamp, validateEnvironment(attributes));
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
issue(id, timestamp, attributes) {
|
|
549
|
-
this.#pushIssue(id, timestamp, attributes);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
issueWithAdmission(id, timestamp, attributes) {
|
|
553
|
-
const issue = this.issue;
|
|
554
|
-
if (issue !== LogBrewClient.#issueCapture) {
|
|
555
|
-
issue.call(this, id, timestamp, attributes);
|
|
556
|
-
return EVENT_ADMISSIONS.unavailable;
|
|
557
|
-
}
|
|
558
|
-
return this.#pushIssue(id, timestamp, attributes);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
log(id, timestamp, attributes) {
|
|
562
|
-
this.#pushEvent("log", id, timestamp, validateLog(attributes));
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
span(id, timestamp, attributes) {
|
|
566
|
-
this.#pushEvent("span", id, timestamp, validateSpan(attributes));
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
action(id, timestamp, attributes) {
|
|
570
|
-
this.#pushEvent("action", id, timestamp, validateAction(attributes));
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
metric(id, timestamp, attributes) {
|
|
574
|
-
this.#pushEvent("metric", id, timestamp, validateMetric(attributes));
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
async flush(transport) {
|
|
578
|
-
if (this.closed) {
|
|
579
|
-
throw new SdkError("shutdown_error", "client is already shut down");
|
|
580
|
-
}
|
|
581
|
-
if (this.closing) {
|
|
582
|
-
throw new SdkError("shutdown_error", "client is shutting down");
|
|
583
|
-
}
|
|
584
|
-
const resolvedTransport = this.#resolveTransport(transport);
|
|
585
|
-
const controlsAutomaticDelivery = this.automaticDelivery && resolvedTransport === this.transport;
|
|
586
|
-
this.#clearDeliveryTimer();
|
|
587
|
-
return this.#runSerialized(async () => {
|
|
588
|
-
this.#clearDeliveryTimer();
|
|
589
|
-
try {
|
|
590
|
-
const response = await this.#flushWithHealth(resolvedTransport);
|
|
591
|
-
if (controlsAutomaticDelivery) {
|
|
592
|
-
this.#recordAutomaticSuccess();
|
|
593
|
-
}
|
|
594
|
-
return response;
|
|
595
|
-
} catch (error) {
|
|
596
|
-
if (controlsAutomaticDelivery) {
|
|
597
|
-
this.#recordAutomaticFailure(error);
|
|
598
|
-
}
|
|
599
|
-
throw error;
|
|
600
|
-
} finally {
|
|
601
|
-
if (this.automaticDelivery && !this.closed && !this.closing) {
|
|
602
|
-
this.#resumeAutomaticDelivery();
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async shutdown(transport) {
|
|
609
|
-
if (this.closed) {
|
|
610
|
-
throw new SdkError("shutdown_error", "client is already shut down");
|
|
611
|
-
}
|
|
612
|
-
if (this.closing) {
|
|
613
|
-
throw new SdkError("shutdown_error", "client is shutting down");
|
|
614
|
-
}
|
|
615
|
-
const resolvedTransport = this.#resolveTransport(transport);
|
|
616
|
-
const controlsAutomaticDelivery = this.automaticDelivery && resolvedTransport === this.transport;
|
|
617
|
-
this.#clearDeliveryTimer();
|
|
618
|
-
this.automaticFlushPending = false;
|
|
619
|
-
this.closing = true;
|
|
620
|
-
try {
|
|
621
|
-
const response = await this.#runSerialized(() => this.#flushWithHealth(resolvedTransport));
|
|
622
|
-
if (controlsAutomaticDelivery) {
|
|
623
|
-
this.#recordAutomaticSuccess();
|
|
624
|
-
}
|
|
625
|
-
this.#saveClientReport(true);
|
|
626
|
-
if (this.eventStore) {
|
|
627
|
-
try {
|
|
628
|
-
requireSynchronousStoreResult("close", this.eventStore.close());
|
|
629
|
-
} catch (error) {
|
|
630
|
-
this.closed = true;
|
|
631
|
-
throw error;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
this.closed = true;
|
|
635
|
-
return response;
|
|
636
|
-
} catch (error) {
|
|
637
|
-
if (!this.closed) {
|
|
638
|
-
this.closing = false;
|
|
639
|
-
if (controlsAutomaticDelivery) {
|
|
640
|
-
this.#recordAutomaticFailure(error);
|
|
641
|
-
}
|
|
642
|
-
this.#resumeAutomaticDelivery();
|
|
643
|
-
}
|
|
644
|
-
throw error;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
#pushIssue(id, timestamp, attributes) {
|
|
649
|
-
if (attributes.breadcrumbs !== undefined) {
|
|
650
|
-
return this.#pushEvent("issue", id, timestamp, validateIssue(attributes));
|
|
651
|
-
}
|
|
652
|
-
const provided = this.#providedBreadcrumbs();
|
|
653
|
-
const merged = provided.breadcrumbs.length === 0
|
|
654
|
-
? this.issueBreadcrumbs
|
|
655
|
-
: [...this.issueBreadcrumbs, ...provided.breadcrumbs]
|
|
656
|
-
.sort((left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp));
|
|
657
|
-
const breadcrumbs = merged.slice(-MAX_ISSUE_BREADCRUMBS);
|
|
658
|
-
const truncated = attributes.breadcrumbsTruncated === true || this.issueBreadcrumbsTruncated
|
|
659
|
-
|| provided.truncated || merged.length > breadcrumbs.length;
|
|
660
|
-
const withBreadcrumbs = breadcrumbs.length === 0
|
|
661
|
-
? attributes
|
|
662
|
-
: { ...attributes, breadcrumbs, ...(truncated ? { breadcrumbsTruncated: true } : {}) };
|
|
663
|
-
return this.#pushEvent("issue", id, timestamp, validateIssue(withBreadcrumbs));
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Provider failures and invalid entries never cost the issue itself.
|
|
667
|
-
#providedBreadcrumbs() {
|
|
668
|
-
let provided;
|
|
669
|
-
try {
|
|
670
|
-
provided = this.breadcrumbProvider?.();
|
|
671
|
-
} catch {
|
|
672
|
-
return { breadcrumbs: [], truncated: false };
|
|
673
|
-
}
|
|
674
|
-
if (!provided || !Array.isArray(provided.breadcrumbs)) {
|
|
675
|
-
return { breadcrumbs: [], truncated: false };
|
|
676
|
-
}
|
|
677
|
-
const breadcrumbs = [];
|
|
678
|
-
let truncated = provided.truncated === true || provided.breadcrumbs.length > MAX_ISSUE_BREADCRUMBS;
|
|
679
|
-
for (const breadcrumb of provided.breadcrumbs.slice(-MAX_ISSUE_BREADCRUMBS)) {
|
|
680
|
-
try {
|
|
681
|
-
breadcrumbs.push(validateIssueBreadcrumb(breadcrumb));
|
|
682
|
-
} catch {
|
|
683
|
-
truncated = true;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
return { breadcrumbs, truncated };
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
#pushEvent(eventType, id, timestamp, attributes) {
|
|
690
|
-
if (this.closed) {
|
|
691
|
-
throw new SdkError("shutdown_error", "client is already shut down");
|
|
692
|
-
}
|
|
693
|
-
if (this.closing) {
|
|
694
|
-
throw new SdkError("shutdown_error", "client is shutting down");
|
|
695
|
-
}
|
|
696
|
-
requireNonEmpty("event id", id);
|
|
697
|
-
requireTimestamp(timestamp);
|
|
698
|
-
const context = mergeTelemetryContexts(
|
|
699
|
-
mergeTelemetryContexts(this.context, this.contextProvider?.()),
|
|
700
|
-
attributes.context
|
|
701
|
-
);
|
|
702
|
-
const eventAttributes = context === undefined
|
|
703
|
-
? attributes
|
|
704
|
-
: { ...attributes, context };
|
|
705
|
-
const event = { type: eventType, id, timestamp, attributes: eventAttributes };
|
|
706
|
-
if (this.eventFilter && this.eventFilter(cloneEvent(event)) === false) {
|
|
707
|
-
return EVENT_ADMISSIONS.filtered;
|
|
708
|
-
}
|
|
709
|
-
const serializedEvent = JSON.stringify(event);
|
|
710
|
-
const eventBytes = utf8ByteLength(serializedEvent);
|
|
711
|
-
if (this.batchPrefixBytes + eventBytes + this.batchSuffixBytes > this.maxBatchBytes) {
|
|
712
|
-
this.#recordDroppedEvent(event, "event_too_large");
|
|
713
|
-
return EVENT_ADMISSIONS.event_too_large;
|
|
714
|
-
}
|
|
715
|
-
const fatal = isFatalEvent(event);
|
|
716
|
-
if (fatal) {
|
|
717
|
-
this.#synchronizeEventStore();
|
|
718
|
-
}
|
|
719
|
-
const overflow = this.#overflowReason(this.events.length, this.queuedEventBytes + eventBytes);
|
|
720
|
-
const evicted = overflow && fatal ? this.#evictForFatal(eventBytes) : [];
|
|
721
|
-
if (overflow && evicted.length === 0) {
|
|
722
|
-
this.#recordDroppedEvent(event, overflow);
|
|
723
|
-
return EVENT_ADMISSIONS[overflow];
|
|
724
|
-
}
|
|
725
|
-
try {
|
|
726
|
-
if (this.eventStore) {
|
|
727
|
-
requireSynchronousStoreResult("append", this.eventStore.append({
|
|
728
|
-
event: cloneEvent(event),
|
|
729
|
-
eventBytes,
|
|
730
|
-
serializedEvent
|
|
731
|
-
}));
|
|
732
|
-
}
|
|
733
|
-
this.events.push(event);
|
|
734
|
-
this.serializedEvents.push(serializedEvent);
|
|
735
|
-
this.serializedEventBytes.push(eventBytes);
|
|
736
|
-
this.queuedEventBytes += eventBytes;
|
|
737
|
-
} finally {
|
|
738
|
-
// Report evictions only after admission so drop callbacks cannot refill the freed room.
|
|
739
|
-
for (const evictedEvent of evicted) {
|
|
740
|
-
this.#recordDroppedEvent(evictedEvent, "evicted_for_fatal");
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
this.#scheduleAutomaticDelivery();
|
|
744
|
-
return EVENT_ADMISSIONS.admitted;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
#overflowReason(count, bytes) {
|
|
748
|
-
if (count >= this.maxQueueSize) {
|
|
749
|
-
return "queue_overflow";
|
|
750
|
-
}
|
|
751
|
-
return bytes > this.maxQueueBytes ? "queue_bytes_overflow" : undefined;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// Evicts the oldest non-fatal events outside the unacknowledged batch, only when that makes room.
|
|
755
|
-
#evictForFatal(eventBytes) {
|
|
756
|
-
if (this.eventStore && typeof this.eventStore.remove !== "function") {
|
|
757
|
-
return [];
|
|
758
|
-
}
|
|
759
|
-
const indexes = [];
|
|
760
|
-
let count = this.events.length;
|
|
761
|
-
let bytes = this.queuedEventBytes + eventBytes;
|
|
762
|
-
for (
|
|
763
|
-
let index = this.unacknowledgedBatch?.eventsCount ?? 0;
|
|
764
|
-
index < this.events.length && this.#overflowReason(count, bytes);
|
|
765
|
-
index += 1
|
|
766
|
-
) {
|
|
767
|
-
if (!isFatalEvent(this.events[index])) {
|
|
768
|
-
indexes.push(index);
|
|
769
|
-
count -= 1;
|
|
770
|
-
bytes -= this.serializedEventBytes[index];
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
if (this.#overflowReason(count, bytes)) {
|
|
774
|
-
return [];
|
|
775
|
-
}
|
|
776
|
-
if (this.eventStore) {
|
|
777
|
-
requireSynchronousStoreResult("remove", this.eventStore.remove(indexes));
|
|
778
|
-
}
|
|
779
|
-
const evicted = indexes.map((index) => this.events[index]);
|
|
780
|
-
for (let position = indexes.length - 1; position >= 0; position -= 1) {
|
|
781
|
-
const index = indexes[position];
|
|
782
|
-
this.events.splice(index, 1);
|
|
783
|
-
this.serializedEvents.splice(index, 1);
|
|
784
|
-
this.queuedEventBytes -= this.serializedEventBytes.splice(index, 1)[0];
|
|
785
|
-
}
|
|
786
|
-
return evicted;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
#recordDroppedEvent(event, reason) {
|
|
790
|
-
this.droppedEventCount = incrementBounded(this.droppedEventCount);
|
|
791
|
-
this.droppedEventsByReason[reason] = incrementBounded(this.droppedEventsByReason[reason]);
|
|
792
|
-
const firstOfReason = this.unreportedDropsByReason[reason] === undefined;
|
|
793
|
-
this.unreportedDropsByReason[reason] = Math.min(MAX_CLIENT_REPORT_COUNT, (this.unreportedDropsByReason[reason] ?? 0) + 1);
|
|
794
|
-
this.#saveClientReport(firstOfReason);
|
|
795
|
-
this.lastDropReason = reason;
|
|
796
|
-
this.lastDroppedAtUnixMs = nextBoundedTimestamp(this.lastDroppedAtUnixMs);
|
|
797
|
-
if (!this.onEventDropped) {
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
try {
|
|
801
|
-
this.onEventDropped({
|
|
802
|
-
droppedEvents: this.droppedEventCount,
|
|
803
|
-
eventId: event.id,
|
|
804
|
-
eventType: event.type,
|
|
805
|
-
reason
|
|
806
|
-
});
|
|
807
|
-
} catch {
|
|
808
|
-
// Drop callbacks are advisory and must not interrupt application logging.
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
#runSerialized(operation) {
|
|
813
|
-
this.pendingOperations = incrementBounded(this.pendingOperations);
|
|
814
|
-
const result = this.operationTail.then(operation).finally(() => {
|
|
815
|
-
this.pendingOperations = Math.max(0, this.pendingOperations - 1);
|
|
816
|
-
});
|
|
817
|
-
this.operationTail = result.then(
|
|
818
|
-
() => undefined,
|
|
819
|
-
() => undefined
|
|
820
|
-
);
|
|
821
|
-
return result;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
#resolveTransport(transport) {
|
|
825
|
-
const resolved = transport ?? this.transport;
|
|
826
|
-
if (resolved === undefined) {
|
|
827
|
-
throw new SdkError("validation_error", "flush and shutdown require transport");
|
|
828
|
-
}
|
|
829
|
-
validateTransport(resolved);
|
|
830
|
-
return resolved;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
async #flushWithHealth(transport) {
|
|
834
|
-
this.deliveryInFlight = true;
|
|
835
|
-
try {
|
|
836
|
-
const response = await this.#flushSnapshot(transport);
|
|
837
|
-
this.successfulFlushCount = incrementBounded(this.successfulFlushCount);
|
|
838
|
-
this.lastDeliveryOutcome = response.batches === 0 ? "empty" : "accepted";
|
|
839
|
-
return response;
|
|
840
|
-
} catch (error) {
|
|
841
|
-
this.failedFlushCount = incrementBounded(this.failedFlushCount);
|
|
842
|
-
this.lastDeliveryOutcome = "failed";
|
|
843
|
-
throw error;
|
|
844
|
-
} finally {
|
|
845
|
-
this.deliveryInFlight = false;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
#scheduleAutomaticDelivery() {
|
|
850
|
-
if (!this.automaticDelivery || this.closed || this.closing || this.events.length === 0 || this.automaticPauseReason !== "none") {
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
if (this.automaticFlushActive) {
|
|
854
|
-
this.automaticFlushPending = true;
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
if (this.retryDelayMs > 0) {
|
|
858
|
-
this.#armAutomaticDeliveryTimer(this.retryDelayMs);
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
if (this.events.length >= this.deliveryQueueThreshold) {
|
|
862
|
-
this.#requestAutomaticFlush();
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
this.#armAutomaticDeliveryTimer();
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
#armAutomaticDeliveryTimer(delayMs = this.deliveryIntervalMs) {
|
|
869
|
-
if (!this.automaticDelivery || this.closed || this.closing || this.events.length === 0 || this.automaticPauseReason !== "none") {
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
if (this.deliveryTimer !== undefined) {
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
const timer = setTimeout(() => {
|
|
876
|
-
if (this.deliveryTimer !== timer) {
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
this.deliveryTimer = undefined;
|
|
880
|
-
this.retryDelayMs = 0;
|
|
881
|
-
this.#requestAutomaticFlush();
|
|
882
|
-
}, delayMs);
|
|
883
|
-
this.deliveryTimer = timer;
|
|
884
|
-
if (timer && typeof timer === "object" && typeof timer.unref === "function") {
|
|
885
|
-
timer.unref();
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
#clearDeliveryTimer() {
|
|
890
|
-
if (this.deliveryTimer === undefined) {
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
globalThis.clearTimeout(this.deliveryTimer);
|
|
894
|
-
this.deliveryTimer = undefined;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
#requestAutomaticFlush() {
|
|
898
|
-
if (!this.automaticDelivery || this.closed || this.closing || this.events.length === 0 || this.automaticPauseReason !== "none") {
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
this.#clearDeliveryTimer();
|
|
902
|
-
if (this.automaticFlushActive) {
|
|
903
|
-
this.automaticFlushPending = true;
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
this.automaticFlushActive = true;
|
|
907
|
-
void Promise.resolve().then(() => this.#runAutomaticFlush());
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
async #runAutomaticFlush() {
|
|
911
|
-
if (this.closed || this.closing) {
|
|
912
|
-
this.automaticFlushActive = false;
|
|
913
|
-
this.automaticFlushPending = false;
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
let succeeded = false;
|
|
917
|
-
try {
|
|
918
|
-
await this.#runSerialized(() => this.#flushWithHealth(this.transport));
|
|
919
|
-
this.#recordAutomaticSuccess();
|
|
920
|
-
succeeded = true;
|
|
921
|
-
} catch (error) {
|
|
922
|
-
this.#recordAutomaticFailure(error);
|
|
923
|
-
} finally {
|
|
924
|
-
this.automaticFlushActive = false;
|
|
925
|
-
if (this.closed || this.closing) {
|
|
926
|
-
this.automaticFlushPending = false;
|
|
927
|
-
} else {
|
|
928
|
-
const drainCoalesced = succeeded && this.automaticFlushPending && this.events.length > 0;
|
|
929
|
-
this.automaticFlushPending = false;
|
|
930
|
-
if (drainCoalesced) {
|
|
931
|
-
this.#requestAutomaticFlush();
|
|
932
|
-
} else if (!succeeded) {
|
|
933
|
-
this.#resumeAutomaticDelivery();
|
|
934
|
-
} else {
|
|
935
|
-
this.#scheduleAutomaticDelivery();
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
async #flushSnapshot(transport) {
|
|
942
|
-
this.#synchronizeEventStore();
|
|
943
|
-
let remainingEvents = this.events.length;
|
|
944
|
-
if (remainingEvents === 0) {
|
|
945
|
-
return { statusCode: 204, attempts: 0, batches: 0 };
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
let attempts = 0;
|
|
949
|
-
let batches = 0;
|
|
950
|
-
let statusCode = 204;
|
|
951
|
-
while (remainingEvents > 0) {
|
|
952
|
-
// Eviction for a fatal event can shrink the snapshot while a batch is in flight.
|
|
953
|
-
this.unacknowledgedBatch ??= Object.freeze(
|
|
954
|
-
this.#nextBatch(Math.min(remainingEvents, this.events.length, this.maxBatchEvents))
|
|
955
|
-
);
|
|
956
|
-
const batch = this.unacknowledgedBatch;
|
|
957
|
-
const response = await this.#sendBatch(transport, batch.body);
|
|
958
|
-
this.unacknowledgedBatch = undefined;
|
|
959
|
-
// Drops recorded while this body was in flight stay pending under a new report ID.
|
|
960
|
-
if (batch.carriesClientReport) {
|
|
961
|
-
this.pendingClientReport = undefined;
|
|
962
|
-
this.#saveClientReport(true);
|
|
963
|
-
}
|
|
964
|
-
this.#acknowledge(batch.eventsCount);
|
|
965
|
-
this.acceptedBatchCount = incrementBounded(this.acceptedBatchCount);
|
|
966
|
-
remainingEvents -= batch.eventsCount;
|
|
967
|
-
attempts += response.attempts;
|
|
968
|
-
batches += 1;
|
|
969
|
-
statusCode = response.statusCode;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
return { statusCode, attempts, batches };
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
#synchronizeEventStore() {
|
|
976
|
-
if (!this.eventStore) {
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
const recovered = loadStoredEvents(this);
|
|
980
|
-
if (!isOrderedSubsequence(this.serializedEvents, recovered.serializedEvents)) {
|
|
981
|
-
throw new SdkError("persistence_error", "event store lost a queued record");
|
|
982
|
-
}
|
|
983
|
-
this.events = recovered.events;
|
|
984
|
-
this.serializedEvents = recovered.serializedEvents;
|
|
985
|
-
this.serializedEventBytes = recovered.serializedEventBytes;
|
|
986
|
-
this.queuedEventBytes = recovered.queuedEventBytes;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
#nextBatch(maxEvents) {
|
|
990
|
-
const report = this.#pendingClientReport();
|
|
991
|
-
const reportSuffix = report && `],"client_report":${JSON.stringify(report)}}`;
|
|
992
|
-
const reportedEvents = reportSuffix ? this.#batchEventCount(maxEvents, utf8ByteLength(reportSuffix)) : 0;
|
|
993
|
-
const eventsCount = reportedEvents || this.#batchEventCount(maxEvents, this.batchSuffixBytes);
|
|
994
|
-
if (eventsCount === 0) {
|
|
995
|
-
throw new SdkError("transport_error", "queued event cannot fit the configured batch byte limit");
|
|
996
|
-
}
|
|
997
|
-
return {
|
|
998
|
-
body: `${this.batchPrefix}${this.serializedEvents.slice(0, eventsCount).join(",")}${reportedEvents ? reportSuffix : this.batchSuffix}`,
|
|
999
|
-
carriesClientReport: reportedEvents > 0,
|
|
1000
|
-
eventsCount
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// Freezes unreported drops under one stable ID, so every resend of them is counted once.
|
|
1005
|
-
#pendingClientReport() {
|
|
1006
|
-
if (this.pendingClientReport === undefined) {
|
|
1007
|
-
const reported = Object.entries(this.unreportedDropsByReason).filter(([, count]) => count > 0);
|
|
1008
|
-
if (reported.length === 0) {
|
|
1009
|
-
return undefined;
|
|
1010
|
-
}
|
|
1011
|
-
const droppedEvents = Object.fromEntries(reported.slice(0, MAX_CLIENT_REPORT_REASONS));
|
|
1012
|
-
for (const reason of Object.keys(droppedEvents)) {
|
|
1013
|
-
delete this.unreportedDropsByReason[reason];
|
|
1014
|
-
}
|
|
1015
|
-
this.pendingClientReport = Object.freeze({ report_id: randomHex(16), dropped_events: droppedEvents });
|
|
1016
|
-
this.#saveClientReport(true);
|
|
1017
|
-
}
|
|
1018
|
-
return this.pendingClientReport;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// Saves at once for a new report or reason, and at most once a second during a drop burst.
|
|
1022
|
-
#saveClientReport(immediately) {
|
|
1023
|
-
const now = Date.now();
|
|
1024
|
-
if (!this.clientReportStore || (!immediately && now - this.clientReportSavedAtMs < CLIENT_REPORT_SAVE_INTERVAL_MS)) {
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
this.clientReportSavedAtMs = now;
|
|
1028
|
-
try {
|
|
1029
|
-
requireSynchronousStoreResult("saveClientReport", this.clientReportStore.saveClientReport({
|
|
1030
|
-
pending: this.pendingClientReport ?? null,
|
|
1031
|
-
unreported: { ...this.unreportedDropsByReason }
|
|
1032
|
-
}));
|
|
1033
|
-
} catch {
|
|
1034
|
-
// Drop counts are advisory: a failed save keeps them in memory and never blocks capture.
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
#batchEventCount(maxEvents, suffixBytes) {
|
|
1039
|
-
let bodyBytes = this.batchPrefixBytes + suffixBytes;
|
|
1040
|
-
let eventsCount = 0;
|
|
1041
|
-
const eventIds = new Set();
|
|
1042
|
-
for (let index = 0; index < maxEvents; index += 1) {
|
|
1043
|
-
if (eventIds.has(this.events[index].id)) {
|
|
1044
|
-
break;
|
|
1045
|
-
}
|
|
1046
|
-
const separatorBytes = eventsCount === 0 ? 0 : 1;
|
|
1047
|
-
const nextBodyBytes = bodyBytes + separatorBytes + this.serializedEventBytes[index];
|
|
1048
|
-
if (nextBodyBytes > this.maxBatchBytes) {
|
|
1049
|
-
break;
|
|
1050
|
-
}
|
|
1051
|
-
eventIds.add(this.events[index].id);
|
|
1052
|
-
bodyBytes = nextBodyBytes;
|
|
1053
|
-
eventsCount += 1;
|
|
1054
|
-
}
|
|
1055
|
-
return eventsCount;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
#acknowledge(eventsCount) {
|
|
1059
|
-
if (this.eventStore) {
|
|
1060
|
-
requireSynchronousStoreResult("acknowledge", this.eventStore.acknowledge(eventsCount));
|
|
1061
|
-
}
|
|
1062
|
-
let acknowledgedBytes = 0;
|
|
1063
|
-
for (let index = 0; index < eventsCount; index += 1) {
|
|
1064
|
-
acknowledgedBytes += this.serializedEventBytes[index];
|
|
1065
|
-
}
|
|
1066
|
-
this.events.splice(0, eventsCount);
|
|
1067
|
-
this.serializedEvents.splice(0, eventsCount);
|
|
1068
|
-
this.serializedEventBytes.splice(0, eventsCount);
|
|
1069
|
-
this.queuedEventBytes -= acknowledgedBytes;
|
|
1070
|
-
this.acceptedEventCount = addBounded(this.acceptedEventCount, eventsCount);
|
|
1071
|
-
this.lastAcceptedAtUnixMs = nextBoundedTimestamp(Math.max(
|
|
1072
|
-
this.lastAcceptedAtUnixMs,
|
|
1073
|
-
this.lastAttemptAtUnixMs
|
|
1074
|
-
));
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
#recordAutomaticSuccess() {
|
|
1078
|
-
this.automaticPauseReason = "none";
|
|
1079
|
-
this.consecutiveDeliveryFailures = 0;
|
|
1080
|
-
this.retryDelayMs = 0;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
#recordAutomaticFailure(error) {
|
|
1084
|
-
this.consecutiveDeliveryFailures = incrementBounded(this.consecutiveDeliveryFailures);
|
|
1085
|
-
this.retryDelayMs = 0;
|
|
1086
|
-
if (error instanceof SdkError && error.code === "unauthenticated") {
|
|
1087
|
-
this.automaticPauseReason = "authentication";
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
if (error instanceof SdkError && error.code === "rate_limited") {
|
|
1091
|
-
if (error.retryable === true) {
|
|
1092
|
-
this.automaticPauseReason = "none";
|
|
1093
|
-
this.retryDelayMs = rateLimitWaitMs(
|
|
1094
|
-
error.retryAfterMs ?? automaticRetryDelayMs(this.deliveryIntervalMs, this.consecutiveDeliveryFailures)
|
|
1095
|
-
);
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
this.automaticPauseReason = "rate_limit";
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
if (error instanceof SdkError && error.retryable === true) {
|
|
1102
|
-
this.automaticPauseReason = "none";
|
|
1103
|
-
this.retryDelayMs = automaticRetryDelayMs(this.deliveryIntervalMs, this.consecutiveDeliveryFailures);
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
this.automaticPauseReason = "non_retryable";
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
#resumeAutomaticDelivery() {
|
|
1110
|
-
if (this.automaticPauseReason !== "none") {
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
if (this.retryDelayMs > 0) {
|
|
1114
|
-
this.#armAutomaticDeliveryTimer(this.retryDelayMs);
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
this.#scheduleAutomaticDelivery();
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
async #sendBatch(transport, body) {
|
|
1121
|
-
const maxAttempts = this.maxRetries + 1;
|
|
1122
|
-
let attempts = 0;
|
|
1123
|
-
|
|
1124
|
-
while (attempts < maxAttempts) {
|
|
1125
|
-
attempts += 1;
|
|
1126
|
-
this.deliveryAttemptCount = incrementBounded(this.deliveryAttemptCount);
|
|
1127
|
-
this.lastAttemptAtUnixMs = nextBoundedTimestamp(this.lastAttemptAtUnixMs);
|
|
1128
|
-
this.lastStatusClass = "transport_error";
|
|
1129
|
-
try {
|
|
1130
|
-
const response = await transport.send(this.apiKey, body);
|
|
1131
|
-
if (
|
|
1132
|
-
!response
|
|
1133
|
-
|| Array.isArray(response)
|
|
1134
|
-
|| typeof response !== "object"
|
|
1135
|
-
|| !Number.isSafeInteger(response.statusCode)
|
|
1136
|
-
|| response.statusCode < 100
|
|
1137
|
-
|| response.statusCode > 599
|
|
1138
|
-
) {
|
|
1139
|
-
this.lastStatusClass = "invalid_response";
|
|
1140
|
-
throw new SdkError("transport_error", "invalid transport response");
|
|
1141
|
-
}
|
|
1142
|
-
this.lastStatusClass = statusClass(response.statusCode);
|
|
1143
|
-
if (response.statusCode === 401) {
|
|
1144
|
-
throw new SdkError("unauthenticated", "transport rejected the API key");
|
|
1145
|
-
}
|
|
1146
|
-
if (response.statusCode === 429) {
|
|
1147
|
-
throw new SdkError("rate_limited", "transport rate limited the batch", {
|
|
1148
|
-
retryAfterMs: response.retryAfterMs,
|
|
1149
|
-
retryable: response.errorCode !== "usage_limit_exceeded"
|
|
1150
|
-
&& (retryAfterMsOrUndefined(response.retryAfterMs) !== undefined || response.errorCode === "rate_limited")
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
1154
|
-
return { statusCode: response.statusCode, attempts };
|
|
1155
|
-
}
|
|
1156
|
-
const retryableStatus = response.statusCode === 408 || response.statusCode >= 500;
|
|
1157
|
-
if (retryableStatus && attempts < maxAttempts) {
|
|
1158
|
-
continue;
|
|
1159
|
-
}
|
|
1160
|
-
throw new SdkError("transport_error", `unexpected transport status ${response.statusCode}`, {
|
|
1161
|
-
retryable: retryableStatus
|
|
1162
|
-
});
|
|
1163
|
-
} catch (error) {
|
|
1164
|
-
if (error instanceof SdkError) {
|
|
1165
|
-
throw error;
|
|
1166
|
-
}
|
|
1167
|
-
if (error instanceof TransportError && error.retryable && attempts < maxAttempts) {
|
|
1168
|
-
this.lastStatusClass = "network_error";
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
|
-
if (error instanceof TransportError) {
|
|
1172
|
-
this.lastStatusClass = "network_error";
|
|
1173
|
-
throw new SdkError(error.code, error.message, { retryable: error.retryable });
|
|
1174
|
-
}
|
|
1175
|
-
throw error;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
throw new SdkError("transport_error", "exhausted retries");
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function automaticRetryDelayMs(deliveryIntervalMs, consecutiveFailures) {
|
|
1184
|
-
const exponent = Math.min(consecutiveFailures - 1, 30);
|
|
1185
|
-
const maximumDelay = Math.min(MAX_DELIVERY_INTERVAL_MS, deliveryIntervalMs * (2 ** exponent));
|
|
1186
|
-
const minimumDelay = Math.ceil(maximumDelay / 2);
|
|
1187
|
-
return minimumDelay + Math.floor(Math.random() * (maximumDelay - minimumDelay + 1));
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function rateLimitWaitMs(delayMs) {
|
|
1191
|
-
const bounded = Math.min(MAX_RATE_LIMIT_WAIT_MS, Math.max(MIN_RATE_LIMIT_WAIT_MS, delayMs));
|
|
1192
|
-
return Math.min(MAX_RATE_LIMIT_WAIT_MS, bounded + Math.floor(Math.random() * (Math.floor(bounded / 10) + 1)));
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function statusClass(statusCode) {
|
|
1196
|
-
if (statusCode >= 200 && statusCode < 300) {
|
|
1197
|
-
return "success";
|
|
1198
|
-
}
|
|
1199
|
-
if (statusCode >= 400 && statusCode < 500) {
|
|
1200
|
-
return "client_error";
|
|
1201
|
-
}
|
|
1202
|
-
if (statusCode >= 500) {
|
|
1203
|
-
return "server_error";
|
|
1204
|
-
}
|
|
1205
|
-
return "other_status";
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
function nextBoundedTimestamp(previous) {
|
|
1209
|
-
const now = Date.now();
|
|
1210
|
-
if (!Number.isFinite(now)) {
|
|
1211
|
-
return previous;
|
|
1212
|
-
}
|
|
1213
|
-
const bounded = Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(now)));
|
|
1214
|
-
return Math.max(previous, bounded);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
function addBounded(current, increment) {
|
|
1218
|
-
return Math.min(Number.MAX_SAFE_INTEGER, current + increment);
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
function utf8ByteLength(value) {
|
|
1222
|
-
let bytes = 0;
|
|
1223
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
1224
|
-
const codeUnit = value.charCodeAt(index);
|
|
1225
|
-
if (codeUnit < 0x80) {
|
|
1226
|
-
bytes += 1;
|
|
1227
|
-
} else if (codeUnit < 0x800) {
|
|
1228
|
-
bytes += 2;
|
|
1229
|
-
} else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff && index + 1 < value.length) {
|
|
1230
|
-
const nextCodeUnit = value.charCodeAt(index + 1);
|
|
1231
|
-
if (nextCodeUnit >= 0xdc00 && nextCodeUnit <= 0xdfff) {
|
|
1232
|
-
bytes += 4;
|
|
1233
|
-
index += 1;
|
|
1234
|
-
} else {
|
|
1235
|
-
bytes += 3;
|
|
1236
|
-
}
|
|
1237
|
-
} else {
|
|
1238
|
-
bytes += 3;
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
return bytes;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// Fatal input severity is stored as critical; a critical issue is a crash report.
|
|
1245
|
-
function isFatalEvent(event) {
|
|
1246
|
-
return event.type === "issue" && event.attributes.level === "critical";
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// Report IDs only need uniqueness, so runtimes without Web Crypto fall back to Math.random.
|
|
1250
|
-
function randomHex(byteLength) {
|
|
1251
|
-
const bytes = new Uint8Array(byteLength);
|
|
1252
|
-
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
|
1253
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
1254
|
-
} else {
|
|
1255
|
-
for (let index = 0; index < bytes.length; index += 1) {
|
|
1256
|
-
bytes[index] = Math.floor(Math.random() * 256);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
function isClientReportStore(store) {
|
|
1263
|
-
return typeof store?.loadClientReport === "function" && typeof store.saveClientReport === "function";
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// Restores saved drop counts; anything unreadable or out of bounds is ignored, never fatal.
|
|
1267
|
-
function loadClientReport(store) {
|
|
1268
|
-
let state;
|
|
1269
|
-
try {
|
|
1270
|
-
state = store ? requireSynchronousStoreResult("loadClientReport", store.loadClientReport()) : undefined;
|
|
1271
|
-
} catch {
|
|
1272
|
-
state = undefined;
|
|
1273
|
-
}
|
|
1274
|
-
const pending = state?.pending;
|
|
1275
|
-
const pendingCounts = validClientReportCounts(pending?.dropped_events);
|
|
1276
|
-
return {
|
|
1277
|
-
pending: CLIENT_REPORT_ID_PATTERN.test(pending?.report_id ?? "") && Object.keys(pendingCounts).length > 0
|
|
1278
|
-
? Object.freeze({ report_id: pending.report_id, dropped_events: pendingCounts })
|
|
1279
|
-
: undefined,
|
|
1280
|
-
unreported: validClientReportCounts(state?.unreported)
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
function validClientReportCounts(counts) {
|
|
1285
|
-
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
1286
|
-
return {};
|
|
1287
|
-
}
|
|
1288
|
-
return Object.fromEntries(Object.entries(counts).filter(([reason, count]) => (
|
|
1289
|
-
CLIENT_REPORT_REASON_PATTERN.test(reason) && Number.isSafeInteger(count) && count >= 1 && count <= MAX_CLIENT_REPORT_COUNT
|
|
1290
|
-
)).slice(0, MAX_CLIENT_REPORT_REASONS));
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
function incrementBounded(value) {
|
|
1294
|
-
return value >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : value + 1;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
function validateTransport(transport) {
|
|
1298
|
-
if (transport === undefined) {
|
|
1299
|
-
return;
|
|
1300
|
-
}
|
|
1301
|
-
if (!transport || Array.isArray(transport) || typeof transport !== "object" || typeof transport.send !== "function") {
|
|
1302
|
-
throw new SdkError("validation_error", "transport.send must be a function");
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function validateEventStore(eventStore) {
|
|
1307
|
-
if (eventStore === undefined) {
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
if (!eventStore || Array.isArray(eventStore) || typeof eventStore !== "object") {
|
|
1311
|
-
throw new SdkError("validation_error", "eventStore must be an object");
|
|
1312
|
-
}
|
|
1313
|
-
for (const method of ["load", "append", "acknowledge", "purge", "close"]) {
|
|
1314
|
-
if (typeof eventStore[method] !== "function") {
|
|
1315
|
-
throw new SdkError("validation_error", `eventStore.${method} must be a function`);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function createEventStoreFromQueueFactory(eventQueueFactory, config) {
|
|
1321
|
-
const queue = eventQueueFactory({
|
|
1322
|
-
...config,
|
|
1323
|
-
restoreEvent: restoreStoredEvent
|
|
1324
|
-
});
|
|
1325
|
-
validateEventQueue(queue);
|
|
1326
|
-
const eventStore = {
|
|
1327
|
-
load() {
|
|
1328
|
-
const events = queue.events();
|
|
1329
|
-
const count = queue.length();
|
|
1330
|
-
if (!Array.isArray(events) || events.length !== count) {
|
|
1331
|
-
throw invalidStoredRecord();
|
|
1332
|
-
}
|
|
1333
|
-
return events.map((event, index) => ({
|
|
1334
|
-
event,
|
|
1335
|
-
eventBytes: queue.eventBytesAt(index),
|
|
1336
|
-
serializedEvent: queue.serializedAt(index)
|
|
1337
|
-
}));
|
|
1338
|
-
},
|
|
1339
|
-
append(record) {
|
|
1340
|
-
return queue.append({
|
|
1341
|
-
event: record.event,
|
|
1342
|
-
byteCount: record.eventBytes,
|
|
1343
|
-
serialized: record.serializedEvent
|
|
1344
|
-
});
|
|
1345
|
-
},
|
|
1346
|
-
acknowledge(count) {
|
|
1347
|
-
return queue.acknowledge(count);
|
|
1348
|
-
},
|
|
1349
|
-
purge() {
|
|
1350
|
-
return queue.acknowledge(queue.length());
|
|
1351
|
-
},
|
|
1352
|
-
close() {
|
|
1353
|
-
return queue.close();
|
|
1354
|
-
}
|
|
1355
|
-
};
|
|
1356
|
-
if (typeof queue.remove === "function") {
|
|
1357
|
-
eventStore.remove = (indexes) => queue.remove(indexes);
|
|
1358
|
-
}
|
|
1359
|
-
if (isClientReportStore(queue)) {
|
|
1360
|
-
eventStore.loadClientReport = () => queue.loadClientReport();
|
|
1361
|
-
eventStore.saveClientReport = (state) => queue.saveClientReport(state);
|
|
1362
|
-
}
|
|
1363
|
-
return eventStore;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
function validateEventQueue(queue) {
|
|
1367
|
-
const methods = [
|
|
1368
|
-
"acknowledge",
|
|
1369
|
-
"append",
|
|
1370
|
-
"byteCount",
|
|
1371
|
-
"close",
|
|
1372
|
-
"eventBytesAt",
|
|
1373
|
-
"events",
|
|
1374
|
-
"length",
|
|
1375
|
-
"serializedAt"
|
|
1376
|
-
];
|
|
1377
|
-
if (!queue || Array.isArray(queue) || typeof queue !== "object" || methods.some((method) => typeof queue[method] !== "function")) {
|
|
1378
|
-
throw new SdkError("validation_error", "event queue factory returned an invalid queue");
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
function restoreStoredEvent(serializedEvent) {
|
|
1383
|
-
if (typeof serializedEvent !== "string" || serializedEvent === "") {
|
|
1384
|
-
throw invalidStoredRecord();
|
|
1385
|
-
}
|
|
1386
|
-
let event;
|
|
1387
|
-
try {
|
|
1388
|
-
event = JSON.parse(serializedEvent);
|
|
1389
|
-
} catch {
|
|
1390
|
-
throw invalidStoredRecord();
|
|
1391
|
-
}
|
|
1392
|
-
return normalizeStoredRecord({
|
|
1393
|
-
event,
|
|
1394
|
-
eventBytes: utf8ByteLength(serializedEvent),
|
|
1395
|
-
serializedEvent
|
|
1396
|
-
}).event;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
function requireSynchronousStoreResult(operation, result) {
|
|
1400
|
-
if (result && typeof result.then === "function") {
|
|
1401
|
-
throw new SdkError("persistence_error", `eventStore.${operation} must complete synchronously`);
|
|
1402
|
-
}
|
|
1403
|
-
return result;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
function loadStoredEvents({
|
|
1407
|
-
batchPrefixBytes,
|
|
1408
|
-
batchSuffixBytes,
|
|
1409
|
-
eventStore,
|
|
1410
|
-
maxBatchBytes,
|
|
1411
|
-
maxQueueBytes,
|
|
1412
|
-
maxQueueSize
|
|
1413
|
-
}) {
|
|
1414
|
-
if (!eventStore) {
|
|
1415
|
-
return {
|
|
1416
|
-
events: [],
|
|
1417
|
-
queuedEventBytes: 0,
|
|
1418
|
-
serializedEventBytes: [],
|
|
1419
|
-
serializedEvents: []
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
const records = requireSynchronousStoreResult("load", eventStore.load());
|
|
1424
|
-
if (!Array.isArray(records)) {
|
|
1425
|
-
throw invalidStoredRecord();
|
|
1426
|
-
}
|
|
1427
|
-
if (records.length > maxQueueSize) {
|
|
1428
|
-
throw new SdkError("persistence_error", "recovered event count exceeds maxQueueSize");
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
const events = [];
|
|
1432
|
-
const serializedEvents = [];
|
|
1433
|
-
const serializedEventBytes = [];
|
|
1434
|
-
let queuedEventBytes = 0;
|
|
1435
|
-
for (const record of records) {
|
|
1436
|
-
const normalized = normalizeStoredRecord(record);
|
|
1437
|
-
if (batchPrefixBytes + normalized.eventBytes + batchSuffixBytes > maxBatchBytes) {
|
|
1438
|
-
throw new SdkError("persistence_error", "recovered event exceeds maxBatchBytes");
|
|
1439
|
-
}
|
|
1440
|
-
queuedEventBytes += normalized.eventBytes;
|
|
1441
|
-
if (queuedEventBytes > maxQueueBytes) {
|
|
1442
|
-
throw new SdkError("persistence_error", "recovered event bytes exceed maxQueueBytes");
|
|
1443
|
-
}
|
|
1444
|
-
events.push(normalized.event);
|
|
1445
|
-
serializedEvents.push(normalized.serializedEvent);
|
|
1446
|
-
serializedEventBytes.push(normalized.eventBytes);
|
|
1447
|
-
}
|
|
1448
|
-
return { events, queuedEventBytes, serializedEventBytes, serializedEvents };
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
function isOrderedSubsequence(expected, actual) {
|
|
1452
|
-
let index = 0;
|
|
1453
|
-
for (const value of actual) {
|
|
1454
|
-
index += value === expected[index] ? 1 : 0;
|
|
1455
|
-
}
|
|
1456
|
-
return index === expected.length;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
function structuralJson(value) {
|
|
1460
|
-
return JSON.stringify(value, (_, entry) => entry && !Array.isArray(entry) && typeof entry === "object"
|
|
1461
|
-
? Object.fromEntries(Object.entries(entry).sort(([left], [right]) => left.localeCompare(right))) : entry);
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
function normalizeStoredRecord(record) {
|
|
1465
|
-
try {
|
|
1466
|
-
if (!record || Array.isArray(record) || typeof record !== "object") {
|
|
1467
|
-
throw invalidStoredRecord();
|
|
1468
|
-
}
|
|
1469
|
-
const { event, eventBytes, serializedEvent } = record;
|
|
1470
|
-
if (!event || Array.isArray(event) || typeof event !== "object") {
|
|
1471
|
-
throw invalidStoredRecord();
|
|
1472
|
-
}
|
|
1473
|
-
const validator = EVENT_VALIDATORS.get(event.type);
|
|
1474
|
-
if (!validator || !Number.isSafeInteger(eventBytes) || eventBytes <= 0 || typeof serializedEvent !== "string") {
|
|
1475
|
-
throw invalidStoredRecord();
|
|
1476
|
-
}
|
|
1477
|
-
requireNonEmpty("event id", event.id);
|
|
1478
|
-
requireTimestamp(event.timestamp);
|
|
1479
|
-
if (!event.attributes || Array.isArray(event.attributes) || typeof event.attributes !== "object") {
|
|
1480
|
-
throw invalidStoredRecord();
|
|
1481
|
-
}
|
|
1482
|
-
if (JSON.stringify(event) !== serializedEvent || utf8ByteLength(serializedEvent) !== eventBytes) {
|
|
1483
|
-
throw invalidStoredRecord();
|
|
1484
|
-
}
|
|
1485
|
-
const recoveredAttributes = event.type === "issue" && event.attributes.level === "fatal"
|
|
1486
|
-
? { ...event.attributes, level: "critical" }
|
|
1487
|
-
: event.attributes;
|
|
1488
|
-
const context = recoveredAttributes.context;
|
|
1489
|
-
const recoveredEvent = context && !Array.isArray(context) && typeof context === "object"
|
|
1490
|
-
&& context.schemaVersion === undefined
|
|
1491
|
-
? { ...event, attributes: { ...recoveredAttributes, context: { schemaVersion: 1, ...context } } }
|
|
1492
|
-
: { ...event, attributes: recoveredAttributes };
|
|
1493
|
-
const normalizedEvent = {
|
|
1494
|
-
type: recoveredEvent.type,
|
|
1495
|
-
id: recoveredEvent.id,
|
|
1496
|
-
timestamp: recoveredEvent.timestamp,
|
|
1497
|
-
attributes: validator(recoveredEvent.attributes)
|
|
1498
|
-
};
|
|
1499
|
-
const normalizedSerializedEvent = JSON.stringify(normalizedEvent);
|
|
1500
|
-
if (structuralJson(normalizedEvent) !== structuralJson(recoveredEvent)) {
|
|
1501
|
-
throw invalidStoredRecord();
|
|
1502
|
-
}
|
|
1503
|
-
return {
|
|
1504
|
-
event: cloneEvent(normalizedEvent),
|
|
1505
|
-
eventBytes: utf8ByteLength(normalizedSerializedEvent),
|
|
1506
|
-
serializedEvent: normalizedSerializedEvent
|
|
1507
|
-
};
|
|
1508
|
-
} catch (error) {
|
|
1509
|
-
if (error instanceof SdkError && error.code === "persistence_error") {
|
|
1510
|
-
throw error;
|
|
1511
|
-
}
|
|
1512
|
-
throw invalidStoredRecord();
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
function invalidStoredRecord() {
|
|
1517
|
-
return new SdkError("persistence_error", "event store returned an invalid record");
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
function installLogBrewConsoleCapture(config) {
|
|
1521
|
-
if (!config || typeof config !== "object") {
|
|
1522
|
-
throw new SdkError("validation_error", "console capture config must be an object");
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
const client = config.client;
|
|
1526
|
-
if (!(client instanceof LogBrewClient)) {
|
|
1527
|
-
throw new SdkError("validation_error", "console capture client must be a LogBrewClient");
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
const targetConsole = config.console ?? globalThis.console;
|
|
1531
|
-
if (!targetConsole || typeof targetConsole !== "object") {
|
|
1532
|
-
throw new SdkError("validation_error", "console capture target must be an object");
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
const transport = config.transport;
|
|
1536
|
-
const flushOnCapture = config.flushOnCapture === true;
|
|
1537
|
-
const includeErrorStack = config.includeErrorStack === true;
|
|
1538
|
-
const logger = config.logger ?? "console";
|
|
1539
|
-
const metadata = compactMetadata(config.metadata);
|
|
1540
|
-
const timestamp = typeof config.timestamp === "function"
|
|
1541
|
-
? config.timestamp
|
|
1542
|
-
: () => new Date().toISOString();
|
|
1543
|
-
const eventIdPrefix = config.eventIdPrefix ?? "console";
|
|
1544
|
-
const onError = typeof config.onError === "function" ? config.onError : () => {};
|
|
1545
|
-
const levels = normalizeConsoleLevels(config.levels);
|
|
1546
|
-
const originals = new Map();
|
|
1547
|
-
const state = {
|
|
1548
|
-
installed: true,
|
|
1549
|
-
captured: 0,
|
|
1550
|
-
pendingFlush: Promise.resolve(null)
|
|
1551
|
-
};
|
|
1552
|
-
|
|
1553
|
-
for (const method of levels) {
|
|
1554
|
-
const original = targetConsole[method];
|
|
1555
|
-
if (typeof original !== "function") {
|
|
1556
|
-
continue;
|
|
1557
|
-
}
|
|
1558
|
-
originals.set(method, original);
|
|
1559
|
-
targetConsole[method] = createConsoleCaptureMethod({
|
|
1560
|
-
client,
|
|
1561
|
-
eventIdPrefix,
|
|
1562
|
-
flushOnCapture,
|
|
1563
|
-
includeErrorStack,
|
|
1564
|
-
logger,
|
|
1565
|
-
metadata,
|
|
1566
|
-
method,
|
|
1567
|
-
onError,
|
|
1568
|
-
original,
|
|
1569
|
-
state,
|
|
1570
|
-
timestamp,
|
|
1571
|
-
transport
|
|
1572
|
-
});
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
return {
|
|
1576
|
-
async flush() {
|
|
1577
|
-
if (transport && client.pendingEvents() > 0) {
|
|
1578
|
-
state.pendingFlush = Promise.resolve(client.flush(transport)).catch((error) => {
|
|
1579
|
-
onError(error);
|
|
1580
|
-
return null;
|
|
1581
|
-
});
|
|
1582
|
-
}
|
|
1583
|
-
return state.pendingFlush;
|
|
1584
|
-
},
|
|
1585
|
-
uninstall() {
|
|
1586
|
-
if (!state.installed) {
|
|
1587
|
-
return;
|
|
1588
|
-
}
|
|
1589
|
-
state.installed = false;
|
|
1590
|
-
for (const [method, original] of originals.entries()) {
|
|
1591
|
-
targetConsole[method] = original;
|
|
1592
|
-
}
|
|
1593
|
-
originals.clear();
|
|
1594
|
-
}
|
|
1595
|
-
};
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
function createConsoleCaptureMethod(config) {
|
|
1599
|
-
/** @this {*} */
|
|
1600
|
-
return function logBrewConsoleMethod(...args) {
|
|
1601
|
-
config.original.apply(this, args);
|
|
1602
|
-
if (!config.state.installed) {
|
|
1603
|
-
return;
|
|
1604
|
-
}
|
|
1605
|
-
try {
|
|
1606
|
-
config.state.captured += 1;
|
|
1607
|
-
config.client.log(
|
|
1608
|
-
`${config.eventIdPrefix}_${config.state.captured}`,
|
|
1609
|
-
config.timestamp(),
|
|
1610
|
-
logAttributesFromConsoleArgs(config.method, args, {
|
|
1611
|
-
includeErrorStack: config.includeErrorStack,
|
|
1612
|
-
logger: config.logger,
|
|
1613
|
-
metadata: config.metadata
|
|
1614
|
-
})
|
|
1615
|
-
);
|
|
1616
|
-
if (config.flushOnCapture && config.transport) {
|
|
1617
|
-
config.state.pendingFlush = Promise.resolve(config.client.flush(config.transport)).catch((error) => {
|
|
1618
|
-
config.onError(error);
|
|
1619
|
-
return null;
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
} catch (error) {
|
|
1623
|
-
config.onError(error);
|
|
1624
|
-
}
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
function logAttributesFromConsoleArgs(method, args, options = {}) {
|
|
1629
|
-
const logLevel = logbrewLevelFromConsoleMethod(method);
|
|
1630
|
-
const includeErrorStack = options.includeErrorStack === true;
|
|
1631
|
-
const message = consoleMessage(args, includeErrorStack);
|
|
1632
|
-
const metadata = {
|
|
1633
|
-
...compactMetadata(options.metadata),
|
|
1634
|
-
consoleMethod: method,
|
|
1635
|
-
argumentCount: Array.isArray(args) ? args.length : 0
|
|
1636
|
-
};
|
|
1637
|
-
for (const value of Array.isArray(args) ? args : []) {
|
|
1638
|
-
if (value instanceof Error) {
|
|
1639
|
-
metadata.errorName = value.name || "Error";
|
|
1640
|
-
if (value.message) {
|
|
1641
|
-
metadata.errorMessage = value.message;
|
|
1642
|
-
}
|
|
1643
|
-
if (includeErrorStack && value.stack) {
|
|
1644
|
-
metadata.errorStack = value.stack;
|
|
1645
|
-
}
|
|
1646
|
-
break;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
return {
|
|
1651
|
-
message,
|
|
1652
|
-
level: logLevel,
|
|
1653
|
-
...(options.logger ? { logger: options.logger } : {}),
|
|
1654
|
-
metadata
|
|
1655
|
-
};
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
function createIssueAttributesFromError(error, options = {}) {
|
|
1659
|
-
if (!options || Array.isArray(options) || typeof options !== "object") {
|
|
1660
|
-
throw new SdkError("validation_error", "error issue options must be an object");
|
|
1661
|
-
}
|
|
1662
|
-
const details = errorDetails(error);
|
|
1663
|
-
const stackEvidence = javascriptStackEvidence(details.stack, options.debugIdMap);
|
|
1664
|
-
const stackFrames = stackEvidence.frames;
|
|
1665
|
-
const frame = stackFrames[0] ?? null;
|
|
1666
|
-
const source = stringOrUndefined(options.source) ?? "javascript.error";
|
|
1667
|
-
const metadata = {
|
|
1668
|
-
...compactMetadata(options.metadata),
|
|
1669
|
-
source,
|
|
1670
|
-
errorName: details.name,
|
|
1671
|
-
...(details.message ? { errorMessage: details.message } : {}),
|
|
1672
|
-
...(frame ? {
|
|
1673
|
-
errorFrameFile: frame.filename,
|
|
1674
|
-
errorFrameLine: frame.line,
|
|
1675
|
-
errorFrameColumn: frame.column
|
|
1676
|
-
} : {}),
|
|
1677
|
-
...issueGroupingMetadata(source, details, frame, options.fingerprint),
|
|
1678
|
-
...errorCauseMetadata(error),
|
|
1679
|
-
...(stringOrUndefined(options.release) ? { release: options.release } : {}),
|
|
1680
|
-
...(stringOrUndefined(options.environment) ? { environment: options.environment } : {}),
|
|
1681
|
-
...(stringOrUndefined(options.service) ? { service: options.service } : {}),
|
|
1682
|
-
...(stringOrUndefined(options.runtime) ? { runtime: options.runtime } : {}),
|
|
1683
|
-
...(stringOrUndefined(options.platform) ? { platform: options.platform } : {}),
|
|
1684
|
-
...traceMetadata(options.trace),
|
|
1685
|
-
...releaseArtifactMetadata(frame),
|
|
1686
|
-
...(options.includeErrorStack === true && details.stack ? { errorStack: details.stack } : {})
|
|
1687
|
-
};
|
|
1688
|
-
|
|
1689
|
-
const exception = createIssueException(
|
|
1690
|
-
boundedIssueExceptionType(details.name),
|
|
1691
|
-
stringOrUndefined(options.mechanism) ?? "javascript.error",
|
|
1692
|
-
options.handled === undefined ? true : options.handled
|
|
1693
|
-
);
|
|
1694
|
-
const exceptionChain = createJavaScriptExceptionChain(error, {
|
|
1695
|
-
details,
|
|
1696
|
-
exception,
|
|
1697
|
-
stackEvidence,
|
|
1698
|
-
debugIdMap: options.debugIdMap
|
|
1699
|
-
});
|
|
1700
|
-
return {
|
|
1701
|
-
title: stringOrUndefined(options.title) ?? details.name,
|
|
1702
|
-
level: normalizeSeverity("issue level", options.level ?? "error"),
|
|
1703
|
-
...(stringOrUndefined(options.message) ? { message: options.message } : details.message ? { message: details.message } : {}),
|
|
1704
|
-
exception,
|
|
1705
|
-
exceptionChain,
|
|
1706
|
-
...(stackFrames.length > 0 ? { stackFrames } : {}),
|
|
1707
|
-
...(options.evidence === undefined ? {} : { evidence: validateIssueEvidence(options.evidence) }),
|
|
1708
|
-
metadata: compactMetadata(metadata)
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
function createJavaScriptExceptionChain(error, root) {
|
|
1713
|
-
const state = {
|
|
1714
|
-
entries: [],
|
|
1715
|
-
seen: new Set(),
|
|
1716
|
-
truncated: false,
|
|
1717
|
-
debugIdMap: root.debugIdMap
|
|
1718
|
-
};
|
|
1719
|
-
if (isObjectLike(error)) {
|
|
1720
|
-
state.seen.add(error);
|
|
1721
|
-
}
|
|
1722
|
-
appendJavaScriptExceptionNode(error, undefined, "reported", state, root);
|
|
1723
|
-
collectJavaScriptExceptionChildren(error, 0, state);
|
|
1724
|
-
return { entries: state.entries, truncated: state.truncated };
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
function appendJavaScriptExceptionNode(value, parentId, relationship, state, known = undefined) {
|
|
1728
|
-
if (state.entries.length >= 8) {
|
|
1729
|
-
state.truncated = true;
|
|
1730
|
-
return undefined;
|
|
1731
|
-
}
|
|
1732
|
-
const details = known?.details ?? errorDetails(value);
|
|
1733
|
-
const stackEvidence = known?.stackEvidence
|
|
1734
|
-
?? javascriptStackEvidence(details.stack, state.debugIdMap);
|
|
1735
|
-
const mechanism = known?.exception?.mechanism ?? {
|
|
1736
|
-
type: relationship === "aggregate_member" ? "javascript.aggregate_member" : "javascript.cause",
|
|
1737
|
-
handled: true
|
|
1738
|
-
};
|
|
1739
|
-
const messageEvidence = exceptionMessageEvidence(details.message);
|
|
1740
|
-
const id = state.entries.length;
|
|
1741
|
-
state.entries.push({
|
|
1742
|
-
id,
|
|
1743
|
-
...(parentId === undefined ? {} : { parentId }),
|
|
1744
|
-
relationship,
|
|
1745
|
-
type: known?.exception?.type ?? errorCauseType(value),
|
|
1746
|
-
...messageEvidence,
|
|
1747
|
-
mechanism,
|
|
1748
|
-
...(stackEvidence.frames.length === 0 ? {} : { stackFrames: stackEvidence.frames }),
|
|
1749
|
-
stackFramesState: stackEvidence.frames.length === 0
|
|
1750
|
-
? "not_captured"
|
|
1751
|
-
: stackEvidence.truncated
|
|
1752
|
-
? "truncated"
|
|
1753
|
-
: "captured"
|
|
1754
|
-
});
|
|
1755
|
-
return id;
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
function collectJavaScriptExceptionChildren(value, parentId, state) {
|
|
1759
|
-
if (!isObjectLike(value)) {
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
const cause = safeProperty(value, "cause");
|
|
1763
|
-
if (cause.available && cause.value !== undefined && cause.value !== null) {
|
|
1764
|
-
collectJavaScriptExceptionChild(cause.value, parentId, "cause", state);
|
|
1765
|
-
} else if (!cause.available) {
|
|
1766
|
-
state.truncated = true;
|
|
1767
|
-
}
|
|
1768
|
-
const errors = safeProperty(value, "errors");
|
|
1769
|
-
if (!errors.available) {
|
|
1770
|
-
state.truncated = true;
|
|
1771
|
-
} else if (Array.isArray(errors.value)) {
|
|
1772
|
-
for (const child of errors.value) {
|
|
1773
|
-
collectJavaScriptExceptionChild(child, parentId, "aggregate_member", state);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
function collectJavaScriptExceptionChild(value, parentId, relationship, state) {
|
|
1779
|
-
if (state.entries.length >= 8) {
|
|
1780
|
-
state.truncated = true;
|
|
1781
|
-
return;
|
|
1782
|
-
}
|
|
1783
|
-
if (isObjectLike(value)) {
|
|
1784
|
-
if (state.seen.has(value)) {
|
|
1785
|
-
state.truncated = true;
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
state.seen.add(value);
|
|
1789
|
-
}
|
|
1790
|
-
const id = appendJavaScriptExceptionNode(value, parentId, relationship, state);
|
|
1791
|
-
if (id !== undefined) {
|
|
1792
|
-
collectJavaScriptExceptionChildren(value, id, state);
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
function exceptionMessageEvidence(value) {
|
|
1797
|
-
if (typeof value !== "string") {
|
|
1798
|
-
return { messageState: "not_captured" };
|
|
1799
|
-
}
|
|
1800
|
-
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
1801
|
-
if (normalized === "") {
|
|
1802
|
-
return { messageState: "not_captured" };
|
|
1803
|
-
}
|
|
1804
|
-
return { messageState: "redacted" };
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
function errorDetails(error) {
|
|
1808
|
-
if (error instanceof Error) {
|
|
1809
|
-
const name = safeProperty(error, "name");
|
|
1810
|
-
const message = safeProperty(error, "message");
|
|
1811
|
-
const stack = safeProperty(error, "stack");
|
|
1812
|
-
return {
|
|
1813
|
-
name: name.available ? stringOrUndefined(name.value) ?? "Error" : "Error",
|
|
1814
|
-
message: message.available ? stringOrUndefined(message.value) : undefined,
|
|
1815
|
-
stack: stack.available && typeof stack.value === "string" && stack.value.trim() !== ""
|
|
1816
|
-
? stack.value
|
|
1817
|
-
: undefined
|
|
1818
|
-
};
|
|
1819
|
-
}
|
|
1820
|
-
if (error && typeof error === "object") {
|
|
1821
|
-
const name = safeProperty(error, "name");
|
|
1822
|
-
const message = safeProperty(error, "message");
|
|
1823
|
-
const stack = safeProperty(error, "stack");
|
|
1824
|
-
return {
|
|
1825
|
-
name: name.available && typeof name.value === "string" && name.value.trim() !== ""
|
|
1826
|
-
? name.value
|
|
1827
|
-
: "Error",
|
|
1828
|
-
message: message.available && typeof message.value === "string" && message.value.trim() !== ""
|
|
1829
|
-
? message.value
|
|
1830
|
-
: undefined,
|
|
1831
|
-
stack: stack.available && typeof stack.value === "string" && stack.value.trim() !== ""
|
|
1832
|
-
? stack.value
|
|
1833
|
-
: undefined
|
|
1834
|
-
};
|
|
1835
|
-
}
|
|
1836
|
-
if (typeof error === "string" && error.trim() !== "") {
|
|
1837
|
-
return { name: "Error", message: error };
|
|
1838
|
-
}
|
|
1839
|
-
return { name: "Error" };
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
function safeProperty(value, name) {
|
|
1843
|
-
try {
|
|
1844
|
-
return { available: true, value: value[name] };
|
|
1845
|
-
} catch {
|
|
1846
|
-
return { available: false, value: undefined };
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
function boundedIssueExceptionType(value) {
|
|
1851
|
-
const normalized = typeof value === "string" ? value.trim() : "";
|
|
1852
|
-
const characters = Array.from(normalized);
|
|
1853
|
-
if (
|
|
1854
|
-
normalized === ""
|
|
1855
|
-
|| characters.length > 256
|
|
1856
|
-
|| /[?#]/u.test(normalized)
|
|
1857
|
-
|| characters.some((character) => {
|
|
1858
|
-
const code = character.codePointAt(0);
|
|
1859
|
-
return code !== undefined && (code <= 31 || (code >= 127 && code <= 159));
|
|
1860
|
-
})
|
|
1861
|
-
) {
|
|
1862
|
-
return "Error";
|
|
1863
|
-
}
|
|
1864
|
-
return normalized;
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
function issueGroupingMetadata(source, details, frame, fingerprint) {
|
|
1868
|
-
const groupingKey = frame
|
|
1869
|
-
? `${source}:${details.name}:${frame.filename}`
|
|
1870
|
-
: `${source}:${details.name}`;
|
|
1871
|
-
const explicitFingerprint = issueFingerprintOrUndefined(fingerprint);
|
|
1872
|
-
return {
|
|
1873
|
-
issueGroupingKey: groupingKey,
|
|
1874
|
-
issueGroupingSource: explicitFingerprint ? "explicit_fingerprint" : frame ? "error_type_and_frame" : "error_type",
|
|
1875
|
-
...(explicitFingerprint ? { issueFingerprint: explicitFingerprint } : {})
|
|
1876
|
-
};
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
function issueFingerprintOrUndefined(value) {
|
|
1880
|
-
if (value === undefined || value === null) {
|
|
1881
|
-
return undefined;
|
|
1882
|
-
}
|
|
1883
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
1884
|
-
throw new SdkError("validation_error", "issue fingerprint must be a non-empty string");
|
|
1885
|
-
}
|
|
1886
|
-
return value.trim();
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
function errorCauseMetadata(error) {
|
|
1890
|
-
const state = {
|
|
1891
|
-
items: [],
|
|
1892
|
-
seen: new Set(),
|
|
1893
|
-
sawExceptionGroup: false,
|
|
1894
|
-
truncated: false
|
|
1895
|
-
};
|
|
1896
|
-
if (isObjectLike(error)) {
|
|
1897
|
-
state.seen.add(error);
|
|
1898
|
-
collectNestedErrorCauses(error, state);
|
|
1899
|
-
}
|
|
1900
|
-
if (state.items.length === 0) {
|
|
1901
|
-
return {};
|
|
1902
|
-
}
|
|
1903
|
-
return {
|
|
1904
|
-
errorCauseCount: state.items.length,
|
|
1905
|
-
errorCauseTypes: state.items.map((item) => item.type).join(","),
|
|
1906
|
-
errorCauseSources: state.items.map((item) => item.source).join(","),
|
|
1907
|
-
...(state.sawExceptionGroup ? { errorExceptionGroup: true } : {}),
|
|
1908
|
-
...(state.truncated ? { errorCauseTruncated: true } : {})
|
|
1909
|
-
};
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
function collectNestedErrorCauses(parent, state) {
|
|
1913
|
-
if (!isObjectLike(parent)) {
|
|
1914
|
-
return;
|
|
1915
|
-
}
|
|
1916
|
-
const cause = safeProperty(parent, "cause");
|
|
1917
|
-
if (!cause.available) {
|
|
1918
|
-
state.truncated = true;
|
|
1919
|
-
} else if (cause.value !== undefined && cause.value !== null) {
|
|
1920
|
-
collectErrorCause(cause.value, "cause", state);
|
|
1921
|
-
}
|
|
1922
|
-
const errors = safeProperty(parent, "errors");
|
|
1923
|
-
if (!errors.available) {
|
|
1924
|
-
state.truncated = true;
|
|
1925
|
-
} else if (Array.isArray(errors.value)) {
|
|
1926
|
-
state.sawExceptionGroup = true;
|
|
1927
|
-
for (const [index, child] of errors.value.entries()) {
|
|
1928
|
-
collectErrorCause(child, `errors[${index}]`, state);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
function collectErrorCause(value, source, state) {
|
|
1934
|
-
if (value === undefined || value === null) {
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
if (state.items.length >= MAX_ERROR_CAUSES) {
|
|
1938
|
-
state.truncated = true;
|
|
1939
|
-
return;
|
|
1940
|
-
}
|
|
1941
|
-
if (isObjectLike(value)) {
|
|
1942
|
-
if (state.seen.has(value)) {
|
|
1943
|
-
state.truncated = true;
|
|
1944
|
-
return;
|
|
1945
|
-
}
|
|
1946
|
-
state.seen.add(value);
|
|
1947
|
-
}
|
|
1948
|
-
state.items.push({
|
|
1949
|
-
source,
|
|
1950
|
-
type: errorCauseType(value)
|
|
1951
|
-
});
|
|
1952
|
-
collectNestedErrorCauses(value, state);
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
function errorCauseType(value) {
|
|
1956
|
-
if (isObjectLike(value)) {
|
|
1957
|
-
const constructor = safeProperty(value, "constructor");
|
|
1958
|
-
const constructorNameValue = constructor.available && isObjectLike(constructor.value)
|
|
1959
|
-
? safeProperty(constructor.value, "name")
|
|
1960
|
-
: { available: false, value: undefined };
|
|
1961
|
-
const constructorName = constructorNameValue.available
|
|
1962
|
-
? safeCauseTypeName(constructorNameValue.value)
|
|
1963
|
-
: undefined;
|
|
1964
|
-
if (value instanceof Error) {
|
|
1965
|
-
if (constructorName && constructorName !== "Error") {
|
|
1966
|
-
return constructorName;
|
|
1967
|
-
}
|
|
1968
|
-
const name = safeProperty(value, "name");
|
|
1969
|
-
const builtinName = name.available && BUILTIN_ERROR_NAMES.has(name.value)
|
|
1970
|
-
? name.value
|
|
1971
|
-
: undefined;
|
|
1972
|
-
return builtinName ?? constructorName ?? "Error";
|
|
1973
|
-
}
|
|
1974
|
-
return constructorName ?? "Object";
|
|
1975
|
-
}
|
|
1976
|
-
return "NonError";
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
function safeCauseTypeName(value) {
|
|
1980
|
-
return typeof value === "string" && /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/u.test(value) ? value : undefined;
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
function isObjectLike(value) {
|
|
1984
|
-
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
function traceMetadata(trace) {
|
|
1988
|
-
if (trace === undefined || trace === null) {
|
|
1989
|
-
return {};
|
|
1990
|
-
}
|
|
1991
|
-
const normalized = normalizeLogTraceContext(trace);
|
|
1992
|
-
if (!normalized) {
|
|
1993
|
-
return {};
|
|
1994
|
-
}
|
|
1995
|
-
return {
|
|
1996
|
-
traceId: normalized.traceId,
|
|
1997
|
-
spanId: normalized.spanId,
|
|
1998
|
-
...(normalized.parentSpanId ? { parentSpanId: normalized.parentSpanId } : {}),
|
|
1999
|
-
...(normalized.sampled !== undefined ? { sampled: normalized.sampled } : {})
|
|
2000
|
-
};
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
function releaseArtifactMetadata(frame) {
|
|
2004
|
-
if (!frame?.debugId) {
|
|
2005
|
-
return {};
|
|
2006
|
-
}
|
|
2007
|
-
return {
|
|
2008
|
-
releaseArtifactType: "sourcemap",
|
|
2009
|
-
releaseArtifactCodeFile: frame.filename,
|
|
2010
|
-
releaseArtifactDebugId: frame.debugId
|
|
2011
|
-
};
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
function logbrewLevelFromConsoleMethod(method) {
|
|
2015
|
-
switch (method) {
|
|
2016
|
-
case "debug":
|
|
2017
|
-
return "info";
|
|
2018
|
-
case "warn":
|
|
2019
|
-
return "warning";
|
|
2020
|
-
case "error":
|
|
2021
|
-
return "error";
|
|
2022
|
-
case "info":
|
|
2023
|
-
case "log":
|
|
2024
|
-
return "info";
|
|
2025
|
-
default:
|
|
2026
|
-
throw new SdkError("validation_error", `console method must be one of: ${Array.from(CONSOLE_METHODS).join(", ")}`);
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
function createProductActionAttributes(action, options = {}) {
|
|
2031
|
-
const details = productActionDetails(action);
|
|
2032
|
-
const routeTemplate = sanitizeRouteTemplate(details.routeTemplate);
|
|
2033
|
-
return {
|
|
2034
|
-
name: details.name,
|
|
2035
|
-
status: details.status,
|
|
2036
|
-
metadata: compactMetadata({
|
|
2037
|
-
source: "product.action",
|
|
2038
|
-
...compactMetadata(options.metadata),
|
|
2039
|
-
...compactMetadata(details.metadata),
|
|
2040
|
-
routeTemplate,
|
|
2041
|
-
sessionId: stringOrUndefined(details.sessionId),
|
|
2042
|
-
traceId: stringOrUndefined(details.traceId),
|
|
2043
|
-
screen: stringOrUndefined(details.screen),
|
|
2044
|
-
funnel: stringOrUndefined(details.funnel),
|
|
2045
|
-
step: stringOrUndefined(details.step),
|
|
2046
|
-
...productAnalyticsMetadata("interaction", routeTemplate || details.screen)
|
|
2047
|
-
})
|
|
2048
|
-
};
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
function createNetworkMilestoneAttributes(request, options = {}) {
|
|
2052
|
-
const details = networkMilestoneDetails(request);
|
|
2053
|
-
return {
|
|
2054
|
-
name: details.name,
|
|
2055
|
-
status: details.status,
|
|
2056
|
-
metadata: compactMetadata({
|
|
2057
|
-
source: "network.milestone",
|
|
2058
|
-
...compactMetadata(options.metadata),
|
|
2059
|
-
...compactMetadata(details.metadata),
|
|
2060
|
-
routeTemplate: details.routeTemplate,
|
|
2061
|
-
method: details.method,
|
|
2062
|
-
statusCode: details.statusCode,
|
|
2063
|
-
durationMs: details.durationMs,
|
|
2064
|
-
sessionId: stringOrUndefined(details.sessionId),
|
|
2065
|
-
traceId: stringOrUndefined(details.traceId)
|
|
2066
|
-
})
|
|
2067
|
-
};
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
const {
|
|
2071
|
-
createLogBrewOpenTelemetrySpanExporter,
|
|
2072
|
-
createLogBrewOpenTelemetrySpanProcessor,
|
|
2073
|
-
logbrewTraceContextFromCurrentOpenTelemetrySpan,
|
|
2074
|
-
logbrewTraceContextFromOpenTelemetrySpan,
|
|
2075
|
-
logbrewTraceContextFromOpenTelemetrySpanContext,
|
|
2076
|
-
spanAttributesFromOpenTelemetryReadableSpan
|
|
2077
|
-
} = buildOpenTelemetryHelpers({
|
|
2078
|
-
compactMetadata,
|
|
2079
|
-
isMetadataValue,
|
|
2080
|
-
LogBrewClient,
|
|
2081
|
-
maxSpanEvents: MAX_SPAN_EVENTS,
|
|
2082
|
-
maxSpanLinks: MAX_SPAN_LINKS,
|
|
2083
|
-
requireNonEmpty,
|
|
2084
|
-
requireSpanId,
|
|
2085
|
-
requireTraceId,
|
|
2086
|
-
SdkError,
|
|
2087
|
-
stringOrUndefined
|
|
2088
|
-
});
|
|
2089
|
-
|
|
2090
|
-
function spanAttributesFromTraceparent(traceparent, attributes) {
|
|
2091
|
-
if (!attributes || Array.isArray(attributes) || typeof attributes !== "object") {
|
|
2092
|
-
throw new SdkError("validation_error", "span attributes must be an object");
|
|
2093
|
-
}
|
|
2094
|
-
const context = parseTraceparent(traceparent);
|
|
2095
|
-
requireNonEmpty("span name", attributes.name);
|
|
2096
|
-
requireSpanId("spanId", attributes.spanId);
|
|
2097
|
-
requireAllowedValue("span status", attributes.status, SPAN_STATUSES);
|
|
2098
|
-
if (attributes.durationMs !== undefined) {
|
|
2099
|
-
if (typeof attributes.durationMs !== "number" || Number.isNaN(attributes.durationMs) || attributes.durationMs < 0) {
|
|
2100
|
-
throw new SdkError("validation_error", "span durationMs must be non-negative");
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
const events = validateSpanEvents(attributes.events);
|
|
2104
|
-
const links = validateSpanLinks(attributes.links);
|
|
2105
|
-
|
|
2106
|
-
return {
|
|
2107
|
-
name: attributes.name,
|
|
2108
|
-
traceId: context.traceId,
|
|
2109
|
-
spanId: attributes.spanId.toLowerCase(),
|
|
2110
|
-
parentSpanId: context.parentSpanId,
|
|
2111
|
-
status: attributes.status,
|
|
2112
|
-
...(attributes.durationMs !== undefined ? { durationMs: attributes.durationMs } : {}),
|
|
2113
|
-
...(events !== undefined ? { events } : {}),
|
|
2114
|
-
...(links !== undefined ? { links } : {}),
|
|
2115
|
-
...(attributes.metadata !== undefined ? { metadata: compactMetadata(attributes.metadata) } : {})
|
|
2116
|
-
};
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
function createLogBrewPinoDestination(config) {
|
|
2120
|
-
if (!config || typeof config !== "object") {
|
|
2121
|
-
throw new SdkError("validation_error", "Pino destination config must be an object");
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
const client = config.client;
|
|
2125
|
-
if (!(client instanceof LogBrewClient)) {
|
|
2126
|
-
throw new SdkError("validation_error", "Pino destination client must be a LogBrewClient");
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
const transport = config.transport;
|
|
2130
|
-
const flushOnWrite = config.flushOnWrite === true;
|
|
2131
|
-
const includeErrorStack = config.includeErrorStack === true;
|
|
2132
|
-
const logger = config.logger ?? "pino";
|
|
2133
|
-
const metadata = compactMetadata(config.metadata);
|
|
2134
|
-
const timestamp = typeof config.timestamp === "function"
|
|
2135
|
-
? config.timestamp
|
|
2136
|
-
: () => new Date().toISOString();
|
|
2137
|
-
const eventIdPrefix = config.eventIdPrefix ?? "pino";
|
|
2138
|
-
const onError = typeof config.onError === "function" ? config.onError : () => {};
|
|
2139
|
-
const traceProvider = typeof config.traceProvider === "function" ? config.traceProvider : null;
|
|
2140
|
-
const state = {
|
|
2141
|
-
captured: 0,
|
|
2142
|
-
pendingFlush: Promise.resolve(null)
|
|
2143
|
-
};
|
|
2144
|
-
|
|
2145
|
-
return {
|
|
2146
|
-
write(chunk) {
|
|
2147
|
-
const lines = String(chunk).split(/\r?\n/u).filter((line) => line.trim() !== "");
|
|
2148
|
-
for (const line of lines) {
|
|
2149
|
-
try {
|
|
2150
|
-
const record = JSON.parse(line);
|
|
2151
|
-
state.captured += 1;
|
|
2152
|
-
client.log(
|
|
2153
|
-
`${eventIdPrefix}_${state.captured}`,
|
|
2154
|
-
timestampFromPinoRecord(record, timestamp),
|
|
2155
|
-
logAttributesFromPinoRecord(record, {
|
|
2156
|
-
includeErrorStack,
|
|
2157
|
-
logger,
|
|
2158
|
-
metadata,
|
|
2159
|
-
trace: traceFromProvider(traceProvider, onError)
|
|
2160
|
-
})
|
|
2161
|
-
);
|
|
2162
|
-
if (flushOnWrite && transport) {
|
|
2163
|
-
state.pendingFlush = Promise.resolve(client.flush(transport)).catch((error) => {
|
|
2164
|
-
onError(error);
|
|
2165
|
-
return null;
|
|
2166
|
-
});
|
|
2167
|
-
}
|
|
2168
|
-
} catch (error) {
|
|
2169
|
-
onError(error);
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
return true;
|
|
2173
|
-
},
|
|
2174
|
-
async flush() {
|
|
2175
|
-
if (transport && client.pendingEvents() > 0) {
|
|
2176
|
-
state.pendingFlush = Promise.resolve(client.flush(transport)).catch((error) => {
|
|
2177
|
-
onError(error);
|
|
2178
|
-
return null;
|
|
2179
|
-
});
|
|
2180
|
-
}
|
|
2181
|
-
return state.pendingFlush;
|
|
2182
|
-
},
|
|
2183
|
-
end() {
|
|
2184
|
-
return this.flush();
|
|
2185
|
-
}
|
|
2186
|
-
};
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
function logAttributesFromPinoRecord(record, options = {}) {
|
|
2190
|
-
if (!record || Array.isArray(record) || typeof record !== "object") {
|
|
2191
|
-
throw new SdkError("validation_error", "Pino record must be an object");
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
const level = logbrewLevelFromPinoLevel(record.level);
|
|
2195
|
-
const metadata = {
|
|
2196
|
-
...compactMetadata(options.metadata),
|
|
2197
|
-
pinoLevel: pinoLevelLabel(record.level),
|
|
2198
|
-
...pinoContextMetadata(record),
|
|
2199
|
-
...traceMetadataFromLogContext(options.trace)
|
|
2200
|
-
};
|
|
2201
|
-
if (typeof record.level === "number" && Number.isFinite(record.level)) {
|
|
2202
|
-
metadata.pinoLevelNumber = record.level;
|
|
2203
|
-
}
|
|
2204
|
-
addPinoErrorMetadata(metadata, record.err ?? record.error, options.includeErrorStack === true);
|
|
2205
|
-
|
|
2206
|
-
return {
|
|
2207
|
-
message: pinoMessage(record),
|
|
2208
|
-
level,
|
|
2209
|
-
...(options.logger ? { logger: options.logger } : {}),
|
|
2210
|
-
metadata
|
|
2211
|
-
};
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
function timestampFromPinoRecord(record, fallbackTimestamp) {
|
|
2215
|
-
const value = record?.time ?? record?.timestamp;
|
|
2216
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2217
|
-
return new Date(value).toISOString();
|
|
2218
|
-
}
|
|
2219
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
2220
|
-
const parsed = new Date(value);
|
|
2221
|
-
if (!Number.isNaN(parsed.valueOf())) {
|
|
2222
|
-
return parsed.toISOString();
|
|
2223
|
-
}
|
|
2224
|
-
return value;
|
|
2225
|
-
}
|
|
2226
|
-
return fallbackTimestamp();
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
function logbrewLevelFromPinoLevel(level) {
|
|
2230
|
-
if (typeof level === "number" && Number.isFinite(level)) {
|
|
2231
|
-
if (level >= 60) {
|
|
2232
|
-
return "critical";
|
|
2233
|
-
}
|
|
2234
|
-
if (level >= 50) {
|
|
2235
|
-
return "error";
|
|
2236
|
-
}
|
|
2237
|
-
if (level >= 40) {
|
|
2238
|
-
return "warning";
|
|
2239
|
-
}
|
|
2240
|
-
if (level >= 30) {
|
|
2241
|
-
return "info";
|
|
2242
|
-
}
|
|
2243
|
-
return "info";
|
|
2244
|
-
}
|
|
2245
|
-
|
|
2246
|
-
switch (String(level).toLowerCase()) {
|
|
2247
|
-
case "trace":
|
|
2248
|
-
case "debug":
|
|
2249
|
-
return "info";
|
|
2250
|
-
case "warn":
|
|
2251
|
-
case "warning":
|
|
2252
|
-
return "warning";
|
|
2253
|
-
case "error":
|
|
2254
|
-
return "error";
|
|
2255
|
-
case "fatal":
|
|
2256
|
-
case "critical":
|
|
2257
|
-
return "critical";
|
|
2258
|
-
case "info":
|
|
2259
|
-
default:
|
|
2260
|
-
return "info";
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
function pinoLevelLabel(level) {
|
|
2265
|
-
if (typeof level === "string" && level.trim() !== "") {
|
|
2266
|
-
return level;
|
|
2267
|
-
}
|
|
2268
|
-
switch (level) {
|
|
2269
|
-
case 10:
|
|
2270
|
-
return "trace";
|
|
2271
|
-
case 20:
|
|
2272
|
-
return "debug";
|
|
2273
|
-
case 30:
|
|
2274
|
-
return "info";
|
|
2275
|
-
case 40:
|
|
2276
|
-
return "warn";
|
|
2277
|
-
case 50:
|
|
2278
|
-
return "error";
|
|
2279
|
-
case 60:
|
|
2280
|
-
return "fatal";
|
|
2281
|
-
default:
|
|
2282
|
-
return typeof level === "number" && Number.isFinite(level) ? String(level) : "info";
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
function pinoMessage(record) {
|
|
2287
|
-
if (typeof record.msg === "string" && record.msg.trim() !== "") {
|
|
2288
|
-
return record.msg;
|
|
2289
|
-
}
|
|
2290
|
-
if (typeof record.message === "string" && record.message.trim() !== "") {
|
|
2291
|
-
return record.message;
|
|
2292
|
-
}
|
|
2293
|
-
const error = record.err ?? record.error;
|
|
2294
|
-
if (error && typeof error === "object" && typeof error.message === "string" && error.message.trim() !== "") {
|
|
2295
|
-
return error.message;
|
|
2296
|
-
}
|
|
2297
|
-
return "pino event";
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
function pinoContextMetadata(record) {
|
|
2301
|
-
const metadata = {};
|
|
2302
|
-
for (const [key, value] of Object.entries(record)) {
|
|
2303
|
-
if (!PINO_RESERVED_FIELDS.has(key) && !isSensitivePinoContextField(key) && isMetadataValue(value)) {
|
|
2304
|
-
metadata[`context.${key}`] = value;
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
return metadata;
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
function isSensitivePinoContextField(key) {
|
|
2311
|
-
const normalized = key.toLowerCase().replace(/[^a-z0-9]+/gu, "");
|
|
2312
|
-
return (
|
|
2313
|
-
PINO_SENSITIVE_CONTEXT_FIELDS.has(normalized)
|
|
2314
|
-
|| normalized.endsWith("accesskey")
|
|
2315
|
-
|| normalized.endsWith("apikey")
|
|
2316
|
-
|| normalized.endsWith("authorization")
|
|
2317
|
-
|| normalized.endsWith("body")
|
|
2318
|
-
|| normalized.endsWith("cookie")
|
|
2319
|
-
|| normalized.endsWith("credentials")
|
|
2320
|
-
|| normalized.endsWith("headers")
|
|
2321
|
-
|| normalized.endsWith("password")
|
|
2322
|
-
|| normalized.endsWith("payload")
|
|
2323
|
-
|| normalized.endsWith("privatekey")
|
|
2324
|
-
|| normalized.endsWith("query")
|
|
2325
|
-
|| normalized.endsWith("secret")
|
|
2326
|
-
|| normalized.endsWith("stack")
|
|
2327
|
-
|| normalized.endsWith("token")
|
|
2328
|
-
|| normalized.endsWith("url")
|
|
2329
|
-
);
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
function addPinoErrorMetadata(metadata, error, includeErrorStack) {
|
|
2333
|
-
if (!error) {
|
|
2334
|
-
return;
|
|
2335
|
-
}
|
|
2336
|
-
if (error instanceof Error) {
|
|
2337
|
-
metadata.errorName = error.name || "Error";
|
|
2338
|
-
if (error.message) {
|
|
2339
|
-
metadata.errorMessage = error.message;
|
|
2340
|
-
}
|
|
2341
|
-
if (includeErrorStack && error.stack) {
|
|
2342
|
-
metadata.errorStack = error.stack;
|
|
2343
|
-
}
|
|
2344
|
-
return;
|
|
2345
|
-
}
|
|
2346
|
-
if (typeof error === "object") {
|
|
2347
|
-
const name = error.type ?? error.name;
|
|
2348
|
-
const message = error.message;
|
|
2349
|
-
const stack = error.stack;
|
|
2350
|
-
if (typeof name === "string" && name.trim() !== "") {
|
|
2351
|
-
metadata.errorName = name;
|
|
2352
|
-
}
|
|
2353
|
-
if (typeof message === "string" && message.trim() !== "") {
|
|
2354
|
-
metadata.errorMessage = message;
|
|
2355
|
-
}
|
|
2356
|
-
if (includeErrorStack && typeof stack === "string" && stack.trim() !== "") {
|
|
2357
|
-
metadata.errorStack = stack;
|
|
2358
|
-
}
|
|
2359
|
-
return;
|
|
2360
|
-
}
|
|
2361
|
-
if (typeof error === "string" && error.trim() !== "") {
|
|
2362
|
-
metadata.errorMessage = error;
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
function cloneMetadata(metadata) {
|
|
2367
|
-
if (metadata === undefined) {
|
|
2368
|
-
return undefined;
|
|
2369
|
-
}
|
|
2370
|
-
if (!metadata || Array.isArray(metadata) || typeof metadata !== "object") {
|
|
2371
|
-
throw new SdkError("validation_error", "metadata must be an object");
|
|
2372
|
-
}
|
|
2373
|
-
return { ...metadata };
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
function cloneSpanEvents(events) {
|
|
2377
|
-
return events.map((event) => event.metadata === undefined
|
|
2378
|
-
? { ...event }
|
|
2379
|
-
: { ...event, metadata: { ...event.metadata } });
|
|
2380
|
-
}
|
|
2381
|
-
|
|
2382
|
-
function cloneSpanLinks(links) {
|
|
2383
|
-
return links.map((link) => link.metadata === undefined
|
|
2384
|
-
? { ...link }
|
|
2385
|
-
: { ...link, metadata: { ...link.metadata } });
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
function cloneEvent(event) {
|
|
2389
|
-
const attributes = { ...event.attributes, ...cloneIssueDiagnostics(event.attributes) };
|
|
2390
|
-
if (Array.isArray(event.attributes.nativeStackFrames)) {
|
|
2391
|
-
attributes.nativeStackFrames = event.attributes.nativeStackFrames.map((frame) => ({ ...frame }));
|
|
2392
|
-
}
|
|
2393
|
-
if (event.attributes.metadata !== undefined) {
|
|
2394
|
-
attributes.metadata = { ...event.attributes.metadata };
|
|
2395
|
-
}
|
|
2396
|
-
if (Array.isArray(event.attributes.events)) {
|
|
2397
|
-
attributes.events = cloneSpanEvents(event.attributes.events);
|
|
2398
|
-
}
|
|
2399
|
-
if (Array.isArray(event.attributes.links)) {
|
|
2400
|
-
attributes.links = cloneSpanLinks(event.attributes.links);
|
|
2401
|
-
}
|
|
2402
|
-
if (event.attributes.context !== undefined) {
|
|
2403
|
-
attributes.context = cloneTelemetryContext(event.attributes.context);
|
|
2404
|
-
}
|
|
2405
|
-
return { ...event, attributes };
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
function validateRelease(attributes) {
|
|
2409
|
-
requireNonEmpty("release version", attributes.version);
|
|
2410
|
-
if (attributes.commit !== undefined) {
|
|
2411
|
-
requireNonEmpty("release commit", attributes.commit);
|
|
2412
|
-
}
|
|
2413
|
-
return withMetadata({
|
|
2414
|
-
version: attributes.version,
|
|
2415
|
-
...(attributes.commit ? { commit: attributes.commit } : {}),
|
|
2416
|
-
...(attributes.notes !== undefined ? { notes: attributes.notes } : {})
|
|
2417
|
-
}, attributes.metadata, attributes.context);
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
function validateEnvironment(attributes) {
|
|
2421
|
-
requireNonEmpty("environment name", attributes.name);
|
|
2422
|
-
return withMetadata({
|
|
2423
|
-
name: attributes.name,
|
|
2424
|
-
...(attributes.region !== undefined ? { region: attributes.region } : {})
|
|
2425
|
-
}, attributes.metadata, attributes.context);
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
function validateIssue(attributes) {
|
|
2429
|
-
requireNonEmpty("issue title", attributes.title);
|
|
2430
|
-
const level = normalizeSeverity("issue level", attributes.level);
|
|
2431
|
-
const stackFrames = validateIssueStackFrames(attributes.stackFrames);
|
|
2432
|
-
const nativeStackFrames = validateNativeStackFrames(attributes.nativeStackFrames);
|
|
2433
|
-
return withMetadata({
|
|
2434
|
-
title: attributes.title,
|
|
2435
|
-
level,
|
|
2436
|
-
...(attributes.message !== undefined ? { message: attributes.message } : {}),
|
|
2437
|
-
...(stackFrames !== undefined ? { stackFrames } : {}),
|
|
2438
|
-
...(nativeStackFrames !== undefined ? { nativeStackFrames } : {}),
|
|
2439
|
-
...validateIssueDiagnostics(attributes)
|
|
2440
|
-
}, attributes.metadata, attributes.context);
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
|
-
function validateLog(attributes) {
|
|
2444
|
-
requireNonEmpty("log message", attributes.message);
|
|
2445
|
-
const level = normalizeSeverity("log level", attributes.level);
|
|
2446
|
-
return withMetadata({
|
|
2447
|
-
message: attributes.message,
|
|
2448
|
-
level,
|
|
2449
|
-
...(attributes.logger !== undefined ? { logger: attributes.logger } : {})
|
|
2450
|
-
}, attributes.metadata, attributes.context);
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
function normalizeSeverity(label, value) {
|
|
2454
|
-
requireAllowedValue(label, value, SEVERITY_VALUES);
|
|
2455
|
-
return SEVERITY_ALIASES.get(value);
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
function validateSpan(attributes) {
|
|
2459
|
-
requireNonEmpty("span name", attributes.name);
|
|
2460
|
-
requireNonEmpty("span traceId", attributes.traceId);
|
|
2461
|
-
requireNonEmpty("span spanId", attributes.spanId);
|
|
2462
|
-
requireAllowedValue("span status", attributes.status, SPAN_STATUSES);
|
|
2463
|
-
if (attributes.parentSpanId !== undefined) {
|
|
2464
|
-
requireNonEmpty("span parentSpanId", attributes.parentSpanId);
|
|
2465
|
-
}
|
|
2466
|
-
if (attributes.durationMs !== undefined) {
|
|
2467
|
-
if (typeof attributes.durationMs !== "number" || Number.isNaN(attributes.durationMs) || attributes.durationMs < 0) {
|
|
2468
|
-
throw new SdkError("validation_error", "span durationMs must be non-negative");
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
const events = validateSpanEvents(attributes.events);
|
|
2472
|
-
const links = validateSpanLinks(attributes.links);
|
|
2473
|
-
return withMetadata({
|
|
2474
|
-
name: attributes.name,
|
|
2475
|
-
traceId: attributes.traceId,
|
|
2476
|
-
spanId: attributes.spanId,
|
|
2477
|
-
status: attributes.status,
|
|
2478
|
-
...(attributes.parentSpanId !== undefined ? { parentSpanId: attributes.parentSpanId } : {}),
|
|
2479
|
-
...(attributes.durationMs !== undefined ? { durationMs: attributes.durationMs } : {}),
|
|
2480
|
-
...(events !== undefined ? { events } : {}),
|
|
2481
|
-
...(links !== undefined ? { links } : {})
|
|
2482
|
-
}, attributes.metadata, attributes.context);
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
function validateSpanEvents(events) {
|
|
2486
|
-
if (events === undefined) {
|
|
2487
|
-
return undefined;
|
|
2488
|
-
}
|
|
2489
|
-
if (!Array.isArray(events)) {
|
|
2490
|
-
throw new SdkError("validation_error", "span events must be an array");
|
|
2491
|
-
}
|
|
2492
|
-
if (events.length > MAX_SPAN_EVENTS) {
|
|
2493
|
-
throw new SdkError("validation_error", `span events must contain at most ${MAX_SPAN_EVENTS} entries`);
|
|
2494
|
-
}
|
|
2495
|
-
if (events.length === 0) {
|
|
2496
|
-
return undefined;
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
return events.map((event) => {
|
|
2500
|
-
if (!event || Array.isArray(event) || typeof event !== "object") {
|
|
2501
|
-
throw new SdkError("validation_error", "span event must be an object");
|
|
2502
|
-
}
|
|
2503
|
-
requireNonEmpty("span event name", event.name);
|
|
2504
|
-
if (event.timestamp !== undefined) {
|
|
2505
|
-
requireTimestamp(event.timestamp);
|
|
2506
|
-
}
|
|
2507
|
-
const summary = {
|
|
2508
|
-
name: event.name,
|
|
2509
|
-
...(event.timestamp !== undefined ? { timestamp: event.timestamp } : {})
|
|
2510
|
-
};
|
|
2511
|
-
if (event.metadata !== undefined) {
|
|
2512
|
-
const metadata = compactMetadata(event.metadata);
|
|
2513
|
-
if (Object.keys(metadata).length > 0) {
|
|
2514
|
-
summary.metadata = metadata;
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
return summary;
|
|
2518
|
-
});
|
|
2519
|
-
}
|
|
2520
|
-
|
|
2521
|
-
function validateSpanLinks(links) {
|
|
2522
|
-
if (links === undefined) {
|
|
2523
|
-
return undefined;
|
|
2524
|
-
}
|
|
2525
|
-
if (!Array.isArray(links)) {
|
|
2526
|
-
throw new SdkError("validation_error", "span links must be an array");
|
|
2527
|
-
}
|
|
2528
|
-
if (links.length > MAX_SPAN_LINKS) {
|
|
2529
|
-
throw new SdkError("validation_error", `span links must contain at most ${MAX_SPAN_LINKS} entries`);
|
|
2530
|
-
}
|
|
2531
|
-
if (links.length === 0) {
|
|
2532
|
-
return undefined;
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
return links.map((link) => {
|
|
2536
|
-
if (!link || Array.isArray(link) || typeof link !== "object") {
|
|
2537
|
-
throw new SdkError("validation_error", "span link must be an object");
|
|
2538
|
-
}
|
|
2539
|
-
requireTraceId(link.traceId);
|
|
2540
|
-
requireSpanId("span link spanId", link.spanId);
|
|
2541
|
-
if (link.sampled !== undefined && typeof link.sampled !== "boolean") {
|
|
2542
|
-
throw new SdkError("validation_error", "span link sampled must be a boolean");
|
|
2543
|
-
}
|
|
2544
|
-
const summary = {
|
|
2545
|
-
traceId: link.traceId.toLowerCase(),
|
|
2546
|
-
spanId: link.spanId.toLowerCase(),
|
|
2547
|
-
...(link.sampled !== undefined ? { sampled: link.sampled } : {})
|
|
2548
|
-
};
|
|
2549
|
-
if (link.metadata !== undefined) {
|
|
2550
|
-
const metadata = compactMetadata(link.metadata);
|
|
2551
|
-
if (Object.keys(metadata).length > 0) {
|
|
2552
|
-
summary.metadata = metadata;
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
return summary;
|
|
2556
|
-
});
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
function validateAction(attributes) {
|
|
2560
|
-
requireNonEmpty("action name", attributes.name);
|
|
2561
|
-
requireAllowedValue("action status", attributes.status, ACTION_STATUSES);
|
|
2562
|
-
return withMetadata({
|
|
2563
|
-
name: attributes.name,
|
|
2564
|
-
status: attributes.status
|
|
2565
|
-
}, attributes.metadata, attributes.context);
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
function validateMetric(attributes) {
|
|
2569
|
-
requireNonEmpty("metric name", attributes.name);
|
|
2570
|
-
requireAllowedValue("metric kind", attributes.kind, METRIC_KINDS);
|
|
2571
|
-
requireFiniteNumber("metric value", attributes.value);
|
|
2572
|
-
requireNonEmpty("metric unit", attributes.unit);
|
|
2573
|
-
|
|
2574
|
-
const allowedTemporalities = METRIC_TEMPORALITIES_BY_KIND.get(attributes.kind);
|
|
2575
|
-
requireAllowedValue(`metric temporality for ${attributes.kind}`, attributes.temporality, allowedTemporalities);
|
|
2576
|
-
if (NON_NEGATIVE_METRIC_KINDS.has(attributes.kind) && attributes.value < 0) {
|
|
2577
|
-
throw new SdkError("validation_error", `metric ${attributes.kind} value must be non-negative`);
|
|
2578
|
-
}
|
|
2579
|
-
const description = metricDescriptionOrUndefined(attributes.description);
|
|
2580
|
-
|
|
2581
|
-
return withMetadata({
|
|
2582
|
-
name: attributes.name,
|
|
2583
|
-
...(description === undefined ? {} : { description }),
|
|
2584
|
-
kind: attributes.kind,
|
|
2585
|
-
value: attributes.value,
|
|
2586
|
-
unit: attributes.unit,
|
|
2587
|
-
temporality: attributes.temporality
|
|
2588
|
-
}, attributes.metadata, attributes.context);
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
function metricDescriptionOrUndefined(value) {
|
|
2592
|
-
if (value === undefined) {
|
|
2593
|
-
return undefined;
|
|
2594
|
-
}
|
|
2595
|
-
const characters = typeof value === "string" ? Array.from(value) : [];
|
|
2596
|
-
if (
|
|
2597
|
-
typeof value !== "string"
|
|
2598
|
-
|| value.trim() === ""
|
|
2599
|
-
|| characters.length > MAX_METRIC_DESCRIPTION_LENGTH
|
|
2600
|
-
|| characters.some((character) => {
|
|
2601
|
-
const code = character.codePointAt(0);
|
|
2602
|
-
return code !== undefined && (
|
|
2603
|
-
code <= 31
|
|
2604
|
-
|| (code >= 127 && code <= 159)
|
|
2605
|
-
|| (code >= 0xd800 && code <= 0xdfff)
|
|
2606
|
-
|| code === 0x2028
|
|
2607
|
-
|| code === 0x2029
|
|
2608
|
-
);
|
|
2609
|
-
})
|
|
2610
|
-
) {
|
|
2611
|
-
throw new SdkError(
|
|
2612
|
-
"validation_error",
|
|
2613
|
-
`metric description must be a non-blank string of at most ${MAX_METRIC_DESCRIPTION_LENGTH} non-control characters`
|
|
2614
|
-
);
|
|
2615
|
-
}
|
|
2616
|
-
return value.trim();
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
function productActionDetails(action) {
|
|
2620
|
-
if (typeof action === "string") {
|
|
2621
|
-
return { name: action, status: "success" };
|
|
2622
|
-
}
|
|
2623
|
-
if (!action || Array.isArray(action) || typeof action !== "object") {
|
|
2624
|
-
throw new SdkError("validation_error", "product action must be a string or object");
|
|
2625
|
-
}
|
|
2626
|
-
requireNonEmpty("product action name", action.name);
|
|
2627
|
-
const status = action.status === undefined ? "success" : action.status;
|
|
2628
|
-
requireAllowedValue("product action status", status, ACTION_STATUSES);
|
|
2629
|
-
return {
|
|
2630
|
-
funnel: action.funnel,
|
|
2631
|
-
metadata: action.metadata,
|
|
2632
|
-
name: action.name,
|
|
2633
|
-
routeTemplate: action.routeTemplate,
|
|
2634
|
-
screen: action.screen,
|
|
2635
|
-
sessionId: action.sessionId,
|
|
2636
|
-
status,
|
|
2637
|
-
step: action.step,
|
|
2638
|
-
traceId: action.traceId
|
|
2639
|
-
};
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
function networkMilestoneDetails(request) {
|
|
2643
|
-
if (typeof request === "string") {
|
|
2644
|
-
return networkMilestoneDetails({ routeTemplate: request });
|
|
2645
|
-
}
|
|
2646
|
-
if (!request || Array.isArray(request) || typeof request !== "object") {
|
|
2647
|
-
throw new SdkError("validation_error", "network milestone must be a string or object");
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
const routeTemplate = sanitizeRouteTemplate(request.routeTemplate);
|
|
2651
|
-
requireNonEmpty("network milestone routeTemplate", routeTemplate);
|
|
2652
|
-
const method = normalizeHttpMethod(request.method);
|
|
2653
|
-
const statusCode = statusCodeOrUndefined(request.statusCode);
|
|
2654
|
-
const status = request.status === undefined
|
|
2655
|
-
? statusFromStatusCode(statusCode)
|
|
2656
|
-
: request.status;
|
|
2657
|
-
requireAllowedValue("network milestone status", status, ACTION_STATUSES);
|
|
2658
|
-
const durationMs = nonNegativeNumberOrUndefined("network milestone durationMs", request.durationMs);
|
|
2659
|
-
const name = typeof request.name === "string" && request.name.trim() !== ""
|
|
2660
|
-
? request.name
|
|
2661
|
-
: `network.${method.toLowerCase()} ${routeTemplate}`;
|
|
2662
|
-
|
|
2663
|
-
return {
|
|
2664
|
-
durationMs,
|
|
2665
|
-
metadata: request.metadata,
|
|
2666
|
-
method,
|
|
2667
|
-
name,
|
|
2668
|
-
routeTemplate,
|
|
2669
|
-
sessionId: request.sessionId,
|
|
2670
|
-
status,
|
|
2671
|
-
statusCode,
|
|
2672
|
-
traceId: request.traceId
|
|
2673
|
-
};
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
function sanitizeRouteTemplate(routeTemplate) {
|
|
2677
|
-
if (routeTemplate === undefined) {
|
|
2678
|
-
return undefined;
|
|
2679
|
-
}
|
|
2680
|
-
if (typeof routeTemplate !== "string") {
|
|
2681
|
-
throw new SdkError("validation_error", "routeTemplate must be a string");
|
|
2682
|
-
}
|
|
2683
|
-
const trimmed = routeTemplate.trim();
|
|
2684
|
-
if (trimmed === "") {
|
|
2685
|
-
return "";
|
|
2686
|
-
}
|
|
2687
|
-
try {
|
|
2688
|
-
const url = new URL(trimmed, "https://logbrew.example");
|
|
2689
|
-
return url.pathname || "/";
|
|
2690
|
-
} catch {
|
|
2691
|
-
return trimmed.split(/[?#]/u)[0] || "/";
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
function productAnalyticsMetadata(kind, surface) {
|
|
2696
|
-
const normalizedSurface = boundedProductAnalyticsSurface(surface);
|
|
2697
|
-
return {
|
|
2698
|
-
analyticsSchemaVersion: PRODUCT_ANALYTICS_SCHEMA_VERSION,
|
|
2699
|
-
analyticsKind: kind,
|
|
2700
|
-
analyticsSurface: normalizedSurface
|
|
2701
|
-
};
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
function boundedProductAnalyticsSurface(surface) {
|
|
2705
|
-
if (typeof surface !== "string") {
|
|
2706
|
-
return undefined;
|
|
2707
|
-
}
|
|
2708
|
-
const normalized = surface.trim();
|
|
2709
|
-
const characters = Array.from(normalized);
|
|
2710
|
-
if (
|
|
2711
|
-
normalized === ""
|
|
2712
|
-
|| characters.length > MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH
|
|
2713
|
-
|| characters.some((character) => {
|
|
2714
|
-
const code = character.codePointAt(0);
|
|
2715
|
-
return code !== undefined && (code <= 31 || (code >= 127 && code <= 159));
|
|
2716
|
-
})
|
|
2717
|
-
) {
|
|
2718
|
-
return undefined;
|
|
2719
|
-
}
|
|
2720
|
-
return normalized;
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
function normalizeHttpMethod(method) {
|
|
2724
|
-
const value = method === undefined ? "GET" : method;
|
|
2725
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
2726
|
-
throw new SdkError("validation_error", "network milestone method must be a non-empty string");
|
|
2727
|
-
}
|
|
2728
|
-
const normalized = value.trim().toUpperCase();
|
|
2729
|
-
if (!/^[A-Z][A-Z0-9_-]*$/u.test(normalized)) {
|
|
2730
|
-
throw new SdkError("validation_error", "network milestone method must be a valid HTTP method");
|
|
2731
|
-
}
|
|
2732
|
-
return normalized;
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
function statusCodeOrUndefined(value) {
|
|
2736
|
-
if (value === undefined) {
|
|
2737
|
-
return undefined;
|
|
2738
|
-
}
|
|
2739
|
-
if (!Number.isInteger(value) || value < 100 || value > 599) {
|
|
2740
|
-
throw new SdkError("validation_error", "network milestone statusCode must be an integer from 100 to 599");
|
|
2741
|
-
}
|
|
2742
|
-
return value;
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
function statusFromStatusCode(statusCode) {
|
|
2746
|
-
if (statusCode !== undefined && statusCode >= 400) {
|
|
2747
|
-
return "failure";
|
|
2748
|
-
}
|
|
2749
|
-
return "success";
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
|
-
function nonNegativeNumberOrUndefined(label, value) {
|
|
2753
|
-
if (value === undefined) {
|
|
2754
|
-
return undefined;
|
|
2755
|
-
}
|
|
2756
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
2757
|
-
throw new SdkError("validation_error", `${label} must be a non-negative number`);
|
|
2758
|
-
}
|
|
2759
|
-
return value;
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2762
|
-
function stringOrUndefined(value) {
|
|
2763
|
-
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
function withMetadata(attributes, metadata, context) {
|
|
2767
|
-
const safeMetadata = cloneMetadata(metadata);
|
|
2768
|
-
const safeContext = validateTelemetryContext(context);
|
|
2769
|
-
return {
|
|
2770
|
-
...attributes,
|
|
2771
|
-
...(safeMetadata === undefined ? {} : { metadata: safeMetadata }),
|
|
2772
|
-
...(safeContext === undefined ? {} : { context: safeContext })
|
|
2773
|
-
};
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
function normalizeConsoleLevels(levels) {
|
|
2777
|
-
const requestedLevels = levels === undefined ? DEFAULT_CONSOLE_LEVELS : levels;
|
|
2778
|
-
if (!Array.isArray(requestedLevels)) {
|
|
2779
|
-
throw new SdkError("validation_error", "console capture levels must be an array");
|
|
2780
|
-
}
|
|
2781
|
-
const normalized = [];
|
|
2782
|
-
for (const method of requestedLevels) {
|
|
2783
|
-
if (!CONSOLE_METHODS.has(method)) {
|
|
2784
|
-
throw new SdkError("validation_error", `console method must be one of: ${Array.from(CONSOLE_METHODS).join(", ")}`);
|
|
2785
|
-
}
|
|
2786
|
-
if (!normalized.includes(method)) {
|
|
2787
|
-
normalized.push(method);
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
return normalized;
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
function consoleMessage(args, includeErrorStack) {
|
|
2794
|
-
const values = Array.isArray(args) ? args : [];
|
|
2795
|
-
const message = values.map((value) => formatConsoleArgument(value, includeErrorStack)).join(" ");
|
|
2796
|
-
return message.trim() === "" ? "console event" : message;
|
|
2797
|
-
}
|
|
2798
|
-
|
|
2799
|
-
function formatConsoleArgument(value, includeErrorStack) {
|
|
2800
|
-
if (value instanceof Error) {
|
|
2801
|
-
if (includeErrorStack && value.stack) {
|
|
2802
|
-
return value.stack;
|
|
2803
|
-
}
|
|
2804
|
-
return value.message ? `${value.name}: ${value.message}` : value.name;
|
|
2805
|
-
}
|
|
2806
|
-
if (typeof value === "string") {
|
|
2807
|
-
return value;
|
|
2808
|
-
}
|
|
2809
|
-
if (typeof value === "number" || typeof value === "boolean" || value === null || value === undefined) {
|
|
2810
|
-
return String(value);
|
|
2811
|
-
}
|
|
2812
|
-
if (typeof value === "bigint" || typeof value === "symbol") {
|
|
2813
|
-
return String(value);
|
|
2814
|
-
}
|
|
2815
|
-
try {
|
|
2816
|
-
const json = JSON.stringify(value);
|
|
2817
|
-
return json === undefined ? String(value) : json;
|
|
2818
|
-
} catch {
|
|
2819
|
-
return Object.prototype.toString.call(value);
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
const { installBrowserBreadcrumbs } = require("./browser-breadcrumbs.cjs");
|
|
36
|
+
const { TransportError } = require("./transport-error.cjs");
|
|
2824
37
|
|
|
2825
38
|
module.exports = {
|
|
2826
39
|
PRODUCT_ANALYTICS_KINDS,
|
|
2827
40
|
installBrowserBreadcrumbs,
|
|
41
|
+
|
|
2828
42
|
PRODUCT_ANALYTICS_SCHEMA_VERSION,
|
|
2829
43
|
createBaggage,
|
|
2830
44
|
createIssueAttributesFromError,
|
|
2831
45
|
createNetworkMilestoneAttributes,
|
|
2832
46
|
createProductActionAttributes,
|
|
47
|
+
|
|
2833
48
|
createLogBrewOpenTelemetrySpanExporter,
|
|
2834
49
|
createLogBrewOpenTelemetrySpanProcessor,
|
|
2835
50
|
createSupportTicketDraft,
|
|
@@ -2837,21 +52,26 @@ module.exports = {
|
|
|
2837
52
|
createTraceparent,
|
|
2838
53
|
createTraceparentHeaders,
|
|
2839
54
|
createTracestate,
|
|
55
|
+
|
|
2840
56
|
createLogBrewPinoDestination,
|
|
2841
57
|
installLogBrewConsoleCapture,
|
|
58
|
+
|
|
2842
59
|
LogBrewClient,
|
|
2843
60
|
logbrewTraceContextFromCurrentOpenTelemetrySpan,
|
|
2844
61
|
logbrewTraceContextFromOpenTelemetrySpan,
|
|
2845
62
|
logbrewTraceContextFromOpenTelemetrySpanContext,
|
|
63
|
+
|
|
2846
64
|
logAttributesFromConsoleArgs,
|
|
2847
65
|
logAttributesFromPinoRecord,
|
|
2848
66
|
logbrewLevelFromConsoleMethod,
|
|
2849
67
|
parseBaggage,
|
|
2850
68
|
parseTraceparent,
|
|
2851
69
|
parseTracestate,
|
|
70
|
+
|
|
2852
71
|
RecordingTransport,
|
|
2853
72
|
SdkError,
|
|
2854
73
|
spanAttributesFromOpenTelemetryReadableSpan,
|
|
2855
74
|
spanAttributesFromTraceparent,
|
|
75
|
+
|
|
2856
76
|
TransportError
|
|
2857
77
|
};
|