@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/dist/privacy.js CHANGED
@@ -2,7 +2,7 @@ import { SpanStatusCode } from "@opentelemetry/api";
2
2
  import { resourceFromAttributes } from "@opentelemetry/resources";
3
3
  import { MAX_CONTENT_BYTES, MAX_RECORD_NODES, REDACTION_CONTEXT_UNITS } from "./config.js";
4
4
  import { scrubToolCredentials, withToolCatalogSummary } from "./tool-definitions.js";
5
- import { hashInlineFiles, INLINE_FILE_LIMIT, isMessageKey } from "./inline-files.js";
5
+ import { hashInlineFiles, mayInlineFiles } from "./inline-files.js";
6
6
  import { truncateUtf8 } from "./safety.js";
7
7
  export { truncateUtf8 };
8
8
  /** Attribute keys (and their dotted children) removed in metadata-only mode. */
@@ -142,18 +142,12 @@ function redactedText(value, path, options, cap) {
142
142
  throw new Error("Redaction produced unsupported text");
143
143
  return result;
144
144
  }
145
- /** Whether a recorded message's text may inline a file over the inline file limit. */
146
- function mayInlineFiles(key, text) {
147
- return (isMessageKey(key) &&
148
- text.length * 3 > INLINE_FILE_LIMIT &&
149
- (text.includes('"blob"') || text.includes('"file"')));
150
- }
151
145
  /** Marks a value the upload step does not place. */
152
146
  const NOT_HELD = Symbol("not held");
153
147
  /**
154
148
  * One of a span's own attribute values, when its values may be uploaded: text through the
155
149
  * credential scrub and the redactor, whole, and collected when it is over the cap or may inline
156
- * a large file; bytes over the cap collected as they are. {@link NOT_HELD} for any other value,
150
+ * media; bytes over the cap collected as they are. {@link NOT_HELD} for any other value,
157
151
  * and for text the queue cut when the record was admitted, which is never the whole value.
158
152
  */
159
153
  function offloadValue(key, value, path, options, budget) {
@@ -168,7 +162,7 @@ function offloadValue(key, value, path, options, budget) {
168
162
  }
169
163
  if (typeof value !== "string")
170
164
  return NOT_HELD;
171
- // Each large inline file the queue shrank to its digest is reported as not uploaded; what is
165
+ // Each inline media part the queue shrank to its digest is reported as not uploaded; what is
172
166
  // left of the message is placed like any other value, and reported too if it is cut.
173
167
  const digested = budget.digested?.get(key);
174
168
  const files = digested?.text === value ? digested.files : 0;
@@ -271,7 +265,7 @@ function attributes(source, options, path, budget, listPrefix = "", own = false)
271
265
  return [
272
266
  [
273
267
  key,
274
- redactValue(scrubToolCredentials(key, hashInlineFiles(key, value)), `${path}.${key}`, options, budget, 0, `${listPrefix}${key}`),
268
+ redactValue(scrubToolCredentials(key, own ? hashInlineFiles(key, value) : value), `${path}.${key}`, options, budget, 0, `${listPrefix}${key}`),
275
269
  ],
276
270
  ];
277
271
  }));
package/dist/safety.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type Span } from "@opentelemetry/api";
2
2
  export declare function noopSpan(): Span;
3
3
  export declare function safeSpan(source: Span, failed: () => void): Span;
4
+ export declare function spanSource(span: Span): Span;
4
5
  /** The bounds `encodeContent` enforces while it copies a value. */
5
6
  export interface EncodeLimits {
6
7
  bytes: number;
package/dist/safety.js CHANGED
@@ -96,8 +96,14 @@ class SafeSpan {
96
96
  this.write(() => this.source.recordException(...args));
97
97
  }
98
98
  }
99
+ const spanSources = new WeakMap();
99
100
  export function safeSpan(source, failed) {
100
- return new SafeSpan(source, failed);
101
+ const span = new SafeSpan(source, failed);
102
+ spanSources.set(span, spanSource(source));
103
+ return span;
104
+ }
105
+ export function spanSource(span) {
106
+ return spanSources.get(span) ?? span;
101
107
  }
