@hue-run/sdk 0.22.0 → 0.23.1
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 +10 -10
- package/README.md +18 -4
- package/dist/client.js +86 -57
- package/dist/transport.d.ts +12 -0
- package/dist/transport.js +139 -24
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
package/CLI.md
CHANGED
|
@@ -42,7 +42,7 @@ 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.
|
|
45
|
+
not a completed integration. TypeScript uses `@hue-run/sdk@0.23.1`, `@opentelemetry/api@1.9.1` and
|
|
46
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
|
|
|
@@ -381,8 +381,8 @@ and Hue never receives or stores them. The command is part of the `hue` executab
|
|
|
381
381
|
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
|
382
382
|
# LANGFUSE_SECRET_KEY=sk-lf-...
|
|
383
383
|
# LANGFUSE_BASE_URL=https://cloud.langfuse.com
|
|
384
|
-
npx -y @hue-run/sdk@0.
|
|
385
|
-
npx -y @hue-run/sdk@0.
|
|
384
|
+
npx -y @hue-run/sdk@0.23.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
|
|
385
|
+
npx -y @hue-run/sdk@0.23.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
|
|
386
386
|
```
|
|
387
387
|
|
|
388
388
|
`LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` come from the environment or `--env-path`
|
|
@@ -892,7 +892,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
|
|
|
892
892
|
# Set project to an existing supported fixture; use the same directory on resume.
|
|
893
893
|
project=/absolute/path/to/supported-fixture
|
|
894
894
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
895
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
895
|
+
--archive .artifacts/typescript/hue-run-sdk-0.23.1.tgz \
|
|
896
896
|
--origin https://STAGING_ORIGIN \
|
|
897
897
|
--project "$project" --command setup \
|
|
898
898
|
--evidence .context/setup-staging-before-claim.json
|
|
@@ -903,7 +903,7 @@ the private local handoff and finish the real browser claim, then reconcile the
|
|
|
903
903
|
|
|
904
904
|
```sh
|
|
905
905
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
906
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
906
|
+
--archive .artifacts/typescript/hue-run-sdk-0.23.1.tgz \
|
|
907
907
|
--origin https://STAGING_ORIGIN \
|
|
908
908
|
--project "$project" --command claim \
|
|
909
909
|
--evidence .context/setup-staging-after-claim.json
|
|
@@ -945,10 +945,10 @@ otherwise. Never hand the agent Hue-native tools (`context.tools`, `context.mcp`
|
|
|
945
945
|
production agent lacks. Pin the SDK, so the agent's repository gains no dependency:
|
|
946
946
|
|
|
947
947
|
```sh
|
|
948
|
-
npx -y @hue-run/sdk@0.
|
|
949
|
-
npx -y @hue-run/sdk@0.
|
|
950
|
-
npx -y @hue-run/sdk@0.
|
|
951
|
-
npx -y @hue-run/sdk@0.
|
|
948
|
+
npx -y @hue-run/sdk@0.23.1 eval --case "Refund an eligible charge" --command "node agent.js"
|
|
949
|
+
npx -y @hue-run/sdk@0.23.1 eval --case https://app.hue.run/projects/demo/scenarios/<id> --command "python agent.py" --timeout 120 --baseline <run id>
|
|
950
|
+
npx -y @hue-run/sdk@0.23.1 eval --set "Billing regressions" --scorer-version <id> --command "node agent.js" --save-version
|
|
951
|
+
npx -y @hue-run/sdk@0.23.1 eval --worker --command "node agent.js" --agent-key support-agent --revision <new-agent-revision>
|
|
952
952
|
```
|
|
953
953
|
|
|
954
954
|
One invocation creates one run holding every selected case; it prints the run URL
|
|
@@ -986,7 +986,7 @@ export default function runMyAgent(inputs: JsonValue, context: SimulationTargetC
|
|
|
986
986
|
```
|
|
987
987
|
|
|
988
988
|
```sh
|
|
989
|
-
npx -y @hue-run/sdk@0.
|
|
989
|
+
npx -y @hue-run/sdk@0.23.1 eval --case "Refund an eligible charge" ./hue-agent.ts --content
|
|
990
990
|
```
|
|
991
991
|
|
|
992
992
|
`--scenario` remains an alias for `--case` for existing scripts. Pass one selection flag.
|
package/README.md
CHANGED
|
@@ -552,8 +552,8 @@ the Langfuse project's own keys (`LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, a
|
|
|
552
552
|
`HUE_API_KEY`:
|
|
553
553
|
|
|
554
554
|
```sh
|
|
555
|
-
npx -y @hue-run/sdk@0.
|
|
556
|
-
npx -y @hue-run/sdk@0.
|
|
555
|
+
npx -y @hue-run/sdk@0.23.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
|
|
556
|
+
npx -y @hue-run/sdk@0.23.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
|
|
557
557
|
```
|
|
558
558
|
|
|
559
559
|
- It reads Langfuse Cloud and self-hosted v4 through the Observations API v2, and self-hosted v3
|
|
@@ -1106,6 +1106,16 @@ own. Stop request production
|
|
|
1106
1106
|
before shutdown so late spans cannot race it. A client does not own instrumented
|
|
1107
1107
|
operations still running in the application.
|
|
1108
1108
|
|
|
1109
|
+
In Vercel Functions, the TypeScript SDK automatically registers admitted spans and logs with
|
|
1110
|
+
[`waitUntil`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil).
|
|
1111
|
+
The response or Workflow step can return while the existing batches and their media uploads finish
|
|
1112
|
+
in the background, within the function's lifetime limit. You can remove a per-step
|
|
1113
|
+
`await hue.flushSafe()` when its purpose is to prevent the function freezing before delivery.
|
|
1114
|
+
Keep an explicit flush when you need its acknowledgement report before continuing. Background
|
|
1115
|
+
delivery preserves capture, redaction, batching and failure reports; it never flushes or shuts down
|
|
1116
|
+
application-owned providers. In other hosts, keep the host's lifetime hook or await a flush before
|
|
1117
|
+
a short-lived process exits.
|
|
1118
|
+
|
|
1109
1119
|
## Live spans
|
|
1110
1120
|
|
|
1111
1121
|
OpenTelemetry exports a span only when it ends, so a long streamed turn would otherwise stay
|
|
@@ -1119,7 +1129,11 @@ arrives.
|
|
|
1119
1129
|
The placeholder then waits for the next batch export like any queued span: up to 500 ms until the
|
|
1120
1130
|
next tick, then the 1 s batch delay when nothing else is queued, so it usually reaches Hue within
|
|
1121
1131
|
about 1.5 s of its span starting. It goes out sooner when a batch is already scheduled, for example
|
|
1122
|
-
because another span has just ended, and at once for a full batch or
|
|
1132
|
+
because another span has just ended, and at once for a full batch or a flush with no completed
|
|
1133
|
+
spans. When a flush has completed spans to deliver, it defers queued placeholders to the next
|
|
1134
|
+
normal announcement after the drain and its uploads finish. This avoids an extra acknowledgement
|
|
1135
|
+
for a still-open parent while preserving delivery of its completed children. A parent that ends
|
|
1136
|
+
before that announcement sends only its completed span. A placeholder goes out later
|
|
1123
1137
|
while an earlier export is still in flight, because the next batch is scheduled only after that
|
|
1124
1138
|
export finishes. A placeholder whose span has ended by the time it is exported is not sent, so a
|
|
1125
1139
|
short span may send none and appear in Hue only when it finishes.
|
|
@@ -1311,7 +1325,7 @@ existing-provider flush callbacks and recovery. Local/CI runners remain availabl
|
|
|
1311
1325
|
|
|
1312
1326
|
## Serving safely
|
|
1313
1327
|
|
|
1314
|
-
Use `createHueSafe(options)` for best-effort startup. Invalid initialization returns a disabled client that keeps your `onExportIssue` hook and records
|
|
1328
|
+
Use `createHueSafe(options)` for best-effort startup. Invalid initialization returns a disabled client that keeps your `onExportIssue` hook and records a fixed initialization-failure diagnostic without inspecting or exposing the original error. Pass `enabled: false` to disable Hue without a key or a `captureContent` choice; disabled helpers still execute the application callback, and `inject()` keeps propagating the application's own trace context. `flushSafe({ timeoutMillis: 1000 })` and `shutdownSafe({ timeoutMillis: 1000 })` return `{ ok, timedOut, report }` without rejecting. Strict initialization, connection checks and `flush()` remain available for diagnostics; do not gate application readiness or responses on them.
|
|
1315
1329
|
|
|
1316
1330
|
Capture/serialization/redaction/provider failures omit unsafe telemetry, record failures, and preserve the original business result/error. Async diagnostic rejections are contained; diagnostics are rate-limited. The default `maxQueueBytes` is 8 MiB across traces/logs including in-flight work, alongside the existing record cap. `pendingBytes` is a current queue gauge; `droppedSpans`, `droppedLogs` and `instrumentationFailures` are cumulative failure counters; `uploadedValues` and `uploadFallbacks` count completed uploads and upload fallbacks for whole attributes, eligible binary or large text media, and opt-in `recordFile` uploads. Whole-attribute fallbacks refused at export keep a prefix; media fallbacks keep a digest; helper upload fallbacks keep the file event; a value whose upload failed after export keeps its entry as well. This is a telemetry budget, not a total process memory ceiling. A timeout bounds the caller and does not cancel a borrowed provider. Never retry the business operation to recover telemetry. See [production setup](https://docs.hue.run/sdks/typescript#production-setup).
|
|
1317
1331
|
|
package/dist/client.js
CHANGED
|
@@ -370,6 +370,8 @@ export class HueClient {
|
|
|
370
370
|
* provider and surface as `hue.mcp.provider` / `hue.mcp.surface`.
|
|
371
371
|
*/
|
|
372
372
|
async tool(name, input, execute, options = {}) {
|
|
373
|
+
if (!this.enabled || this.closed)
|
|
374
|
+
return this.withSpan("execute_tool", () => execute());
|
|
373
375
|
const attributes = {
|
|
374
376
|
"gen_ai.operation.name": "execute_tool",
|
|
375
377
|
"gen_ai.tool.name": name,
|
|
@@ -380,22 +382,25 @@ export class HueClient {
|
|
|
380
382
|
// A blank or non-string label is omitted and counted; the tool call itself still runs.
|
|
381
383
|
if (isLabel(value))
|
|
382
384
|
attributes[key] = value;
|
|
383
|
-
else
|
|
385
|
+
else
|
|
384
386
|
this.instrumentationFailed();
|
|
385
387
|
};
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
388
|
+
let spanOptions = { attributes };
|
|
389
|
+
try {
|
|
390
|
+
stamp("gen_ai.tool.call.id", options.callId);
|
|
391
|
+
stamp("mcp.server.name", options.mcp?.name);
|
|
392
|
+
stamp("mcp.server.version", options.mcp?.version);
|
|
393
|
+
stamp("hue.mcp.provider", options.mcp?.provider);
|
|
394
|
+
stamp("hue.mcp.surface", options.mcp?.surface);
|
|
395
|
+
if (this.captureContent && options.endpoint !== undefined)
|
|
396
|
+
Object.assign(attributes, toolEndpointAttributes(options.endpoint));
|
|
397
|
+
spanOptions = {
|
|
398
|
+
attributes,
|
|
399
|
+
...(options.parentContext ? { parentContext: options.parentContext } : {}),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
this.instrumentationFailed();
|
|
399
404
|
}
|
|
400
405
|
return this.withSpan(`execute_tool ${name}`, async ({ span }) => {
|
|
401
406
|
this.setContent(span, "gen_ai.tool.call.arguments", input);
|
|
@@ -403,10 +408,7 @@ export class HueClient {
|
|
|
403
408
|
if (result !== undefined)
|
|
404
409
|
this.setContent(span, "gen_ai.tool.call.result", result);
|
|
405
410
|
return result;
|
|
406
|
-
},
|
|
407
|
-
attributes,
|
|
408
|
-
...(options.parentContext ? { parentContext: options.parentContext } : {}),
|
|
409
|
-
});
|
|
411
|
+
}, spanOptions);
|
|
410
412
|
}
|
|
411
413
|
/**
|
|
412
414
|
* Runs `callback` as a sub-agent: inside an `invoke_agent {name}` span carrying
|
|
@@ -420,11 +422,27 @@ export class HueClient {
|
|
|
420
422
|
* in `withSpan`. A blank or non-string name is recorded as `unknown` and counted.
|
|
421
423
|
*/
|
|
422
424
|
async subagent(name, callback, options = {}) {
|
|
425
|
+
if (!this.enabled || this.closed)
|
|
426
|
+
return this.withSpan("invoke_agent", callback);
|
|
423
427
|
let agent = name;
|
|
424
428
|
if (!isLabel(name)) {
|
|
425
429
|
agent = "unknown";
|
|
426
|
-
|
|
427
|
-
|
|
430
|
+
this.instrumentationFailed();
|
|
431
|
+
}
|
|
432
|
+
let spanOptions = {};
|
|
433
|
+
try {
|
|
434
|
+
spanOptions = {
|
|
435
|
+
...options,
|
|
436
|
+
attributes: {
|
|
437
|
+
...options.attributes,
|
|
438
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
439
|
+
"gen_ai.agent.name": agent,
|
|
440
|
+
"hue.agent.role": "subagent",
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
this.instrumentationFailed();
|
|
428
446
|
}
|
|
429
447
|
return this.withSpan(`invoke_agent ${agent}`, async (span) => {
|
|
430
448
|
let outputSet = false;
|
|
@@ -438,15 +456,7 @@ export class HueClient {
|
|
|
438
456
|
if (result !== undefined && !outputSet)
|
|
439
457
|
span.setOutput(result);
|
|
440
458
|
return result;
|
|
441
|
-
},
|
|
442
|
-
...options,
|
|
443
|
-
attributes: {
|
|
444
|
-
...options.attributes,
|
|
445
|
-
"gen_ai.operation.name": "invoke_agent",
|
|
446
|
-
"gen_ai.agent.name": agent,
|
|
447
|
-
"hue.agent.role": "subagent",
|
|
448
|
-
},
|
|
449
|
-
});
|
|
459
|
+
}, spanOptions);
|
|
450
460
|
}
|
|
451
461
|
/**
|
|
452
462
|
* Runs `callback` inside a GenAI client span for one direct provider call, named
|
|
@@ -458,24 +468,44 @@ export class HueClient {
|
|
|
458
468
|
* recorded as `gen_ai.system_instructions` and `gen_ai.tool.definitions`, content like `input`.
|
|
459
469
|
*/
|
|
460
470
|
async model(model, callback, options) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
471
|
+
if (!this.enabled || this.closed)
|
|
472
|
+
return this.withSpan("model", callback);
|
|
473
|
+
let prepared = { provider: "unknown" };
|
|
474
|
+
try {
|
|
475
|
+
prepared = {
|
|
476
|
+
operation: options?.operation,
|
|
477
|
+
provider: options?.provider,
|
|
478
|
+
name: options?.name,
|
|
479
|
+
sessionId: options?.sessionId,
|
|
480
|
+
userId: options?.userId,
|
|
481
|
+
workspaceId: options?.workspaceId,
|
|
482
|
+
parentContext: options?.parentContext,
|
|
483
|
+
...(this.captureContent
|
|
484
|
+
? {
|
|
485
|
+
input: options?.input,
|
|
486
|
+
systemInstructions: options?.systemInstructions,
|
|
487
|
+
tools: options?.tools,
|
|
488
|
+
}
|
|
489
|
+
: {}),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
this.instrumentationFailed();
|
|
494
|
+
}
|
|
464
495
|
const label = (value, fallback) => {
|
|
465
496
|
if (isLabel(value))
|
|
466
497
|
return value;
|
|
467
|
-
|
|
468
|
-
this.instrumentationFailed();
|
|
498
|
+
this.instrumentationFailed();
|
|
469
499
|
return fallback;
|
|
470
500
|
};
|
|
471
501
|
const requestModel = label(model, "unknown");
|
|
472
|
-
const operation = label(
|
|
473
|
-
const provider = label(
|
|
474
|
-
const name =
|
|
502
|
+
const operation = label(prepared.operation ?? "chat", "chat");
|
|
503
|
+
const provider = label(prepared.provider, "unknown");
|
|
504
|
+
const name = prepared.name === undefined
|
|
475
505
|
? `${operation} ${requestModel}`
|
|
476
|
-
: label(
|
|
506
|
+
: label(prepared.name, `${operation} ${requestModel}`);
|
|
477
507
|
const metadata = { operation, provider, requestModel };
|
|
478
|
-
const { sessionId, userId, workspaceId, parentContext, input, systemInstructions, tools
|
|
508
|
+
const { sessionId, userId, workspaceId, parentContext, input, systemInstructions, tools } = prepared;
|
|
479
509
|
return this.withSpan(name, (span) => {
|
|
480
510
|
const handle = {
|
|
481
511
|
...span,
|
|
@@ -507,22 +537,22 @@ export class HueClient {
|
|
|
507
537
|
setUsage(span, usage) {
|
|
508
538
|
if (!this.enabled || this.closed)
|
|
509
539
|
return;
|
|
510
|
-
|
|
511
|
-
[
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
540
|
+
try {
|
|
541
|
+
for (const [key, value] of [
|
|
542
|
+
["gen_ai.usage.input_tokens", usage?.inputTokens],
|
|
543
|
+
["gen_ai.usage.output_tokens", usage?.outputTokens],
|
|
544
|
+
]) {
|
|
545
|
+
if (value === undefined)
|
|
546
|
+
continue;
|
|
547
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
548
|
+
this.instrumentationFailed();
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
521
551
|
span.setAttribute(key, value);
|
|
522
552
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
this.instrumentationFailed();
|
|
526
556
|
}
|
|
527
557
|
}
|
|
528
558
|
/**
|
|
@@ -1275,11 +1305,10 @@ export function createHueSafe(options) {
|
|
|
1275
1305
|
try {
|
|
1276
1306
|
return new HueClient(options);
|
|
1277
1307
|
}
|
|
1278
|
-
catch
|
|
1279
|
-
//
|
|
1308
|
+
catch {
|
|
1309
|
+
// Provider errors can contain credentials or expose throwing message accessors.
|
|
1280
1310
|
const disabled = new HueClient({ enabled: false, onExportIssue: issueCallback(options) });
|
|
1281
|
-
|
|
1282
|
-
disabled.transport.instrumentationFailure("traces", `Hue is disabled: ${reason.slice(0, 256)}`);
|
|
1311
|
+
disabled.transport.instrumentationFailure("traces", "Hue is disabled: initialization failed");
|
|
1283
1312
|
return disabled;
|
|
1284
1313
|
}
|
|
1285
1314
|
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -53,6 +53,9 @@ export declare class HueTransport {
|
|
|
53
53
|
private failed;
|
|
54
54
|
private spans;
|
|
55
55
|
private logs;
|
|
56
|
+
private deliveryCohort?;
|
|
57
|
+
private deliveries;
|
|
58
|
+
private deliveryUploads;
|
|
56
59
|
private pendingBytes;
|
|
57
60
|
private dropped;
|
|
58
61
|
/** Records of each trace the SDK never sent (dropped from the queue, refused as invalid or
|
|
@@ -81,6 +84,8 @@ export declare class HueTransport {
|
|
|
81
84
|
private placeholdersRejected;
|
|
82
85
|
private placeholders;
|
|
83
86
|
private placeholderSources;
|
|
87
|
+
private forcedSpanDrains;
|
|
88
|
+
private completedSpanDrains;
|
|
84
89
|
private batchSpans?;
|
|
85
90
|
private limits;
|
|
86
91
|
/** The rate-limit holds in progress, each ended early by calling it. */
|
|
@@ -120,11 +125,16 @@ export declare class HueTransport {
|
|
|
120
125
|
* skipped silently.
|
|
121
126
|
*/
|
|
122
127
|
private enqueue;
|
|
128
|
+
/** Register in the caller's request before it can return; outside Vercel this is a no-op. */
|
|
129
|
+
private protectDelivery;
|
|
130
|
+
private holdDelivery;
|
|
131
|
+
private finishDelivery;
|
|
123
132
|
private track;
|
|
124
133
|
/** Placeholders are built lazily, so input set right after a span starts is included. */
|
|
125
134
|
private announceLiveSpans;
|
|
126
135
|
private stopLiveTimer;
|
|
127
136
|
private stopLiveSpans;
|
|
137
|
+
private beginSpanDrain;
|
|
128
138
|
/** Holds the staged attachments of a log record being admitted with the queued record, within
|
|
129
139
|
* the held-value budget; those the budget refuses are counted as not uploaded. */
|
|
130
140
|
private admitLogParts;
|
|
@@ -144,6 +154,8 @@ export declare class HueTransport {
|
|
|
144
154
|
private protectMedia;
|
|
145
155
|
private releaseMediaIdentity;
|
|
146
156
|
private sweepMedia;
|
|
157
|
+
/** Keep an export's warning aggregation within each cohort's upload ownership. */
|
|
158
|
+
private collectDeliveryUploads;
|
|
147
159
|
/** Bounded: a trace whose root never arrives (an abandoned run) gives way to newer ones. */
|
|
148
160
|
private countDropped;
|
|
149
161
|
/** Cumulative counters and current queue gauges. */
|
package/dist/transport.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { gzip } from "node:zlib";
|
|
3
|
+
import { waitUntil } from "@vercel/functions";
|
|
3
4
|
import { ExportResultCode } from "@opentelemetry/core";
|
|
4
5
|
import { createOtlpNetworkExportDelegate, OTLPExporterBase, OTLPExporterError, } from "@opentelemetry/otlp-exporter-base";
|
|
5
6
|
import { createOtlpHttpExporterMetrics } from "@opentelemetry/otlp-exporter-base/node-http";
|
|
@@ -182,6 +183,10 @@ export class HueTransport {
|
|
|
182
183
|
failed = { traces: 0, logs: 0 };
|
|
183
184
|
spans = new Map();
|
|
184
185
|
logs = new Map();
|
|
186
|
+
// A cohort closes when export starts, so later requests cannot extend its lifetime.
|
|
187
|
+
deliveryCohort;
|
|
188
|
+
deliveries = new WeakMap();
|
|
189
|
+
deliveryUploads = new WeakMap();
|
|
185
190
|
pendingBytes = 0;
|
|
186
191
|
dropped = { traces: 0, logs: 0 };
|
|
187
192
|
/** Records of each trace the SDK never sent (dropped from the queue, refused as invalid or
|
|
@@ -214,6 +219,8 @@ export class HueTransport {
|
|
|
214
219
|
placeholders = new WeakMap();
|
|
215
220
|
// The running span each admitted placeholder announces, so one it has outlived is not sent.
|
|
216
221
|
placeholderSources = new WeakMap();
|
|
222
|
+
forcedSpanDrains = 0;
|
|
223
|
+
completedSpanDrains = 0;
|
|
217
224
|
batchSpans;
|
|
218
225
|
// What the receiver accepts, as it last advertised; the limits of this release until it does.
|
|
219
226
|
limits = DEFAULT_RECEIVER_LIMITS;
|
|
@@ -340,6 +347,7 @@ export class HueTransport {
|
|
|
340
347
|
this.protectMedia(admitted);
|
|
341
348
|
this.countEnded(admitted);
|
|
342
349
|
spans.onEnd(admitted);
|
|
350
|
+
this.protectDelivery(admitted);
|
|
343
351
|
}
|
|
344
352
|
catch {
|
|
345
353
|
if (admitted)
|
|
@@ -352,12 +360,18 @@ export class HueTransport {
|
|
|
352
360
|
// The spans are sent first, then the uploads queued so far are waited for, unless a
|
|
353
361
|
// flush sends them: that flush waits for the uploads after its send.
|
|
354
362
|
forceFlush: async () => {
|
|
363
|
+
const finish = this.beginSpanDrain();
|
|
355
364
|
try {
|
|
356
365
|
await spans.forceFlush();
|
|
357
366
|
}
|
|
358
367
|
finally {
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
try {
|
|
369
|
+
if (!this.sendingRecords.getStore())
|
|
370
|
+
await this.drainUploads();
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
finish();
|
|
374
|
+
}
|
|
361
375
|
}
|
|
362
376
|
},
|
|
363
377
|
shutdown: async () => {
|
|
@@ -391,6 +405,7 @@ export class HueTransport {
|
|
|
391
405
|
admitted = queued;
|
|
392
406
|
this.admitLogParts(log, queued);
|
|
393
407
|
logs.onEmit(queued);
|
|
408
|
+
this.protectDelivery(admitted);
|
|
394
409
|
}
|
|
395
410
|
catch {
|
|
396
411
|
if (admitted)
|
|
@@ -399,12 +414,18 @@ export class HueTransport {
|
|
|
399
414
|
}
|
|
400
415
|
},
|
|
401
416
|
forceFlush: async () => {
|
|
417
|
+
const finish = this.beginSpanDrain();
|
|
402
418
|
try {
|
|
403
419
|
await logs.forceFlush();
|
|
404
420
|
}
|
|
405
421
|
finally {
|
|
406
|
-
|
|
407
|
-
|
|
422
|
+
try {
|
|
423
|
+
if (!this.sendingRecords.getStore())
|
|
424
|
+
await this.drainUploads();
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
finish();
|
|
428
|
+
}
|
|
408
429
|
}
|
|
409
430
|
},
|
|
410
431
|
shutdown: async () => {
|
|
@@ -472,6 +493,14 @@ export class HueTransport {
|
|
|
472
493
|
this.logs.set(snapshot.record, snapshot.bytes);
|
|
473
494
|
if (snapshot.unresolvedResource && !advisory)
|
|
474
495
|
this.issue(signal, "warning", 0, "Unresolved resource attributes omitted from the telemetry snapshot");
|
|
496
|
+
if (!advisory) {
|
|
497
|
+
const cohort = (this.deliveryCohort ??= {
|
|
498
|
+
pending: 0,
|
|
499
|
+
...Promise.withResolvers(),
|
|
500
|
+
});
|
|
501
|
+
cohort.pending++;
|
|
502
|
+
this.deliveries.set(snapshot.record, cohort);
|
|
503
|
+
}
|
|
475
504
|
return snapshot.record;
|
|
476
505
|
}
|
|
477
506
|
catch {
|
|
@@ -481,11 +510,42 @@ export class HueTransport {
|
|
|
481
510
|
return undefined;
|
|
482
511
|
}
|
|
483
512
|
}
|
|
513
|
+
/** Register in the caller's request before it can return; outside Vercel this is a no-op. */
|
|
514
|
+
protectDelivery(record) {
|
|
515
|
+
const cohort = this.deliveries.get(record);
|
|
516
|
+
if (!cohort)
|
|
517
|
+
return;
|
|
518
|
+
try {
|
|
519
|
+
waitUntil(cohort.promise);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
// A host hook must never undo admission or change the application's result.
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/** @internal Seal the batch's cohort before export can yield to another request. */
|
|
526
|
+
exporting(records) {
|
|
527
|
+
if (records.some((record) => this.deliveries.get(record) === this.deliveryCohort))
|
|
528
|
+
this.deliveryCohort = undefined;
|
|
529
|
+
}
|
|
530
|
+
holdDelivery(cohort, work) {
|
|
531
|
+
if (!cohort)
|
|
532
|
+
return;
|
|
533
|
+
cohort.pending++;
|
|
534
|
+
const finish = () => this.finishDelivery(cohort);
|
|
535
|
+
void work.then(finish, finish);
|
|
536
|
+
}
|
|
537
|
+
finishDelivery(cohort) {
|
|
538
|
+
if (--cohort.pending !== 0)
|
|
539
|
+
return;
|
|
540
|
+
if (this.deliveryCohort === cohort)
|
|
541
|
+
this.deliveryCohort = undefined;
|
|
542
|
+
cohort.resolve();
|
|
543
|
+
}
|
|
484
544
|
track(span) {
|
|
485
545
|
try {
|
|
486
546
|
if (!this.liveSpans || this.live.size >= MAX_LIVE_SPANS)
|
|
487
547
|
return;
|
|
488
|
-
if (!(span.spanContext().traceFlags & 1) ||
|
|
548
|
+
if (!(span.spanContext().traceFlags & 1) || span.ended || !announcesLiveSpan(span))
|
|
489
549
|
return;
|
|
490
550
|
this.live.add(span);
|
|
491
551
|
if (!this.liveTimer) {
|
|
@@ -500,6 +560,8 @@ export class HueTransport {
|
|
|
500
560
|
}
|
|
501
561
|
/** Placeholders are built lazily, so input set right after a span starts is included. */
|
|
502
562
|
announceLiveSpans() {
|
|
563
|
+
if (this.forcedSpanDrains)
|
|
564
|
+
return;
|
|
503
565
|
for (const span of this.live) {
|
|
504
566
|
this.live.delete(span);
|
|
505
567
|
try {
|
|
@@ -547,6 +609,28 @@ export class HueTransport {
|
|
|
547
609
|
placeholderSettled(record) {
|
|
548
610
|
return this.placeholderSources.get(record)?.ended === true;
|
|
549
611
|
}
|
|
612
|
+
beginSpanDrain() {
|
|
613
|
+
const completed = [...this.spans.keys()].some((span) => !this.placeholders.has(span));
|
|
614
|
+
this.forcedSpanDrains++;
|
|
615
|
+
if (completed)
|
|
616
|
+
this.completedSpanDrains++;
|
|
617
|
+
return () => {
|
|
618
|
+
this.forcedSpanDrains--;
|
|
619
|
+
if (completed)
|
|
620
|
+
this.completedSpanDrains--;
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/** @internal A forced drain sends completed spans without another advisory round trip. */
|
|
624
|
+
deferPlaceholders(records) {
|
|
625
|
+
if (!this.completedSpanDrains)
|
|
626
|
+
return false;
|
|
627
|
+
for (const record of records) {
|
|
628
|
+
const source = this.placeholderSources.get(record);
|
|
629
|
+
if (!this.closed && source)
|
|
630
|
+
this.track(source);
|
|
631
|
+
}
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
550
634
|
/** @internal Exporter callback: false once the receiver refused placeholders; queued ones are then dropped. */
|
|
551
635
|
sendsPlaceholders() {
|
|
552
636
|
return !this.placeholdersRejected;
|
|
@@ -570,6 +654,11 @@ export class HueTransport {
|
|
|
570
654
|
/** @internal Exporter callback: releases queued records after an export attempt settles. */
|
|
571
655
|
finish(signal, records) {
|
|
572
656
|
for (const record of records) {
|
|
657
|
+
const cohort = this.deliveries.get(record);
|
|
658
|
+
if (cohort) {
|
|
659
|
+
this.deliveries.delete(record);
|
|
660
|
+
this.finishDelivery(cohort);
|
|
661
|
+
}
|
|
573
662
|
this.releaseMedia(record);
|
|
574
663
|
this.logMedia.delete(record);
|
|
575
664
|
const pending = signal === "traces" ? this.spans : this.logs;
|
|
@@ -737,6 +826,7 @@ export class HueTransport {
|
|
|
737
826
|
* them. */
|
|
738
827
|
uploadQueued(record, settled, shared, released) {
|
|
739
828
|
this.awaitUploads(settled);
|
|
829
|
+
this.holdDelivery(this.deliveries.get(record), settled);
|
|
740
830
|
const identity = this.mediaIdentity(record);
|
|
741
831
|
const staged = identity
|
|
742
832
|
? (this.media.get(identity) ?? []).reduce((total, file) => total + heldBytesOf(file.bytes), 0)
|
|
@@ -853,8 +943,8 @@ export class HueTransport {
|
|
|
853
943
|
return this.offloadsValues() && this.uploader?.available() === true;
|
|
854
944
|
}
|
|
855
945
|
/** @internal Exporter callback: a redacted span with the values its redaction left to the
|
|
856
|
-
* upload step placed: listed under `hue.blobs` with their uploads queued (
|
|
857
|
-
*
|
|
946
|
+
* upload step placed: listed under `hue.blobs` with their uploads queued (reported once settled,
|
|
947
|
+
* and charged in place of the `queued` record they came from), or cut as before.
|
|
858
948
|
* Never rejects. */
|
|
859
949
|
async offloadSpan(span, collector, upload, tally, late, queued) {
|
|
860
950
|
// A record without a readable context cannot name its trace: its values are cut.
|
|
@@ -878,8 +968,7 @@ export class HueTransport {
|
|
|
878
968
|
memo: this.mediaMemo,
|
|
879
969
|
background: own,
|
|
880
970
|
});
|
|
881
|
-
|
|
882
|
-
late.settled.push(mergeWhenSettled(late.tally, own.tally, own.settled));
|
|
971
|
+
this.collectDeliveryUploads(queued, own, late);
|
|
883
972
|
return { ...span, attributes: attributes };
|
|
884
973
|
}
|
|
885
974
|
catch {
|
|
@@ -919,14 +1008,37 @@ export class HueTransport {
|
|
|
919
1008
|
memo: this.mediaMemo,
|
|
920
1009
|
background: own,
|
|
921
1010
|
});
|
|
922
|
-
|
|
923
|
-
late.settled.push(mergeWhenSettled(late.tally, own.tally, own.settled));
|
|
1011
|
+
this.collectDeliveryUploads(queued, own, late);
|
|
924
1012
|
return placed(result);
|
|
925
1013
|
}
|
|
926
1014
|
catch {
|
|
927
1015
|
return placed(fallbackRecord(log.attributes, collector, valueBytes, tally, traceId, this.mediaMemo));
|
|
928
1016
|
}
|
|
929
1017
|
}
|
|
1018
|
+
/** Keep an export's warning aggregation within each cohort's upload ownership. */
|
|
1019
|
+
collectDeliveryUploads(record, uploads, late) {
|
|
1020
|
+
const { settled, tally } = uploads;
|
|
1021
|
+
if (!settled.length)
|
|
1022
|
+
return;
|
|
1023
|
+
let groups = this.deliveryUploads.get(late);
|
|
1024
|
+
if (!groups)
|
|
1025
|
+
this.deliveryUploads.set(late, (groups = new Map()));
|
|
1026
|
+
const cohort = this.deliveries.get(record);
|
|
1027
|
+
let group = groups.get(cohort);
|
|
1028
|
+
if (!group)
|
|
1029
|
+
groups.set(cohort, (group = { tally: newTally(), settled: [] }));
|
|
1030
|
+
group.settled.push(mergeWhenSettled(group.tally, tally, settled));
|
|
1031
|
+
}
|
|
1032
|
+
/** @internal Report only the uploads each cohort owns, without retaining their records. */
|
|
1033
|
+
reportUploads(late, signal) {
|
|
1034
|
+
const groups = this.deliveryUploads.get(late);
|
|
1035
|
+
this.deliveryUploads.delete(late);
|
|
1036
|
+
for (const [cohort, { settled, tally }] of groups ?? []) {
|
|
1037
|
+
const report = Promise.all(settled).then(() => this.reportOffload(tally, signal, true));
|
|
1038
|
+
this.awaitUploads(report);
|
|
1039
|
+
this.holdDelivery(cohort, report);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
930
1042
|
/** @internal Exporter callback: counts an export's uploads and reports each kind of fallback
|
|
931
1043
|
* once, as a warning naming its traces: the values were exported cut as before, or, `late`,
|
|
932
1044
|
* their uploads failed after their records listed them. */
|
|
@@ -1147,13 +1259,19 @@ export class HueTransport {
|
|
|
1147
1259
|
*/
|
|
1148
1260
|
flushRecords(providers) {
|
|
1149
1261
|
const from = this.observedSequence;
|
|
1150
|
-
|
|
1262
|
+
let finish;
|
|
1263
|
+
const join = () => {
|
|
1264
|
+
finish = this.beginSpanDrain();
|
|
1265
|
+
return this.sendRecords();
|
|
1266
|
+
};
|
|
1151
1267
|
const sent = providers ? this.sendingRecords.run(true, providers).then(join, join) : join();
|
|
1152
|
-
const report = sent
|
|
1268
|
+
const report = sent
|
|
1269
|
+
.then(async (processors) => {
|
|
1153
1270
|
// Exports the processors did not start (a timer's, still in flight) queued uploads too.
|
|
1154
1271
|
await this.drainUploads();
|
|
1155
1272
|
return this.flushReport(from, processors);
|
|
1156
|
-
})
|
|
1273
|
+
})
|
|
1274
|
+
.finally(() => finish?.());
|
|
1157
1275
|
return { sent, report };
|
|
1158
1276
|
}
|
|
1159
1277
|
/** Sends what the processors and exporters hold, once the previous flush's send ended. */
|
|
@@ -1387,6 +1505,7 @@ class ReportingExporter {
|
|
|
1387
1505
|
this.redact = redact;
|
|
1388
1506
|
}
|
|
1389
1507
|
export(records, callback) {
|
|
1508
|
+
this.transport.exporting(records);
|
|
1390
1509
|
const work = this.exportRecords(records)
|
|
1391
1510
|
.then(() => callback({ code: ExportResultCode.SUCCESS }), () => callback({
|
|
1392
1511
|
code: ExportResultCode.FAILED,
|
|
@@ -1423,23 +1542,16 @@ class ReportingExporter {
|
|
|
1423
1542
|
real.push(placeholder);
|
|
1424
1543
|
return real;
|
|
1425
1544
|
}
|
|
1426
|
-
/**
|
|
1427
|
-
* settle, after the records were sent, never awaited here. */
|
|
1545
|
+
/** Queue final upload reports before releasing the records that own their cohorts. */
|
|
1428
1546
|
async exportRecords(records) {
|
|
1429
1547
|
const late = { tally: newTally(), settled: [] };
|
|
1430
1548
|
try {
|
|
1431
1549
|
await this.placeAndSend(records, late);
|
|
1432
1550
|
}
|
|
1433
1551
|
finally {
|
|
1434
|
-
|
|
1435
|
-
this.reportWhenSettled(late);
|
|
1552
|
+
this.transport.reportUploads(late, this.signal);
|
|
1436
1553
|
}
|
|
1437
1554
|
}
|
|
1438
|
-
/** Reports an export's uploads once they all settle. Built apart from the export, so a pending
|
|
1439
|
-
* upload never keeps the export's records alive. */
|
|
1440
|
-
reportWhenSettled(late) {
|
|
1441
|
-
this.transport.awaitUploads(Promise.all(late.settled).then(() => this.transport.reportOffload(late.tally, this.signal, true)));
|
|
1442
|
-
}
|
|
1443
1555
|
async placeAndSend(records, late) {
|
|
1444
1556
|
// Completed records and placeholders travel in separate requests, completed records first,
|
|
1445
1557
|
// so a rejection count is always one kind of record's. Placeholders are advisory: losing one
|
|
@@ -1850,7 +1962,10 @@ class ReportingExporter {
|
|
|
1850
1962
|
// records' acknowledgements may have just shown, nor after this export's completed records were
|
|
1851
1963
|
// refused or unanswered: a receiver that just refused or timed out gets no second request with
|
|
1852
1964
|
// its own retries. A record this side could not send keeps the placeholders going out.
|
|
1853
|
-
if (pending.length &&
|
|
1965
|
+
if (pending.length &&
|
|
1966
|
+
!refused &&
|
|
1967
|
+
this.transport.sendsPlaceholders() &&
|
|
1968
|
+
!this.transport.deferPlaceholders(records))
|
|
1854
1969
|
await pack(pending, true);
|
|
1855
1970
|
if (failed)
|
|
1856
1971
|
throw new Error("Hue telemetry export failed");
|
package/dist/version.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Package version shared by the instrumentation scope and the export User-Agent. */
|
|
2
|
-
export declare const sdkVersion = "0.
|
|
2
|
+
export declare const sdkVersion = "0.23.1";
|
|
3
3
|
/** OTLP dependency version sent in the export User-Agent. */
|
|
4
4
|
export declare const otlpExporterVersion = "0.223.0";
|
package/dist/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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.
|
|
3
|
+
export const sdkVersion = "0.23.1";
|
|
4
4
|
/** OTLP dependency version sent in the export User-Agent. */
|
|
5
5
|
export const otlpExporterVersion = "0.223.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hue-run/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"@opentelemetry/resources": "2.12.0",
|
|
99
99
|
"@opentelemetry/sdk-logs": "0.223.0",
|
|
100
100
|
"@opentelemetry/sdk-trace": "2.12.0",
|
|
101
|
+
"@vercel/functions": "3.9.7",
|
|
101
102
|
"zod": "^3.25.76 || ^4.6.5"
|
|
102
103
|
},
|
|
103
104
|
"devDependencies": {
|