@logbrew/sdk 0.1.2 → 0.1.4

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/index.cjs CHANGED
@@ -1,3 +1,8 @@
1
+ const { buildCreateSupportTicketDraft } = require("./support-ticket.cjs");
2
+ const { buildIssueStackHelpers } = require("./issue-stack.cjs");
3
+ const { buildOpenTelemetryHelpers } = require("./opentelemetry.cjs");
4
+ const { buildTraceContextHelpers } = require("./trace-context.cjs");
5
+
1
6
  const SEVERITY_ALIASES = new Map([
2
7
  ["trace", "info"],
3
8
  ["debug", "info"],
@@ -26,15 +31,54 @@ const WINSTON_RESERVED_FIELDS = new Set(["level", "message", "timestamp", "time"
26
31
  const TRACEPARENT_PATTERN = /^([0-9a-fA-F]{2})-([0-9a-fA-F]{32})-([0-9a-fA-F]{16})-([0-9a-fA-F]{2})$/u;
27
32
  const ZERO_TRACE_ID = "00000000000000000000000000000000";
28
33
  const ZERO_SPAN_ID = "0000000000000000";
29
-
34
+ const DEFAULT_MAX_QUEUE_SIZE = 1000;
35
+ const DEFAULT_MAX_QUEUE_BYTES = 4 * 1024 * 1024;
36
+ const DEFAULT_MAX_BATCH_EVENTS = 100;
37
+ const DEFAULT_MAX_BATCH_BYTES = 256 * 1024;
38
+ const DEFAULT_DELIVERY_INTERVAL_MS = 5000;
39
+ const MAX_DELIVERY_INTERVAL_MS = 60 * 1000;
40
+ const DEFAULT_DELIVERY_QUEUE_THRESHOLD = 50;
41
+ const DELIVERY_HEALTH_SCHEMA_VERSION = 1;
42
+ const EVENT_QUEUE_FACTORY = Symbol.for("@logbrew/sdk.eventQueueFactory");
43
+ const MAX_ERROR_CAUSES = 5;
44
+ const BUILTIN_ERROR_NAMES = new Set([
45
+ "AggregateError",
46
+ "Error",
47
+ "EvalError",
48
+ "RangeError",
49
+ "ReferenceError",
50
+ "SyntaxError",
51
+ "TypeError",
52
+ "URIError"
53
+ ]);
54
+ const MAX_SPAN_EVENTS = 8;
55
+ const MAX_SPAN_LINKS = 8;
56
+ const EVENT_VALIDATORS = new Map([
57
+ ["release", validateRelease],
58
+ ["environment", validateEnvironment],
59
+ ["issue", validateIssue],
60
+ ["log", validateLog],
61
+ ["span", validateSpan],
62
+ ["action", validateAction],
63
+ ["metric", validateMetric]
64
+ ]);
30
65
  class SdkError extends Error {
31
- constructor(code, message) {
66
+ constructor(code, message, details = {}) {
32
67
  super(message);
33
68
  this.name = "SdkError";
34
69
  this.code = code;
70
+ const retryAfterMs = retryAfterMsOrUndefined(details?.retryAfterMs);
71
+ if (retryAfterMs !== undefined) {
72
+ this.retryAfterMs = retryAfterMs;
73
+ }
74
+ if (details?.retryable === true) {
75
+ this.retryable = true;
76
+ }
35
77
  }
36
78
  }
37
79
 
80
+ const { javascriptStackFrames, validateIssueStackFrames } = buildIssueStackHelpers({ SdkError });
81
+
38
82
  class TransportError extends Error {
39
83
  constructor(code, message, retryable = false) {
40
84
  super(message);
@@ -74,48 +118,296 @@ class RecordingTransport {
74
118
  throw next;
75
119
  }
76
120
 
77
- return { statusCode: next.statusCode, attempts: 1 };
121
+ const retryAfterMs = retryAfterMsOrUndefined(next.retryAfterMs);
122
+ return retryAfterMs === undefined
123
+ ? { statusCode: next.statusCode, attempts: 1 }
124
+ : { statusCode: next.statusCode, attempts: 1, retryAfterMs };
78
125
  }
79
126
  }
80
127
 
81
128
  class LogBrewClient {
82
- static create({ apiKey, sdkName, sdkVersion, maxRetries = 2, eventFilter }) {
129
+ static create({
130
+ apiKey,
131
+ sdkName,
132
+ sdkVersion,
133
+ maxRetries = 2,
134
+ eventFilter,
135
+ maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
136
+ maxQueueBytes = DEFAULT_MAX_QUEUE_BYTES,
137
+ maxBatchEvents = DEFAULT_MAX_BATCH_EVENTS,
138
+ maxBatchBytes = DEFAULT_MAX_BATCH_BYTES,
139
+ onEventDropped,
140
+ eventStore,
141
+ [EVENT_QUEUE_FACTORY]: eventQueueFactory,
142
+ transport,
143
+ automaticDelivery = transport !== undefined,
144
+ deliveryIntervalMs = DEFAULT_DELIVERY_INTERVAL_MS,
145
+ deliveryQueueThreshold = Math.min(DEFAULT_DELIVERY_QUEUE_THRESHOLD, maxQueueSize)
146
+ }) {
83
147
  requireNonEmpty("apiKey", apiKey);
84
148
  requireNonEmpty("sdkName", sdkName);
85
149
  requireNonEmpty("sdkVersion", sdkVersion);
150
+ requireNonNegativeInteger("maxRetries", maxRetries);
86
151
  if (eventFilter !== undefined && typeof eventFilter !== "function") {
87
152
  throw new SdkError("validation_error", "eventFilter must be a function");
88
153
  }
154
+ requirePositiveInteger("maxQueueSize", maxQueueSize);
155
+ requirePositiveInteger("maxQueueBytes", maxQueueBytes);
156
+ requirePositiveInteger("maxBatchEvents", maxBatchEvents);
157
+ requirePositiveInteger("maxBatchBytes", maxBatchBytes);
158
+ if (onEventDropped !== undefined && typeof onEventDropped !== "function") {
159
+ throw new SdkError("validation_error", "onEventDropped must be a function");
160
+ }
161
+ validateEventStore(eventStore);
162
+ if (eventQueueFactory !== undefined && typeof eventQueueFactory !== "function") {
163
+ throw new SdkError("validation_error", "event queue factory must be a function");
164
+ }
165
+ if (eventStore !== undefined && eventQueueFactory !== undefined) {
166
+ throw new SdkError("validation_error", "eventStore and event queue factory are mutually exclusive");
167
+ }
168
+ validateTransport(transport);
169
+ if (typeof automaticDelivery !== "boolean") {
170
+ throw new SdkError("validation_error", "automaticDelivery must be a boolean");
171
+ }
172
+ if (automaticDelivery && transport === undefined) {
173
+ throw new SdkError("validation_error", "automaticDelivery requires transport");
174
+ }
175
+ requirePositiveInteger("deliveryIntervalMs", deliveryIntervalMs);
176
+ if (deliveryIntervalMs > MAX_DELIVERY_INTERVAL_MS) {
177
+ throw new SdkError("validation_error", `deliveryIntervalMs must be at most ${MAX_DELIVERY_INTERVAL_MS}`);
178
+ }
179
+ requirePositiveInteger("deliveryQueueThreshold", deliveryQueueThreshold);
180
+ if (deliveryQueueThreshold > maxQueueSize) {
181
+ throw new SdkError("validation_error", "deliveryQueueThreshold must not exceed maxQueueSize");
182
+ }
89
183
 
90
184
  return new LogBrewClient({
91
185
  apiKey,
186
+ automaticDelivery,
187
+ deliveryIntervalMs,
188
+ deliveryQueueThreshold,
189
+ eventStore,
190
+ eventQueueFactory,
92
191
  eventFilter,
192
+ maxBatchBytes,
193
+ maxBatchEvents,
194
+ maxQueueBytes,
195
+ maxQueueSize,
196
+ onEventDropped,
93
197
  sdk: {
94
198
  name: sdkName,
95
199
  language: "javascript",
96
200
  version: sdkVersion
97
201
  },
98
- maxRetries
202
+ maxRetries,
203
+ transport
99
204
  });
100
205
  }
101
206
 
102
- constructor({ apiKey, sdk, maxRetries, eventFilter }) {
207
+ constructor({
208
+ apiKey,
209
+ sdk,
210
+ maxRetries,
211
+ eventFilter,
212
+ maxBatchBytes,
213
+ maxBatchEvents,
214
+ maxQueueBytes,
215
+ maxQueueSize,
216
+ onEventDropped,
217
+ eventStore,
218
+ eventQueueFactory,
219
+ transport,
220
+ automaticDelivery,
221
+ deliveryIntervalMs,
222
+ deliveryQueueThreshold
223
+ }) {
103
224
  this.apiKey = apiKey;
225
+ this.automaticDelivery = automaticDelivery;
226
+ this.deliveryIntervalMs = deliveryIntervalMs;
227
+ this.deliveryQueueThreshold = deliveryQueueThreshold;
104
228
  this.eventFilter = eventFilter;
229
+ this.maxBatchBytes = maxBatchBytes;
230
+ this.maxBatchEvents = maxBatchEvents;
231
+ this.maxQueueBytes = maxQueueBytes;
232
+ this.maxQueueSize = maxQueueSize;
233
+ this.onEventDropped = onEventDropped;
234
+ this.transport = transport;
105
235
  this.sdk = sdk;
106
236
  this.maxRetries = maxRetries;
107
- this.events = [];
237
+ this.batchPrefix = `{"sdk":${JSON.stringify(this.sdk)},"events":[`;
238
+ this.batchPrefixBytes = utf8ByteLength(this.batchPrefix);
239
+ this.batchSuffix = "]}";
240
+ this.batchSuffixBytes = utf8ByteLength(this.batchSuffix);
241
+ const ownsEventStore = eventQueueFactory !== undefined;
242
+ const resolvedEventStore = ownsEventStore
243
+ ? createEventStoreFromQueueFactory(eventQueueFactory, {
244
+ batchPrefixBytes: this.batchPrefixBytes,
245
+ batchSuffixBytes: this.batchSuffixBytes,
246
+ maxBatchBytes,
247
+ maxQueueBytes,
248
+ maxQueueSize
249
+ })
250
+ : eventStore;
251
+ this.eventStore = resolvedEventStore;
252
+ let recovered;
253
+ try {
254
+ recovered = loadStoredEvents({
255
+ batchPrefixBytes: this.batchPrefixBytes,
256
+ batchSuffixBytes: this.batchSuffixBytes,
257
+ eventStore: resolvedEventStore,
258
+ maxBatchBytes,
259
+ maxQueueBytes,
260
+ maxQueueSize
261
+ });
262
+ } catch (error) {
263
+ if (ownsEventStore) {
264
+ try {
265
+ requireSynchronousStoreResult("close", resolvedEventStore.close());
266
+ } catch {
267
+ // Preserve the queue construction or recovery failure.
268
+ }
269
+ }
270
+ throw error;
271
+ }
272
+ this.events = recovered.events;
273
+ this.serializedEvents = recovered.serializedEvents;
274
+ this.serializedEventBytes = recovered.serializedEventBytes;
275
+ this.queuedEventBytes = recovered.queuedEventBytes;
276
+ this.operationTail = Promise.resolve();
277
+ this.pendingOperations = 0;
278
+ this.closing = false;
108
279
  this.closed = false;
280
+ this.droppedEventCount = 0;
281
+ this.droppedEventsByReason = {
282
+ event_too_large: 0,
283
+ queue_bytes_overflow: 0,
284
+ queue_overflow: 0
285
+ };
286
+ this.lastDropReason = "none";
287
+ this.storage = resolvedEventStore ? "persistent" : "memory";
288
+ this.hydratedEventCount = recovered.events.length;
289
+ this.hydratedEventBytes = recovered.queuedEventBytes;
290
+ this.deliveryTimer = undefined;
291
+ this.automaticFlushActive = false;
292
+ this.automaticFlushPending = false;
293
+ this.deliveryInFlight = false;
294
+ this.lastDeliveryOutcome = "idle";
295
+ this.automaticPauseReason = "none";
296
+ this.consecutiveDeliveryFailures = 0;
297
+ this.retryDelayMs = 0;
298
+ this.successfulFlushCount = 0;
299
+ this.failedFlushCount = 0;
300
+ this.deliveryAttemptCount = 0;
301
+ this.acceptedBatchCount = 0;
302
+ this.acceptedEventCount = 0;
303
+ this.lastStatusClass = "none";
304
+ this.lastAttemptAtUnixMs = 0;
305
+ this.lastAcceptedAtUnixMs = 0;
306
+ this.lastDroppedAtUnixMs = 0;
307
+ this.failedBatch = undefined;
308
+ this.#scheduleAutomaticDelivery();
109
309
  }
110
310
 
111
311
  pendingEvents() {
112
312
  return this.events.length;
113
313
  }
114
314
 
315
+ pendingBytes() {
316
+ return this.queuedEventBytes;
317
+ }
318
+
319
+ droppedEvents() {
320
+ return this.droppedEventCount;
321
+ }
322
+
323
+ deliveryHealth() {
324
+ const droppedByReason = Object.freeze({ ...this.droppedEventsByReason });
325
+ return Object.freeze({
326
+ schemaVersion: DELIVERY_HEALTH_SCHEMA_VERSION,
327
+ automaticDelivery: this.automaticDelivery,
328
+ lifecycle: this.closed ? "closed" : this.closing ? "shutting_down" : "active",
329
+ deliveryState: this.#deliveryState(),
330
+ storage: this.storage,
331
+ queueEvents: this.events.length,
332
+ queueBytes: this.queuedEventBytes,
333
+ hydratedEvents: this.hydratedEventCount,
334
+ hydratedBytes: this.hydratedEventBytes,
335
+ droppedEvents: this.droppedEventCount,
336
+ droppedByReason,
337
+ lastDropReason: this.lastDropReason,
338
+ scheduled: this.deliveryTimer !== undefined,
339
+ inFlight: this.deliveryInFlight,
340
+ coalesced: this.automaticFlushPending,
341
+ pendingOperations: this.pendingOperations,
342
+ lastOutcome: this.lastDeliveryOutcome,
343
+ lastStatusClass: this.lastStatusClass,
344
+ pausedReason: this.automaticPauseReason,
345
+ consecutiveFailures: this.consecutiveDeliveryFailures,
346
+ retryDelayMs: this.retryDelayMs,
347
+ flushes: this.successfulFlushCount,
348
+ failures: this.failedFlushCount,
349
+ attempts: this.deliveryAttemptCount,
350
+ batches: this.acceptedBatchCount,
351
+ acceptedEvents: this.acceptedEventCount,
352
+ lastAttemptAtUnixMs: this.lastAttemptAtUnixMs,
353
+ lastAcceptedAtUnixMs: this.lastAcceptedAtUnixMs,
354
+ lastDroppedAtUnixMs: this.lastDroppedAtUnixMs
355
+ });
356
+ }
357
+
358
+ #deliveryState() {
359
+ if (this.deliveryInFlight) {
360
+ return "in_flight";
361
+ }
362
+ if (this.retryDelayMs > 0) {
363
+ return "retrying";
364
+ }
365
+ if (this.automaticPauseReason !== "none") {
366
+ return "paused";
367
+ }
368
+ if (this.events.length > 0) {
369
+ return this.deliveryTimer !== undefined ? "scheduled" : "queued";
370
+ }
371
+ if (this.lastDeliveryOutcome === "accepted") {
372
+ return "accepted";
373
+ }
374
+ if (this.lastDeliveryOutcome === "failed") {
375
+ return "failed";
376
+ }
377
+ if (this.droppedEventCount > 0) {
378
+ return "dropped";
379
+ }
380
+ return "idle";
381
+ }
382
+
115
383
  previewJson() {
116
384
  return JSON.stringify({ sdk: this.sdk, events: this.events }, null, 2);
117
385
  }
118
386
 
387
+ purgePendingEvents() {
388
+ if (this.closed) {
389
+ throw new SdkError("shutdown_error", "client is already shut down");
390
+ }
391
+ if (this.closing) {
392
+ throw new SdkError("shutdown_error", "client is shutting down");
393
+ }
394
+ if (this.pendingOperations > 0 || this.automaticFlushActive) {
395
+ throw new SdkError("persistence_error", "cannot purge while a delivery operation is active");
396
+ }
397
+ const purgedEvents = this.events.length;
398
+ if (this.eventStore) {
399
+ requireSynchronousStoreResult("purge", this.eventStore.purge());
400
+ }
401
+ this.#clearDeliveryTimer();
402
+ this.automaticFlushPending = false;
403
+ this.failedBatch = undefined;
404
+ this.events.splice(0, this.events.length);
405
+ this.serializedEvents.splice(0, this.serializedEvents.length);
406
+ this.serializedEventBytes.splice(0, this.serializedEventBytes.length);
407
+ this.queuedEventBytes = 0;
408
+ return purgedEvents;
409
+ }
410
+
119
411
  release(id, timestamp, attributes) {
120
412
  this.#pushEvent("release", id, timestamp, validateRelease(attributes));
121
413
  }
@@ -148,64 +440,420 @@ class LogBrewClient {
148
440
  if (this.closed) {
149
441
  throw new SdkError("shutdown_error", "client is already shut down");
150
442
  }
151
- return this.#flushInternal(transport);
443
+ if (this.closing) {
444
+ throw new SdkError("shutdown_error", "client is shutting down");
445
+ }
446
+ const resolvedTransport = this.#resolveTransport(transport);
447
+ const controlsAutomaticDelivery = this.automaticDelivery && resolvedTransport === this.transport;
448
+ this.#clearDeliveryTimer();
449
+ return this.#runSerialized(async () => {
450
+ this.#clearDeliveryTimer();
451
+ try {
452
+ const response = await this.#flushWithHealth(resolvedTransport);
453
+ if (controlsAutomaticDelivery) {
454
+ this.#recordAutomaticSuccess();
455
+ }
456
+ return response;
457
+ } catch (error) {
458
+ if (controlsAutomaticDelivery) {
459
+ this.#recordAutomaticFailure(error);
460
+ }
461
+ throw error;
462
+ } finally {
463
+ if (this.automaticDelivery && !this.closed && !this.closing) {
464
+ this.#resumeAutomaticDelivery();
465
+ }
466
+ }
467
+ });
152
468
  }
153
469
 
154
470
  async shutdown(transport) {
155
471
  if (this.closed) {
156
472
  throw new SdkError("shutdown_error", "client is already shut down");
157
473
  }
158
- const response = await this.#flushInternal(transport);
159
- this.closed = true;
160
- return response;
474
+ if (this.closing) {
475
+ throw new SdkError("shutdown_error", "client is shutting down");
476
+ }
477
+ const resolvedTransport = this.#resolveTransport(transport);
478
+ const controlsAutomaticDelivery = this.automaticDelivery && resolvedTransport === this.transport;
479
+ this.#clearDeliveryTimer();
480
+ this.automaticFlushPending = false;
481
+ this.closing = true;
482
+ try {
483
+ const response = await this.#runSerialized(() => this.#flushWithHealth(resolvedTransport));
484
+ if (controlsAutomaticDelivery) {
485
+ this.#recordAutomaticSuccess();
486
+ }
487
+ if (this.eventStore) {
488
+ try {
489
+ requireSynchronousStoreResult("close", this.eventStore.close());
490
+ } catch (error) {
491
+ this.closed = true;
492
+ throw error;
493
+ }
494
+ }
495
+ this.closed = true;
496
+ return response;
497
+ } catch (error) {
498
+ if (!this.closed) {
499
+ this.closing = false;
500
+ if (controlsAutomaticDelivery) {
501
+ this.#recordAutomaticFailure(error);
502
+ }
503
+ this.#resumeAutomaticDelivery();
504
+ }
505
+ throw error;
506
+ }
161
507
  }
162
508
 
163
509
  #pushEvent(eventType, id, timestamp, attributes) {
164
510
  if (this.closed) {
165
511
  throw new SdkError("shutdown_error", "client is already shut down");
166
512
  }
513
+ if (this.closing) {
514
+ throw new SdkError("shutdown_error", "client is shutting down");
515
+ }
167
516
  requireNonEmpty("event id", id);
168
517
  requireTimestamp(timestamp);
169
518
  const event = { type: eventType, id, timestamp, attributes };
170
519
  if (this.eventFilter && this.eventFilter(cloneEvent(event)) === false) {
171
520
  return;
172
521
  }
522
+ const serializedEvent = JSON.stringify(event);
523
+ const eventBytes = utf8ByteLength(serializedEvent);
524
+ if (this.batchPrefixBytes + eventBytes + this.batchSuffixBytes > this.maxBatchBytes) {
525
+ this.#recordDroppedEvent(event, "event_too_large");
526
+ return;
527
+ }
528
+ if (this.events.length >= this.maxQueueSize) {
529
+ this.#recordDroppedEvent(event, "queue_overflow");
530
+ return;
531
+ }
532
+ if (this.queuedEventBytes + eventBytes > this.maxQueueBytes) {
533
+ this.#recordDroppedEvent(event, "queue_bytes_overflow");
534
+ return;
535
+ }
536
+ if (this.eventStore) {
537
+ requireSynchronousStoreResult("append", this.eventStore.append({
538
+ event: cloneEvent(event),
539
+ eventBytes,
540
+ serializedEvent
541
+ }));
542
+ }
173
543
  this.events.push(event);
544
+ this.serializedEvents.push(serializedEvent);
545
+ this.serializedEventBytes.push(eventBytes);
546
+ this.queuedEventBytes += eventBytes;
547
+ this.#scheduleAutomaticDelivery();
548
+ }
549
+
550
+ #recordDroppedEvent(event, reason) {
551
+ this.droppedEventCount = incrementBounded(this.droppedEventCount);
552
+ this.droppedEventsByReason[reason] = incrementBounded(this.droppedEventsByReason[reason]);
553
+ this.lastDropReason = reason;
554
+ this.lastDroppedAtUnixMs = nextBoundedTimestamp(this.lastDroppedAtUnixMs);
555
+ if (!this.onEventDropped) {
556
+ return;
557
+ }
558
+ try {
559
+ this.onEventDropped({
560
+ droppedEvents: this.droppedEventCount,
561
+ eventId: event.id,
562
+ eventType: event.type,
563
+ reason
564
+ });
565
+ } catch {
566
+ // Drop callbacks are advisory and must not interrupt application logging.
567
+ }
568
+ }
569
+
570
+ #runSerialized(operation) {
571
+ this.pendingOperations = incrementBounded(this.pendingOperations);
572
+ const result = this.operationTail.then(operation).finally(() => {
573
+ this.pendingOperations = Math.max(0, this.pendingOperations - 1);
574
+ });
575
+ this.operationTail = result.then(
576
+ () => undefined,
577
+ () => undefined
578
+ );
579
+ return result;
580
+ }
581
+
582
+ #resolveTransport(transport) {
583
+ const resolved = transport ?? this.transport;
584
+ if (resolved === undefined) {
585
+ throw new SdkError("validation_error", "flush and shutdown require transport");
586
+ }
587
+ validateTransport(resolved);
588
+ return resolved;
589
+ }
590
+
591
+ async #flushWithHealth(transport) {
592
+ this.deliveryInFlight = true;
593
+ try {
594
+ const response = await this.#flushSnapshot(transport);
595
+ this.successfulFlushCount = incrementBounded(this.successfulFlushCount);
596
+ this.lastDeliveryOutcome = response.batches === 0 ? "empty" : "accepted";
597
+ return response;
598
+ } catch (error) {
599
+ this.failedFlushCount = incrementBounded(this.failedFlushCount);
600
+ this.lastDeliveryOutcome = "failed";
601
+ throw error;
602
+ } finally {
603
+ this.deliveryInFlight = false;
604
+ }
605
+ }
606
+
607
+ #scheduleAutomaticDelivery() {
608
+ if (!this.automaticDelivery || this.closed || this.closing || this.events.length === 0 || this.automaticPauseReason !== "none") {
609
+ return;
610
+ }
611
+ if (this.automaticFlushActive) {
612
+ this.automaticFlushPending = true;
613
+ return;
614
+ }
615
+ if (this.retryDelayMs > 0) {
616
+ this.#armAutomaticDeliveryTimer(this.retryDelayMs);
617
+ return;
618
+ }
619
+ if (this.events.length >= this.deliveryQueueThreshold) {
620
+ this.#requestAutomaticFlush();
621
+ return;
622
+ }
623
+ this.#armAutomaticDeliveryTimer();
624
+ }
625
+
626
+ #armAutomaticDeliveryTimer(delayMs = this.deliveryIntervalMs) {
627
+ if (!this.automaticDelivery || this.closed || this.closing || this.events.length === 0 || this.automaticPauseReason !== "none") {
628
+ return;
629
+ }
630
+ if (this.deliveryTimer !== undefined) {
631
+ return;
632
+ }
633
+ const timer = setTimeout(() => {
634
+ if (this.deliveryTimer !== timer) {
635
+ return;
636
+ }
637
+ this.deliveryTimer = undefined;
638
+ this.retryDelayMs = 0;
639
+ this.#requestAutomaticFlush();
640
+ }, delayMs);
641
+ this.deliveryTimer = timer;
642
+ if (timer && typeof timer === "object" && typeof timer.unref === "function") {
643
+ timer.unref();
644
+ }
645
+ }
646
+
647
+ #clearDeliveryTimer() {
648
+ if (this.deliveryTimer === undefined) {
649
+ return;
650
+ }
651
+ globalThis.clearTimeout(this.deliveryTimer);
652
+ this.deliveryTimer = undefined;
653
+ }
654
+
655
+ #requestAutomaticFlush() {
656
+ if (!this.automaticDelivery || this.closed || this.closing || this.events.length === 0 || this.automaticPauseReason !== "none") {
657
+ return;
658
+ }
659
+ this.#clearDeliveryTimer();
660
+ if (this.automaticFlushActive) {
661
+ this.automaticFlushPending = true;
662
+ return;
663
+ }
664
+ this.automaticFlushActive = true;
665
+ void Promise.resolve().then(() => this.#runAutomaticFlush());
666
+ }
667
+
668
+ async #runAutomaticFlush() {
669
+ if (this.closed || this.closing) {
670
+ this.automaticFlushActive = false;
671
+ this.automaticFlushPending = false;
672
+ return;
673
+ }
674
+ let succeeded = false;
675
+ try {
676
+ await this.#runSerialized(() => this.#flushWithHealth(this.transport));
677
+ this.#recordAutomaticSuccess();
678
+ succeeded = true;
679
+ } catch (error) {
680
+ this.#recordAutomaticFailure(error);
681
+ } finally {
682
+ this.automaticFlushActive = false;
683
+ if (this.closed || this.closing) {
684
+ this.automaticFlushPending = false;
685
+ } else {
686
+ const drainCoalesced = succeeded && this.automaticFlushPending && this.events.length > 0;
687
+ this.automaticFlushPending = false;
688
+ if (drainCoalesced) {
689
+ this.#requestAutomaticFlush();
690
+ } else if (!succeeded) {
691
+ this.#resumeAutomaticDelivery();
692
+ } else {
693
+ this.#scheduleAutomaticDelivery();
694
+ }
695
+ }
696
+ }
174
697
  }
