@hue-run/sdk 0.16.0 → 0.17.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/README.md +68 -43
- package/dist/blobs.d.ts +35 -17
- package/dist/blobs.js +141 -75
- package/dist/cli/eval-direct.d.ts +3 -2
- package/dist/cli/eval-direct.js +49 -6
- package/dist/client.d.ts +3 -1
- package/dist/client.js +70 -20
- package/dist/evals/files.js +12 -0
- package/dist/inline-files.d.ts +38 -29
- package/dist/inline-files.js +288 -83
- package/dist/privacy.d.ts +1 -1
- package/dist/privacy.js +4 -10
- package/dist/safety.d.ts +1 -0
- package/dist/safety.js +7 -1
- package/dist/snapshot.d.ts +6 -3
- package/dist/snapshot.js +70 -25
- package/dist/transport.d.ts +11 -0
- package/dist/transport.js +296 -20
- package/dist/types.d.ts +7 -5
- 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,46 @@ 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, MAX_HELD_BLOB_BYTES, newTally, offloadAttributes, 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";
|
|
13
14
|
import { snapshotLog, snapshotSpan } from "./snapshot.js";
|
|
14
15
|
import { DROPPED_RECORDS_KEY, isContentKey, isTruncatedMarker, redactLog, redactSpan, TRUNCATED_KEY, truncatedMarker, withTruncatedKeys, } from "./privacy.js";
|
|
15
16
|
import { sdkVersion } from "./version.js";
|
|
17
|
+
/** Each surviving event authorizes one staged request, including repeated digests. */
|
|
18
|
+
function survivingMedia(files, span, tally) {
|
|
19
|
+
const consumed = new Set();
|
|
20
|
+
const traceId = span.spanContext().traceId;
|
|
21
|
+
return files.flatMap((file) => {
|
|
22
|
+
const index = span.events.findIndex((event, index) => !consumed.has(index) &&
|
|
23
|
+
event.name === "hue.file" &&
|
|
24
|
+
(!file.eventTime ||
|
|
25
|
+
(event.time[0] === file.eventTime[0] && event.time[1] === file.eventTime[1])) &&
|
|
26
|
+
event.attributes?.["hue.file.sha256"] === file.digest.sha256 &&
|
|
27
|
+
event.attributes?.["hue.file.size"] === file.digest.size &&
|
|
28
|
+
event.attributes?.["hue.file.media_type"] === file.contentType);
|
|
29
|
+
if (index < 0) {
|
|
30
|
+
countFallback(tally, "budget", traceId);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
consumed.add(index);
|
|
34
|
+
const name = span.events[index].attributes?.["hue.file.name"];
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
bytes: file.bytes,
|
|
38
|
+
digest: file.digest,
|
|
39
|
+
contentType: file.contentType,
|
|
40
|
+
...(file.eventTime ? { eventTime: file.eventTime } : {}),
|
|
41
|
+
...(isMediaName(name) ? { name } : {}),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function mediaEntry(file) {
|
|
47
|
+
return blobEntry(`event:hue.file:${file.digest.sha256}`, file.digest.sha256, file.digest.size, file.contentType, file.name);
|
|
48
|
+
}
|
|
16
49
|
/**
|
|
17
50
|
* How long one export holds its records for a receiver that refused them for their rate (HTTP 429)
|
|
18
51
|
* and asked to be retried later than a request's deadline allows: the records stay queued, counted
|
|
@@ -125,11 +158,23 @@ export class HueTransport {
|
|
|
125
158
|
admissionCuts = new WeakMap();
|
|
126
159
|
// The span's own messages whose large inline files each admitted span shrank to their digest.
|
|
127
160
|
admissionDigests = new WeakMap();
|
|
161
|
+
// The span's own messages each admitted span inspected, or left to export to inspect.
|
|
162
|
+
admissionInspected = new WeakMap();
|
|
128
163
|
/** Uploads values over the inline limit apart from their spans; absent when disabled. */
|
|
129
164
|
uploader;
|
|
130
165
|
/** Bytes of values over the inline limit queued spans hold whole for upload, and each span's. */
|
|
131
166
|
heldBytes = 0;
|
|
132
167
|
held = new WeakMap();
|
|
168
|
+
mediaMemo = new MediaDigestMemo();
|
|
169
|
+
// Context identity also matches safe API wrappers to the provider's ended span. Abandoned
|
|
170
|
+
// spans retain no span objects, and share the 2048-span / 128 MiB holding bounds.
|
|
171
|
+
media = new Map();
|
|
172
|
+
mediaOwners = new Map();
|
|
173
|
+
mediaTimer;
|
|
174
|
+
mediaFinalizer = new FinalizationRegistry(({ identity, token }) => {
|
|
175
|
+
if (this.mediaOwners.get(identity)?.token === token)
|
|
176
|
+
this.releaseMediaIdentity(identity);
|
|
177
|
+
});
|
|
133
178
|
uploaded = 0;
|
|
134
179
|
uploadFallbacks = 0;
|
|
135
180
|
constructor(options) {
|
|
@@ -137,12 +182,17 @@ export class HueTransport {
|
|
|
137
182
|
Object.defineProperty(this, "options", { enumerable: false });
|
|
138
183
|
this.liveSpans = this.options.enabled !== false && this.options.liveSpans;
|
|
139
184
|
this.announcesLiveSpans = this.liveSpans;
|
|
140
|
-
this.traceExporter = new ReportingExporter(this, "traces", ProtobufTraceSerializer, TraceExporterMetricsHelper, (span, cache, offload) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
this.traceExporter = new ReportingExporter(this, "traces", ProtobufTraceSerializer, TraceExporterMetricsHelper, (span, cache, offload) => {
|
|
186
|
+
// Export inspects only the messages admission inspected within the record's allowance.
|
|
187
|
+
if (offload)
|
|
188
|
+
offload.inspected = this.admissionInspected.get(span);
|
|
189
|
+
return redactSpan(span, this.options, cache, {
|
|
190
|
+
valueBytes: this.limits.valueBytes,
|
|
191
|
+
cut: this.admissionCuts.get(span),
|
|
192
|
+
digested: this.admissionDigests.get(span),
|
|
193
|
+
...(offload ? { offload } : {}),
|
|
194
|
+
});
|
|
195
|
+
});
|
|
146
196
|
this.logExporter = new ReportingExporter(this, "logs", ProtobufLogsSerializer, LogsExporterMetricsHelper, (log, cache) => redactLog(log, this.options, cache, {
|
|
147
197
|
valueBytes: this.limits.valueBytes,
|
|
148
198
|
cut: this.admissionCuts.get(log),
|
|
@@ -187,18 +237,30 @@ export class HueTransport {
|
|
|
187
237
|
this.stopLiveTimer();
|
|
188
238
|
let admitted;
|
|
189
239
|
try {
|
|
190
|
-
if (!(span.spanContext().traceFlags & 1))
|
|
240
|
+
if (!(span.spanContext().traceFlags & 1)) {
|
|
241
|
+
this.releaseMedia(span);
|
|
191
242
|
return;
|
|
243
|
+
}
|
|
192
244
|
const queued = this.enqueue("traces", span);
|
|
193
|
-
if (!queued)
|
|
245
|
+
if (!queued) {
|
|
246
|
+
const tally = newTally();
|
|
247
|
+
for (const file of this.mediaAttachments(span))
|
|
248
|
+
if (file.bytes.byteLength)
|
|
249
|
+
countFallback(tally, "budget", span.spanContext().traceId);
|
|
250
|
+
this.reportOffload(tally);
|
|
251
|
+
this.releaseMedia(span);
|
|
194
252
|
return;
|
|
253
|
+
}
|
|
195
254
|
admitted = queued;
|
|
255
|
+
this.protectMedia(admitted);
|
|
196
256
|
this.countEnded(admitted);
|
|
197
257
|
spans.onEnd(admitted);
|
|
198
258
|
}
|
|
199
259
|
catch {
|
|
200
260
|
if (admitted)
|
|
201
261
|
this.finish("traces", [admitted]);
|
|
262
|
+
else
|
|
263
|
+
this.releaseMedia(span);
|
|
202
264
|
this.issue("traces", "invalid", 1, "Telemetry processor could not accept a record", undefined, traceIdsOf([span]));
|
|
203
265
|
}
|
|
204
266
|
},
|
|
@@ -262,12 +324,14 @@ export class HueTransport {
|
|
|
262
324
|
}
|
|
263
325
|
: undefined;
|
|
264
326
|
const snapshot = signal === "traces"
|
|
265
|
-
? snapshotSpan(record, remaining, this.options.captureContent, valueUnits, hold)
|
|
327
|
+
? snapshotSpan(record, remaining, this.options.captureContent, valueUnits, hold, this.mediaMemo)
|
|
266
328
|
: snapshotLog(record, remaining, this.options.captureContent, valueUnits);
|
|
267
329
|
if (snapshot.cut.size)
|
|
268
330
|
this.admissionCuts.set(snapshot.record, snapshot.cut);
|
|
269
331
|
if (snapshot.digested.size)
|
|
270
332
|
this.admissionDigests.set(snapshot.record, snapshot.digested);
|
|
333
|
+
if (signal === "traces")
|
|
334
|
+
this.admissionInspected.set(snapshot.record, snapshot.inspected);
|
|
271
335
|
if (snapshot.held) {
|
|
272
336
|
this.heldBytes += snapshot.held;
|
|
273
337
|
this.held.set(snapshot.record, snapshot.held);
|
|
@@ -311,7 +375,11 @@ export class HueTransport {
|
|
|
311
375
|
this.live.delete(span);
|
|
312
376
|
try {
|
|
313
377
|
// Ended without reaching onEnd: a wrapping processor filtered it, so no real span follows.
|
|
314
|
-
if (
|
|
378
|
+
if (span.ended) {
|
|
379
|
+
this.releaseMedia(span);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (!this.liveSpans || !this.batchSpans)
|
|
315
383
|
continue;
|
|
316
384
|
const { record, markers } = pendingPlaceholder(span);
|
|
317
385
|
const admitted = this.enqueue("traces", record, true);
|
|
@@ -373,6 +441,7 @@ export class HueTransport {
|
|
|
373
441
|
/** @internal Exporter callback: releases queued records after an export attempt settles. */
|
|
374
442
|
finish(signal, records) {
|
|
375
443
|
for (const record of records) {
|
|
444
|
+
this.releaseMedia(record);
|
|
376
445
|
const pending = signal === "traces" ? this.spans : this.logs;
|
|
377
446
|
this.pendingBytes -= pending.get(record) ?? 0;
|
|
378
447
|
pending.delete(record);
|
|
@@ -383,7 +452,112 @@ export class HueTransport {
|
|
|
383
452
|
}
|
|
384
453
|
}
|
|
385
454
|
}
|
|
386
|
-
/** @internal
|
|
455
|
+
/** @internal The same bounded digest memo is used by explicit and inline media. */
|
|
456
|
+
digestMedia(bytes, traceId) {
|
|
457
|
+
return this.mediaMemo.bytes(bytes, traceId);
|
|
458
|
+
}
|
|
459
|
+
/** @internal Retain a defensive copy within the existing held-value budget. */
|
|
460
|
+
stageMedia(span, file) {
|
|
461
|
+
if (this.closed || !this.offloadsValues() || file?.bytes.byteLength === 0)
|
|
462
|
+
return true;
|
|
463
|
+
if (!file) {
|
|
464
|
+
const tally = newTally();
|
|
465
|
+
countFallback(tally, "budget", span.spanContext().traceId);
|
|
466
|
+
this.reportOffload(tally);
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
if (!this.holdsValues()) {
|
|
470
|
+
this.uploadFallbacks++;
|
|
471
|
+
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]);
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
const identity = this.mediaIdentity(span);
|
|
475
|
+
this.sweepMedia();
|
|
476
|
+
const files = this.media.get(identity) ?? [];
|
|
477
|
+
if (files.length >= 64 ||
|
|
478
|
+
(!this.media.has(identity) && this.media.size >= 2048) ||
|
|
479
|
+
this.heldBytes + file.bytes.byteLength > MAX_HELD_BLOB_BYTES) {
|
|
480
|
+
this.uploadFallbacks++;
|
|
481
|
+
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]);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const bytes = Uint8Array.from(file.bytes);
|
|
485
|
+
files.push({ ...file, bytes });
|
|
486
|
+
this.media.set(identity, files);
|
|
487
|
+
if (!this.mediaOwners.has(identity)) {
|
|
488
|
+
const source = spanSource(span);
|
|
489
|
+
const token = {};
|
|
490
|
+
this.mediaOwners.set(identity, { source: new WeakRef(source), token });
|
|
491
|
+
this.mediaFinalizer.register(source, { identity, token }, token);
|
|
492
|
+
if (!this.mediaTimer) {
|
|
493
|
+
this.mediaTimer = setInterval(() => this.sweepMedia(), LIVE_SPAN_INTERVAL_MILLIS);
|
|
494
|
+
this.mediaTimer.unref();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
this.heldBytes += bytes.byteLength;
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
mediaIdentity(record) {
|
|
501
|
+
if (!("spanContext" in record) || typeof record.spanContext !== "function")
|
|
502
|
+
return undefined;
|
|
503
|
+
const { traceId, spanId } = record.spanContext();
|
|
504
|
+
return `${traceId}:${spanId}`;
|
|
505
|
+
}
|
|
506
|
+
releaseMedia(record) {
|
|
507
|
+
const identity = this.mediaIdentity(record);
|
|
508
|
+
if (!identity)
|
|
509
|
+
return;
|
|
510
|
+
this.releaseMediaIdentity(identity);
|
|
511
|
+
}
|
|
512
|
+
protectMedia(record) {
|
|
513
|
+
const identity = this.mediaIdentity(record);
|
|
514
|
+
if (!identity)
|
|
515
|
+
return;
|
|
516
|
+
const owner = this.mediaOwners.get(identity);
|
|
517
|
+
if (owner)
|
|
518
|
+
this.mediaFinalizer.unregister(owner.token);
|
|
519
|
+
this.mediaOwners.delete(identity);
|
|
520
|
+
if (!this.mediaOwners.size) {
|
|
521
|
+
clearInterval(this.mediaTimer);
|
|
522
|
+
this.mediaTimer = undefined;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
releaseMediaIdentity(identity) {
|
|
526
|
+
const owner = this.mediaOwners.get(identity);
|
|
527
|
+
if (owner)
|
|
528
|
+
this.mediaFinalizer.unregister(owner.token);
|
|
529
|
+
this.mediaOwners.delete(identity);
|
|
530
|
+
if (!this.mediaOwners.size) {
|
|
531
|
+
clearInterval(this.mediaTimer);
|
|
532
|
+
this.mediaTimer = undefined;
|
|
533
|
+
}
|
|
534
|
+
const files = this.media.get(identity);
|
|
535
|
+
if (!files)
|
|
536
|
+
return;
|
|
537
|
+
for (const file of files)
|
|
538
|
+
this.heldBytes -= file.bytes.byteLength;
|
|
539
|
+
this.media.delete(identity);
|
|
540
|
+
}
|
|
541
|
+
sweepMedia() {
|
|
542
|
+
for (const [identity, owner] of this.mediaOwners) {
|
|
543
|
+
try {
|
|
544
|
+
const source = owner.source.deref();
|
|
545
|
+
if (!source ||
|
|
546
|
+
source.ended === true ||
|
|
547
|
+
source.isRecording?.() === false)
|
|
548
|
+
this.releaseMediaIdentity(identity);
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/** @internal Staged requests are matched against surviving events before upload. */
|
|
556
|
+
mediaAttachments(record) {
|
|
557
|
+
const identity = this.mediaIdentity(record);
|
|
558
|
+
return identity ? (this.media.get(identity) ?? []) : [];
|
|
559
|
+
}
|
|
560
|
+
/** @internal Whether spans' content and media may be uploaded: content is captured and
|
|
387
561
|
* the key is a project key (setup credentials send metadata only). A value that then cannot be
|
|
388
562
|
* uploaded is cut, reported and counted. */
|
|
389
563
|
offloadsValues() {
|
|
@@ -391,7 +565,7 @@ export class HueTransport {
|
|
|
391
565
|
this.options.captureContent &&
|
|
392
566
|
!this.options.apiKey.startsWith("hue_setup_"));
|
|
393
567
|
}
|
|
394
|
-
/** @internal Whether
|
|
568
|
+
/** @internal Whether content and media are worth holding whole for upload now: they
|
|
395
569
|
* are uploaded, and the receiver has not shown it lacks the upload route. */
|
|
396
570
|
holdsValues() {
|
|
397
571
|
return this.offloadsValues() && this.uploader?.available() === true;
|
|
@@ -416,13 +590,14 @@ export class HueTransport {
|
|
|
416
590
|
valueBytes,
|
|
417
591
|
upload,
|
|
418
592
|
tally,
|
|
593
|
+
memo: this.mediaMemo,
|
|
419
594
|
});
|
|
420
595
|
return { ...span, attributes: attributes };
|
|
421
596
|
}
|
|
422
597
|
catch {
|
|
423
598
|
return {
|
|
424
599
|
...span,
|
|
425
|
-
attributes: fallbackAttributes(span.attributes, collector, valueBytes, tally, traceId),
|
|
600
|
+
attributes: fallbackAttributes(span.attributes, collector, valueBytes, tally, traceId, this.mediaMemo),
|
|
426
601
|
};
|
|
427
602
|
}
|
|
428
603
|
}
|
|
@@ -669,6 +844,11 @@ export class HueTransport {
|
|
|
669
844
|
this.spanProcessor.shutdown(),
|
|
670
845
|
this.logRecordProcessor.shutdown(),
|
|
671
846
|
]);
|
|
847
|
+
await Promise.allSettled([this.traceExporter.forceFlush(), this.logExporter.forceFlush()]);
|
|
848
|
+
this.uploader?.close();
|
|
849
|
+
for (const identity of this.media.keys())
|
|
850
|
+
this.releaseMediaIdentity(identity);
|
|
851
|
+
this.mediaMemo.clear();
|
|
672
852
|
}
|
|
673
853
|
})();
|
|
674
854
|
return this.shutdownPromise;
|
|
@@ -896,6 +1076,7 @@ class ReportingExporter {
|
|
|
896
1076
|
},
|
|
897
1077
|
};
|
|
898
1078
|
const tally = newTally();
|
|
1079
|
+
const helpers = new Map();
|
|
899
1080
|
const placed = [];
|
|
900
1081
|
for (const { record, markers } of this.ordered(records)) {
|
|
901
1082
|
try {
|
|
@@ -918,8 +1099,12 @@ class ReportingExporter {
|
|
|
918
1099
|
? { candidates: [], cut: [] }
|
|
919
1100
|
: undefined;
|
|
920
1101
|
const redacted = this.redact(record, cache, offload);
|
|
1102
|
+
const files = offload
|
|
1103
|
+
? survivingMedia(this.transport.mediaAttachments(record), redacted, tally)
|
|
1104
|
+
: [];
|
|
921
1105
|
placed.push({
|
|
922
1106
|
record,
|
|
1107
|
+
files,
|
|
923
1108
|
...(markers ? { markers } : {}),
|
|
924
1109
|
redacted: offload && (offload.candidates.length || offload.cut.length)
|
|
925
1110
|
? this.transport.offloadSpan(redacted, offload, upload, tally)
|
|
@@ -927,10 +1112,16 @@ class ReportingExporter {
|
|
|
927
1112
|
});
|
|
928
1113
|
}
|
|
929
1114
|
catch {
|
|
1115
|
+
if (!markers && this.signal === "traces" && this.transport.offloadsValues()) {
|
|
1116
|
+
const files = this.transport.mediaAttachments(record);
|
|
1117
|
+
for (let index = 0; index < files.length; index++)
|
|
1118
|
+
countFallback(tally, "budget", record.spanContext().traceId);
|
|
1119
|
+
}
|
|
930
1120
|
invalid(markers !== undefined, "Telemetry record could not be redacted or exceeds supported content limits", record);
|
|
931
1121
|
}
|
|
932
1122
|
}
|
|
933
|
-
for (const { record, markers, redacted: placing } of placed) {
|
|
1123
|
+
for (const { record, markers, files, redacted: placing } of placed) {
|
|
1124
|
+
let retained = files;
|
|
934
1125
|
try {
|
|
935
1126
|
let redacted = await placing;
|
|
936
1127
|
// Added after redaction, so a redactor cannot alter the markers or the parent identity.
|
|
@@ -945,6 +1136,27 @@ class ReportingExporter {
|
|
|
945
1136
|
if (kept !== attributes)
|
|
946
1137
|
redacted = { ...redacted, attributes: kept };
|
|
947
1138
|
}
|
|
1139
|
+
if (files.length) {
|
|
1140
|
+
const span = redacted;
|
|
1141
|
+
const listed = span.attributes[BLOBS_KEY];
|
|
1142
|
+
const baseline = Array.isArray(listed)
|
|
1143
|
+
? listed.filter((entry) => typeof entry === "string")
|
|
1144
|
+
: [];
|
|
1145
|
+
retained = files.slice(0, Math.max(0, MAX_BLOB_ENTRIES - baseline.length));
|
|
1146
|
+
for (let index = retained.length; index < files.length; index++)
|
|
1147
|
+
countFallback(tally, "budget", span.spanContext().traceId);
|
|
1148
|
+
if (retained.length) {
|
|
1149
|
+
// Fit the event and its reference together before sending customer bytes.
|
|
1150
|
+
redacted = {
|
|
1151
|
+
...redacted,
|
|
1152
|
+
attributes: {
|
|
1153
|
+
...span.attributes,
|
|
1154
|
+
[BLOBS_KEY]: [...baseline, ...retained.map(mediaEntry)],
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
1157
|
+
helpers.set(redacted, { files: retained, baseline });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
948
1160
|
const bytes = 512 +
|
|
949
1161
|
estimateRecordBytes(recordData(redacted, this.signal), this.transport.options.maxQueueBytes - redactedBytes);
|
|
950
1162
|
if (redactedBytes + bytes > this.transport.options.maxQueueBytes)
|
|
@@ -953,6 +1165,8 @@ class ReportingExporter {
|
|
|
953
1165
|
(markers ? pending : real).push(redacted);
|
|
954
1166
|
}
|
|
955
1167
|
catch {
|
|
1168
|
+
for (let index = 0; index < retained.length; index++)
|
|
1169
|
+
countFallback(tally, "budget", record.spanContext().traceId);
|
|
956
1170
|
invalid(markers !== undefined, "Telemetry record could not be redacted or exceeds supported content limits", record);
|
|
957
1171
|
}
|
|
958
1172
|
}
|
|
@@ -1036,7 +1250,6 @@ class ReportingExporter {
|
|
|
1036
1250
|
// export may have adopted lower ones meanwhile, and each attempt checks them again.
|
|
1037
1251
|
return fitsLimits(body, this.transport.receiverLimits()) ? body : undefined;
|
|
1038
1252
|
};
|
|
1039
|
-
this.transport.reportOffload(tally);
|
|
1040
1253
|
/** Sends one kind of record (completed records, or placeholders alone: `advisory`) in one
|
|
1041
1254
|
* request, or in halves when it is over the receiver's limits. A trace's root among completed
|
|
1042
1255
|
* records carries the trace's dropped-record count as it stands when its request is made, so
|
|
@@ -1109,25 +1322,88 @@ class ReportingExporter {
|
|
|
1109
1322
|
const pack = async (accepted, advisory) => {
|
|
1110
1323
|
// Every record is measured, shed and, where it cannot be sent, counted lost before any root
|
|
1111
1324
|
// is written, so a root carries the losses of children that ended after it.
|
|
1112
|
-
const
|
|
1325
|
+
const admitted = [];
|
|
1113
1326
|
for (const record of accepted) {
|
|
1114
1327
|
let fitted;
|
|
1328
|
+
let attachments;
|
|
1329
|
+
const helper = helpers.get(record);
|
|
1330
|
+
let remaining = helper?.files ?? [];
|
|
1115
1331
|
try {
|
|
1116
1332
|
fitted = await fit(record);
|
|
1333
|
+
if (fitted && helper) {
|
|
1334
|
+
const span = fitted.record;
|
|
1335
|
+
const matched = survivingMedia(remaining, span, tally);
|
|
1336
|
+
remaining = matched;
|
|
1337
|
+
const listed = span.attributes[BLOBS_KEY];
|
|
1338
|
+
const entries = Array.isArray(listed) ? [...listed] : [];
|
|
1339
|
+
const files = matched.filter((file) => {
|
|
1340
|
+
const index = entries.indexOf(mediaEntry(file), helper.baseline.length);
|
|
1341
|
+
if (index < 0) {
|
|
1342
|
+
countFallback(tally, "budget", span.spanContext().traceId);
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
entries.splice(index, 1);
|
|
1346
|
+
return true;
|
|
1347
|
+
});
|
|
1348
|
+
remaining = files;
|
|
1349
|
+
// Drop provisional entries, retaining only the original references after fitting.
|
|
1350
|
+
const attributes = { ...span.attributes };
|
|
1351
|
+
if (Array.isArray(listed)) {
|
|
1352
|
+
const baseline = listed.slice(0, helper.baseline.length);
|
|
1353
|
+
if (baseline.length)
|
|
1354
|
+
attributes[BLOBS_KEY] = baseline;
|
|
1355
|
+
else
|
|
1356
|
+
delete attributes[BLOBS_KEY];
|
|
1357
|
+
}
|
|
1358
|
+
attachments = { span: { ...span, attributes }, files };
|
|
1359
|
+
remaining = [];
|
|
1360
|
+
}
|
|
1117
1361
|
}
|
|
1118
1362
|
catch {
|
|
1363
|
+
for (let index = 0; index < remaining.length; index++)
|
|
1364
|
+
countFallback(tally, "budget", record.spanContext().traceId);
|
|
1119
1365
|
invalid(advisory, "Telemetry record could not be serialized", record);
|
|
1120
1366
|
continue;
|
|
1121
1367
|
}
|
|
1122
1368
|
if (!fitted) {
|
|
1369
|
+
for (let index = 0; index < remaining.length; index++)
|
|
1370
|
+
countFallback(tally, "budget", record.spanContext().traceId);
|
|
1123
1371
|
invalid(advisory, OVER_REQUEST_LIMIT, record);
|
|
1124
1372
|
continue;
|
|
1125
1373
|
}
|
|
1126
|
-
|
|
1374
|
+
admitted.push({ fitted, attachments });
|
|
1127
1375
|
}
|
|
1376
|
+
// Decide every event/reference admission before starting helper uploads. The shared
|
|
1377
|
+
// uploader's four workers place files across spans under the same upload deadline.
|
|
1378
|
+
const placed = await Promise.all(admitted.map(async ({ fitted, attachments }) => {
|
|
1379
|
+
if (!attachments)
|
|
1380
|
+
return fitted;
|
|
1381
|
+
try {
|
|
1382
|
+
const uploaded = await this.transport.offloadSpan(attachments.span, { candidates: [], cut: [], attachments: attachments.files }, upload, tally);
|
|
1383
|
+
measured.delete(fitted.record);
|
|
1384
|
+
// Actual references can differ when an existing object supplies its media type.
|
|
1385
|
+
const final = await probe(uploaded);
|
|
1386
|
+
if (final?.request)
|
|
1387
|
+
measured.set(final.record, final.request);
|
|
1388
|
+
if (!final)
|
|
1389
|
+
invalid(advisory, OVER_REQUEST_LIMIT, fitted.record);
|
|
1390
|
+
return final;
|
|
1391
|
+
}
|
|
1392
|
+
catch {
|
|
1393
|
+
invalid(advisory, "Telemetry record could not be serialized", fitted.record);
|
|
1394
|
+
return undefined;
|
|
1395
|
+
}
|
|
1396
|
+
}));
|
|
1397
|
+
// Preparation includes explicit file uploads. Report their outcome once, before delivery
|
|
1398
|
+
// failures can consume the diagnostic callback's rate-limit slot. Placeholders never upload.
|
|
1399
|
+
if (!advisory)
|
|
1400
|
+
this.transport.reportOffload(tally);
|
|
1128
1401
|
let batch = [];
|
|
1129
1402
|
let batchBytes = 0;
|
|
1130
|
-
for (const
|
|
1403
|
+
for (const fitted of placed) {
|
|
1404
|
+
if (!fitted)
|
|
1405
|
+
continue;
|
|
1406
|
+
const { record, bytes: recordBytes } = fitted;
|
|
1131
1407
|
const framedBytes = recordBytes + RECORD_FRAMING_BYTES;
|
|
1132
1408
|
const target = Math.min(this.transport.receiverLimits().decodedBytes, BATCH_TARGET_BYTES);
|
|
1133
1409
|
if (batch.length && batchBytes + framedBytes > target) {
|
package/dist/types.d.ts
CHANGED
|
@@ -124,11 +124,11 @@ export interface ExportReport {
|
|
|
124
124
|
pendingBytes: number;
|
|
125
125
|
/** Helper capture or instrumentation failures that omitted telemetry while preserving application results. */
|
|
126
126
|
instrumentationFailures: number;
|
|
127
|
-
/**
|
|
128
|
-
* Hue), each listed under the span's `hue.blobs
|
|
127
|
+
/** Whole span attributes, inline media, and opted-in file helper uploads stored apart from
|
|
128
|
+
* their spans (or already held by Hue), each listed under the span's `hue.blobs`. */
|
|
129
129
|
uploadedValues: number;
|
|
130
|
-
/**
|
|
131
|
-
*
|
|
130
|
+
/** Whole-attribute, inline-media, and opted-in file helper uploads that could not be attached.
|
|
131
|
+
* Content falls back to truncation, media digests, or file metadata, with a warning issue. */
|
|
132
132
|
uploadFallbacks: number;
|
|
133
133
|
}
|
|
134
134
|
/** Caller budget for {@link HueClient.flushSafe} and {@link HueClient.shutdownSafe}. */
|
|
@@ -237,7 +237,9 @@ export interface FileRecord {
|
|
|
237
237
|
mediaType: string;
|
|
238
238
|
/** Hex SHA-256 of the file's bytes; computed from `data` when omitted. */
|
|
239
239
|
sha256?: string;
|
|
240
|
-
/**
|
|
240
|
+
/** Upload supplied bytes on this span when content capture is enabled. Defaults to false. */
|
|
241
|
+
upload?: boolean;
|
|
242
|
+
/** The file's bytes, hashed and measured locally by default; a string is hashed as UTF-8.
|
|
241
243
|
* Data larger than 25 MiB is omitted and counted as an instrumentation failure. */
|
|
242
244
|
data?: Uint8Array | string;
|
|
243
245
|
/** Size in bytes; computed from `data` when omitted. */
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Package version shared by the instrumentation scope and the export User-Agent. */
|
|
2
|
-
export declare const sdkVersion = "0.
|
|
2
|
+
export declare const sdkVersion = "0.17.0";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hue-run/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"@opentelemetry/api": "1.9.1",
|
|
100
100
|
"@opentelemetry/context-async-hooks": "2.11.0",
|
|
101
101
|
"@types/bun": "1.4.2",
|
|
102
|
-
"@types/node": "26.
|
|
102
|
+
"@types/node": "26.6.4",
|
|
103
103
|
"ai": "7.0.99",
|
|
104
104
|
"ajv": "8.20.0",
|
|
105
105
|
"protobufjs": "8.8.0",
|