102
108
  /** One content attribute's bounds: the value cap, and as many values as the Python SDK's content
103
109
  * snapshot allows, so a structured value of small members is cut at the cap rather than refused
@@ -1,5 +1,6 @@
1
1
  import type { ReadableSpan } from "@opentelemetry/sdk-trace";
2
2
  import type { ReadableLogRecord, ReadWriteLogRecord } from "@opentelemetry/sdk-logs";
3
+ import { type MediaDigestMemo } from "./inline-files.js";
3
4
  /**
4
5
  * How a span's own values over the receiver's value cap are held whole for export to upload:
5
6
  * the cap, the bytes the held-value budget has left, and whether nothing else is held (a single
@@ -12,7 +13,7 @@ export interface HoldLimits {
12
13
  budget: number;
13
14
  first: boolean;
14
15
  }
15
- /** A span's own message whose large inline files shrank to their digest when it was queued: the
16
+ /** A span's own message whose inline media shrank to their digest when it was queued: the
16
17
  * text queued, and how many files it lost. */
17
18
  export interface DigestedMessage {
18
19
  text: string;
@@ -20,7 +21,7 @@ export interface DigestedMessage {
20
21
  }
21
22
  /** What admission keeps of a record: its copy, the bytes charged for it, whether resource
22
23
  * attributes were still unresolved, the value strings it cut, the bytes of the values it held
23
- * whole for upload, and the span's own messages whose large inline files it shrank to their
24
+ * whole for upload, and the span's own messages whose inline media it shrank to their
24
25
  * digest (with how many files each). */
25
26
  export interface RecordSnapshot<T> {
26
27
  record: T;
@@ -29,6 +30,8 @@ export interface RecordSnapshot<T> {
29
30
  cut: ReadonlySet<string>;
30
31
  held: number;
31
32
  digested: ReadonlyMap<string, DigestedMessage>;
33
+ /** The span's own message attributes admission inspected or left to export to inspect. */
34
+ inspected: ReadonlySet<string>;
32
35
  }
33
- export declare function snapshotSpan(source: ReadableSpan, limit: number, captureContent?: boolean, valueUnits?: number, hold?: HoldLimits): RecordSnapshot<ReadableSpan>;
36
+ export declare function snapshotSpan(source: ReadableSpan, limit: number, captureContent?: boolean, valueUnits?: number, hold?: HoldLimits, memo?: MediaDigestMemo): RecordSnapshot<ReadableSpan>;
34
37
  export declare function snapshotLog(source: ReadableLogRecord, limit: number, captureContent?: boolean, valueUnits?: number): RecordSnapshot<ReadWriteLogRecord>;
package/dist/snapshot.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createTraceState } from "@opentelemetry/api";
2
2
  import { types as utilTypes } from "node:util";
3
3
  import { resourceFromAttributes } from "@opentelemetry/resources";
4
- import { hashInlineFiles, INLINE_FILE_LIMIT, INLINE_FILE_TEXT_PER_RECORD, isMessageKey, } from "./inline-files.js";
4
+ import { hashInlineFiles, hashNestedInlineFiles, INLINE_FILE_TEXT_PER_RECORD, MAX_INLINE_FILE_TEXT, isMessageKey, mayInlineFiles, } from "./inline-files.js";
5
5
  import { scrubsToolCredentials, scrubToolCredentials } from "./tool-definitions.js";
6
6
  import { MAX_RECORD_NODES } from "./config.js";
7
7
  import { truncatedMarker } from "./privacy.js";
@@ -30,6 +30,8 @@ class Snapshot {
30
30
  captureContent;
31
31
  valueUnits;
32
32
  hold;
33
+ memo;
34
+ traceId;
33
35
  bytes = 512;
34
36
  unresolvedResource = false;
35
37
  /** The value strings this snapshot cut to `valueUnits`; export recognizes them, so the
@@ -38,28 +40,36 @@ class Snapshot {
38
40
  nodes = 0;
39
41
  ancestors = new Set();
40
42
  copied = new Map();
43
+ attributesCopied = new Map();
44
+ ownAttributesCopied = new Map();
41
45
  hashed = new Map();
46
+ nestedHashed = new Map();
42
47
  inspected = 0;
43
48
  /** Bytes of values held whole for upload, charged to the held-value budget. */
44
49
  held = 0;
45
50
  /** Set while the span's own attributes are copied: only they may be held for upload. */
46
51
  own = false;
47
- /** The span's own messages whose large inline files shrank to their digest when queued, by
52
+ /** The span's own messages whose inline media shrank to their digest when queued, by
48
53
  * attribute: the text queued (cut or not), and how many files it lost. */
49
54
  digested = new Map();
50
55
  /** How many files each message text {@link inlineFiles} hashed replaced by their digest. */
51
56
  hashedFiles = new Map();
57
+ /** The span's own message attributes inspected within the record's allowance, whether here or
58
+ * left to export for a message held whole: export inspects only these. */
59
+ inspectedKeys = new Set();
52
60
  /**
53
61
  * In metadata-only mode the recorded messages, which export strips, are not copied at all. A
54
62
  * value string (an attribute's, an event's or link's attribute's, a log body or a status
55
63
  * message) longer than `valueUnits` is cut to that length: export cuts it to the value cap
56
64
  * anyway, and charging it whole could drop the record before that cut ever ran.
57
65
  */
58
- constructor(limit, captureContent = true, valueUnits = Infinity, hold) {
66
+ constructor(limit, captureContent = true, valueUnits = Infinity, hold, memo, traceId = "") {
59
67
  this.limit = limit;
60
68
  this.captureContent = captureContent;
61
69
  this.valueUnits = valueUnits;
62
70
  this.hold = hold;
71
+ this.memo = memo;
72
+ this.traceId = traceId;
63
73
  }
64
74
  /** Whether the held-value budget takes `bytes` more, charging them when it does. */
65
75
  holds(bytes) {
@@ -74,7 +84,7 @@ class Snapshot {
74
84
  * longer than the queued value length or bytes over the cap, within the held-value budget, and
75
85
  * a recorded message that may inline a large file kept as it is (export uploads the file).
76
86
  * Undefined for a value copied as before; text over the length the budget cannot hold is then
77
- * cut and its large inline files shrink to their digest, as before uploads existed.
87
+ * cut and its inline media shrink to their digest, as before uploads existed.
78
88
  */
79
89
  heldValue(key, value) {
80
90
  if (!this.hold || !this.own)
@@ -86,9 +96,16 @@ class Snapshot {
86
96
  if (this.bytes + fallback > this.limit || !this.holds(value.length * 2))
87
97
  return undefined;
88
98
  this.charge(fallback);
99
+ // Export parses the held message; its inspection allowance is taken here, in order.
100
+ if (value.length <= MAX_INLINE_FILE_TEXT &&
101
+ mayInlineFiles(key, value) &&
102
+ this.inspected + value.length <= INLINE_FILE_TEXT_PER_RECORD) {
103
+ this.inspected += value.length;
104
+ this.inspectedKeys.add(key);
105
+ }
89
106
  return { value };
90
107
  }
91
- // Within the queued length: a recorded message holding a large inline file is held whole
108
+ // Within the queued length: a recorded message holding an inline media part is held whole
92
109
  // for export to upload the file, within the held-value budget, and the queue is charged
93
110
  // its copy with the files as their digest, as before. Past the budget, the files shrink to
94
111
  // their digest when queued, as before uploads existed.
@@ -119,7 +136,12 @@ class Snapshot {
119
136
  throw new BudgetExceeded("Telemetry byte budget exceeded");
120
137
  }
121
138
  mark() {
122
- return { bytes: this.bytes, nodes: this.nodes, copied: this.copied.size };
139
+ return {
140
+ bytes: this.bytes,
141
+ nodes: this.nodes,
142
+ copied: this.copied.size,
143
+ attributesCopied: this.attributesCopied.size,
144
+ };
123
145
  }
124
146
  /** Back to `mark`, forgetting the values copied since: a later copy must not reuse a partial
125
147
  * copy of an item that was left out. */
@@ -131,6 +153,10 @@ class Snapshot {
131
153
  for (const key of [...this.copied.keys()])
132
154
  if (index++ >= mark.copied)
133
155
  this.copied.delete(key);
156
+ index = 0;
157
+ for (const key of [...this.attributesCopied.keys()])
158
+ if (index++ >= mark.attributesCopied)
159
+ this.attributesCopied.delete(key);
134
160
  }
135
161
  /**
136
162
  * As many of a span's newest `items` (its events or links) as the record's budget still holds,
@@ -163,7 +189,7 @@ class Snapshot {
163
189
  return { kept, dropped: count - kept.length };
164
190
  }
165
191
  /**
166
- * An attribute map. A large inline file in a message attribute shrinks to its digest before it
192
+ * An attribute map. A inline media part in a message attribute shrinks to its digest before it
167
193
  * is charged: export replaces it anyway, and charging the file could drop the whole record.
168
194
  */
169
195
  attributes(value) {
@@ -179,22 +205,32 @@ class Snapshot {
179
205
  this.own = false;
180
206
  }
181
207
  }
182
- /** A message attribute with its large inline files hashed. The same text is hashed once per
183
- * record, and a record inspects at most {@link INLINE_FILE_TEXT_PER_RECORD} of it in all;
184
- * beyond that, messages are charged as recorded. */
208
+ /** A message attribute with its inline media hashed. Own and nested attributes cache
209
+ * separately because small binary media stays inline only in nested attributes. A record
210
+ * inspects at most {@link INLINE_FILE_TEXT_PER_RECORD} code units in all; beyond that,
211
+ * messages are charged as recorded. */
185
212
  inlineFiles(key, value) {
186
- // A UTF-16 unit is at most three UTF-8 bytes, so shorter text cannot hold a file to hash.
187
- if (typeof value !== "string" || !isMessageKey(key) || value.length * 3 <= INLINE_FILE_LIMIT)
213
+ if (typeof value !== "string" ||
214
+ value.length > MAX_INLINE_FILE_TEXT ||
215
+ !mayInlineFiles(key, value))
188
216
  return value;
189
- if (this.hashed.has(value))
190
- return this.hashed.get(value);
217
+ const hashed = this.own ? this.hashed : this.nestedHashed;
218
+ if (hashed.has(value)) {
219
+ if (this.own)
220
+ this.inspectedKeys.add(key);
221
+ return hashed.get(value);
222
+ }
191
223
  if (this.inspected + value.length > INLINE_FILE_TEXT_PER_RECORD)
192
224
  return value;
193
225
  this.inspected += value.length;
226
+ if (this.own)
227
+ this.inspectedKeys.add(key);
194
228
  const count = { files: 0 };
195
- const result = hashInlineFiles(key, value, count);
196
- this.hashed.set(value, result);
197
- if (count.files)
229
+ const result = this.own
230
+ ? hashInlineFiles(key, value, count, this.memo, this.traceId)
231
+ : hashNestedInlineFiles(key, value);
232
+ hashed.set(value, result);
233
+ if (this.own && count.files)
198
234
  this.hashedFiles.set(value, count.files);
199
235
  return result;
200
236
  }
@@ -266,8 +302,15 @@ class Snapshot {
266
302
  throw new TypeError("Telemetry proxies are unsupported");
267
303
  if (this.ancestors.has(value))
268
304
  throw new TypeError("Cyclic telemetry value");
269
- if (this.copied.has(value))
270
- return this.copied.get(value);
305
+ // One object can be reused as own attributes, nested attributes and an ordinary log body.
306
+ // Their copies differ: only attributes inspect media, and only own attributes upload it.
307
+ const copies = shape === "attributes"
308
+ ? this.own
309
+ ? this.ownAttributesCopied
310
+ : this.attributesCopied
311
+ : this.copied;
312
+ if (copies.has(value))
313
+ return copies.get(value);
271
314
  if (utilTypes.isUint8Array(value)) {
272
315
  // Own accessors/subclasses cannot disguise the retained byte count.
273
316
  // A length-tracking SharedArrayBuffer view can grow on another thread
@@ -277,14 +320,14 @@ class Snapshot {
277
320
  this.charge(length);
278
321
  const copy = new Uint8Array(length);
279
322
  typedArraySet.call(copy, value);
280
- this.copied.set(value, copy);
323
+ copies.set(value, copy);
281
324
  return copy;
282
325
  }
283
326
  const array = Array.isArray(value);
284
327
  if (!array && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
285
328
  throw new TypeError("Telemetry must contain data objects");
286
329
  const copy = array ? [] : Object.create(null);
287
- this.copied.set(value, copy);
330
+ copies.set(value, copy);
288
331
  this.ancestors.add(value);
289
332
  if (array) {
290
333
  if (value.length > MAX_RECORD_NODES)
@@ -316,7 +359,7 @@ class Snapshot {
316
359
  ? this.scrubbedBeforeCut(key, this.inlineFiles(key, source))
317
360
  : source, depth + 1, shape === "event" && key === "attributes" ? "attributes" : "value", content || shape === "attributes");
318
361
  copy[key] = copied;
319
- // A span's own message whose large inline files shrank to their digest is named as it
362
+ // A span's own message whose inline media shrank to their digest is named as it
320
363
  // was queued, cut or not, so export reports each file as not uploaded.
321
364
  const files = this.own && shape === "attributes" && typeof source === "string"
322
365
  ? this.hashedFiles.get(source)
@@ -360,7 +403,7 @@ class Snapshot {
360
403
  if (value == null || Object.hasOwn(attributes, key))
361
404
  continue;
362
405
  this.charge(key.length * 2 + 16);
363
- attributes[key] = this.copy(this.scrubbedBeforeCut(key, value), 0, "value", true);
406
+ attributes[key] = this.copy(this.scrubbedBeforeCut(key, this.inlineFiles(key, value)), 0, "value", true);
364
407
  }
365
408
  return resourceFromAttributes(attributes, { schemaUrl: this.copy(source.schemaUrl) });
366
409
  }
@@ -368,8 +411,8 @@ class Snapshot {
368
411
  function contextReader(context) {
369
412
  return () => context;
370
413
  }
371
- export function snapshotSpan(source, limit, captureContent = true, valueUnits = Infinity, hold) {
372
- const snapshot = new Snapshot(limit, captureContent, valueUnits, hold);
414
+ export function snapshotSpan(source, limit, captureContent = true, valueUnits = Infinity, hold, memo) {
415
+ const snapshot = new Snapshot(limit, captureContent, valueUnits, hold, memo, source.spanContext().traceId);
373
416
  const context = snapshot.context(source.spanContext());
374
417
  const fields = snapshot.copy({
375
418
  name: source.name,
@@ -416,6 +459,7 @@ export function snapshotSpan(source, limit, captureContent = true, valueUnits =
416
459
  cut: snapshot.cut,
417
460
  held: snapshot.held,
418
461
  digested: snapshot.digested,
462
+ inspected: snapshot.inspectedKeys,
419
463
  };
420
464
  }
421
465
  export function snapshotLog(source, limit, captureContent = true, valueUnits = Infinity) {
@@ -462,5 +506,6 @@ export function snapshotLog(source, limit, captureContent = true, valueUnits = I
462
506
  cut: snapshot.cut,
463
507
  held: 0,
464
508
  digested: new Map(),
509
+ inspected: new Set(),
465
510
  };
466
511
  }
@@ -79,11 +79,17 @@ export declare class HueTransport {
79
79
  private holdsReleased;
80
80
  private admissionCuts;
81
81
  private admissionDigests;
82
+ private admissionInspected;
82
83
  /** Uploads values over the inline limit apart from their spans; absent when disabled. */
83
84
  private uploader?;
84
85
  /** Bytes of values over the inline limit queued spans hold whole for upload, and each span's. */
85
86
  private heldBytes;
86
87
  private held;
88
+ private mediaMemo;
89
+ private media;
90
+ private mediaOwners;
91
+ private mediaTimer?;
92
+ private mediaFinalizer;
87
93
  private uploaded;
88
94
  private uploadFallbacks;
89
95
  constructor(options: HueOptions);
@@ -98,6 +104,11 @@ export declare class HueTransport {
98
104
  private announceLiveSpans;
99
105
  private stopLiveTimer;
100
106
  private stopLiveSpans;
107
+ private mediaIdentity;
108
+ private releaseMedia;
109
+ private protectMedia;
110
+ private releaseMediaIdentity;
111
+ private sweepMedia;
101
112
  /** Bounded: a trace whose root never arrives (an abandoned run) gives way to newer ones. */
102
113
  private countDropped;
103
114
  /** Cumulative counters and current queue gauges. */