@hue-run/sdk 0.6.0 → 0.7.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 CHANGED
@@ -42,8 +42,8 @@ project manifest are refused because managers can update ancestor locks; Python
42
42
  The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
43
43
  `capture_content=False`. For a supported application, setup installs the dependency and adds the
44
44
  managed import and middleware registration to the existing entrypoint; an unreferenced helper is
45
- not a completed integration. TypeScript uses `@hue-run/sdk@0.6.0`, `@opentelemetry/api@1.9.1` and
46
- `@opentelemetry/context-async-hooks@2.11.0`; Python uses published `hue-run==0.2.2`.
45
+ not a completed integration. TypeScript uses `@hue-run/sdk@0.7.0`, `@opentelemetry/api@1.9.1` and
46
+ `@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
47
47
  Content capture requires an ordinary account-managed key and a later explicit application decision.
48
48
 
49
49
  The generated bootstrap supplies standard active SERVER-span context across asynchronous/streaming
@@ -231,7 +231,8 @@ capability `setup_telemetry_write`.
231
231
  Normal project credentials and unknown token shapes are refused in setup responses and private
232
232
  managed state. These credentials authorize metadata-only OTLP at `/api/v1/otlp/v1/traces` and exact
233
233
  content-free receipt verification at `/api/v1/setup/traces/{traceId}/receipt`. They do not authorize
234
- generic project, receipt, evaluation, log or browsing APIs. Published Python `0.2.2` can export with the setup
234
+ generic project, receipt, evaluation, log or browsing APIs. The Python package version
235
+ pinned by setup can export with the setup
235
236
  credential; the CLI verifies the dedicated setup receipt instead of Python's generic receipt helper.
236
237
 
237
238
  Each command uses bounded timeouts and retries. Provisioning records at most five attempts per local
@@ -300,7 +301,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
300
301
  # Set project to an existing supported fixture; use the same directory on resume.
301
302
  project=/absolute/path/to/supported-fixture
302
303
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
303
- --archive .artifacts/typescript/hue-run-sdk-0.6.0.tgz \
304
+ --archive .artifacts/typescript/hue-run-sdk-0.7.0.tgz \
304
305
  --origin https://STAGING_ORIGIN \
305
306
  --project "$project" --command setup \
306
307
  --evidence .context/setup-staging-before-claim.json
@@ -311,7 +312,7 @@ the private local handoff and finish the real browser claim, then reconcile the
311
312
 
312
313
  ```sh
313
314
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
314
- --archive .artifacts/typescript/hue-run-sdk-0.6.0.tgz \
315
+ --archive .artifacts/typescript/hue-run-sdk-0.7.0.tgz \
315
316
  --origin https://STAGING_ORIGIN \
316
317
  --project "$project" --command claim \
317
318
  --evidence .context/setup-staging-after-claim.json
package/README.md CHANGED
@@ -329,7 +329,8 @@ const hue = createHue({
329
329
 
330
330
  ## Delivery behavior
331
331
 
332
- Exports use official OTel retry handling for temporary HTTP/network failures. Each
332
+ Exports retry temporary HTTP/network failures (429, 502, 503, 504 and connection errors,
333
+ honoring `Retry-After`) within the export timeout, by OpenTelemetry's OTLP/HTTP exporter rules. Each
333
334
  request is limited to 1 MiB before gzip (with space reserved for gzip overhead) and each content value to 256 KiB. Batches
334
335
  split at record boundaries. Each signal queues at most 2,048 records, including
335
336
  exports in flight; overflow is reported through the callback, counters and next
@@ -349,6 +350,36 @@ drain, including records emitted before its call. Stop request production
349
350
  before shutdown so late spans cannot race it. A client does not own instrumented
350
351
  operations still running in the application.
351
352
 
353
+ ## Live spans
354
+
355
+ OpenTelemetry exports a span only when it ends, so a long streamed turn would otherwise stay
356
+ invisible until it finishes. When a Hue or AI span is still open at the transport's next 500 ms
357
+ tick, the transport queues a placeholder: an ordinary OTLP span whose parent is the running span,
358
+ with its name, kind, start time and current attributes, an end time of 0,
359
+ `hue.span_type = "pending_span"` and `hue.pending_parent_id` (the running span's own parent,
360
+ omitted for a root). Hue shows the span as running and replaces the placeholder when the real span
361
+ arrives. A placeholder whose span has ended by the time it is exported is not sent.
362
+
363
+ - Only spans from the client's tracer (`withSpan`, `tool`, `model`, `hue.tracer` and the AI SDK
364
+ adapters) and spans with a `gen_ai.`, `ai.`, `llm.` or `traceloop.` attribute at start, or a
365
+ name starting with `ai.`, are announced. HTTP, database and other framework spans are not.
366
+ - Placeholder attributes follow `captureContent` and `redact` like the real span. Tool
367
+ definitions, system instructions and any value over 64 KiB are left out.
368
+ - Placeholders are advisory. They are queued only while the queue is under a quarter of its
369
+ record and byte budgets, and skipped silently otherwise. While queued they count in
370
+ `pendingSpans` and `pendingBytes`, but never as accepted, rejected, failed or dropped records.
371
+ Losing only placeholders records a warning and does not make `flush()` throw.
372
+ - A Hue server that accepts placeholders sends `Hue-Pending-Spans: 1` on trace acknowledgements.
373
+ When a response to a request carrying placeholders lacks it, the receiver predates them: the
374
+ transport attributes up to one rejection per placeholder to them, records one warning and stops
375
+ sending placeholders for that client. Other rejections count against real spans as usual.
376
+ - Opt out with `liveSpans: false`. Setup credentials never send placeholders.
377
+ - With an existing provider, announcements start in `spanProcessor.onStart`. A wrapping processor
378
+ that forwards `onStart` should forward `onEnd` for the same spans: a span that ends without
379
+ reaching Hue is forgotten at the next tick, but a placeholder already sent keeps it showing as
380
+ running until Hue marks the trace stalled. The filtering wrapper above forwards no starts, so it
381
+ sends no placeholders.
382
+
352
383
  ## Verify a stored application trace
353
384
 
354
385
  After exercising a real application request and finishing its stream, flush the
package/dist/client.js CHANGED
@@ -6,6 +6,7 @@ import { W3CTraceContextPropagator } from "@opentelemetry/core";
6
6
  import { LoggerProvider } from "@opentelemetry/sdk-logs";
7
7
  import { TracerProvider } from "@opentelemetry/sdk-trace";
8
8
  import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
9
+ import { HUE_SCOPE } from "./config.js";
9
10
  import { encodeContent, noopSpan, safeSpan } from "./safety.js";
10
11
  import { createHueTransport, HueExportError } from "./transport.js";
11
12
  import { verifyTrace } from "./receipt.js";
@@ -197,8 +198,8 @@ export class HueClient {
197
198
  }
198
199
  this.captureContent = this.transport.options.captureContent;
199
200
  this.enabled = this.transport.options.enabled !== false;
200
- this.tracer = new ContextualTracer(this.tracerProvider.getTracer("@hue-run/sdk", sdkVersion), this.storage, () => this.enabled && !this.closed, () => this.transport.instrumentationFailure());
201
- this.logger = this.loggerProvider.getLogger("@hue-run/sdk", sdkVersion);
201
+ this.tracer = new ContextualTracer(this.tracerProvider.getTracer(HUE_SCOPE, sdkVersion), this.storage, () => this.enabled && !this.closed, () => this.transport.instrumentationFailure());
202
+ this.logger = this.loggerProvider.getLogger(HUE_SCOPE, sdkVersion);
202
203
  }
