@tallyrow/safesignal 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ import { j as Transport } from './types-BiRyHi1e.cjs';
2
+
3
+ /**
4
+ * `createOtlpTransport` — factory for the `./transport-otlp` subpath.
5
+ *
6
+ * Composes the subpath primitives into a `Transport` that delivers
7
+ * fully-processed `LogEvent`s to an OTLP logs backend as OTLP/HTTP+JSON,
8
+ * batched, fire-and-forget (no retry), fail-closed.
9
+ *
10
+ * Delivery policy (research D6/D7, contracts TO-2..TO-8):
11
+ *
12
+ * send(event)
13
+ * ├── if shutdownComplete: no-op
14
+ * ├── lazily install the pagehide best-effort flush (first send)
15
+ * ├── if serialized record > maxRecordBytes → oversized_event drop
16
+ * ├── if buffered >= maxBufferedEvents → buffer_overflow drop
17
+ * └── batcher.push(event) // flush on size / age
18
+ *
19
+ * flush(batch) // batcher callback
20
+ * ├── serialize(batch) (fail-closed: serialize_failed → drop)
21
+ * └── deliver(endpoint, headers, body) // fetch keepalive, never throws
22
+ * ├── delivered ─────────────────── done
23
+ * ├── unavailable ───────────────── delivery_unavailable notice
24
+ * ├── send_failed ───────────────── send_failed notice (+cause)
25
+ * └── partial_rejection ─────────── partial_rejection notice
26
+ *
27
+ * Every notice is rate-limited to one per failure class per instance per
28
+ * session and NEVER carries a configured header/secret value (FR-009).
29
+ * `send`/`flush`/`shutdown` NEVER throw or reject to the caller; only
30
+ * construction-time validation throws, at the consumer's call site.
31
+ *
32
+ * Boundary discipline (TO-7): the only `src/` import is a type-only import
33
+ * from `'../api/types.js'`. No `@opentelemetry/*` and no
34
+ * `../internal/telemetry/otel/` import — the payload is hand-serialized.
35
+ *
36
+ * Specs: `specs/007-transport-otlp/contracts/*`, `data-model.md`.
37
+ */
38
+
39
+ interface OtlpTransportOptions {
40
+ /** Full OTLP logs endpoint URL (e.g. `https://otlp.example.com/v1/logs`). */
41
+ endpoint: string;
42
+ /** Static request headers (e.g. auth). Sent only on the wire. */
43
+ headers?: Record<string, string>;
44
+ /** Batch flush triggers. */
45
+ batching?: {
46
+ maxBatchSize: number;
47
+ maxBatchAgeMs?: number;
48
+ };
49
+ /** Hard cap on buffered events; over-cap events are dropped. Default 1000. */
50
+ maxBufferedEvents?: number;
51
+ /** Per-record size guard in bytes; larger records are dropped. Default 64 KiB. */
52
+ maxRecordBytes?: number;
53
+ /** Stable diagnostic identifier (`Transport.name`). Default `'otlp'`. */
54
+ name?: string;
55
+ /** Permit `http://` localhost/127.0.0.1/[::1] only. Default false. */
56
+ allowInsecureLoopback?: boolean;
57
+ /** Receives rate-limited diagnostic notices. Never carries header values. */
58
+ onInternalError?: (err: Error) => void;
59
+ }
60
+ declare function createOtlpTransport(options: OtlpTransportOptions): Transport;
61
+
62
+ export { type OtlpTransportOptions, createOtlpTransport };
@@ -0,0 +1,62 @@
1
+ import { j as Transport } from './types-BiRyHi1e.js';
2
+
3
+ /**
4
+ * `createOtlpTransport` — factory for the `./transport-otlp` subpath.
5
+ *
6
+ * Composes the subpath primitives into a `Transport` that delivers
7
+ * fully-processed `LogEvent`s to an OTLP logs backend as OTLP/HTTP+JSON,
8
+ * batched, fire-and-forget (no retry), fail-closed.
9
+ *
10
+ * Delivery policy (research D6/D7, contracts TO-2..TO-8):
11
+ *
12
+ * send(event)
13
+ * ├── if shutdownComplete: no-op
14
+ * ├── lazily install the pagehide best-effort flush (first send)
15
+ * ├── if serialized record > maxRecordBytes → oversized_event drop
16
+ * ├── if buffered >= maxBufferedEvents → buffer_overflow drop
17
+ * └── batcher.push(event) // flush on size / age
18
+ *
19
+ * flush(batch) // batcher callback
20
+ * ├── serialize(batch) (fail-closed: serialize_failed → drop)
21
+ * └── deliver(endpoint, headers, body) // fetch keepalive, never throws
22
+ * ├── delivered ─────────────────── done
23
+ * ├── unavailable ───────────────── delivery_unavailable notice
24
+ * ├── send_failed ───────────────── send_failed notice (+cause)
25
+ * └── partial_rejection ─────────── partial_rejection notice
26
+ *
27
+ * Every notice is rate-limited to one per failure class per instance per
28
+ * session and NEVER carries a configured header/secret value (FR-009).
29
+ * `send`/`flush`/`shutdown` NEVER throw or reject to the caller; only
30
+ * construction-time validation throws, at the consumer's call site.
31
+ *
32
+ * Boundary discipline (TO-7): the only `src/` import is a type-only import
33
+ * from `'../api/types.js'`. No `@opentelemetry/*` and no
34
+ * `../internal/telemetry/otel/` import — the payload is hand-serialized.
35
+ *
36
+ * Specs: `specs/007-transport-otlp/contracts/*`, `data-model.md`.
37
+ */
38
+
39
+ interface OtlpTransportOptions {
40
+ /** Full OTLP logs endpoint URL (e.g. `https://otlp.example.com/v1/logs`). */
41
+ endpoint: string;
42
+ /** Static request headers (e.g. auth). Sent only on the wire. */
43
+ headers?: Record<string, string>;
44
+ /** Batch flush triggers. */
45
+ batching?: {
46
+ maxBatchSize: number;
47
+ maxBatchAgeMs?: number;
48
+ };
49
+ /** Hard cap on buffered events; over-cap events are dropped. Default 1000. */
50
+ maxBufferedEvents?: number;
51
+ /** Per-record size guard in bytes; larger records are dropped. Default 64 KiB. */
52
+ maxRecordBytes?: number;
53
+ /** Stable diagnostic identifier (`Transport.name`). Default `'otlp'`. */
54
+ name?: string;
55
+ /** Permit `http://` localhost/127.0.0.1/[::1] only. Default false. */
56
+ allowInsecureLoopback?: boolean;
57
+ /** Receives rate-limited diagnostic notices. Never carries header values. */
58
+ onInternalError?: (err: Error) => void;
59
+ }
60
+ declare function createOtlpTransport(options: OtlpTransportOptions): Transport;
61
+
62
+ export { type OtlpTransportOptions, createOtlpTransport };
@@ -0,0 +1,539 @@
1
+ // src/transport-otlp/batcher.ts
2
+ function createBatcher(opts) {
3
+ const buffer = [];
4
+ let maxAgeTimer = null;
5
+ let flushCallback = opts.flush;
6
+ const clearTimer = () => {
7
+ if (maxAgeTimer !== null) {
8
+ clearTimeout(maxAgeTimer);
9
+ maxAgeTimer = null;
10
+ }
11
+ };
12
+ const armTimer = () => {
13
+ if (opts.maxBatchAgeMs === void 0) return;
14
+ maxAgeTimer = setTimeout(() => {
15
+ maxAgeTimer = null;
16
+ doFlush();
17
+ }, opts.maxBatchAgeMs);
18
+ };
19
+ const doFlush = () => {
20
+ if (buffer.length === 0) return;
21
+ if (flushCallback === null) {
22
+ buffer.length = 0;
23
+ clearTimer();
24
+ return;
25
+ }
26
+ const events = buffer.slice();
27
+ buffer.length = 0;
28
+ clearTimer();
29
+ try {
30
+ flushCallback(events);
31
+ } catch {
32
+ }
33
+ };
34
+ return {
35
+ push(event) {
36
+ if (flushCallback === null) return;
37
+ buffer.push(event);
38
+ if (buffer.length === 1 && opts.maxBatchAgeMs !== void 0) {
39
+ armTimer();
40
+ }
41
+ if (buffer.length >= opts.maxBatchSize) {
42
+ doFlush();
43
+ }
44
+ },
45
+ flush() {
46
+ doFlush();
47
+ },
48
+ shutdown() {
49
+ clearTimer();
50
+ buffer.length = 0;
51
+ flushCallback = null;
52
+ },
53
+ size() {
54
+ return buffer.length;
55
+ }
56
+ };
57
+ }
58
+
59
+ // src/transport-otlp/delivery.ts
60
+ async function deliver(endpoint, headers, body) {
61
+ const fetchFn = globalThis.fetch;
62
+ if (typeof fetchFn !== "function") {
63
+ return { kind: "unavailable" };
64
+ }
65
+ let response;
66
+ try {
67
+ response = await fetchFn(endpoint, {
68
+ method: "POST",
69
+ body,
70
+ headers: { "content-type": "application/json", ...headers },
71
+ keepalive: true,
72
+ credentials: "same-origin"
73
+ });
74
+ } catch (cause) {
75
+ return { kind: "send_failed", detail: "fetch rejected", cause };
76
+ }
77
+ if (!response.ok) {
78
+ return { kind: "send_failed", detail: `HTTP ${response.status}` };
79
+ }
80
+ const rejected = await readRejectedCount(response);
81
+ if (rejected > 0) {
82
+ return { kind: "partial_rejection", rejected };
83
+ }
84
+ return { kind: "delivered" };
85
+ }
86
+ async function readRejectedCount(response) {
87
+ try {
88
+ if (typeof response.json !== "function") return 0;
89
+ const parsed = await response.json();
90
+ if (typeof parsed !== "object" || parsed === null) return 0;
91
+ const partial = parsed.partialSuccess;
92
+ if (typeof partial !== "object" || partial === null) return 0;
93
+ const raw = partial.rejectedLogRecords;
94
+ const n = typeof raw === "string" ? Number(raw) : raw;
95
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : 0;
96
+ } catch {
97
+ return 0;
98
+ }
99
+ }
100
+
101
+ // src/transport-otlp/endpoint-validation.ts
102
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
103
+ "localhost",
104
+ "127.0.0.1",
105
+ "[::1]"
106
+ ]);
107
+ function validateEndpoint(endpoint, allowInsecureLoopback) {
108
+ if (typeof endpoint !== "string") {
109
+ throw new TypeError(
110
+ `otlp transport: endpoint must be a string, got ${typeName(endpoint)}`
111
+ );
112
+ }
113
+ let parsed;
114
+ try {
115
+ parsed = new URL(endpoint);
116
+ } catch {
117
+ throw new TypeError(`otlp transport: invalid endpoint URL: '${endpoint}'`);
118
+ }
119
+ if (parsed.protocol === "https:") {
120
+ return parsed;
121
+ }
122
+ if (parsed.protocol === "http:") {
123
+ if (!allowInsecureLoopback) {
124
+ throw new Error(
125
+ `otlp transport refuses non-HTTPS endpoint '${endpoint}'`
126
+ );
127
+ }
128
+ if (!LOOPBACK_HOSTS.has(parsed.hostname)) {
129
+ throw new Error(
130
+ `otlp transport: allowInsecureLoopback permits only localhost / 127.0.0.1 / [::1]; got '${parsed.hostname}' in '${endpoint}'`
131
+ );
132
+ }
133
+ return parsed;
134
+ }
135
+ throw new Error(
136
+ `otlp transport refuses non-HTTPS endpoint '${endpoint}' (scheme '${parsed.protocol}' is not permitted)`
137
+ );
138
+ }
139
+ function typeName(value) {
140
+ if (value === null) return "null";
141
+ return typeof value;
142
+ }
143
+
144
+ // src/transport-otlp/errors.ts
145
+ var OtlpError = class extends Error {
146
+ constructor(code, transportName, message, cause) {
147
+ super(message);
148
+ this.name = "OtlpError";
149
+ this.code = code;
150
+ this.transportName = transportName;
151
+ if (cause !== void 0) {
152
+ Object.defineProperty(this, "cause", {
153
+ value: cause,
154
+ enumerable: true,
155
+ writable: false,
156
+ configurable: false
157
+ });
158
+ }
159
+ }
160
+ };
161
+ function notifyOnce(ctx, code, message, cause) {
162
+ if (ctx.notified[code]) return;
163
+ ctx.notified[code] = true;
164
+ const err = new OtlpError(
165
+ code,
166
+ ctx.name,
167
+ `otlp transport '${ctx.name}': ${message}`,
168
+ cause
169
+ );
170
+ try {
171
+ ctx.onInternalError(err);
172
+ } catch {
173
+ }
174
+ }
175
+ function freshNotifiedLedger() {
176
+ return {
177
+ oversized_event: false,
178
+ buffer_overflow: false,
179
+ delivery_unavailable: false,
180
+ send_failed: false,
181
+ partial_rejection: false,
182
+ serialize_failed: false,
183
+ shutdown_failed: false
184
+ };
185
+ }
186
+
187
+ // src/transport-otlp/attributes.ts
188
+ function toAnyValue(value) {
189
+ if (value === null) {
190
+ return {};
191
+ }
192
+ switch (typeof value) {
193
+ case "string":
194
+ return { stringValue: value };
195
+ case "boolean":
196
+ return { boolValue: value };
197
+ case "number":
198
+ return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
199
+ }
200
+ if (Array.isArray(value)) {
201
+ return { arrayValue: { values: value.map(toAnyValue) } };
202
+ }
203
+ return { kvlistValue: { values: toKeyValues(value) } };
204
+ }
205
+ function toKeyValues(record, keyPrefix = "") {
206
+ const out = [];
207
+ for (const key of Object.keys(record)) {
208
+ const value = record[key];
209
+ if (value === void 0) continue;
210
+ out.push({ key: keyPrefix + key, value: toAnyValue(value) });
211
+ }
212
+ return out;
213
+ }
214
+
215
+ // src/transport-otlp/resource.ts
216
+ function buildResource(context) {
217
+ const attributes = [];
218
+ const push = (key, value) => {
219
+ if (typeof value === "string" && value.length > 0) {
220
+ attributes.push({ key, value: { stringValue: value } });
221
+ }
222
+ };
223
+ push("service.name", context.application?.name);
224
+ push("service.version", context.application?.version);
225
+ push("deployment.environment", context.environment);
226
+ return { attributes };
227
+ }
228
+
229
+ // src/transport-otlp/otlp-serializer.ts
230
+ var SCOPE_NAME = "@tallyrow/safesignal";
231
+ var LEVEL_TO_SEVERITY_NUMBER = {
232
+ debug: 5,
233
+ info: 9,
234
+ warn: 13,
235
+ error: 17
236
+ };
237
+ var LEVEL_TO_SEVERITY_TEXT = {
238
+ debug: "DEBUG",
239
+ info: "INFO",
240
+ warn: "WARN",
241
+ error: "ERROR"
242
+ };
243
+ function toLogRecord(event, fallbackTimeMs) {
244
+ const ms = toEpochMs(event.timestamp, fallbackTimeMs);
245
+ const nano = String(ms * 1e6);
246
+ const attributes = toKeyValues(event.attributes);
247
+ const context = event.context;
248
+ if (context.attributes !== void 0) {
249
+ attributes.push(...toKeyValues(context.attributes, "context."));
250
+ }
251
+ pushModuleIdentity(attributes, context);
252
+ pushException(attributes, event);
253
+ const record = {
254
+ timeUnixNano: nano,
255
+ observedTimeUnixNano: nano,
256
+ severityNumber: LEVEL_TO_SEVERITY_NUMBER[event.level],
257
+ severityText: LEVEL_TO_SEVERITY_TEXT[event.level],
258
+ body: { stringValue: event.message },
259
+ attributes
260
+ };
261
+ const trace = context.trace;
262
+ if (trace !== void 0) {
263
+ record.traceId = trace.traceId;
264
+ record.spanId = trace.spanId;
265
+ if (trace.traceFlags !== void 0) {
266
+ record.flags = trace.traceFlags;
267
+ }
268
+ }
269
+ return record;
270
+ }
271
+ function serializeBatch(batch, fallbackTimeMs) {
272
+ const first = batch[0];
273
+ const resource = buildResource(first ? first.context : {});
274
+ const logRecords = batch.map((e) => toLogRecord(e, fallbackTimeMs));
275
+ return {
276
+ resourceLogs: [
277
+ {
278
+ resource,
279
+ scopeLogs: [{ scope: { name: SCOPE_NAME }, logRecords }]
280
+ }
281
+ ]
282
+ };
283
+ }
284
+ function encode(request) {
285
+ return JSON.stringify(request);
286
+ }
287
+ function toEpochMs(iso, fallbackMs) {
288
+ const parsed = Date.parse(iso);
289
+ return Number.isFinite(parsed) ? parsed : fallbackMs;
290
+ }
291
+ function pushModuleIdentity(out, context) {
292
+ const mod = context.module;
293
+ if (mod === void 0) return;
294
+ if (typeof mod.name === "string" && mod.name.length > 0) {
295
+ out.push({ key: "module.name", value: { stringValue: mod.name } });
296
+ }
297
+ if (typeof mod.version === "string" && mod.version.length > 0) {
298
+ out.push({ key: "module.version", value: { stringValue: mod.version } });
299
+ }
300
+ }
301
+ function pushException(out, event) {
302
+ const err = event.error;
303
+ if (err === void 0) return;
304
+ out.push({ key: "exception.type", value: { stringValue: err.name } });
305
+ out.push({ key: "exception.message", value: { stringValue: err.message } });
306
+ if (typeof err.stack === "string" && err.stack.length > 0) {
307
+ out.push({
308
+ key: "exception.stacktrace",
309
+ value: { stringValue: err.stack }
310
+ });
311
+ }
312
+ }
313
+
314
+ // src/transport-otlp/otlp-transport.ts
315
+ var DEFAULTS = {
316
+ maxBatchSize: 20,
317
+ maxBatchAgeMs: 5e3,
318
+ maxBufferedEvents: 1e3,
319
+ maxRecordBytes: 65536,
320
+ name: "otlp"
321
+ };
322
+ function validateOptions(options) {
323
+ if (typeof options !== "object" || options === null) {
324
+ throw new TypeError("otlp transport: options must be a non-null object");
325
+ }
326
+ const { headers, batching, maxBufferedEvents, maxRecordBytes } = options;
327
+ if (headers !== void 0) {
328
+ if (typeof headers !== "object" || headers === null) {
329
+ throw new TypeError("otlp transport: headers must be an object");
330
+ }
331
+ for (const key of Object.keys(headers)) {
332
+ if (typeof headers[key] !== "string") {
333
+ throw new TypeError(
334
+ `otlp transport: header '${key}' must be a string value`
335
+ );
336
+ }
337
+ }
338
+ }
339
+ const maxBatchSize = batching?.maxBatchSize ?? DEFAULTS.maxBatchSize;
340
+ if (!Number.isInteger(maxBatchSize) || maxBatchSize < 1) {
341
+ throw new RangeError(
342
+ "otlp transport: batching.maxBatchSize must be an integer >= 1"
343
+ );
344
+ }
345
+ const cap = maxBufferedEvents ?? DEFAULTS.maxBufferedEvents;
346
+ if (!Number.isInteger(cap) || cap < maxBatchSize) {
347
+ throw new RangeError(
348
+ "otlp transport: maxBufferedEvents must be an integer >= maxBatchSize"
349
+ );
350
+ }
351
+ const recBytes = maxRecordBytes ?? DEFAULTS.maxRecordBytes;
352
+ if (!Number.isInteger(recBytes) || recBytes < 1) {
353
+ throw new RangeError(
354
+ "otlp transport: maxRecordBytes must be an integer >= 1"
355
+ );
356
+ }
357
+ }
358
+ function createOtlpTransport(options) {
359
+ validateOptions(options);
360
+ const allowInsecureLoopback = options.allowInsecureLoopback ?? false;
361
+ validateEndpoint(options.endpoint, allowInsecureLoopback);
362
+ const name = options.name ?? DEFAULTS.name;
363
+ const maxBatchSize = options.batching?.maxBatchSize ?? DEFAULTS.maxBatchSize;
364
+ const maxBatchAgeMs = options.batching?.maxBatchAgeMs ?? DEFAULTS.maxBatchAgeMs;
365
+ const state = {
366
+ endpoint: options.endpoint,
367
+ // Copy + freeze the headers so later consumer mutation cannot change
368
+ // what we send, and so nothing outside delivery can read them.
369
+ headers: Object.freeze({ ...options.headers ?? {} }),
370
+ name,
371
+ onInternalError: options.onInternalError ?? (() => void 0),
372
+ maxBufferedEvents: options.maxBufferedEvents ?? DEFAULTS.maxBufferedEvents,
373
+ maxRecordBytes: options.maxRecordBytes ?? DEFAULTS.maxRecordBytes,
374
+ notified: freshNotifiedLedger(),
375
+ // Placeholder; real batcher assigned below once the flush closure exists.
376
+ batcher: void 0,
377
+ pagehideInstalled: false,
378
+ pagehideUninstall: null,
379
+ shutdownComplete: false,
380
+ inFlight: /* @__PURE__ */ new Set(),
381
+ pending: 0
382
+ };
383
+ state.batcher = createBatcher({
384
+ maxBatchSize,
385
+ maxBatchAgeMs,
386
+ flush: (events) => {
387
+ void flushBatch(state, events);
388
+ }
389
+ });
390
+ return {
391
+ name,
392
+ send(event) {
393
+ if (state.shutdownComplete) return;
394
+ ensurePagehide(state);
395
+ try {
396
+ const record = toLogRecord(event, Date.now());
397
+ if (byteLength(JSON.stringify(record)) > state.maxRecordBytes) {
398
+ notifyOnce(
399
+ state,
400
+ "oversized_event",
401
+ `dropped an event whose serialized record exceeds ${state.maxRecordBytes} bytes`
402
+ );
403
+ return;
404
+ }
405
+ } catch (cause) {
406
+ notifyOnce(
407
+ state,
408
+ "serialize_failed",
409
+ "dropped an event that failed to serialize",
410
+ cause
411
+ );
412
+ return;
413
+ }
414
+ if (state.pending >= state.maxBufferedEvents) {
415
+ notifyOnce(
416
+ state,
417
+ "buffer_overflow",
418
+ `${state.maxBufferedEvents} events undelivered; dropping event`
419
+ );
420
+ return;
421
+ }
422
+ state.pending += 1;
423
+ state.batcher.push(event);
424
+ },
425
+ async flush() {
426
+ state.batcher.flush();
427
+ await settleInFlight(state);
428
+ },
429
+ async shutdown() {
430
+ if (state.shutdownComplete) {
431
+ await settleInFlight(state);
432
+ return;
433
+ }
434
+ state.shutdownComplete = true;
435
+ try {
436
+ state.batcher.flush();
437
+ await settleInFlight(state);
438
+ } catch (cause) {
439
+ notifyOnce(state, "shutdown_failed", "shutdown flush failed", cause);
440
+ } finally {
441
+ teardownPagehide(state);
442
+ state.batcher.shutdown();
443
+ }
444
+ }
445
+ };
446
+ }
447
+ async function flushBatch(state, events) {
448
+ const count = events.length;
449
+ let body;
450
+ try {
451
+ body = encode(serializeBatch(events, Date.now()));
452
+ } catch (cause) {
453
+ state.pending = Math.max(0, state.pending - count);
454
+ notifyOnce(
455
+ state,
456
+ "serialize_failed",
457
+ "dropped a batch that failed to serialize",
458
+ cause
459
+ );
460
+ return;
461
+ }
462
+ const promise = (async () => {
463
+ const result = await deliver(
464
+ state.endpoint,
465
+ state.headers,
466
+ body
467
+ );
468
+ mapResult(state, result);
469
+ })().catch(() => {
470
+ });
471
+ state.inFlight.add(promise);
472
+ void promise.finally(() => {
473
+ state.inFlight.delete(promise);
474
+ state.pending = Math.max(0, state.pending - count);
475
+ });
476
+ }
477
+ function mapResult(state, result) {
478
+ switch (result.kind) {
479
+ case "delivered":
480
+ return;
481
+ case "unavailable":
482
+ notifyOnce(
483
+ state,
484
+ "delivery_unavailable",
485
+ "fetch is unavailable; dropping batch"
486
+ );
487
+ return;
488
+ case "send_failed":
489
+ notifyOnce(
490
+ state,
491
+ "send_failed",
492
+ `delivery failed (${result.detail})`,
493
+ result.cause
494
+ );
495
+ return;
496
+ case "partial_rejection":
497
+ notifyOnce(
498
+ state,
499
+ "partial_rejection",
500
+ `backend rejected ${result.rejected} record(s)`
501
+ );
502
+ return;
503
+ }
504
+ }
505
+ async function settleInFlight(state) {
506
+ await Promise.all([...state.inFlight]);
507
+ }
508
+ function ensurePagehide(state) {
509
+ if (state.pagehideInstalled) return;
510
+ const target = globalThis;
511
+ state.pagehideInstalled = true;
512
+ if (typeof target.addEventListener !== "function") {
513
+ state.pagehideUninstall = null;
514
+ return;
515
+ }
516
+ const handler = () => {
517
+ state.batcher.flush();
518
+ };
519
+ target.addEventListener("pagehide", handler);
520
+ state.pagehideUninstall = () => {
521
+ if (typeof target.removeEventListener === "function") {
522
+ target.removeEventListener("pagehide", handler);
523
+ }
524
+ };
525
+ }
526
+ function teardownPagehide(state) {
527
+ if (state.pagehideUninstall !== null) {
528
+ state.pagehideUninstall();
529
+ state.pagehideUninstall = null;
530
+ }
531
+ state.pagehideInstalled = false;
532
+ }
533
+ function byteLength(s) {
534
+ return new TextEncoder().encode(s).length;
535
+ }
536
+
537
+ export { createOtlpTransport };
538
+ //# sourceMappingURL=transport-otlp.mjs.map
539
+ //# sourceMappingURL=transport-otlp.mjs.map