@hue-run/sdk 0.15.2 → 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/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) => redactSpan(span, this.options, cache, {
141
- valueBytes: this.limits.valueBytes,
142
- cut: this.admissionCuts.get(span),
143
- digested: this.admissionDigests.get(span),
144
- ...(offload ? { offload } : {}),
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 (!this.liveSpans || span.ended || !this.batchSpans)
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 Whether spans' values over the inline limit are uploaded: content is captured and
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 values over the inline limit are worth holding whole for upload now: they
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 prepared = [];
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
- prepared.push(fitted);
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 { record, bytes: recordBytes } of prepared) {
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
- /** Span values over Hue's inline limit uploaded apart from their spans (or already stored by
128
- * Hue), each listed under the span's `hue.blobs` with its first 16 KiB kept inline. */
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
- /** Span values over Hue's inline limit that could not be uploaded and were exported cut to the
131
- * limit (an inline file as its digest), each reported in a warning issue. */
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
- /** The file's bytes, only hashed and measured, never exported; a string is hashed as UTF-8.
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.15.2";
2
+ export declare const sdkVersion = "0.17.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/write-version.mjs from package.json; do not edit by hand.
2
2
  /** Package version shared by the instrumentation scope and the export User-Agent. */
3
- export const sdkVersion = "0.15.2";
3
+ export const sdkVersion = "0.17.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hue-run/sdk",
3
- "version": "0.15.2",
3
+ "version": "0.17.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -91,26 +91,25 @@
91
91
  "@opentelemetry/otlp-transformer": "0.222.0",
92
92
  "@opentelemetry/resources": "2.11.0",
93
93
  "@opentelemetry/sdk-logs": "0.222.0",
94
- "@opentelemetry/sdk-trace": "2.11.0"
94
+ "@opentelemetry/sdk-trace": "2.11.0",
95
+ "zod": "^3.25.76 || ^4.6.5"
95
96
  },
96
97
  "devDependencies": {
97
98
  "@ai-sdk/otel": "1.0.99",
98
99
  "@opentelemetry/api": "1.9.1",
99
100
  "@opentelemetry/context-async-hooks": "2.11.0",
100
101
  "@types/bun": "1.4.2",
101
- "@types/node": "26.5.1",
102
+ "@types/node": "26.6.4",
102
103
  "ai": "7.0.99",
103
104
  "ajv": "8.20.0",
104
105
  "protobufjs": "8.8.0",
105
- "typescript": "7.0.2",
106
- "zod": "4.6.5"
106
+ "typescript": "7.0.2"
107
107
  },
108
108
  "peerDependencies": {
109
109
  "@ai-sdk/otel": "^1.0.99",
110
110
  "@opentelemetry/api": "^1.9.1",
111
111
  "ai": "^6.0.0 || ^7.0.99",
112
- "ajv": "^8.17.0",
113
- "zod": "^4.6.5"
112
+ "ajv": "^8.17.0"
114
113
  },
115
114
  "peerDependenciesMeta": {
116
115
  "@ai-sdk/otel": {
@@ -121,9 +120,6 @@
121
120
  },
122
121
  "ajv": {
123
122
  "optional": true
124
- },
125
- "zod": {
126
- "optional": true
127
123
  }
128
124
  },
129
125
  "engines": {