175
698
 
176
- async #flushInternal(transport) {
177
- if (this.events.length === 0) {
178
- return { statusCode: 204, attempts: 0 };
699
+ async #flushSnapshot(transport) {
700
+ let remainingEvents = this.events.length;
701
+ if (remainingEvents === 0) {
702
+ return { statusCode: 204, attempts: 0, batches: 0 };
179
703
  }
180
704
 
181
- const body = this.previewJson();
705
+ let attempts = 0;
706
+ let batches = 0;
707
+ let statusCode = 204;
708
+ while (remainingEvents > 0) {
709
+ const batch = this.failedBatch ?? this.#nextBatch(Math.min(remainingEvents, this.maxBatchEvents));
710
+ let response;
711
+ try {
712
+ response = await this.#sendBatch(transport, batch.body);
713
+ } catch (error) {
714
+ this.failedBatch ??= Object.freeze({ body: batch.body, eventsCount: batch.eventsCount });
715
+ throw error;
716
+ }
717
+ this.failedBatch = undefined;
718
+ this.#acknowledge(batch.eventsCount);
719
+ this.acceptedBatchCount = incrementBounded(this.acceptedBatchCount);
720
+ remainingEvents -= batch.eventsCount;
721
+ attempts += response.attempts;
722
+ batches += 1;
723
+ statusCode = response.statusCode;
724
+ }
725
+
726
+ return { statusCode, attempts, batches };
727
+ }
728
+
729
+ #nextBatch(maxEvents) {
730
+ let bodyBytes = this.batchPrefixBytes + this.batchSuffixBytes;
731
+ let eventsCount = 0;
732
+ for (let index = 0; index < maxEvents; index += 1) {
733
+ const separatorBytes = eventsCount === 0 ? 0 : 1;
734
+ const nextBodyBytes = bodyBytes + separatorBytes + this.serializedEventBytes[index];
735
+ if (nextBodyBytes > this.maxBatchBytes) {
736
+ break;
737
+ }
738
+ bodyBytes = nextBodyBytes;
739
+ eventsCount += 1;
740
+ }
741
+ if (eventsCount === 0) {
742
+ throw new SdkError("transport_error", "queued event cannot fit the configured batch byte limit");
743
+ }
744
+ return {
745
+ body: `${this.batchPrefix}${this.serializedEvents.slice(0, eventsCount).join(",")}${this.batchSuffix}`,
746
+ eventsCount
747
+ };
748
+ }
749
+
750
+ #acknowledge(eventsCount) {
751
+ if (this.eventStore) {
752
+ requireSynchronousStoreResult("acknowledge", this.eventStore.acknowledge(eventsCount));
753
+ }
754
+ let acknowledgedBytes = 0;
755
+ for (let index = 0; index < eventsCount; index += 1) {
756
+ acknowledgedBytes += this.serializedEventBytes[index];
757
+ }
758
+ this.events.splice(0, eventsCount);
759
+ this.serializedEvents.splice(0, eventsCount);
760
+ this.serializedEventBytes.splice(0, eventsCount);
761
+ this.queuedEventBytes -= acknowledgedBytes;
762
+ this.acceptedEventCount = addBounded(this.acceptedEventCount, eventsCount);
763
+ this.lastAcceptedAtUnixMs = nextBoundedTimestamp(Math.max(
764
+ this.lastAcceptedAtUnixMs,
765
+ this.lastAttemptAtUnixMs
766
+ ));
767
+ }
768
+
769
+ #recordAutomaticSuccess() {
770
+ this.automaticPauseReason = "none";
771
+ this.consecutiveDeliveryFailures = 0;
772
+ this.retryDelayMs = 0;
773
+ }
774
+
775
+ #recordAutomaticFailure(error) {
776
+ this.consecutiveDeliveryFailures = incrementBounded(this.consecutiveDeliveryFailures);
777
+ this.retryDelayMs = 0;
778
+ if (error instanceof SdkError && error.code === "unauthenticated") {
779
+ this.automaticPauseReason = "authentication";
780
+ return;
781
+ }
782
+ if (error instanceof SdkError && error.code === "rate_limited") {
783
+ this.automaticPauseReason = "rate_limit";
784
+ return;
785
+ }
786
+ if (error instanceof SdkError && error.retryable === true) {
787
+ this.automaticPauseReason = "none";
788
+ this.retryDelayMs = automaticRetryDelayMs(this.deliveryIntervalMs, this.consecutiveDeliveryFailures);
789
+ return;
790
+ }
791
+ this.automaticPauseReason = "non_retryable";
792
+ }
793
+
794
+ #resumeAutomaticDelivery() {
795
+ if (this.automaticPauseReason !== "none") {
796
+ return;
797
+ }
798
+ if (this.retryDelayMs > 0) {
799
+ this.#armAutomaticDeliveryTimer(this.retryDelayMs);
800
+ return;
801
+ }
802
+ this.#scheduleAutomaticDelivery();
803
+ }
804
+
805
+ async #sendBatch(transport, body) {
182
806
  const maxAttempts = this.maxRetries + 1;