203
204
  /**
204
205
  * Verifies that Hue stored a trace by ID, optionally waiting for expected span IDs and normalized
package/dist/config.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import type { HueOptions, SharedHueOptions } from "./types.js";
2
2
  export declare const MAX_BODY_BYTES: number;
3
3
  export declare const MAX_CONTENT_BYTES: number;
4
+ /** Instrumentation scope of the client's own tracer and logger. */
5
+ export declare const HUE_SCOPE = "@hue-run/sdk";
4
6
  /** Loopback hostnames that may use plain HTTP without opting in. */
5
7
  export declare function isLoopbackHost(hostname: string): boolean;
6
8
  /** True when a validated origin exports over plain HTTP to a host other than loopback. */
7
9
  export declare function isInsecureOrigin(baseUrl: string): boolean;
8
- export declare function validateOptions(options: HueOptions): HueOptions & Required<Pick<SharedHueOptions, "captureContent" | "baseUrl" | "timeoutMillis" | "maxQueueBytes">> & {
10
+ export declare function validateOptions(options: HueOptions): HueOptions & Required<Pick<SharedHueOptions, "captureContent" | "baseUrl" | "timeoutMillis" | "maxQueueBytes" | "liveSpans">> & {
9
11
  /** Project key after validation; empty for a disabled client. */
10
12
  apiKey: string;
11
13
  /** Service name after validation; `hue-disabled` for a disabled client. */
package/dist/config.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export const MAX_BODY_BYTES = 1024 * 1024;
2
2
  export const MAX_CONTENT_BYTES = 256 * 1024;
3
+ /** Instrumentation scope of the client's own tracer and logger. */
4
+ export const HUE_SCOPE = "@hue-run/sdk";
3
5
  /** Loopback hostnames that may use plain HTTP without opting in. */
4
6
  export function isLoopbackHost(hostname) {
5
7
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
@@ -30,6 +32,7 @@ export function validateOptions(options) {
30
32
  baseUrl: "https://app.hue.run",
31
33
  timeoutMillis: 10000,
32
34
  maxQueueBytes: 8 * 1024 * 1024,
35
+ liveSpans: false,
33
36
  };
34
37
  }
35
38
  if (typeof options.captureContent !== "boolean")
@@ -74,5 +77,9 @@ export function validateOptions(options) {
74
77
  maxQueueBytes < 1024 ||
75
78
  maxQueueBytes > 64 * 1024 * 1024)
76
79
  throw new TypeError("maxQueueBytes must be 1024–67108864");
77
- return { ...options, baseUrl: url.origin, timeoutMillis, maxQueueBytes };
80
+ if (options.liveSpans !== undefined && typeof options.liveSpans !== "boolean")
81
+ throw new TypeError("liveSpans must be a boolean");
82
+ // Setup credentials send installer telemetry only, never in-progress placeholders.
83
+ const liveSpans = options.liveSpans !== false && !options.apiKey.startsWith("hue_setup_");
84
+ return { ...options, baseUrl: url.origin, timeoutMillis, maxQueueBytes, liveSpans };
78
85
  }
@@ -0,0 +1,25 @@
1
+ import { type Attributes } from "@opentelemetry/api";
2
+ import { type ReadableSpan } from "@opentelemetry/sdk-trace";
3
+ /**
4
+ * A finished span never carries the placeholder markers, whatever an application set: Hue would
5
+ * read such a span as a malformed placeholder and reject it.
6
+ */
7
+ export declare function withoutPlaceholderMarkers(attributes: Attributes): Attributes;
8
+ /** Open spans tracked for announcement at once; later starts are not announced. */
9
+ export declare const MAX_LIVE_SPANS = 1024;
10
+ export declare const LIVE_SPAN_INTERVAL_MILLIS = 500;
11
+ /**
12
+ * Response header on every trace acknowledgement from a Hue that accepts placeholders. Without
13
+ * it the receiver predates them and rejects each one by its zero end time.
14
+ */
15
+ export declare const PLACEHOLDERS_HEADER = "hue-pending-spans";
16
+ /** Hue's own spans and recognizable AI spans, judged from what is known when the span starts. */
17
+ export declare function announcesLiveSpan(span: ReadableSpan): boolean;
18
+ /**
19
+ * A normal OTLP span announcing `span` while it runs: a new identity whose parent is the real
20
+ * span, the real start time and an end time of 0. The markers are added after redaction.
21
+ */
22
+ export declare function pendingPlaceholder(span: ReadableSpan): {
23
+ record: ReadableSpan;
24
+ markers: Attributes;
25
+ };
@@ -0,0 +1,115 @@
1
+ import { SpanStatusCode } from "@opentelemetry/api";
2
+ import { RandomIdGenerator } from "@opentelemetry/sdk-trace";
3
+ import { HUE_SCOPE } from "./config.js";
4
+ /** Marks a placeholder for a span that has started but not ended. The value versions the shape. */
5
+ const PENDING_SPAN_TYPE_KEY = "hue.span_type";
6
+ const PENDING_SPAN_TYPE = "pending_span";
7
+ /** The real span's own parent. Absent when the real span is a root. */
8
+ const PENDING_PARENT_KEY = "hue.pending_parent_id";
9
+ const markerKeys = [PENDING_SPAN_TYPE_KEY, PENDING_PARENT_KEY];
10
+ /**
11
+ * A finished span never carries the placeholder markers, whatever an application set: Hue would
12
+ * read such a span as a malformed placeholder and reject it.
13
+ */
14
+ export function withoutPlaceholderMarkers(attributes) {
15
+ if (!markerKeys.some((key) => Object.hasOwn(attributes, key)))
16
+ return attributes;
17
+ const kept = { ...attributes };
18
+ for (const key of markerKeys)
19
+ delete kept[key];
20
+ return kept;
21
+ }
22
+ /** Open spans tracked for announcement at once; later starts are not announced. */
23
+ export const MAX_LIVE_SPANS = 1024;
24
+ export const LIVE_SPAN_INTERVAL_MILLIS = 500;
25
+ /**
26
+ * Response header on every trace acknowledgement from a Hue that accepts placeholders. Without
27
+ * it the receiver predates them and rejects each one by its zero end time.
28
+ */
29
+ export const PLACEHOLDERS_HEADER = "hue-pending-spans";
30
+ const MAX_PLACEHOLDER_VALUE_BYTES = 64 * 1024;
31
+ // Kept narrow on purpose: common application processors export only these spans, and a
32
+ // placeholder whose real span is filtered later would read as running until the trace stalls.
33
+ const livePrefixes = ["gen_ai.", "ai.", "llm.", "traceloop."];
34
+ // Definitions and instructions are large and rarely useful while a span runs; the real span
35
+ // still carries them. Copied markers would misplace the placeholder.
36
+ const omittedKeys = [
37
+ "gen_ai.tool.definitions",
38
+ "gen_ai.system_instructions",
39
+ PENDING_SPAN_TYPE_KEY,
40
+ PENDING_PARENT_KEY,
41
+ ];
42
+ const ids = new RandomIdGenerator();
43
+ /** Hue's own spans and recognizable AI spans, judged from what is known when the span starts. */
44
+ export function announcesLiveSpan(span) {
45
+ if (span.instrumentationScope.name === HUE_SCOPE || span.name.startsWith("ai."))
46
+ return true;
47
+ for (const key in span.attributes)
48
+ if (livePrefixes.some((prefix) => key.startsWith(prefix)))
49
+ return true;
50
+ return false;
51
+ }
52
+ function valueBytes(value) {
53
+ if (typeof value === "string")
54
+ return Buffer.byteLength(value);
55
+ if (!Array.isArray(value))
56
+ return 8;
57
+ let bytes = 0;
58
+ for (const item of value)
59
+ bytes += typeof item === "string" ? Buffer.byteLength(item) : 8;
60
+ return bytes;
61
+ }
62
+ function placeholderAttributes(source) {
63
+ const attributes = {};
64
+ for (const key in source) {
65
+ if (!Object.hasOwn(source, key))
66
+ continue;
67
+ if (omittedKeys.some((omitted) => key === omitted || key.startsWith(`${omitted}.`)))
68
+ continue;
69
+ const value = source[key];
70
+ if (valueBytes(value) <= MAX_PLACEHOLDER_VALUE_BYTES)
71
+ attributes[key] = value;
72
+ }
73
+ return attributes;
74
+ }
75
+ /**
76
+ * A normal OTLP span announcing `span` while it runs: a new identity whose parent is the real
77
+ * span, the real start time and an end time of 0. The markers are added after redaction.
78
+ */
79
+ export function pendingPlaceholder(span) {
80
+ const real = span.spanContext();
81
+ const own = {
82
+ traceId: real.traceId,
83
+ spanId: ids.generateSpanId(),
84
+ traceFlags: real.traceFlags,
85
+ ...(real.traceState ? { traceState: real.traceState } : {}),
86
+ };
87
+ const parent = span.parentSpanContext?.spanId;
88
+ const record = {
89
+ name: span.name,
90
+ kind: span.kind,
91
+ spanContext: () => own,
92
+ // Hue re-keys a placeholder as its real span, so the flags describe the real span's parent.
93
+ parentSpanContext: { ...real, isRemote: span.parentSpanContext?.isRemote === true },
94
+ startTime: span.startTime,
95
+ endTime: [0, 0],
96
+ duration: [0, 0],
97
+ ended: true,
98
+ status: { code: SpanStatusCode.UNSET },
99
+ attributes: placeholderAttributes(span.attributes),
100
+ links: [],
101
+ events: [],
102
+ resource: span.resource,
103
+ instrumentationScope: span.instrumentationScope,
104
+ droppedAttributesCount: 0,
105
+ droppedEventsCount: 0,
106
+ droppedLinksCount: 0,
107
+ };
108
+ return {
109
+ record,
110
+ markers: {
111
+ [PENDING_SPAN_TYPE_KEY]: PENDING_SPAN_TYPE,
112
+ ...(parent ? { [PENDING_PARENT_KEY]: parent } : {}),
113
+ },
114
+ };
115
+ }
@@ -50,8 +50,25 @@ export declare class HueTransport {
50
50
  private closed;
51
51
  private shutdownPromise?;
52
52
  private flushPromise?;
53
+ private live;
54
+ private liveTimer?;
55
+ private liveSpans;
56
+ private placeholdersRejected;
57
+ private placeholders;
58
+ private placeholderSources;
59
+ private batchSpans?;
53
60
  constructor(options: HueOptions);
61
+ /**
62
+ * Advisory records (placeholders) are admitted only while the queue is under a quarter of its
63
+ * record and byte budgets, so they never take more than a quarter from real records. They are
64
+ * skipped silently.
65
+ */
54
66
  private enqueue;
67
+ private track;
68
+ /** Placeholders are built lazily, so input set right after a span starts is included. */
69
+ private announceLiveSpans;
70
+ private stopLiveTimer;
71
+ private stopLiveSpans;
55
72
  /** Cumulative counters and current queue gauges. */
56
73
  getReport(): ExportReport;
57
74
  /** Copies of the latest 128 sanitized issues, oldest first. */
package/dist/transport.js CHANGED
@@ -1,10 +1,12 @@
1
+ import { gzip } from "node:zlib";
1
2
  import { ExportResultCode } from "@opentelemetry/core";
2
- import { CompressionAlgorithm, OTLPExporterBase, OTLPExporterError, } from "@opentelemetry/otlp-exporter-base";
3
- import { createOtlpHttpExportDelegate } from "@opentelemetry/otlp-exporter-base/node-http";
3
+ import { createOtlpNetworkExportDelegate, OTLPExporterBase, OTLPExporterError, } from "@opentelemetry/otlp-exporter-base";
4
+ import { createOtlpHttpExporterMetrics } from "@opentelemetry/otlp-exporter-base/node-http";
4
5
  import { LogsExporterMetricsHelper, ProtobufLogsSerializer, ProtobufTraceSerializer, TraceExporterMetricsHelper, } from "@opentelemetry/otlp-transformer";
5
6
  import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace";
6
7
  import { BatchLogRecordProcessor, } from "@opentelemetry/sdk-logs";
7
8
  import { isInsecureOrigin, MAX_BODY_BYTES, validateOptions } from "./config.js";
9
+ import { announcesLiveSpan, LIVE_SPAN_INTERVAL_MILLIS, MAX_LIVE_SPANS, pendingPlaceholder, PLACEHOLDERS_HEADER, withoutPlaceholderMarkers, } from "./live-spans.js";
8
10
  import { estimateRecordBytes } from "./safety.js";
9
11
  import { snapshotLog, snapshotSpan } from "./snapshot.js";
10
12
  import { redactLog, redactSpan } from "./privacy.js";
@@ -83,9 +85,20 @@ export class HueTransport {
83
85
  closed = false;
84
86
  shutdownPromise;
85
87
  flushPromise;
88
+ // Open spans not yet announced. Each is considered once, at the next tick after it starts.
89
+ live = new Set();
90
+ liveTimer;
91
+ liveSpans;
92
+ placeholdersRejected = false;
93
+ // Admitted placeholder snapshots and the marker attributes added after redaction.
94
+ placeholders = new WeakMap();
95
+ // The running span each admitted placeholder announces, so one it has outlived is not sent.
96
+ placeholderSources = new WeakMap();
97
+ batchSpans;
86
98
  constructor(options) {
87
99
  this.options = validateOptions(options);
88
100
  Object.defineProperty(this, "options", { enumerable: false });
101
+ this.liveSpans = this.options.enabled !== false && this.options.liveSpans;
89
102
  this.traceExporter = new ReportingExporter(this, "traces", ProtobufTraceSerializer, TraceExporterMetricsHelper, (span, cache) => redactSpan(span, this.options, cache));
90
103
  this.logExporter = new ReportingExporter(this, "logs", ProtobufLogsSerializer, LogsExporterMetricsHelper, (log, cache) => redactLog(log, this.options, cache));
91
104
  if (this.options.enabled === false) {
@@ -103,6 +116,7 @@ export class HueTransport {
103
116
  };
104
117
  const spans = new BatchSpanProcessor({ exporter: this.traceExporter, ...batching });
105
118
  const logs = new BatchLogRecordProcessor({ exporter: this.logExporter, ...batching });
119
+ this.batchSpans = spans;
106
120
  this.spanProcessor = {
107
121
  onStart: (span, parent) => {
108
122
  try {
@@ -111,8 +125,11 @@ export class HueTransport {
111
125
  catch {
112
126
  this.instrumentationFailure();
113
127
  }
128
+ this.track(span);
114
129
  },
115
130
  onEnd: (span) => {
131
+ if (this.live.delete(span) && !this.live.size)
132
+ this.stopLiveTimer();
116
133
  let admitted;
117
134
  try {
118
135
  if (!(span.spanContext().traceFlags & 1))
@@ -130,7 +147,10 @@ export class HueTransport {
130
147
  }
131
148
  },
132
149
  forceFlush: () => spans.forceFlush(),
133
- shutdown: () => spans.shutdown(),
150
+ shutdown: () => {
151
+ this.stopLiveSpans();
152
+ return spans.shutdown();
153
+ },
134
154
  };
135
155
  this.logRecordProcessor = {
136
156
  onEmit: (log) => {
@@ -152,8 +172,15 @@ export class HueTransport {
152
172
  shutdown: () => logs.shutdown(),
153
173
  };
154
174
  }
155
- enqueue(signal, record) {
175
+ /**
176
+ * Advisory records (placeholders) are admitted only while the queue is under a quarter of its
177
+ * record and byte budgets, so they never take more than a quarter from real records. They are
178
+ * skipped silently.
179
+ */
180
+ enqueue(signal, record, advisory = false) {
156
181
  const pending = signal === "traces" ? this.spans : this.logs;
182
+ if (advisory && (this.closed || pending.size >= 2048 / 4))
183
+ return undefined;
157
184
  if (this.closed || pending.size >= 2048) {
158
185
  this.issue(signal, "dropped", 1, this.closed
159
186
  ? "Telemetry emitted after transport shutdown"
@@ -161,7 +188,8 @@ export class HueTransport {
161
188
  return undefined;
162
189
  }
163
190
  try {
164
- const remaining = this.options.maxQueueBytes - this.pendingBytes;
191
+ const remaining = (advisory ? this.options.maxQueueBytes / 4 : this.options.maxQueueBytes) -
192
+ this.pendingBytes;
165
193
  const snapshot = signal === "traces"
166
194
  ? snapshotSpan(record, remaining)
167
195
  : snapshotLog(record, remaining);
@@ -170,15 +198,94 @@ export class HueTransport {
170
198
  this.spans.set(snapshot.record, snapshot.bytes);
171
199
  else
172
200
  this.logs.set(snapshot.record, snapshot.bytes);
173
- if (snapshot.unresolvedResource)
201
+ if (snapshot.unresolvedResource && !advisory)
174
202
  this.issue(signal, "warning", 0, "Unresolved resource attributes omitted from the telemetry snapshot");
175
203
  return snapshot.record;
176
204
  }
177
205
  catch {
206
+ if (advisory)
207
+ return undefined;
178
208
  this.issue(signal, "dropped", 1, "Telemetry snapshot exceeded its byte or complexity budget or contained unsupported data");
179
209
  return undefined;
180
210
  }
181
211
  }
212
+ track(span) {
213
+ try {
214
+ if (!this.liveSpans || this.live.size >= MAX_LIVE_SPANS)
215
+ return;
216
+ if (!(span.spanContext().traceFlags & 1) || !span.isRecording() || !announcesLiveSpan(span))
217
+ return;
218
+ this.live.add(span);
219
+ if (!this.liveTimer) {
220
+ const timer = setInterval(() => this.announceLiveSpans(), LIVE_SPAN_INTERVAL_MILLIS);
221
+ timer.unref();
222
+ this.liveTimer = timer;
223
+ }
224
+ }
225
+ catch {
226
+ // Live announcements are advisory. They never report failures or affect the span.
227
+ }
228
+ }
229
+ /** Placeholders are built lazily, so input set right after a span starts is included. */
230
+ announceLiveSpans() {
231
+ for (const span of this.live) {
232
+ this.live.delete(span);
233
+ try {
234
+ // Ended without reaching onEnd: a wrapping processor filtered it, so no real span follows.
235
+ if (!this.liveSpans || span.ended || !this.batchSpans)
236
+ continue;
237
+ const { record, markers } = pendingPlaceholder(span);
238
+ const admitted = this.enqueue("traces", record, true);
239
+ if (!admitted)
240
+ continue;
241
+ this.placeholders.set(admitted, markers);
242
+ this.placeholderSources.set(admitted, span);
243
+ try {
244
+ this.batchSpans.onEnd(admitted);
245
+ }
246
+ catch {
247
+ this.finish("traces", [admitted]);
248
+ }
249
+ }
250
+ catch {
251
+ // Skipped silently, like any placeholder the queue has no room for.
252
+ }
253
+ }
254
+ if (!this.live.size)
255
+ this.stopLiveTimer();
256
+ }
257
+ stopLiveTimer() {
258
+ clearInterval(this.liveTimer);
259
+ this.liveTimer = undefined;
260
+ }
261
+ stopLiveSpans() {
262
+ this.liveSpans = false;
263
+ this.live.clear();
264
+ this.stopLiveTimer();
265
+ }
266
+ /** @internal Exporter callback: marker attributes when `record` is an admitted placeholder. */
267
+ placeholderMarkers(record) {
268
+ return this.placeholders.get(record);
269
+ }
270
+ /** @internal Exporter callback: whether a placeholder's span has ended, so it announces nothing. */
271
+ placeholderSettled(record) {
272
+ return this.placeholderSources.get(record)?.ended === true;
273
+ }
274
+ /** @internal Exporter callback: false once the receiver refused placeholders; queued ones are then dropped. */
275
+ sendsPlaceholders() {
276
+ return !this.placeholdersRejected;
277
+ }
278
+ /**
279
+ * @internal Exporter callback: a receiver without placeholder support rejects each by its zero
280
+ * end time. Stops announcing for this transport and records one warning.
281
+ */
282
+ rejectPlaceholders(count) {
283
+ this.stopLiveSpans();
284
+ if (this.placeholdersRejected)
285
+ return;
286
+ this.placeholdersRejected = true;
287
+ this.issue("traces", "warning", count, "This Hue server does not accept in-progress span placeholders; live spans are disabled");
288
+ }
182
289
  /** @internal Exporter callback: releases queued records after an export attempt settles. */
183
290
  finish(signal, records) {
184
291
  for (const record of records) {
@@ -345,13 +452,52 @@ class ReportingExporter {
345
452
  .catch(() => { });
346
453
  this.pending.add(work);
347
454
  }
455
+ /** Real records first, then placeholders whose real span is not already in this batch. */
456
+ ordered(records) {
457
+ if (this.signal !== "traces")
458
+ return records.map((record) => ({ record }));
459
+ const real = [];
460
+ const placeholders = [];
461
+ for (const record of records) {
462
+ const markers = this.transport.placeholderMarkers(record);
463
+ if (markers)
464
+ placeholders.push({ record, markers });
465
+ else
466
+ real.push({ record });
467
+ }
468
+ if (!placeholders.length || !this.transport.sendsPlaceholders())
469
+ return real;
470
+ const ended = new Set(real.map(({ record }) => record.spanContext().spanId));
471
+ // A span that already ended, in this batch or not (even one a wrapping processor filtered),
472
+ // is no longer running, so its placeholder is not sent.
473
+ for (const placeholder of placeholders)
474
+ if (!ended.has(placeholder.record.parentSpanContext?.spanId ?? "") &&
475
+ !this.transport.placeholderSettled(placeholder.record))
476
+ real.push(placeholder);
477
+ return real;
478
+ }
348
479
  async exportRecords(records) {
349
480
  const accepted = [];
481
+ // Placeholders are advisory: losing one is a warning, never an export failure.
482
+ const placeholders = new Set();
350
483
  const cache = new WeakMap();
351
484
  let failed = false;
352
485
  let redactedBytes = 0;
486
+ const invalid = (placeholder, message) => {
487
+ if (placeholder)
488
+ this.transport.issue(this.signal, "warning", 1, `${message} (in-progress span placeholder)`);
489
+ else {
490
+ failed = true;
491
+ this.transport.issue(this.signal, "invalid", 1, message);
492
+ }
493
+ };
494
+ const send = async (batch) => {
495
+ const count = batch.filter((record) => placeholders.has(record)).length;
496
+ if (!(await this.send(batch, count)))
497
+ failed = true;
498
+ };
353
499
  const resourceDeadline = Date.now() + this.transport.options.timeoutMillis;
354
- for (const record of records) {
500
+ for (const { record, markers } of this.ordered(records)) {
355
501
  try {
356
502
  const ready = record.resource.waitForAsyncAttributes?.();
357
503
  if (ready) {
@@ -368,17 +514,30 @@ class ReportingExporter {
368
514
  clearTimeout(timer);
369
515
  }
370
516
  }
371
- const redacted = this.redact(record, cache);
517
+ let redacted = this.redact(record, cache);
518
+ // Added after redaction, so a redactor cannot alter the markers or the parent identity.
519
+ if (markers)
520
+ redacted = {
521
+ ...redacted,
522
+ attributes: { ...redacted.attributes, ...markers },
523
+ };
524
+ else if (this.signal === "traces") {
525
+ const attributes = redacted.attributes;
526
+ const kept = withoutPlaceholderMarkers(attributes);
527
+ if (kept !== attributes)
528
+ redacted = { ...redacted, attributes: kept };
529
+ }
372
530
  const bytes = 512 +
373
531
  estimateRecordBytes(recordData(redacted, this.signal), this.transport.options.maxQueueBytes - redactedBytes);
374
532
  if (redactedBytes + bytes > this.transport.options.maxQueueBytes)
375
533
  throw new RangeError("Redacted batch exceeds byte budget");
376
534
  redactedBytes += bytes;
377
535
  accepted.push(redacted);
536
+ if (markers)
537
+ placeholders.add(redacted);
378
538
  }
379
539
  catch {
380
- failed = true;
381
- this.transport.issue(this.signal, "invalid", 1, "Telemetry record could not be redacted or exceeds supported content limits");
540
+ invalid(markers !== undefined, "Telemetry record could not be redacted or exceeds supported content limits");
382
541
  }
383
542
  }
384
543
  // Each record is encoded once to measure it; a request is encoded once more when it is sent.
@@ -394,37 +553,41 @@ class ReportingExporter {
394
553
  recordBytes = this.serializer.serializeRequest([record])?.byteLength ?? Infinity;
395
554
  }
396
555
  catch {
397
- failed = true;
398
- this.transport.issue(this.signal, "invalid", 1, "Telemetry record could not be serialized");
556
+ invalid(placeholders.has(record), "Telemetry record could not be serialized");
399
557
  continue;
400
558
  }
401
559
  const framedBytes = recordBytes + RECORD_FRAMING_BYTES;
402
560
  if (batch.length && batchBytes + framedBytes > limit) {
403
- if (!(await this.send(batch)))
404
- failed = true;
561
+ await send(batch);
405
562
  batch = [];
406
563
  batchBytes = 0;
407
564
  }
408
565
  if (recordBytes > limit) {
409
- failed = true;
410
- this.transport.issue(this.signal, "invalid", 1, "Telemetry record exceeds the 1 MiB request limit");
566
+ invalid(placeholders.has(record), "Telemetry record exceeds the 1 MiB request limit");
411
567
  continue;
412
568
  }
413
569
  batch.push(record);
414
570
  batchBytes += framedBytes;
415
571
  }
416
- if (batch.length && !(await this.send(batch)))
417
- failed = true;
572
+ if (batch.length)
573
+ await send(batch);
418
574
  if (failed)
419
575
  throw new Error("Hue telemetry export failed");
420
576
  }
421
- async send(records) {
577
+ /** Sends one request; `placeholders` of `records` are advisory and never count as lost. */
578
+ async send(records, placeholders = 0) {
422
579
  const options = this.transport.options;
580
+ const real = records.length - placeholders;
581
+ // A loss involving only placeholders is a warning. Mixed losses count only real records.
582
+ const lose = (message, status) => {
583
+ this.transport.issue(this.signal, real ? "failed" : "warning", real || placeholders, real ? message : `${message} (in-progress span placeholders only)`, status);
584
+ return !real;
585
+ };
423
586
  let rejected = 0;
424
587
  let validResponse = true;
425
588
  let receivedResponse = false;
589
+ let acceptsPlaceholders = false;
426
590
  let expired = false;
427
- const agents = new Set();
428
591
  const deadline = Date.now() + options.timeoutMillis;
429
592
  let timer;
430
593
  const serializer = {
@@ -439,17 +602,27 @@ class ReportingExporter {
439
602
  const count = Number(partial?.[this.signal === "traces" ? "rejectedSpans" : "rejectedLogRecords"] ?? 0);
440
603
  if (!Number.isSafeInteger(count) || count < 0 || count > records.length)
441
604
  throw new Error("Invalid rejection count");
442
- rejected = count;
443
- if (count || partial?.errorMessage)
444
- this.transport.issue(this.signal, count ? "rejected" : "warning", count, count
605
+ // A receiver that accepts placeholders never rejects them for being placeholders, so its
606
+ // rejections count as before. One without the header predates them and rejects each.
607
+ const downgrade = placeholders > 0 && !acceptsPlaceholders;
608
+ const placeholderRejections = downgrade ? Math.min(count, placeholders) : 0;
609
+ if (downgrade)
610
+ this.transport.rejectPlaceholders(placeholderRejections);
611
+ const remaining = count - placeholderRejections;
612
+ // Rejections are not matched to records. Attribute them to real records first.
613
+ rejected = Math.min(remaining, real);
614
+ if (remaining || (partial?.errorMessage && !downgrade))
615
+ this.transport.issue(this.signal, rejected ? "rejected" : "warning", rejected || remaining, rejected
445
616
  ? "Hue rejected telemetry records; inspect the project ingestion settings and supported limits"
446
- : "Hue returned an ingestion warning");
617
+ : remaining
618
+ ? "Hue rejected in-progress span placeholders"
619
+ : "Hue returned an ingestion warning");
447
620
  // Do not pass backend error text or raw response bytes into the global OTel diagnostic logger.
448
621
  return {};
449
622
  }
450
623
  catch {
451
624
  validResponse = false;
452
- this.transport.issue(this.signal, "failed", records.length, "Hue returned an invalid OTLP acknowledgement; acceptance is uncertain");
625
+ lose("Hue returned an invalid OTLP acknowledgement; acceptance is uncertain");
453
626
  return {};
454
627
  }
455
628
  },
@@ -457,64 +630,44 @@ class ReportingExporter {
457
630
  const endpoint = `${options.baseUrl}/api/v1/otlp/v1/${this.signal}`;
458
631
  // Explicit configuration only. OTEL_EXPORTER_OTLP_* environment variables are meant
459
632
  // for generic exporters; merging them here could send another vendor's headers to Hue.
460
- const delegate = createOtlpHttpExportDelegate({
461
- url: endpoint,
462
- headers: async () => ({
463
- "Content-Type": "application/x-protobuf",
464
- Authorization: `Bearer ${options.apiKey}`,
465
- }),
466
- // The transport prefixes this to OpenTelemetry's own User-Agent token.
467
- userAgent: `hue-sdk-typescript/${sdkVersion}`,
468
- timeoutMillis: options.timeoutMillis,
469
- concurrencyLimit: 1,
470
- compression: CompressionAlgorithm.GZIP,
471
- agentFactory: async (protocol) => {
472
- if (expired || Date.now() >= deadline)
473
- throw new Error("Hue export deadline exceeded");
474
- const { Agent } = await import(protocol === "https:" ? "node:https" : "node:http");
475
- const agent = new Agent({ keepAlive: false });
476
- agents.add(agent);
477
- if (expired)
478
- agent.destroy();
479
- return agent;
480
- },
481
- }, serializer, this.signal === "traces" ? "otlp_http_span_exporter" : "otlp_http_log_exporter", this.metrics, undefined);
633
+ const transport = new OtlpHttpTransport(endpoint, {
634
+ "Content-Type": "application/x-protobuf",
635
+ Authorization: `Bearer ${options.apiKey}`,
636
+ "User-Agent": `hue-sdk-typescript/${sdkVersion} ${OTEL_USER_AGENT}`,
637
+ }, (headers) => {
638
+ acceptsPlaceholders = headers[PLACEHOLDERS_HEADER] === "1";
639
+ });
640
+ const delegate = createOtlpNetworkExportDelegate({ timeoutMillis: options.timeoutMillis, concurrencyLimit: 1, compression: "gzip" }, serializer, createOtlpHttpExporterMetrics(this.signal === "traces" ? "otlp_http_span_exporter" : "otlp_http_log_exporter", this.metrics, endpoint, undefined), transport);
482
641
  const exporter = new OTLPExporterBase(delegate);
483
642
  try {
484
643
  const result = await new Promise((resolve) => {
485
644
  timer = setTimeout(() => {
486
645
  expired = true;
487
- for (const agent of agents)
488
- agent.destroy();
646
+ transport.abort();
489
647
  resolve({ code: ExportResultCode.FAILED });
490
648
  }, Math.max(1, deadline - Date.now()));
491
649
  exporter.export(records, resolve);
492
650
  });
493
651
  if (result.code === ExportResultCode.SUCCESS) {
494
- if (!receivedResponse) {
495
- this.transport.issue(this.signal, "failed", records.length, "Hue response ended without a complete OTLP acknowledgement; acceptance is uncertain");
496
- return false;
497
- }
652
+ if (!receivedResponse)
653
+ return lose("Hue response ended without a complete OTLP acknowledgement; acceptance is uncertain");
498
654
  if (validResponse)
499
- this.transport.acceptedRecords(this.signal, records.length - rejected);
500
- return validResponse;
655
+ this.transport.acceptedRecords(this.signal, real - rejected);
656
+ return validResponse || !real;
501
657
  }
502
658
  else {
503
659
  const status = result.error instanceof OTLPExporterError && Number.isInteger(result.error.code)
504
660
  ? result.error.code
505
661
  : undefined;
506
- this.transport.issue(this.signal, "failed", records.length, "Hue telemetry request failed", status);
507
- return false;
662
+ return lose("Hue telemetry request failed", status);
508
663
  }
509
664
  }
510
665
  catch {
511
- this.transport.issue(this.signal, "failed", records.length, "Hue telemetry request failed");
512
- return false;
666
+ return lose("Hue telemetry request failed");
513
667
  }
514
668
  finally {
515
669
  clearTimeout(timer);
516
- for (const agent of agents)
517
- agent.destroy();
670
+ transport.abort();
518
671
  // Delegate cleanup cannot extend the hard request wait. Sockets are closed
519
672
  // and the cleanup promise is always observed, even after a caller timeout.
520
673
  void exporter.shutdown().catch(() => { });
@@ -527,6 +680,182 @@ class ReportingExporter {
527
680
  await this.forceFlush();
528
681
  }
529
682
  }
683
+ // OpenTelemetry's OTLP/HTTP transport rules (otlp-exporter-base 0.222), restated because that
684
+ // transport discards response headers, which Hue uses to announce features.
685
+ const MAX_RETRIES = 5;
686
+ const INITIAL_BACKOFF_MILLIS = 1000;
687
+ const MAX_BACKOFF_MILLIS = 5000;
688
+ const BACKOFF_MULTIPLIER = 1.5;
689
+ const JITTER = 0.2;
690
+ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
691
+ const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
692
+ const RETRYABLE_NETWORK_ERRORS = new Set([
693
+ "ECONNRESET",
694
+ "ECONNREFUSED",
695
+ "EPIPE",
696
+ "ETIMEDOUT",
697
+ "EAI_AGAIN",
698
+ "ENOTFOUND",
699
+ "ENETUNREACH",
700
+ "EHOSTUNREACH",
701
+ ]);
702
+ /** OpenTelemetry's exporter token, which follows Hue's in the User-Agent as it always has. */
703
+ const OTEL_USER_AGENT = "OTel-OTLP-Exporter-JavaScript/0.222.0";
704
+ function retryAfterMillis(value) {
705
+ if (value == null)
706
+ return undefined;
707
+ const seconds = Number.parseInt(value, 10);
708
+ if (Number.isInteger(seconds))
709
+ return seconds > 0 ? seconds * 1000 : -1;
710
+ const delay = new Date(value).getTime() - Date.now();
711
+ return delay >= 0 ? delay : 0;
712
+ }
713
+ function networkFailure(error) {
714
+ const code = error.code;
715
+ return typeof code === "string" && RETRYABLE_NETWORK_ERRORS.has(code)
716
+ ? { status: "retryable", error }
717
+ : { status: "failure", error };
718
+ }
719
+ function compress(data) {
720
+ return new Promise((resolve, reject) => gzip(data, (error, result) => (error ? reject(error) : resolve(result))));
721
+ }
722
+ /**
723
+ * Gzip-compressed OTLP/HTTP POSTs that never follow redirects, retried like OpenTelemetry's
724
+ * exporter within the request budget. `onSuccess` receives the headers of the 2xx response whose
725
+ * body becomes the acknowledgement, before that body is decoded. One instance serves a single
726
+ * export request: its connections are never reused, and `abort()` closes them at the export
727
+ * deadline.
728
+ */
729
+ class OtlpHttpTransport {
730
+ url;
731
+ headers;
732
+ onSuccess;
733
+ // Requests in flight and how to settle each, so an abort never leaves an attempt pending.
734
+ requests = new Map();
735
+ agent;
736
+ aborted = false;
737
+ constructor(url, headers, onSuccess) {
738
+ this.url = url;
739
+ this.headers = headers;
740
+ this.onSuccess = onSuccess;
741
+ }
742
+ async send(data, timeoutMillis) {
743
+ const deadline = Date.now() + timeoutMillis;
744
+ let backoff = INITIAL_BACKOFF_MILLIS;
745
+ let result = await this.attempt(data, timeoutMillis);
746
+ for (let retries = MAX_RETRIES; result.status === "retryable" && retries > 0; retries--) {
747
+ const jitter = Math.random() * 2 * JITTER - JITTER;
748
+ const wait = result.retryInMillis ?? Math.max(Math.min(backoff * (1 + jitter), MAX_BACKOFF_MILLIS), 0);
749
+ backoff *= BACKOFF_MULTIPLIER;
750
+ // Return when the next attempt would start after the export deadline.
751
+ if (this.aborted || wait > deadline - Date.now())
752
+ return result;
753
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, wait)));
754
+ result = await this.attempt(data, Math.max(1, deadline - Date.now()));
755
+ }
756
+ return result;
757
+ }
758
+ async attempt(data, timeoutMillis) {
759
+ try {
760
+ if (this.aborted)
761
+ throw new Error("Hue export deadline exceeded");
762
+ const url = new URL(this.url);
763
+ const protocol = url.protocol;
764
+ // Loaded on first use, as OpenTelemetry's exporter does, so importing Hue never loads http
765
+ // before the application's http instrumentation can patch it.
766
+ const [{ Agent: ConnectionAgent, request }, body] = await Promise.all([
767
+ import(protocol === "https:" ? "node:https" : "node:http"),
768
+ compress(data),
769
+ ]);
770
+ if (this.aborted)
771
+ throw new Error("Hue export deadline exceeded");
772
+ // Never kept alive: each attempt's socket closes with its response or at the deadline.
773
+ this.agent ??= new ConnectionAgent({ keepAlive: false });
774
+ return await new Promise((resolve) => {
775
+ const req = request(url, {
776
+ method: "POST",
777
+ agent: this.agent,
778
+ headers: {
779
+ ...this.headers,
780
+ "Content-Encoding": "gzip",
781
+ "Content-Length": body.byteLength,
782
+ },
783
+ }, (res) => {
784
+ const chunks = [];
785
+ let size = 0;
786
+ const status = res.statusCode ?? 0;
787
+ const success = status >= 200 && status <= 299;
788
+ res.on("data", (chunk) => {
789
+ size += chunk.length;
790
+ if (size > MAX_RESPONSE_BYTES) {
791
+ // Oversized responses fail regardless of status; resolve before tearing down.
792
+ resolve({ status: "failure", error: new Error("OTLP response exceeded 4 MiB") });
793
+ res.destroy();
794
+ return;
795
+ }
796
+ chunks.push(chunk);
797
+ });
798
+ res.on("end", () => {
799
+ if (success) {
800
+ this.onSuccess(res.headers);
801
+ resolve({ status: "success", data: Buffer.concat(chunks) });
802
+ }
803
+ else if (RETRYABLE_STATUS.has(status))
804
+ resolve({
805
+ status: "retryable",
806
+ retryInMillis: retryAfterMillis(res.headers["retry-after"]),
807
+ });
808
+ else
809
+ resolve({
810
+ status: "failure",
811
+ error: new OTLPExporterError(res.statusMessage, status, Buffer.concat(chunks).toString()),
812
+ });
813
+ });
814
+ res.on("error", (error) => {
815
+ // Sent, but the acknowledgement was not read: success without a body to decode.
816
+ if (success)
817
+ resolve({ status: "success" });
818
+ else if (RETRYABLE_STATUS.has(status))
819
+ resolve({
820
+ status: "retryable",
821
+ error,
822
+ retryInMillis: retryAfterMillis(res.headers["retry-after"]),
823
+ });
824
+ else
825
+ resolve({ status: "failure", error });
826
+ });
827
+ });
828
+ this.requests.set(req, resolve);
829
+ req.on("close", () => this.requests.delete(req));
830
+ req.setTimeout(timeoutMillis, () => {
831
+ req.destroy();
832
+ resolve({ status: "retryable", error: new Error("Request timed out") });
833
+ });
834
+ req.on("error", (error) => resolve(networkFailure(error)));
835
+ req.end(body);
836
+ });
837
+ }
838
+ catch (error) {
839
+ return {
840
+ status: "failure",
841
+ error: error instanceof Error ? error : new Error(String(error)),
842
+ };
843
+ }
844
+ }
845
+ /** Fails and destroys requests in flight and their sockets, and every later attempt. */
846
+ abort() {
847
+ this.aborted = true;
848
+ for (const [req, settle] of this.requests) {
849
+ settle({ status: "failure", error: new Error("Hue export deadline exceeded") });
850
+ req.destroy();
851
+ }
852
+ this.requests.clear();
853
+ this.agent?.destroy();
854
+ }
855
+ shutdown() {
856
+ this.abort();
857
+ }
858
+ }
530
859
  /**
531
860
  * Creates the export pipeline for attach mode; pass it with the application's providers to
532
861
  * {@link createHue}. Validates options like an owned client.
package/dist/types.d.ts CHANGED
@@ -43,6 +43,12 @@ export interface SharedHueOptions {
43
43
  timeoutMillis?: number;
44
44
  /** Aggregate estimated retained telemetry bytes across both signals, including in-flight work. Default 8 MiB. */
45
45
  maxQueueBytes?: number;
46
+ /**
47
+ * Announces AI and Hue spans that are still running with in-progress placeholder spans, so Hue
48
+ * shows a trace while it runs. Default `true`; always off for setup credentials (`hue_setup_…`),
49
+ * and turned off for the client when its receiver does not accept placeholders.
50
+ */
51
+ liveSpans?: boolean;
46
52
  }
47
53
  /**
48
54
  * Options for a client that owns its OpenTelemetry providers. An enabled client needs a project
@@ -75,7 +81,7 @@ export interface ExportIssue {
75
81
  signal: Signal;
76
82
  /** `rejected` by Hue, `failed` to deliver, `dropped` from the queue, `invalid` record or capture, or a non-failing `warning`. */
77
83
  kind: "rejected" | "failed" | "dropped" | "invalid" | "warning";
78
- /** Records affected; zero for warnings and capture failures. */
84
+ /** Records affected; zero for capture failures and for warnings other than lost in-progress span placeholders. */
79
85
  count: number;
80
86
  /** HTTP status when the issue came from a response. */
81
87
  status?: number;
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.6.0";
2
+ export declare const sdkVersion = "0.7.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.6.0";
3
+ export const sdkVersion = "0.7.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hue-run/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {