@hue-run/sdk 0.16.0 → 0.18.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.
- package/CLI.md +18 -11
- package/EVALUATIONS.md +53 -2
- package/README.md +295 -45
- package/dist/attachment.d.ts +82 -0
- package/dist/attachment.js +225 -0
- package/dist/blobs.d.ts +73 -17
- package/dist/blobs.js +247 -96
- package/dist/cli/eval-direct.d.ts +3 -2
- package/dist/cli/eval-direct.js +49 -6
- package/dist/client.d.ts +21 -1
- package/dist/client.js +217 -32
- package/dist/evals/client.d.ts +14 -1
- package/dist/evals/client.js +73 -9
- package/dist/evals/files.d.ts +11 -1
- package/dist/evals/files.js +172 -20
- package/dist/evals/media.d.ts +217 -0
- package/dist/evals/media.js +907 -0
- package/dist/evals/runner.js +130 -106
- package/dist/evals/types.d.ts +48 -0
- package/dist/evals.d.ts +2 -0
- package/dist/evals.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inline-files.d.ts +160 -28
- package/dist/inline-files.js +809 -86
- package/dist/json-edits.d.ts +24 -0
- package/dist/json-edits.js +187 -0
- package/dist/privacy.d.ts +14 -1
- package/dist/privacy.js +86 -25
- package/dist/safety.d.ts +7 -2
- package/dist/safety.js +22 -4
- package/dist/snapshot.d.ts +7 -4
- package/dist/snapshot.js +177 -38
- package/dist/transport.d.ts +18 -0
- package/dist/transport.js +544 -38
- package/dist/types.d.ts +28 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/transport.js
CHANGED
|
@@ -6,13 +6,101 @@ import { LogsExporterMetricsHelper, ProtobufLogsSerializer, ProtobufTraceSeriali
|
|
|
6
6
|
import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace";
|
|
7
7
|
import { BatchLogRecordProcessor, } from "@opentelemetry/sdk-logs";
|
|
8
8
|
import { isInsecureOrigin, REDACTION_CONTEXT_UNITS, validateOptions } from "./config.js";
|
|
9
|
-
import { BlobUploader, FALLBACK_MESSAGES, fallbackAttributes, MAX_HELD_BLOB_BYTES, newTally, offloadAttributes, uploadBudgetMillis, } from "./blobs.js";
|
|
9
|
+
import { BLOBS_KEY, MAX_BLOB_ENTRIES, blobEntry, countFallback, BlobUploader, FALLBACK_MESSAGES, fallbackAttributes, fallbackRecord, MAX_HELD_BLOB_BYTES, newTally, mediaKey, offloadAttributes, offloadRecord, uploadBudgetMillis, } from "./blobs.js";
|
|
10
10
|
import { advertisedLimits, BATCH_TARGET_BYTES, DEFAULT_RECEIVER_LIMITS, fitsWithoutCompression, } from "./limits.js";
|
|
11
11
|
import { announcesLiveSpan, LIVE_SPAN_INTERVAL_MILLIS, MAX_LIVE_SPANS, pendingPlaceholder, PLACEHOLDERS_HEADER, withoutPlaceholderMarkers, } from "./live-spans.js";
|
|
12
|
-
import { estimateRecordBytes } from "./safety.js";
|
|
12
|
+
import { estimateRecordBytes, spanSource } from "./safety.js";
|
|
13
|
+
import { MediaDigestMemo, isMediaName } from "./inline-files.js";
|
|
14
|
+
import { locateParts } from "./attachment.js";
|
|
13
15
|
import { snapshotLog, snapshotSpan } from "./snapshot.js";
|
|
14
16
|
import { DROPPED_RECORDS_KEY, isContentKey, isTruncatedMarker, redactLog, redactSpan, TRUNCATED_KEY, truncatedMarker, withTruncatedKeys, } from "./privacy.js";
|
|
15
17
|
import { sdkVersion } from "./version.js";
|
|
18
|
+
/** A record's trace: a span's context, a log record's span identifiers; "" when unreadable. */
|
|
19
|
+
function recordTraceId(record) {
|
|
20
|
+
try {
|
|
21
|
+
return "spanContext" in record && typeof record.spanContext === "function"
|
|
22
|
+
? record.spanContext().traceId
|
|
23
|
+
: (record.spanContext?.traceId ?? "");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The value of a record's attribute, or a log record's body, that holds placed attachments. */
|
|
30
|
+
function placedValue(record, attribute) {
|
|
31
|
+
if (attribute === "body" &&
|
|
32
|
+
!("spanContext" in record && typeof record.spanContext === "function"))
|
|
33
|
+
return record.body;
|
|
34
|
+
return record.attributes[attribute];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The staged files a record still authorizes. Each surviving `hue.file` event authorizes one
|
|
38
|
+
* staged request, including repeated digests. An attachment placed in a value is authorized by
|
|
39
|
+
* its own part in the value as exported: after redaction (`before` absent) each part is found in
|
|
40
|
+
* its value, and keyed at its pointer there, a part the redactor removed taking its upload with
|
|
41
|
+
* it; after fitting (`before`, the record as it was fitted) a part survives while fitting left its
|
|
42
|
+
* value as it was.
|
|
43
|
+
*/
|
|
44
|
+
function survivingMedia(files, record, tally, before) {
|
|
45
|
+
const consumed = new Set();
|
|
46
|
+
const traceId = recordTraceId(record);
|
|
47
|
+
const events = "events" in record ? record.events : [];
|
|
48
|
+
const located = new Map();
|
|
49
|
+
const locate = (attribute) => {
|
|
50
|
+
let parts = located.get(attribute);
|
|
51
|
+
if (parts)
|
|
52
|
+
return [];
|
|
53
|
+
let value = placedValue(record, attribute);
|
|
54
|
+
try {
|
|
55
|
+
if (typeof value === "string")
|
|
56
|
+
value = JSON.parse(value);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
value = undefined;
|
|
60
|
+
}
|
|
61
|
+
parts = locateParts(value, files.filter((file) => file.attribute === attribute)).map(({ file, pointer, name }) => {
|
|
62
|
+
const { name: _staged, ...rest } = file;
|
|
63
|
+
return { ...rest, key: `${attribute}#${pointer}`, ...(name ? { name } : {}) };
|
|
64
|
+
});
|
|
65
|
+
located.set(attribute, parts);
|
|
66
|
+
return parts;
|
|
67
|
+
};
|
|
68
|
+
return files.flatMap((file) => {
|
|
69
|
+
if (file.attribute !== undefined) {
|
|
70
|
+
if (!before)
|
|
71
|
+
return locate(file.attribute);
|
|
72
|
+
if (placedValue(record, file.attribute) === placedValue(before, file.attribute))
|
|
73
|
+
return [file];
|
|
74
|
+
countFallback(tally, "budget", traceId);
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const index = events.findIndex((event, index) => !consumed.has(index) &&
|
|
78
|
+
event.name === "hue.file" &&
|
|
79
|
+
(!file.eventTime ||
|
|
80
|
+
(event.time[0] === file.eventTime[0] && event.time[1] === file.eventTime[1])) &&
|
|
81
|
+
event.attributes?.["hue.file.sha256"] === file.digest.sha256 &&
|
|
82
|
+
event.attributes?.["hue.file.size"] === file.digest.size &&
|
|
83
|
+
event.attributes?.["hue.file.media_type"] === file.contentType);
|
|
84
|
+
if (index < 0) {
|
|
85
|
+
countFallback(tally, "budget", traceId);
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
consumed.add(index);
|
|
89
|
+
const name = events[index].attributes?.["hue.file.name"];
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
bytes: file.bytes,
|
|
93
|
+
digest: file.digest,
|
|
94
|
+
contentType: file.contentType,
|
|
95
|
+
...(file.eventTime ? { eventTime: file.eventTime } : {}),
|
|
96
|
+
...(isMediaName(name) ? { name } : {}),
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function mediaEntry(file) {
|
|
102
|
+
return blobEntry(mediaKey(file), file.digest.sha256, file.digest.size, file.contentType, file.name);
|
|
103
|
+
}
|
|
16
104
|
/**
|
|
17
105
|
* How long one export holds its records for a receiver that refused them for their rate (HTTP 429)
|
|
18
106
|
* and asked to be retried later than a request's deadline allows: the records stay queued, counted
|
|
@@ -125,11 +213,27 @@ export class HueTransport {
|
|
|
125
213
|
admissionCuts = new WeakMap();
|
|
126
214
|
// The span's own messages whose large inline files each admitted span shrank to their digest.
|
|
127
215
|
admissionDigests = new WeakMap();
|
|
216
|
+
// The span's own messages each admitted span inspected, or left to export to inspect.
|
|
217
|
+
admissionInspected = new WeakMap();
|
|
128
218
|
/** Uploads values over the inline limit apart from their spans; absent when disabled. */
|
|
129
219
|
uploader;
|
|
130
220
|
/** Bytes of values over the inline limit queued spans hold whole for upload, and each span's. */
|
|
131
221
|
heldBytes = 0;
|
|
132
222
|
held = new WeakMap();
|
|
223
|
+
mediaMemo = new MediaDigestMemo();
|
|
224
|
+
// Context identity also matches safe API wrappers to the provider's ended span. Abandoned
|
|
225
|
+
// spans retain no span objects, and share the 2048-span / 128 MiB holding bounds.
|
|
226
|
+
media = new Map();
|
|
227
|
+
// Attachments placed in a log record's body, from `emit` until the record is admitted, and
|
|
228
|
+
// the admitted records' own.
|
|
229
|
+
logParts = new Map();
|
|
230
|
+
logMedia = new WeakMap();
|
|
231
|
+
mediaOwners = new Map();
|
|
232
|
+
mediaTimer;
|
|
233
|
+
mediaFinalizer = new FinalizationRegistry(({ identity, token }) => {
|
|
234
|
+
if (this.mediaOwners.get(identity)?.token === token)
|
|
235
|
+
this.releaseMediaIdentity(identity);
|
|
236
|
+
});
|
|
133
237
|
uploaded = 0;
|
|
134
238
|
uploadFallbacks = 0;
|
|
135
239
|
constructor(options) {
|
|
@@ -137,15 +241,21 @@ export class HueTransport {
|
|
|
137
241
|
Object.defineProperty(this, "options", { enumerable: false });
|
|
138
242
|
this.liveSpans = this.options.enabled !== false && this.options.liveSpans;
|
|
139
243
|
this.announcesLiveSpans = this.liveSpans;
|
|
140
|
-
this.traceExporter = new ReportingExporter(this, "traces", ProtobufTraceSerializer, TraceExporterMetricsHelper, (span, cache, offload) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
244
|
+
this.traceExporter = new ReportingExporter(this, "traces", ProtobufTraceSerializer, TraceExporterMetricsHelper, (span, cache, offload) => {
|
|
245
|
+
// Export inspects only the messages admission inspected within the record's allowance.
|
|
246
|
+
if (offload)
|
|
247
|
+
offload.inspected = this.admissionInspected.get(span);
|
|
248
|
+
return redactSpan(span, this.options, cache, {
|
|
249
|
+
valueBytes: this.limits.valueBytes,
|
|
250
|
+
cut: this.admissionCuts.get(span),
|
|
251
|
+
digested: this.admissionDigests.get(span),
|
|
252
|
+
...(offload ? { offload } : {}),
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
this.logExporter = new ReportingExporter(this, "logs", ProtobufLogsSerializer, LogsExporterMetricsHelper, (log, cache, offload) => redactLog(log, this.options, cache, {
|
|
147
256
|
valueBytes: this.limits.valueBytes,
|
|
148
257
|
cut: this.admissionCuts.get(log),
|
|
258
|
+
...(offload ? { offload } : {}),
|
|
149
259
|
}));
|
|
150
260
|
if (this.options.enabled !== false)
|
|
151
261
|
this.uploader = new BlobUploader({
|
|
@@ -187,18 +297,30 @@ export class HueTransport {
|
|
|
187
297
|
this.stopLiveTimer();
|
|
188
298
|
let admitted;
|
|
189
299
|
try {
|
|
190
|
-
if (!(span.spanContext().traceFlags & 1))
|
|
300
|
+
if (!(span.spanContext().traceFlags & 1)) {
|
|
301
|
+
this.releaseMedia(span);
|
|
191
302
|
return;
|
|
303
|
+
}
|
|
192
304
|
const queued = this.enqueue("traces", span);
|
|
193
|
-
if (!queued)
|
|
305
|
+
if (!queued) {
|
|
306
|
+
const tally = newTally();
|
|
307
|
+
for (const file of this.mediaAttachments(span))
|
|
308
|
+
if (file.bytes.byteLength)
|
|
309
|
+
countFallback(tally, "budget", span.spanContext().traceId);
|
|
310
|
+
this.reportOffload(tally);
|
|
311
|
+
this.releaseMedia(span);
|
|
194
312
|
return;
|
|
313
|
+
}
|
|
195
314
|
admitted = queued;
|
|
315
|
+
this.protectMedia(admitted);
|
|
196
316
|
this.countEnded(admitted);
|
|
197
317
|
spans.onEnd(admitted);
|
|
198
318
|
}
|
|
199
319
|
catch {
|
|
200
320
|
if (admitted)
|
|
201
321
|
this.finish("traces", [admitted]);
|
|
322
|
+
else
|
|
323
|
+
this.releaseMedia(span);
|
|
202
324
|
this.issue("traces", "invalid", 1, "Telemetry processor could not accept a record", undefined, traceIdsOf([span]));
|
|
203
325
|
}
|
|
204
326
|
},
|
|
@@ -213,9 +335,21 @@ export class HueTransport {
|
|
|
213
335
|
let admitted;
|
|
214
336
|
try {
|
|
215
337
|
const queued = this.enqueue("logs", log);
|
|
216
|
-
if (!queued)
|
|
338
|
+
if (!queued) {
|
|
339
|
+
const pending = log.body !== null && typeof log.body === "object"
|
|
340
|
+
? this.logParts.get(log.body)
|
|
341
|
+
: undefined;
|
|
342
|
+
if (pending) {
|
|
343
|
+
const tally = newTally();
|
|
344
|
+
for (const _file of pending.files)
|
|
345
|
+
countFallback(tally, "budget", pending.traceId);
|
|
346
|
+
this.reportOffload(tally, "logs");
|
|
347
|
+
this.logParts.delete(log.body);
|
|
348
|
+
}
|
|
217
349
|
return;
|
|
350
|
+
}
|
|
218
351
|
admitted = queued;
|
|
352
|
+
this.admitLogParts(log, queued);
|
|
219
353
|
logs.onEmit(queued);
|
|
220
354
|
}
|
|
221
355
|
catch {
|
|
@@ -254,7 +388,7 @@ export class HueTransport {
|
|
|
254
388
|
// and its full size could drop the record before that cut ever ran. A span's own value
|
|
255
389
|
// that export may upload is held whole instead, within the held-value budget.
|
|
256
390
|
const valueUnits = this.limits.valueBytes + REDACTION_CONTEXT_UNITS;
|
|
257
|
-
const hold =
|
|
391
|
+
const hold = !advisory && this.holdsValues()
|
|
258
392
|
? {
|
|
259
393
|
valueBytes: this.limits.valueBytes,
|
|
260
394
|
budget: Math.max(0, MAX_HELD_BLOB_BYTES - this.heldBytes),
|
|
@@ -262,12 +396,14 @@ export class HueTransport {
|
|
|
262
396
|
}
|
|
263
397
|
: undefined;
|
|
264
398
|
const snapshot = signal === "traces"
|
|
265
|
-
? snapshotSpan(record, remaining, this.options.captureContent, valueUnits, hold)
|
|
266
|
-
: snapshotLog(record, remaining, this.options.captureContent, valueUnits);
|
|
399
|
+
? snapshotSpan(record, remaining, this.options.captureContent, valueUnits, hold, this.mediaMemo)
|
|
400
|
+
: snapshotLog(record, remaining, this.options.captureContent, valueUnits, hold, this.mediaMemo);
|
|
267
401
|
if (snapshot.cut.size)
|
|
268
402
|
this.admissionCuts.set(snapshot.record, snapshot.cut);
|
|
269
403
|
if (snapshot.digested.size)
|
|
270
404
|
this.admissionDigests.set(snapshot.record, snapshot.digested);
|
|
405
|
+
if (signal === "traces")
|
|
406
|
+
this.admissionInspected.set(snapshot.record, snapshot.inspected);
|
|
271
407
|
if (snapshot.held) {
|
|
272
408
|
this.heldBytes += snapshot.held;
|
|
273
409
|
this.held.set(snapshot.record, snapshot.held);
|
|
@@ -311,7 +447,11 @@ export class HueTransport {
|
|
|
311
447
|
this.live.delete(span);
|
|
312
448
|
try {
|
|
313
449
|
// Ended without reaching onEnd: a wrapping processor filtered it, so no real span follows.
|
|
314
|
-
if (
|
|
450
|
+
if (span.ended) {
|
|
451
|
+
this.releaseMedia(span);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (!this.liveSpans || !this.batchSpans)
|
|
315
455
|
continue;
|
|
316
456
|
const { record, markers } = pendingPlaceholder(span);
|
|
317
457
|
const admitted = this.enqueue("traces", record, true);
|
|
@@ -373,6 +513,8 @@ export class HueTransport {
|
|
|
373
513
|
/** @internal Exporter callback: releases queued records after an export attempt settles. */
|
|
374
514
|
finish(signal, records) {
|
|
375
515
|
for (const record of records) {
|
|
516
|
+
this.releaseMedia(record);
|
|
517
|
+
this.logMedia.delete(record);
|
|
376
518
|
const pending = signal === "traces" ? this.spans : this.logs;
|
|
377
519
|
this.pendingBytes -= pending.get(record) ?? 0;
|
|
378
520
|
pending.delete(record);
|
|
@@ -383,7 +525,218 @@ export class HueTransport {
|
|
|
383
525
|
}
|
|
384
526
|
}
|
|
385
527
|
}
|
|
386
|
-
/** @internal
|
|
528
|
+
/** @internal The same bounded digest memo is used by explicit and inline media. */
|
|
529
|
+
digestMedia(bytes, traceId) {
|
|
530
|
+
return this.mediaMemo.bytes(bytes, traceId);
|
|
531
|
+
}
|
|
532
|
+
/** @internal Retain a defensive copy within the existing held-value budget. */
|
|
533
|
+
stageMedia(span, file) {
|
|
534
|
+
if (this.closed || !this.offloadsValues() || file?.bytes.byteLength === 0)
|
|
535
|
+
return true;
|
|
536
|
+
if (!file) {
|
|
537
|
+
const tally = newTally();
|
|
538
|
+
countFallback(tally, "budget", span.spanContext().traceId);
|
|
539
|
+
this.reportOffload(tally);
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
if (!this.holdsValues()) {
|
|
543
|
+
this.uploadFallbacks++;
|
|
544
|
+
this.issue("traces", "warning", 1, "File bytes could not be uploaded while the upload service was unavailable; their hue.file metadata was recorded", undefined, [span.spanContext().traceId]);
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
const identity = this.mediaIdentity(span);
|
|
548
|
+
this.sweepMedia();
|
|
549
|
+
const files = this.media.get(identity) ?? [];
|
|
550
|
+
if (files.length >= 64 ||
|
|
551
|
+
(!this.media.has(identity) && this.media.size >= 2048) ||
|
|
552
|
+
this.heldBytes + file.bytes.byteLength > MAX_HELD_BLOB_BYTES) {
|
|
553
|
+
this.uploadFallbacks++;
|
|
554
|
+
this.issue("traces", "warning", 1, "File bytes did not fit the SDK's upload budget; their hue.file metadata was recorded", undefined, [span.spanContext().traceId]);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
const bytes = Uint8Array.from(file.bytes);
|
|
558
|
+
files.push({ ...file, bytes });
|
|
559
|
+
this.media.set(identity, files);
|
|
560
|
+
this.ownMedia(span, identity);
|
|
561
|
+
this.heldBytes += bytes.byteLength;
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* @internal Stages the attachments placed in a span's `attribute`, replacing those an earlier
|
|
566
|
+
* value of the attribute placed: each is uploaded at export when its part survives there. A
|
|
567
|
+
* file that cannot be held is counted as not uploaded; it keeps its digest part.
|
|
568
|
+
*/
|
|
569
|
+
stageParts(span, attribute, files) {
|
|
570
|
+
if (this.closed || !this.offloadsValues())
|
|
571
|
+
return;
|
|
572
|
+
const traceId = span.spanContext().traceId;
|
|
573
|
+
const identity = this.mediaIdentity(span);
|
|
574
|
+
const previous = this.media.get(identity);
|
|
575
|
+
if (previous?.some((file) => file.attribute === attribute)) {
|
|
576
|
+
const kept = previous.filter((file) => {
|
|
577
|
+
if (file.attribute !== attribute)
|
|
578
|
+
return true;
|
|
579
|
+
this.heldBytes -= file.bytes.byteLength;
|
|
580
|
+
return false;
|
|
581
|
+
});
|
|
582
|
+
if (kept.length)
|
|
583
|
+
this.media.set(identity, kept);
|
|
584
|
+
else
|
|
585
|
+
this.media.delete(identity);
|
|
586
|
+
}
|
|
587
|
+
const tally = newTally();
|
|
588
|
+
for (const file of files) {
|
|
589
|
+
if (!file.bytes.byteLength)
|
|
590
|
+
continue;
|
|
591
|
+
const refused = this.uploader?.refusal();
|
|
592
|
+
if (!this.holdsValues() || refused) {
|
|
593
|
+
countFallback(tally, refused ?? "unsupported", traceId);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
this.sweepMedia();
|
|
597
|
+
const staged = this.media.get(identity) ?? [];
|
|
598
|
+
if (staged.length >= MAX_BLOB_ENTRIES ||
|
|
599
|
+
(!this.media.has(identity) && this.media.size >= 2048) ||
|
|
600
|
+
this.heldBytes + file.bytes.byteLength > MAX_HELD_BLOB_BYTES) {
|
|
601
|
+
countFallback(tally, "budget", traceId);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
// Attachment bytes are the attachment's own copy: held as they are.
|
|
605
|
+
staged.push({ ...file, attribute });
|
|
606
|
+
this.media.set(identity, staged);
|
|
607
|
+
this.heldBytes += file.bytes.byteLength;
|
|
608
|
+
this.ownMedia(span, identity);
|
|
609
|
+
}
|
|
610
|
+
if (tally.fallbacks.size)
|
|
611
|
+
this.reportOffload(tally);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* @internal Stages the attachments placed in a log record's body until the record is emitted
|
|
615
|
+
* (`body` is the object passed to `emit`); {@link unstageLogParts} drops what no processor of
|
|
616
|
+
* this transport admitted.
|
|
617
|
+
*/
|
|
618
|
+
stageLogParts(body, traceId, files) {
|
|
619
|
+
if (this.closed || !this.offloadsValues())
|
|
620
|
+
return;
|
|
621
|
+
const tally = newTally();
|
|
622
|
+
const staged = [];
|
|
623
|
+
for (const file of files) {
|
|
624
|
+
if (!file.bytes.byteLength)
|
|
625
|
+
continue;
|
|
626
|
+
const refused = this.uploader?.refusal();
|
|
627
|
+
if (!this.holdsValues() || refused)
|
|
628
|
+
countFallback(tally, refused ?? "unsupported", traceId);
|
|
629
|
+
else if (staged.length >= MAX_BLOB_ENTRIES)
|
|
630
|
+
countFallback(tally, "budget", traceId);
|
|
631
|
+
else
|
|
632
|
+
staged.push({ ...file, attribute: "body" });
|
|
633
|
+
}
|
|
634
|
+
if (tally.fallbacks.size)
|
|
635
|
+
this.reportOffload(tally, "logs");
|
|
636
|
+
if (staged.length)
|
|
637
|
+
this.logParts.set(body, { traceId, files: staged });
|
|
638
|
+
}
|
|
639
|
+
/** @internal Drops a log record's staged attachments once it was emitted. */
|
|
640
|
+
unstageLogParts(body) {
|
|
641
|
+
this.logParts.delete(body);
|
|
642
|
+
}
|
|
643
|
+
/** Holds the staged attachments of a log record being admitted with the queued record, within
|
|
644
|
+
* the held-value budget; those the budget refuses are counted as not uploaded. */
|
|
645
|
+
admitLogParts(log, queued) {
|
|
646
|
+
const body = log.body;
|
|
647
|
+
const pending = body !== null && typeof body === "object" ? this.logParts.get(body) : undefined;
|
|
648
|
+
if (!pending)
|
|
649
|
+
return;
|
|
650
|
+
this.logParts.delete(body);
|
|
651
|
+
const size = pending.files.reduce((total, file) => total + file.bytes.byteLength, 0);
|
|
652
|
+
if (this.heldBytes + size > MAX_HELD_BLOB_BYTES) {
|
|
653
|
+
const tally = newTally();
|
|
654
|
+
for (const _file of pending.files)
|
|
655
|
+
countFallback(tally, "budget", pending.traceId);
|
|
656
|
+
this.reportOffload(tally, "logs");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
this.logMedia.set(queued, pending.files);
|
|
660
|
+
this.heldBytes += size;
|
|
661
|
+
this.held.set(queued, (this.held.get(queued) ?? 0) + size);
|
|
662
|
+
}
|
|
663
|
+
/** Registers a span whose staged files are released if it is never exported. */
|
|
664
|
+
ownMedia(span, identity) {
|
|
665
|
+
if (this.mediaOwners.has(identity))
|
|
666
|
+
return;
|
|
667
|
+
const source = spanSource(span);
|
|
668
|
+
const token = {};
|
|
669
|
+
this.mediaOwners.set(identity, { source: new WeakRef(source), token });
|
|
670
|
+
this.mediaFinalizer.register(source, { identity, token }, token);
|
|
671
|
+
if (!this.mediaTimer) {
|
|
672
|
+
this.mediaTimer = setInterval(() => this.sweepMedia(), LIVE_SPAN_INTERVAL_MILLIS);
|
|
673
|
+
this.mediaTimer.unref();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
mediaIdentity(record) {
|
|
677
|
+
if (!("spanContext" in record) || typeof record.spanContext !== "function")
|
|
678
|
+
return undefined;
|
|
679
|
+
const { traceId, spanId } = record.spanContext();
|
|
680
|
+
return `${traceId}:${spanId}`;
|
|
681
|
+
}
|
|
682
|
+
releaseMedia(record) {
|
|
683
|
+
const identity = this.mediaIdentity(record);
|
|
684
|
+
if (!identity)
|
|
685
|
+
return;
|
|
686
|
+
this.releaseMediaIdentity(identity);
|
|
687
|
+
}
|
|
688
|
+
protectMedia(record) {
|
|
689
|
+
const identity = this.mediaIdentity(record);
|
|
690
|
+
if (!identity)
|
|
691
|
+
return;
|
|
692
|
+
const owner = this.mediaOwners.get(identity);
|
|
693
|
+
if (owner)
|
|
694
|
+
this.mediaFinalizer.unregister(owner.token);
|
|
695
|
+
this.mediaOwners.delete(identity);
|
|
696
|
+
if (!this.mediaOwners.size) {
|
|
697
|
+
clearInterval(this.mediaTimer);
|
|
698
|
+
this.mediaTimer = undefined;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
releaseMediaIdentity(identity) {
|
|
702
|
+
const owner = this.mediaOwners.get(identity);
|
|
703
|
+
if (owner)
|
|
704
|
+
this.mediaFinalizer.unregister(owner.token);
|
|
705
|
+
this.mediaOwners.delete(identity);
|
|
706
|
+
if (!this.mediaOwners.size) {
|
|
707
|
+
clearInterval(this.mediaTimer);
|
|
708
|
+
this.mediaTimer = undefined;
|
|
709
|
+
}
|
|
710
|
+
const files = this.media.get(identity);
|
|
711
|
+
if (!files)
|
|
712
|
+
return;
|
|
713
|
+
for (const file of files)
|
|
714
|
+
this.heldBytes -= file.bytes.byteLength;
|
|
715
|
+
this.media.delete(identity);
|
|
716
|
+
}
|
|
717
|
+
sweepMedia() {
|
|
718
|
+
for (const [identity, owner] of this.mediaOwners) {
|
|
719
|
+
try {
|
|
720
|
+
const source = owner.source.deref();
|
|
721
|
+
if (!source ||
|
|
722
|
+
source.ended === true ||
|
|
723
|
+
source.isRecording?.() === false)
|
|
724
|
+
this.releaseMediaIdentity(identity);
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/** @internal Staged requests are matched against surviving events and parts before upload. */
|
|
732
|
+
mediaAttachments(record) {
|
|
733
|
+
const logged = this.logMedia.get(record);
|
|
734
|
+
if (logged)
|
|
735
|
+
return logged;
|
|
736
|
+
const identity = this.mediaIdentity(record);
|
|
737
|
+
return identity ? (this.media.get(identity) ?? []) : [];
|
|
738
|
+
}
|
|
739
|
+
/** @internal Whether spans' content and media may be uploaded: content is captured and
|
|
387
740
|
* the key is a project key (setup credentials send metadata only). A value that then cannot be
|
|
388
741
|
* uploaded is cut, reported and counted. */
|
|
389
742
|
offloadsValues() {
|
|
@@ -391,7 +744,7 @@ export class HueTransport {
|
|
|
391
744
|
this.options.captureContent &&
|
|
392
745
|
!this.options.apiKey.startsWith("hue_setup_"));
|
|
393
746
|
}
|
|
394
|
-
/** @internal Whether
|
|
747
|
+
/** @internal Whether content and media are worth holding whole for upload now: they
|
|
395
748
|
* are uploaded, and the receiver has not shown it lacks the upload route. */
|
|
396
749
|
holdsValues() {
|
|
397
750
|
return this.offloadsValues() && this.uploader?.available() === true;
|
|
@@ -399,13 +752,8 @@ export class HueTransport {
|
|
|
399
752
|
/** @internal Exporter callback: a redacted span with the values its redaction left to the
|
|
400
753
|
* upload step placed: uploaded and listed under `hue.blobs`, or cut as before. Never rejects. */
|
|
401
754
|
async offloadSpan(span, collector, upload, tally) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
traceId = span.spanContext().traceId;
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
// A span without a readable context cannot name its trace: its values are cut.
|
|
408
|
-
}
|
|
755
|
+
// A record without a readable context cannot name its trace: its values are cut.
|
|
756
|
+
const traceId = recordTraceId(span);
|
|
409
757
|
const valueBytes = this.limits.valueBytes;
|
|
410
758
|
try {
|
|
411
759
|
if (!this.uploader)
|
|
@@ -416,23 +764,52 @@ export class HueTransport {
|
|
|
416
764
|
valueBytes,
|
|
417
765
|
upload,
|
|
418
766
|
tally,
|
|
767
|
+
memo: this.mediaMemo,
|
|
419
768
|
});
|
|
420
769
|
return { ...span, attributes: attributes };
|
|
421
770
|
}
|
|
422
771
|
catch {
|
|
423
772
|
return {
|
|
424
773
|
...span,
|
|
425
|
-
attributes: fallbackAttributes(span.attributes, collector, valueBytes, tally, traceId),
|
|
774
|
+
attributes: fallbackAttributes(span.attributes, collector, valueBytes, tally, traceId, this.mediaMemo),
|
|
426
775
|
};
|
|
427
776
|
}
|
|
428
777
|
}
|
|
778
|
+
/** @internal Exporter callback: a redacted log record with the media its redaction left to
|
|
779
|
+
* the upload step placed: its attributes' and its body's files uploaded and listed under its own
|
|
780
|
+
* `hue.blobs` (keys `<attribute>#<pointer>`, `body#<pointer>`), under the log record's trace;
|
|
781
|
+
* a file not uploaded keeps its bytes inline, and the record is cut as before. Never rejects. */
|
|
782
|
+
async offloadLog(log, collector, upload, tally) {
|
|
783
|
+
const traceId = log.spanContext?.traceId ?? "";
|
|
784
|
+
const valueBytes = this.limits.valueBytes;
|
|
785
|
+
const placed = (result) => ({
|
|
786
|
+
...log,
|
|
787
|
+
attributes: result.attributes,
|
|
788
|
+
...(collector.body ? { body: result.body } : {}),
|
|
789
|
+
});
|
|
790
|
+
try {
|
|
791
|
+
if (!this.uploader)
|
|
792
|
+
throw new Error("Uploads are unavailable");
|
|
793
|
+
return placed(await offloadRecord(log.attributes, collector, {
|
|
794
|
+
uploader: this.uploader,
|
|
795
|
+
traceId,
|
|
796
|
+
valueBytes,
|
|
797
|
+
upload,
|
|
798
|
+
tally,
|
|
799
|
+
memo: this.mediaMemo,
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
return placed(fallbackRecord(log.attributes, collector, valueBytes, tally, traceId, this.mediaMemo));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
429
806
|
/** @internal Exporter callback: counts an export's uploads and reports each kind of fallback
|
|
430
807
|
* once, as a warning naming its traces: the values were exported, cut as before. */
|
|
431
|
-
reportOffload(tally) {
|
|
808
|
+
reportOffload(tally, signal = "traces") {
|
|
432
809
|
this.uploaded += tally.uploaded;
|
|
433
810
|
for (const [reason, { count, traceIds }] of tally.fallbacks) {
|
|
434
811
|
this.uploadFallbacks += count;
|
|
435
|
-
this.issue(
|
|
812
|
+
this.issue(signal, "warning", count, FALLBACK_MESSAGES[reason], undefined, traceIds.size && traceIds.size <= MAX_ISSUE_TRACE_IDS ? [...traceIds] : undefined);
|
|
436
813
|
}
|
|
437
814
|
}
|
|
438
815
|
/** @internal Exporter callback: counts records the collector acknowledged. */
|
|
@@ -669,6 +1046,12 @@ export class HueTransport {
|
|
|
669
1046
|
this.spanProcessor.shutdown(),
|
|
670
1047
|
this.logRecordProcessor.shutdown(),
|
|
671
1048
|
]);
|
|
1049
|
+
await Promise.allSettled([this.traceExporter.forceFlush(), this.logExporter.forceFlush()]);
|
|
1050
|
+
this.uploader?.close();
|
|
1051
|
+
for (const identity of this.media.keys())
|
|
1052
|
+
this.releaseMediaIdentity(identity);
|
|
1053
|
+
this.logParts.clear();
|
|
1054
|
+
this.mediaMemo.clear();
|
|
672
1055
|
}
|
|
673
1056
|
})();
|
|
674
1057
|
return this.shutdownPromise;
|
|
@@ -808,6 +1191,10 @@ function shedContent(record, signal, shed) {
|
|
|
808
1191
|
: span.links,
|
|
809
1192
|
};
|
|
810
1193
|
}
|
|
1194
|
+
/** Whether a trace id can name an upload's trace: 32 lowercase hex digits, not all zero. */
|
|
1195
|
+
function isUploadTraceId(traceId) {
|
|
1196
|
+
return typeof traceId === "string" && /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
|
|
1197
|
+
}
|
|
811
1198
|
class ReportingExporter {
|
|
812
1199
|
transport;
|
|
813
1200
|
signal;
|
|
@@ -896,6 +1283,7 @@ class ReportingExporter {
|
|
|
896
1283
|
},
|
|
897
1284
|
};
|
|
898
1285
|
const tally = newTally();
|
|
1286
|
+
const helpers = new Map();
|
|
899
1287
|
const placed = [];
|
|
900
1288
|
for (const { record, markers } of this.ordered(records)) {
|
|
901
1289
|
try {
|
|
@@ -914,23 +1302,56 @@ class ReportingExporter {
|
|
|
914
1302
|
clearTimeout(timer);
|
|
915
1303
|
}
|
|
916
1304
|
}
|
|
917
|
-
const
|
|
918
|
-
|
|
1305
|
+
const uploads = !markers && this.transport.offloadsValues();
|
|
1306
|
+
// A log record's media upload under its own trace: one without a trace is not offered.
|
|
1307
|
+
const offload = uploads &&
|
|
1308
|
+
(this.signal === "traces" ||
|
|
1309
|
+
isUploadTraceId(record.spanContext?.traceId))
|
|
1310
|
+
? this.signal === "traces"
|
|
1311
|
+
? { candidates: [], cut: [] }
|
|
1312
|
+
: { candidates: [], cut: [], log: true }
|
|
919
1313
|
: undefined;
|
|
920
1314
|
const redacted = this.redact(record, cache, offload);
|
|
1315
|
+
let files = uploads
|
|
1316
|
+
? survivingMedia(this.transport.mediaAttachments(record), redacted, tally)
|
|
1317
|
+
: [];
|
|
1318
|
+
if (this.signal === "logs" && files.length) {
|
|
1319
|
+
// A log record's attachments upload with the media its body holds, in the one step
|
|
1320
|
+
// that places a log record's files; a record without a trace uploads none.
|
|
1321
|
+
if (offload)
|
|
1322
|
+
offload.attachments = files;
|
|
1323
|
+
else
|
|
1324
|
+
for (let index = 0; index < files.length; index++)
|
|
1325
|
+
countFallback(tally, "budget", recordTraceId(record));
|
|
1326
|
+
files = [];
|
|
1327
|
+
}
|
|
1328
|
+
const offloaded = offload &&
|
|
1329
|
+
(offload.candidates.length ||
|
|
1330
|
+
offload.cut.length ||
|
|
1331
|
+
offload.body ||
|
|
1332
|
+
offload.attachments?.length)
|
|
1333
|
+
? this.signal === "traces"
|
|
1334
|
+
? this.transport.offloadSpan(redacted, offload, upload, tally)
|
|
1335
|
+
: this.transport.offloadLog(redacted, offload, upload, tally)
|
|
1336
|
+
: undefined;
|
|
921
1337
|
placed.push({
|
|
922
1338
|
record,
|
|
1339
|
+
files,
|
|
923
1340
|
...(markers ? { markers } : {}),
|
|
924
|
-
redacted:
|
|
925
|
-
? this.transport.offloadSpan(redacted, offload, upload, tally)
|
|
926
|
-
: redacted,
|
|
1341
|
+
redacted: offloaded ?? redacted,
|
|
927
1342
|
});
|
|
928
1343
|
}
|
|
929
1344
|
catch {
|
|
1345
|
+
if (!markers && this.transport.offloadsValues()) {
|
|
1346
|
+
const files = this.transport.mediaAttachments(record);
|
|
1347
|
+
for (let index = 0; index < files.length; index++)
|
|
1348
|
+
countFallback(tally, "budget", recordTraceId(record));
|
|
1349
|
+
}
|
|
930
1350
|
invalid(markers !== undefined, "Telemetry record could not be redacted or exceeds supported content limits", record);
|
|
931
1351
|
}
|
|
932
1352
|
}
|
|
933
|
-
for (const { record, markers, redacted: placing } of placed) {
|
|
1353
|
+
for (const { record, markers, files, redacted: placing } of placed) {
|
|
1354
|
+
let retained = files;
|
|
934
1355
|
try {
|
|
935
1356
|
let redacted = await placing;
|
|
936
1357
|
// Added after redaction, so a redactor cannot alter the markers or the parent identity.
|
|
@@ -945,6 +1366,26 @@ class ReportingExporter {
|
|
|
945
1366
|
if (kept !== attributes)
|
|
946
1367
|
redacted = { ...redacted, attributes: kept };
|
|
947
1368
|
}
|
|
1369
|
+
if (files.length) {
|
|
1370
|
+
const listed = redacted.attributes[BLOBS_KEY];
|
|
1371
|
+
const baseline = Array.isArray(listed)
|
|
1372
|
+
? listed.filter((entry) => typeof entry === "string")
|
|
1373
|
+
: [];
|
|
1374
|
+
retained = files.slice(0, Math.max(0, MAX_BLOB_ENTRIES - baseline.length));
|
|
1375
|
+
for (let index = retained.length; index < files.length; index++)
|
|
1376
|
+
countFallback(tally, "budget", recordTraceId(redacted));
|
|
1377
|
+
if (retained.length) {
|
|
1378
|
+
// Fit the event or part and its reference together before sending customer bytes.
|
|
1379
|
+
redacted = {
|
|
1380
|
+
...redacted,
|
|
1381
|
+
attributes: {
|
|
1382
|
+
...redacted.attributes,
|
|
1383
|
+
[BLOBS_KEY]: [...baseline, ...retained.map(mediaEntry)],
|
|
1384
|
+
},
|
|
1385
|
+
};
|
|
1386
|
+
helpers.set(redacted, { files: retained, baseline });
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
948
1389
|
const bytes = 512 +
|
|
949
1390
|
estimateRecordBytes(recordData(redacted, this.signal), this.transport.options.maxQueueBytes - redactedBytes);
|
|
950
1391
|
if (redactedBytes + bytes > this.transport.options.maxQueueBytes)
|
|
@@ -953,6 +1394,8 @@ class ReportingExporter {
|
|
|
953
1394
|
(markers ? pending : real).push(redacted);
|
|
954
1395
|
}
|
|
955
1396
|
catch {
|
|
1397
|
+
for (let index = 0; index < retained.length; index++)
|
|
1398
|
+
countFallback(tally, "budget", recordTraceId(record));
|
|
956
1399
|
invalid(markers !== undefined, "Telemetry record could not be redacted or exceeds supported content limits", record);
|
|
957
1400
|
}
|
|
958
1401
|
}
|
|
@@ -1036,7 +1479,6 @@ class ReportingExporter {
|
|
|
1036
1479
|
// export may have adopted lower ones meanwhile, and each attempt checks them again.
|
|
1037
1480
|
return fitsLimits(body, this.transport.receiverLimits()) ? body : undefined;
|
|
1038
1481
|
};
|
|
1039
|
-
this.transport.reportOffload(tally);
|
|
1040
1482
|
/** Sends one kind of record (completed records, or placeholders alone: `advisory`) in one
|
|
1041
1483
|
* request, or in halves when it is over the receiver's limits. A trace's root among completed
|
|
1042
1484
|
* records carries the trace's dropped-record count as it stands when its request is made, so
|
|
@@ -1109,25 +1551,89 @@ class ReportingExporter {
|
|
|
1109
1551
|
const pack = async (accepted, advisory) => {
|
|
1110
1552
|
// Every record is measured, shed and, where it cannot be sent, counted lost before any root
|
|
1111
1553
|
// is written, so a root carries the losses of children that ended after it.
|
|
1112
|
-
const
|
|
1554
|
+
const admitted = [];
|
|
1113
1555
|
for (const record of accepted) {
|
|
1114
1556
|
let fitted;
|
|
1557
|
+
let attachments;
|
|
1558
|
+
const helper = helpers.get(record);
|
|
1559
|
+
let remaining = helper?.files ?? [];
|
|
1115
1560
|
try {
|
|
1116
1561
|
fitted = await fit(record);
|
|
1562
|
+
if (fitted && helper) {
|
|
1563
|
+
const fittedRecord = fitted.record;
|
|
1564
|
+
const traceId = recordTraceId(fittedRecord);
|
|
1565
|
+
const matched = survivingMedia(remaining, fittedRecord, tally, record);
|
|
1566
|
+
remaining = matched;
|
|
1567
|
+
const listed = fittedRecord.attributes[BLOBS_KEY];
|
|
1568
|
+
const entries = Array.isArray(listed) ? [...listed] : [];
|
|
1569
|
+
const files = matched.filter((file) => {
|
|
1570
|
+
const index = entries.indexOf(mediaEntry(file), helper.baseline.length);
|
|
1571
|
+
if (index < 0) {
|
|
1572
|
+
countFallback(tally, "budget", traceId);
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
entries.splice(index, 1);
|
|
1576
|
+
return true;
|
|
1577
|
+
});
|
|
1578
|
+
remaining = files;
|
|
1579
|
+
// Drop provisional entries, retaining only the original references after fitting.
|
|
1580
|
+
const attributes = { ...fittedRecord.attributes };
|
|
1581
|
+
if (Array.isArray(listed)) {
|
|
1582
|
+
const baseline = listed.slice(0, helper.baseline.length);
|
|
1583
|
+
if (baseline.length)
|
|
1584
|
+
attributes[BLOBS_KEY] = baseline;
|
|
1585
|
+
else
|
|
1586
|
+
delete attributes[BLOBS_KEY];
|
|
1587
|
+
}
|
|
1588
|
+
attachments = { record: { ...fittedRecord, attributes }, files };
|
|
1589
|
+
remaining = [];
|
|
1590
|
+
}
|
|
1117
1591
|
}
|
|
1118
1592
|
catch {
|
|
1593
|
+
for (let index = 0; index < remaining.length; index++)
|
|
1594
|
+
countFallback(tally, "budget", recordTraceId(record));
|
|
1119
1595
|
invalid(advisory, "Telemetry record could not be serialized", record);
|
|
1120
1596
|
continue;
|
|
1121
1597
|
}
|
|
1122
1598
|
if (!fitted) {
|
|
1599
|
+
for (let index = 0; index < remaining.length; index++)
|
|
1600
|
+
countFallback(tally, "budget", recordTraceId(record));
|
|
1123
1601
|
invalid(advisory, OVER_REQUEST_LIMIT, record);
|
|
1124
1602
|
continue;
|
|
1125
1603
|
}
|
|
1126
|
-
|
|
1604
|
+
admitted.push({ fitted, attachments });
|
|
1127
1605
|
}
|
|
1606
|
+
// Decide every event/reference admission before starting helper uploads. The shared
|
|
1607
|
+
// uploader's four workers place files across spans under the same upload deadline.
|
|
1608
|
+
const placed = await Promise.all(admitted.map(async ({ fitted, attachments }) => {
|
|
1609
|
+
if (!attachments)
|
|
1610
|
+
return fitted;
|
|
1611
|
+
try {
|
|
1612
|
+
const uploaded = await this.transport.offloadSpan(attachments.record, { candidates: [], cut: [], attachments: attachments.files }, upload, tally);
|
|
1613
|
+
measured.delete(fitted.record);
|
|
1614
|
+
// Actual references can differ when an existing object supplies its media type.
|
|
1615
|
+
const final = await probe(uploaded);
|
|
1616
|
+
if (final?.request)
|
|
1617
|
+
measured.set(final.record, final.request);
|
|
1618
|
+
if (!final)
|
|
1619
|
+
invalid(advisory, OVER_REQUEST_LIMIT, fitted.record);
|
|
1620
|
+
return final;
|
|
1621
|
+
}
|
|
1622
|
+
catch {
|
|
1623
|
+
invalid(advisory, "Telemetry record could not be serialized", fitted.record);
|
|
1624
|
+
return undefined;
|
|
1625
|
+
}
|
|
1626
|
+
}));
|
|
1627
|
+
// Preparation includes explicit file uploads. Report their outcome once, before delivery
|
|
1628
|
+
// failures can consume the diagnostic callback's rate-limit slot. Placeholders never upload.
|
|
1629
|
+
if (!advisory)
|
|
1630
|
+
this.transport.reportOffload(tally, this.signal);
|
|
1128
1631
|
let batch = [];
|
|
1129
1632
|
let batchBytes = 0;
|
|
1130
|
-
for (const
|
|
1633
|
+
for (const fitted of placed) {
|
|
1634
|
+
if (!fitted)
|
|
1635
|
+
continue;
|
|
1636
|
+
const { record, bytes: recordBytes } = fitted;
|
|
1131
1637
|
const framedBytes = recordBytes + RECORD_FRAMING_BYTES;
|
|
1132
1638
|
const target = Math.min(this.transport.receiverLimits().decodedBytes, BATCH_TARGET_BYTES);
|
|
1133
1639
|
if (batch.length && batchBytes + framedBytes > target) {
|