183
807
  let attempts = 0;
184
808
 
185
809
  while (attempts < maxAttempts) {
186
810
  attempts += 1;
811
+ this.deliveryAttemptCount = incrementBounded(this.deliveryAttemptCount);
812
+ this.lastAttemptAtUnixMs = nextBoundedTimestamp(this.lastAttemptAtUnixMs);
813
+ this.lastStatusClass = "transport_error";
187
814
  try {
188
815
  const response = await transport.send(this.apiKey, body);
816
+ if (
817
+ !response
818
+ || Array.isArray(response)
819
+ || typeof response !== "object"
820
+ || !Number.isSafeInteger(response.statusCode)
821
+ || response.statusCode < 100
822
+ || response.statusCode > 599
823
+ ) {
824
+ this.lastStatusClass = "invalid_response";
825
+ throw new SdkError("transport_error", "invalid transport response");
826
+ }
827
+ this.lastStatusClass = statusClass(response.statusCode);
189
828
  if (response.statusCode === 401) {
190
829
  throw new SdkError("unauthenticated", "transport rejected the API key");
191
830
  }
831
+ if (response.statusCode === 429) {
832
+ throw new SdkError("rate_limited", "transport rate limited the batch", {
833
+ retryAfterMs: response.retryAfterMs
834
+ });
835
+ }
192
836
  if (response.statusCode >= 200 && response.statusCode < 300) {
193
- this.events = [];
194
837
  return { statusCode: response.statusCode, attempts };
195
838
  }
196
- if (response.statusCode >= 500 && attempts < maxAttempts) {
839
+ const retryableStatus = response.statusCode === 408 || response.statusCode >= 500;
840
+ if (retryableStatus && attempts < maxAttempts) {
197
841
  continue;
198
842
  }
199
- throw new SdkError("transport_error", `unexpected transport status ${response.statusCode}`);
843
+ throw new SdkError("transport_error", `unexpected transport status ${response.statusCode}`, {
844
+ retryable: retryableStatus
845
+ });
200
846
  } catch (error) {
201
847
  if (error instanceof SdkError) {
202
848
  throw error;
203
849
  }
204
850
  if (error instanceof TransportError && error.retryable && attempts < maxAttempts) {
851
+ this.lastStatusClass = "network_error";
205
852
  continue;
206
853
  }
207
854
  if (error instanceof TransportError) {
208
- throw new SdkError(error.code, error.message);
855
+ this.lastStatusClass = "network_error";
856
+ throw new SdkError(error.code, error.message, { retryable: error.retryable });
209
857
  }
210
858
  throw error;
211
859
  }
@@ -215,6 +863,256 @@ class LogBrewClient {
215
863
  }
216
864
  }
217
865
 
866
+ function automaticRetryDelayMs(deliveryIntervalMs, consecutiveFailures) {
867
+ const exponent = Math.min(consecutiveFailures - 1, 30);
868
+ const maximumDelay = Math.min(MAX_DELIVERY_INTERVAL_MS, deliveryIntervalMs * (2 ** exponent));
869
+ const minimumDelay = Math.ceil(maximumDelay / 2);
870
+ return minimumDelay + Math.floor(Math.random() * (maximumDelay - minimumDelay + 1));
871
+ }
872
+
873
+ function statusClass(statusCode) {
874
+ if (statusCode >= 200 && statusCode < 300) {
875
+ return "success";
876
+ }
877
+ if (statusCode >= 400 && statusCode < 500) {
878
+ return "client_error";
879
+ }
880
+ if (statusCode >= 500) {
881
+ return "server_error";
882
+ }
883
+ return "other_status";
884
+ }
885
+
886
+ function nextBoundedTimestamp(previous) {
887
+ const now = Date.now();
888
+ if (!Number.isFinite(now)) {
889
+ return previous;
890
+ }
891
+ const bounded = Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(now)));
892
+ return Math.max(previous, bounded);
893
+ }
894
+
895
+ function addBounded(current, increment) {
896
+ return Math.min(Number.MAX_SAFE_INTEGER, current + increment);
897
+ }
898
+
899
+ function utf8ByteLength(value) {
900
+ let bytes = 0;
901
+ for (let index = 0; index < value.length; index += 1) {
902
+ const codeUnit = value.charCodeAt(index);
903
+ if (codeUnit < 0x80) {
904
+ bytes += 1;
905
+ } else if (codeUnit < 0x800) {
906
+ bytes += 2;
907
+ } else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff && index + 1 < value.length) {
908
+ const nextCodeUnit = value.charCodeAt(index + 1);
909
+ if (nextCodeUnit >= 0xdc00 && nextCodeUnit <= 0xdfff) {
910
+ bytes += 4;
911
+ index += 1;
912
+ } else {
913
+ bytes += 3;
914
+ }
915
+ } else {
916
+ bytes += 3;
917
+ }
918
+ }
919
+ return bytes;
920
+ }
921
+
922
+ function incrementBounded(value) {
923
+ return value >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : value + 1;
924
+ }
925
+
926
+ function validateTransport(transport) {
927
+ if (transport === undefined) {
928
+ return;
929
+ }
930
+ if (!transport || Array.isArray(transport) || typeof transport !== "object" || typeof transport.send !== "function") {
931
+ throw new SdkError("validation_error", "transport.send must be a function");
932
+ }
933
+ }
934
+
935
+ function validateEventStore(eventStore) {
936
+ if (eventStore === undefined) {
937
+ return;
938
+ }
939
+ if (!eventStore || Array.isArray(eventStore) || typeof eventStore !== "object") {
940
+ throw new SdkError("validation_error", "eventStore must be an object");
941
+ }
942
+ for (const method of ["load", "append", "acknowledge", "purge", "close"]) {
943
+ if (typeof eventStore[method] !== "function") {
944
+ throw new SdkError("validation_error", `eventStore.${method} must be a function`);
945
+ }
946
+ }
947
+ }
948
+
949
+ function createEventStoreFromQueueFactory(eventQueueFactory, config) {
950
+ const queue = eventQueueFactory({
951
+ ...config,
952
+ restoreEvent: restoreStoredEvent
953
+ });
954
+ validateEventQueue(queue);
955
+ return {
956
+ load() {
957
+ const events = queue.events();
958
+ const count = queue.length();
959
+ if (!Array.isArray(events) || events.length !== count) {
960
+ throw invalidStoredRecord();
961
+ }
962
+ return events.map((event, index) => ({
963
+ event,
964
+ eventBytes: queue.eventBytesAt(index),
965
+ serializedEvent: queue.serializedAt(index)
966
+ }));
967
+ },
968
+ append(record) {
969
+ return queue.append({
970
+ event: record.event,
971
+ byteCount: record.eventBytes,
972
+ serialized: record.serializedEvent
973
+ });
974
+ },
975
+ acknowledge(count) {
976
+ return queue.acknowledge(count);
977
+ },
978
+ purge() {
979
+ return queue.acknowledge(queue.length());
980
+ },
981
+ close() {
982
+ return queue.close();
983
+ }
984
+ };
985
+ }
986
+
987
+ function validateEventQueue(queue) {
988
+ const methods = [
989
+ "acknowledge",
990
+ "append",
991
+ "byteCount",
992
+ "close",
993
+ "eventBytesAt",
994
+ "events",
995
+ "length",
996
+ "serializedAt"
997
+ ];
998
+ if (!queue || Array.isArray(queue) || typeof queue !== "object" || methods.some((method) => typeof queue[method] !== "function")) {
999
+ throw new SdkError("validation_error", "event queue factory returned an invalid queue");
1000
+ }
1001
+ }
1002
+
1003
+ function restoreStoredEvent(serializedEvent) {
1004
+ if (typeof serializedEvent !== "string" || serializedEvent === "") {
1005
+ throw invalidStoredRecord();
1006
+ }
1007
+ let event;
1008
+ try {
1009
+ event = JSON.parse(serializedEvent);
1010
+ } catch {
1011
+ throw invalidStoredRecord();
1012
+ }
1013
+ return normalizeStoredRecord({
1014
+ event,
1015
+ eventBytes: utf8ByteLength(serializedEvent),
1016
+ serializedEvent
1017
+ }).event;
1018
+ }
1019
+
1020
+ function requireSynchronousStoreResult(operation, result) {
1021
+ if (result && typeof result.then === "function") {
1022
+ throw new SdkError("persistence_error", `eventStore.${operation} must complete synchronously`);
1023
+ }
1024
+ return result;
1025
+ }
1026
+
1027
+ function loadStoredEvents({
1028
+ batchPrefixBytes,
1029
+ batchSuffixBytes,
1030
+ eventStore,
1031
+ maxBatchBytes,
1032
+ maxQueueBytes,
1033
+ maxQueueSize
1034
+ }) {
1035
+ if (!eventStore) {
1036
+ return {
1037
+ events: [],
1038
+ queuedEventBytes: 0,
1039
+ serializedEventBytes: [],
1040
+ serializedEvents: []
1041
+ };
1042
+ }
1043
+
1044
+ const records = requireSynchronousStoreResult("load", eventStore.load());
1045
+ if (!Array.isArray(records)) {
1046
+ throw invalidStoredRecord();
1047
+ }
1048
+ if (records.length > maxQueueSize) {
1049
+ throw new SdkError("persistence_error", "recovered event count exceeds maxQueueSize");
1050
+ }
1051
+
1052
+ const events = [];
1053
+ const serializedEvents = [];
1054
+ const serializedEventBytes = [];
1055
+ let queuedEventBytes = 0;
1056
+ for (const record of records) {
1057
+ const normalized = normalizeStoredRecord(record);
1058
+ if (batchPrefixBytes + normalized.eventBytes + batchSuffixBytes > maxBatchBytes) {
1059
+ throw new SdkError("persistence_error", "recovered event exceeds maxBatchBytes");
1060
+ }
1061
+ queuedEventBytes += normalized.eventBytes;
1062
+ if (queuedEventBytes > maxQueueBytes) {
1063
+ throw new SdkError("persistence_error", "recovered event bytes exceed maxQueueBytes");
1064
+ }
1065
+ events.push(normalized.event);
1066
+ serializedEvents.push(normalized.serializedEvent);
1067
+ serializedEventBytes.push(normalized.eventBytes);
1068
+ }
1069
+ return { events, queuedEventBytes, serializedEventBytes, serializedEvents };
1070
+ }
1071
+
1072
+ function normalizeStoredRecord(record) {
1073
+ try {
1074
+ if (!record || Array.isArray(record) || typeof record !== "object") {
1075
+ throw invalidStoredRecord();
1076
+ }
1077
+ const { event, eventBytes, serializedEvent } = record;
1078
+ if (!event || Array.isArray(event) || typeof event !== "object") {
1079
+ throw invalidStoredRecord();
1080
+ }
1081
+ const validator = EVENT_VALIDATORS.get(event.type);
1082
+ if (!validator || !Number.isSafeInteger(eventBytes) || eventBytes <= 0 || typeof serializedEvent !== "string") {
1083
+ throw invalidStoredRecord();
1084
+ }
1085
+ requireNonEmpty("event id", event.id);
1086
+ requireTimestamp(event.timestamp);
1087
+ if (!event.attributes || Array.isArray(event.attributes) || typeof event.attributes !== "object") {
1088
+ throw invalidStoredRecord();
1089
+ }
1090
+ const normalizedEvent = {
1091
+ type: event.type,
1092
+ id: event.id,
1093
+ timestamp: event.timestamp,
1094
+ attributes: validator(event.attributes)
1095
+ };
1096
+ if (JSON.stringify(normalizedEvent) !== serializedEvent || utf8ByteLength(serializedEvent) !== eventBytes) {
1097
+ throw invalidStoredRecord();
1098
+ }
1099
+ return {
1100
+ event: cloneEvent(normalizedEvent),
1101
+ eventBytes,
1102
+ serializedEvent
1103
+ };
1104
+ } catch (error) {
1105
+ if (error instanceof SdkError && error.code === "persistence_error") {
1106
+ throw error;
1107
+ }
1108
+ throw invalidStoredRecord();
1109
+ }
1110
+ }
1111
+
1112
+ function invalidStoredRecord() {
1113
+ return new SdkError("persistence_error", "event store returned an invalid record");
1114
+ }
1115
+
218
1116
  function installLogBrewConsoleCapture(config) {
219
1117
  if (!config || typeof config !== "object") {
220
1118
  throw new SdkError("validation_error", "console capture config must be an object");
@@ -352,6 +1250,197 @@ function logAttributesFromConsoleArgs(method, args, options = {}) {
352
1250
  };
353
1251
  }
354
1252
 
1253
+ function createIssueAttributesFromError(error, options = {}) {
1254
+ if (!options || Array.isArray(options) || typeof options !== "object") {
1255
+ throw new SdkError("validation_error", "error issue options must be an object");
1256
+ }
1257
+ const details = errorDetails(error);
1258
+ const stackFrames = javascriptStackFrames(details.stack, options.debugIdMap);
1259
+ const frame = stackFrames[0] ?? null;
1260
+ const source = stringOrUndefined(options.source) ?? "javascript.error";
1261
+ const metadata = {
1262
+ ...compactMetadata(options.metadata),
1263
+ source,
1264
+ errorName: details.name,
1265
+ ...(details.message ? { errorMessage: details.message } : {}),
1266
+ ...(frame ? {
1267
+ errorFrameFile: frame.filename,
1268
+ errorFrameLine: frame.line,
1269
+ errorFrameColumn: frame.column
1270
+ } : {}),
1271
+ ...issueGroupingMetadata(source, details, frame, options.fingerprint),
1272
+ ...errorCauseMetadata(error),
1273
+ ...(stringOrUndefined(options.release) ? { release: options.release } : {}),
1274
+ ...(stringOrUndefined(options.environment) ? { environment: options.environment } : {}),
1275
+ ...(stringOrUndefined(options.service) ? { service: options.service } : {}),
1276
+ ...(stringOrUndefined(options.runtime) ? { runtime: options.runtime } : {}),
1277
+ ...(stringOrUndefined(options.platform) ? { platform: options.platform } : {}),
1278
+ ...traceMetadata(options.trace),
1279
+ ...releaseArtifactMetadata(frame),
1280
+ ...(options.includeErrorStack === true && details.stack ? { errorStack: details.stack } : {})
1281
+ };
1282
+
1283
+ return {
1284
+ title: stringOrUndefined(options.title) ?? details.name,
1285
+ level: normalizeSeverity("issue level", options.level ?? "error"),
1286
+ ...(stringOrUndefined(options.message) ? { message: options.message } : details.message ? { message: details.message } : {}),
1287
+ ...(stackFrames.length > 0 ? { stackFrames } : {}),
1288
+ metadata: compactMetadata(metadata)
1289
+ };
1290
+ }
1291
+
1292
+ function errorDetails(error) {
1293
+ if (error instanceof Error) {
1294
+ return {
1295
+ name: stringOrUndefined(error.name) ?? "Error",
1296
+ message: stringOrUndefined(error.message),
1297
+ stack: typeof error.stack === "string" && error.stack.trim() !== "" ? error.stack : undefined
1298
+ };
1299
+ }
1300
+ if (error && typeof error === "object") {
1301
+ const name = typeof error.name === "string" && error.name.trim() !== "" ? error.name : "Error";
1302
+ const message = typeof error.message === "string" && error.message.trim() !== "" ? error.message : undefined;
1303
+ const stack = typeof error.stack === "string" && error.stack.trim() !== "" ? error.stack : undefined;
1304
+ return { name, message, stack };
1305
+ }
1306
+ if (typeof error === "string" && error.trim() !== "") {
1307
+ return { name: "Error", message: error };
1308
+ }
1309
+ return { name: "Error" };
1310
+ }
1311
+
1312
+ function issueGroupingMetadata(source, details, frame, fingerprint) {
1313
+ const groupingKey = frame
1314
+ ? `${source}:${details.name}:${frame.filename}`
1315
+ : `${source}:${details.name}`;
1316
+ const explicitFingerprint = issueFingerprintOrUndefined(fingerprint);
1317
+ return {
1318
+ issueGroupingKey: groupingKey,
1319
+ issueGroupingSource: explicitFingerprint ? "explicit_fingerprint" : frame ? "error_type_and_frame" : "error_type",
1320
+ ...(explicitFingerprint ? { issueFingerprint: explicitFingerprint } : {})
1321
+ };
1322
+ }
1323
+
1324
+ function issueFingerprintOrUndefined(value) {
1325
+ if (value === undefined || value === null) {
1326
+ return undefined;
1327
+ }
1328
+ if (typeof value !== "string" || value.trim() === "") {
1329
+ throw new SdkError("validation_error", "issue fingerprint must be a non-empty string");
1330
+ }
1331
+ return value.trim();
1332
+ }
1333
+
1334
+ function errorCauseMetadata(error) {
1335
+ const state = {
1336
+ items: [],
1337
+ seen: new Set(),
1338
+ sawExceptionGroup: false,
1339
+ truncated: false
1340
+ };
1341
+ if (isObjectLike(error)) {
1342
+ state.seen.add(error);
1343
+ collectNestedErrorCauses(error, state);
1344
+ }
1345
+ if (state.items.length === 0) {
1346
+ return {};
1347
+ }
1348
+ return {
1349
+ errorCauseCount: state.items.length,
1350
+ errorCauseTypes: state.items.map((item) => item.type).join(","),
1351
+ errorCauseSources: state.items.map((item) => item.source).join(","),
1352
+ ...(state.sawExceptionGroup ? { errorExceptionGroup: true } : {}),
1353
+ ...(state.truncated ? { errorCauseTruncated: true } : {})
1354
+ };
1355
+ }
1356
+
1357
+ function collectNestedErrorCauses(parent, state) {
1358
+ if (!isObjectLike(parent)) {
1359
+ return;
1360
+ }
1361
+ if ("cause" in parent) {
1362
+ collectErrorCause(parent.cause, "cause", state);
1363
+ }
1364
+ if (Array.isArray(parent.errors)) {
1365
+ state.sawExceptionGroup = true;
1366
+ for (const [index, child] of parent.errors.entries()) {
1367
+ collectErrorCause(child, `errors[${index}]`, state);
1368
+ }
1369
+ }
1370
+ }
1371
+
1372
+ function collectErrorCause(value, source, state) {
1373
+ if (value === undefined || value === null) {
1374
+ return;
1375
+ }
1376
+ if (state.items.length >= MAX_ERROR_CAUSES) {
1377
+ state.truncated = true;
1378
+ return;
1379
+ }
1380
+ if (isObjectLike(value)) {
1381
+ if (state.seen.has(value)) {
1382
+ state.truncated = true;
1383
+ return;
1384
+ }
1385
+ state.seen.add(value);
1386
+ }
1387
+ state.items.push({
1388
+ source,
1389
+ type: errorCauseType(value)
1390
+ });
1391
+ collectNestedErrorCauses(value, state);
1392
+ }
1393
+
1394
+ function errorCauseType(value) {
1395
+ if (isObjectLike(value)) {
1396
+ const constructorName = safeCauseTypeName(value.constructor?.name);
1397
+ if (value instanceof Error) {
1398
+ if (constructorName && constructorName !== "Error") {
1399
+ return constructorName;
1400
+ }
1401
+ const builtinName = BUILTIN_ERROR_NAMES.has(value.name) ? value.name : undefined;
1402
+ return builtinName ?? constructorName ?? "Error";
1403
+ }
1404
+ return constructorName ?? "Object";
1405
+ }
1406
+ return "NonError";
1407
+ }
1408
+
1409
+ function safeCauseTypeName(value) {
1410
+ return typeof value === "string" && /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/u.test(value) ? value : undefined;
1411
+ }
1412
+
1413
+ function isObjectLike(value) {
1414
+ return value !== null && (typeof value === "object" || typeof value === "function");
1415
+ }
1416
+
1417
+ function traceMetadata(trace) {
1418
+ if (trace === undefined || trace === null) {
1419
+ return {};
1420
+ }
1421
+ const normalized = normalizeLogTraceContext(trace);
1422
+ if (!normalized) {
1423
+ return {};
1424
+ }
1425
+ return {
1426
+ traceId: normalized.traceId,
1427
+ spanId: normalized.spanId,
1428
+ ...(normalized.parentSpanId ? { parentSpanId: normalized.parentSpanId } : {}),
1429
+ ...(normalized.sampled !== undefined ? { sampled: normalized.sampled } : {})
1430
+ };
1431
+ }
1432
+
1433
+ function releaseArtifactMetadata(frame) {
1434
+ if (!frame?.debugId) {
1435
+ return {};
1436
+ }
1437
+ return {
1438
+ releaseArtifactType: "sourcemap",
1439
+ releaseArtifactCodeFile: frame.filename,
1440
+ releaseArtifactDebugId: frame.debugId
1441
+ };
1442
+ }
1443
+
355
1444
  function logbrewLevelFromConsoleMethod(method) {
356
1445
  switch (method) {
357
1446
  case "debug":
@@ -450,6 +1539,44 @@ function createTraceparentHeaders(input) {
450
1539
  return { traceparent: createTraceparent(input) };
451
1540
  }
452
1541
 
1542
+ const {
1543
+ createBaggage,
1544
+ createTraceContextHeaders,
1545
+ createTracestate,
1546
+ parseBaggage,
1547
+ parseTracestate
1548
+ } = buildTraceContextHelpers({
1549
+ SdkError,
1550
+ createTraceparent
1551
+ });
1552
+
1553
+ const createSupportTicketDraft = buildCreateSupportTicketDraft({
1554
+ SdkError,
1555
+ requireAllowedValue,
1556
+ requireNonEmpty,
1557
+ requireTraceId
1558
+ });
1559
+
1560
+ const {
1561
+ createLogBrewOpenTelemetrySpanExporter,
1562
+ createLogBrewOpenTelemetrySpanProcessor,
1563
+ logbrewTraceContextFromCurrentOpenTelemetrySpan,
1564
+ logbrewTraceContextFromOpenTelemetrySpan,
1565
+ logbrewTraceContextFromOpenTelemetrySpanContext,
1566
+ spanAttributesFromOpenTelemetryReadableSpan
1567
+ } = buildOpenTelemetryHelpers({
1568
+ compactMetadata,
1569
+ isMetadataValue,
1570
+ LogBrewClient,
1571
+ maxSpanEvents: MAX_SPAN_EVENTS,
1572
+ maxSpanLinks: MAX_SPAN_LINKS,
1573
+ requireNonEmpty,
1574
+ requireSpanId,
1575
+ requireTraceId,
1576
+ SdkError,
1577
+ stringOrUndefined
1578
+ });
1579
+
453
1580
  function spanAttributesFromTraceparent(traceparent, attributes) {
454
1581
  if (!attributes || Array.isArray(attributes) || typeof attributes !== "object") {
455
1582
  throw new SdkError("validation_error", "span attributes must be an object");
@@ -463,6 +1590,8 @@ function spanAttributesFromTraceparent(traceparent, attributes) {
463
1590
  throw new SdkError("validation_error", "span durationMs must be non-negative");
464
1591
  }
465
1592
  }
1593
+ const events = validateSpanEvents(attributes.events);
1594
+ const links = validateSpanLinks(attributes.links);
466
1595
 
467
1596
  return {
468
1597
  name: attributes.name,
@@ -471,6 +1600,8 @@ function spanAttributesFromTraceparent(traceparent, attributes) {
471
1600
  parentSpanId: context.parentSpanId,
472
1601
  status: attributes.status,
473
1602
  ...(attributes.durationMs !== undefined ? { durationMs: attributes.durationMs } : {}),
1603
+ ...(events !== undefined ? { events } : {}),
1604
+ ...(links !== undefined ? { links } : {}),
474
1605
  ...(attributes.metadata !== undefined ? { metadata: compactMetadata(attributes.metadata) } : {})
475
1606
  };
476
1607
  }
@@ -495,6 +1626,7 @@ function createLogBrewPinoDestination(config) {
495
1626
  : () => new Date().toISOString();
496
1627
  const eventIdPrefix = config.eventIdPrefix ?? "pino";
497
1628
  const onError = typeof config.onError === "function" ? config.onError : () => {};
1629
+ const traceProvider = typeof config.traceProvider === "function" ? config.traceProvider : null;
498
1630
  const state = {
499
1631
  captured: 0,
500
1632
  pendingFlush: Promise.resolve(null)
@@ -513,7 +1645,8 @@ function createLogBrewPinoDestination(config) {
513
1645
  logAttributesFromPinoRecord(record, {
514
1646
  includeErrorStack,
515
1647
  logger,
516
- metadata
1648
+ metadata,
1649
+ trace: traceFromProvider(traceProvider, onError)
517
1650
  })
518
1651
  );
519
1652
  if (flushOnWrite && transport) {
@@ -552,7 +1685,8 @@ function logAttributesFromPinoRecord(record, options = {}) {
552
1685
  const metadata = {
553
1686
  ...compactMetadata(options.metadata),
554
1687
  pinoLevel: pinoLevelLabel(record.level),
555
- ...pinoContextMetadata(record)
1688
+ ...pinoContextMetadata(record),
1689
+ ...traceMetadataFromLogContext(options.trace)
556
1690
  };
557
1691
  if (typeof record.level === "number" && Number.isFinite(record.level)) {
558
1692
  metadata.pinoLevelNumber = record.level;
@@ -587,6 +1721,7 @@ function createLogBrewWinstonTransport(config) {
587
1721
  : () => new Date().toISOString();
588
1722
  const eventIdPrefix = config.eventIdPrefix ?? "winston";
589
1723
  const onError = typeof config.onError === "function" ? config.onError : () => {};
1724
+ const traceProvider = typeof config.traceProvider === "function" ? config.traceProvider : null;
590
1725
  const state = {
591
1726
  captured: 0,
592
1727
  pendingFlush: Promise.resolve(null)
@@ -608,6 +1743,7 @@ function createLogBrewWinstonTransport(config) {
608
1743
  onError,
609
1744
  state,
610
1745
  timestamp,
1746
+ traceProvider,
611
1747
  transport
612
1748
  });
613
1749
  } catch (error) {
@@ -663,7 +1799,8 @@ function captureWinstonInfo(config) {
663
1799
  logAttributesFromWinstonInfo(config.info, {
664
1800
  includeErrorStack: config.includeErrorStack,
665
1801
  logger: config.logger,
666
- metadata: config.metadata
1802
+ metadata: config.metadata,
1803
+ trace: traceFromProvider(config.traceProvider, config.onError)
667
1804
  })
668
1805
  );
669
1806
  if (config.flushOnWrite && config.transport) {
@@ -683,7 +1820,8 @@ function logAttributesFromWinstonInfo(info, options = {}) {
683
1820
  const metadata = {
684
1821
  ...compactMetadata(options.metadata),
685
1822
  winstonLevel: winstonLevelLabel(info.level),
686
- ...winstonContextMetadata(info)
1823
+ ...winstonContextMetadata(info),
1824
+ ...traceMetadataFromLogContext(options.trace)
687
1825
  };
688
1826
  addWinstonErrorMetadata(metadata, info, options.includeErrorStack === true);
689
1827
 
@@ -942,6 +2080,67 @@ function addPinoErrorMetadata(metadata, error, includeErrorStack) {
942
2080
  }
943
2081
  }
944
2082
 
2083
+ function traceFromProvider(provider, onError) {
2084
+ if (!provider) {
2085
+ return undefined;
2086
+ }
2087
+ try {
2088
+ return provider();
2089
+ } catch (error) {
2090
+ onError(error);
2091
+ return undefined;
2092
+ }
2093
+ }
2094
+
2095
+ function traceMetadataFromLogContext(trace) {
2096
+ const normalized = normalizeLogTraceContext(trace);
2097
+ if (!normalized) {
2098
+ return {};
2099
+ }
2100
+ return {
2101
+ traceId: normalized.traceId,
2102
+ spanId: normalized.spanId,
2103
+ ...(normalized.parentSpanId !== undefined ? { parentSpanId: normalized.parentSpanId } : {}),
2104
+ ...(normalized.sampled !== undefined ? { sampled: normalized.sampled } : {})
2105
+ };
2106
+ }
2107
+
2108
+ function normalizeLogTraceContext(trace) {
2109
+ if (!trace || Array.isArray(trace) || typeof trace !== "object") {
2110
+ return undefined;
2111
+ }
2112
+ const traceId = normalizeTraceId(trace.traceId);
2113
+ const spanId = normalizeSpanId(trace.spanId);
2114
+ if (!traceId || !spanId) {
2115
+ return undefined;
2116
+ }
2117
+ const parentSpanId = normalizeSpanId(trace.parentSpanId);
2118
+ return {
2119
+ traceId,
2120
+ spanId,
2121
+ ...(parentSpanId !== undefined ? { parentSpanId } : {}),
2122
+ ...(typeof trace.sampled === "boolean" ? { sampled: trace.sampled } : {})
2123
+ };
2124
+ }
2125
+
2126
+ function normalizeTraceId(traceId) {
2127
+ try {
2128
+ requireTraceId(traceId);
2129
+ } catch {
2130
+ return undefined;
2131
+ }
2132
+ return traceId.toLowerCase();
2133
+ }
2134
+
2135
+ function normalizeSpanId(spanId) {
2136
+ try {
2137
+ requireSpanId("trace spanId", spanId);
2138
+ } catch {
2139
+ return undefined;
2140
+ }
2141
+ return spanId.toLowerCase();
2142
+ }
2143
+
945
2144
  function requireNonEmpty(label, value) {
946
2145
  if (typeof value !== "string" || value.trim() === "") {
947
2146
  throw new SdkError("validation_error", `${label} must be non-empty`);
@@ -964,6 +2163,25 @@ function requireFiniteNumber(label, value) {
964
2163
  }
965
2164
  }
966
2165
 
2166
+ function requirePositiveInteger(label, value) {
2167
+ if (!Number.isSafeInteger(value) || value <= 0) {
2168
+ throw new SdkError("validation_error", `${label} must be a positive integer`);
2169
+ }
2170
+ }
2171
+
2172
+ function requireNonNegativeInteger(label, value) {
2173
+ if (!Number.isSafeInteger(value) || value < 0) {
2174
+ throw new SdkError("validation_error", `${label} must be a non-negative integer`);
2175
+ }
2176
+ }
2177
+
2178
+ function retryAfterMsOrUndefined(value) {
2179
+ if (value === undefined) {
2180
+ return undefined;
2181
+ }
2182
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
2183
+ }
2184
+
967
2185
  function requireTraceId(traceId) {
968
2186
  if (typeof traceId !== "string" || !/^[0-9a-fA-F]{32}$/u.test(traceId)) {
969
2187
  throw new SdkError("validation_error", "traceId must be 32 lowercase or uppercase hex characters");
@@ -1013,10 +2231,29 @@ function cloneMetadata(metadata) {
1013
2231
  return { ...metadata };
1014
2232
  }
1015
2233
 
2234
+ function cloneSpanEvents(events) {
2235
+ return events.map((event) => event.metadata === undefined
2236
+ ? { ...event }
2237
+ : { ...event, metadata: { ...event.metadata } });
2238
+ }
2239
+
2240
+ function cloneSpanLinks(links) {
2241
+ return links.map((link) => link.metadata === undefined
2242
+ ? { ...link }
2243
+ : { ...link, metadata: { ...link.metadata } });
2244
+ }
2245
+
1016
2246
  function cloneEvent(event) {
1017
- const attributes = event.attributes.metadata === undefined
1018
- ? { ...event.attributes }
1019
- : { ...event.attributes, metadata: { ...event.attributes.metadata } };
2247
+ const attributes = { ...event.attributes };
2248
+ if (event.attributes.metadata !== undefined) {
2249
+ attributes.metadata = { ...event.attributes.metadata };
2250
+ }
2251
+ if (Array.isArray(event.attributes.events)) {
2252
+ attributes.events = cloneSpanEvents(event.attributes.events);
2253
+ }
2254
+ if (Array.isArray(event.attributes.links)) {
2255
+ attributes.links = cloneSpanLinks(event.attributes.links);
2256
+ }
1020
2257
  return { ...event, attributes };
1021
2258
  }
1022
2259
 
@@ -1043,10 +2280,12 @@ function validateEnvironment(attributes) {
1043
2280
  function validateIssue(attributes) {
1044
2281
  requireNonEmpty("issue title", attributes.title);
1045
2282
  const level = normalizeSeverity("issue level", attributes.level);
2283
+ const stackFrames = validateIssueStackFrames(attributes.stackFrames);
1046
2284
  return withMetadata({
1047
2285
  title: attributes.title,
1048
2286
  level,
1049
- ...(attributes.message !== undefined ? { message: attributes.message } : {})
2287
+ ...(attributes.message !== undefined ? { message: attributes.message } : {}),
2288
+ ...(stackFrames !== undefined ? { stackFrames } : {})
1050
2289
  }, attributes.metadata);
1051
2290
  }
1052
2291
 
@@ -1078,16 +2317,94 @@ function validateSpan(attributes) {
1078
2317
  throw new SdkError("validation_error", "span durationMs must be non-negative");
1079
2318
  }
1080
2319
  }
2320
+ const events = validateSpanEvents(attributes.events);
2321
+ const links = validateSpanLinks(attributes.links);
1081
2322
  return withMetadata({
1082
2323
  name: attributes.name,
1083
2324
  traceId: attributes.traceId,
1084
2325
  spanId: attributes.spanId,
1085
2326
  status: attributes.status,
1086
2327
  ...(attributes.parentSpanId !== undefined ? { parentSpanId: attributes.parentSpanId } : {}),
1087
- ...(attributes.durationMs !== undefined ? { durationMs: attributes.durationMs } : {})
2328
+ ...(attributes.durationMs !== undefined ? { durationMs: attributes.durationMs } : {}),
2329
+ ...(events !== undefined ? { events } : {}),
2330
+ ...(links !== undefined ? { links } : {})
1088
2331
  }, attributes.metadata);
1089
2332
  }
1090
2333
 
2334
+ function validateSpanEvents(events) {
2335
+ if (events === undefined) {
2336
+ return undefined;
2337
+ }
2338
+ if (!Array.isArray(events)) {
2339
+ throw new SdkError("validation_error", "span events must be an array");
2340
+ }
2341
+ if (events.length > MAX_SPAN_EVENTS) {
2342
+ throw new SdkError("validation_error", `span events must contain at most ${MAX_SPAN_EVENTS} entries`);
2343
+ }
2344
+ if (events.length === 0) {
2345
+ return undefined;
2346
+ }
2347
+
2348
+ return events.map((event) => {
2349
+ if (!event || Array.isArray(event) || typeof event !== "object") {
2350
+ throw new SdkError("validation_error", "span event must be an object");
2351
+ }
2352
+ requireNonEmpty("span event name", event.name);
2353
+ if (event.timestamp !== undefined) {
2354
+ requireTimestamp(event.timestamp);
2355
+ }
2356
+ const summary = {
2357
+ name: event.name,
2358
+ ...(event.timestamp !== undefined ? { timestamp: event.timestamp } : {})
2359
+ };
2360
+ if (event.metadata !== undefined) {
2361
+ const metadata = compactMetadata(event.metadata);
2362
+ if (Object.keys(metadata).length > 0) {
2363
+ summary.metadata = metadata;
2364
+ }
2365
+ }
2366
+ return summary;
2367
+ });
2368
+ }
2369
+
2370
+ function validateSpanLinks(links) {
2371
+ if (links === undefined) {
2372
+ return undefined;
2373
+ }
2374
+ if (!Array.isArray(links)) {
2375
+ throw new SdkError("validation_error", "span links must be an array");
2376
+ }
2377
+ if (links.length > MAX_SPAN_LINKS) {
2378
+ throw new SdkError("validation_error", `span links must contain at most ${MAX_SPAN_LINKS} entries`);
2379
+ }
2380
+ if (links.length === 0) {
2381
+ return undefined;
2382
+ }
2383
+
2384
+ return links.map((link) => {
2385
+ if (!link || Array.isArray(link) || typeof link !== "object") {
2386
+ throw new SdkError("validation_error", "span link must be an object");
2387
+ }
2388
+ requireTraceId(link.traceId);
2389
+ requireSpanId("span link spanId", link.spanId);
2390
+ if (link.sampled !== undefined && typeof link.sampled !== "boolean") {
2391
+ throw new SdkError("validation_error", "span link sampled must be a boolean");
2392
+ }
2393
+ const summary = {
2394
+ traceId: link.traceId.toLowerCase(),
2395
+ spanId: link.spanId.toLowerCase(),
2396
+ ...(link.sampled !== undefined ? { sampled: link.sampled } : {})
2397
+ };
2398
+ if (link.metadata !== undefined) {
2399
+ const metadata = compactMetadata(link.metadata);
2400
+ if (Object.keys(metadata).length > 0) {
2401
+ summary.metadata = metadata;
2402
+ }
2403
+ }
2404
+ return summary;
2405
+ });
2406
+ }
2407
+
1091
2408
  function validateAction(attributes) {
1092
2409
  requireNonEmpty("action name", attributes.name);
1093
2410
  requireAllowedValue("action status", attributes.status, ACTION_STATUSES);
@@ -1317,21 +2634,34 @@ function formatConsoleArgument(value, includeErrorStack) {
1317
2634
  }
1318
2635
 
1319
2636
  module.exports = {
2637
+ createBaggage,
2638
+ createIssueAttributesFromError,
1320
2639
  createNetworkMilestoneAttributes,
1321
2640
  createProductActionAttributes,
2641
+ createLogBrewOpenTelemetrySpanExporter,
2642
+ createLogBrewOpenTelemetrySpanProcessor,
2643
+ createSupportTicketDraft,
2644
+ createTraceContextHeaders,
1322
2645
  createTraceparent,
1323
2646
  createTraceparentHeaders,
2647
+ createTracestate,
1324
2648
  createLogBrewPinoDestination,
1325
2649
  createLogBrewWinstonTransport,
1326
2650
  installLogBrewConsoleCapture,
1327
2651
  LogBrewClient,
2652
+ logbrewTraceContextFromCurrentOpenTelemetrySpan,
2653
+ logbrewTraceContextFromOpenTelemetrySpan,
2654
+ logbrewTraceContextFromOpenTelemetrySpanContext,
1328
2655
  logAttributesFromConsoleArgs,
1329
2656
  logAttributesFromPinoRecord,
1330
2657
  logAttributesFromWinstonInfo,
1331
2658
  logbrewLevelFromConsoleMethod,
2659
+ parseBaggage,
1332
2660
  parseTraceparent,
2661
+ parseTracestate,
1333
2662
  RecordingTransport,
1334
2663
  SdkError,
2664
+ spanAttributesFromOpenTelemetryReadableSpan,
1335
2665
  spanAttributesFromTraceparent,
1336
2666
  TransportError
1337
2667
  };