@hue-run/sdk 0.1.4 → 0.1.5
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/README.md +11 -4
- package/dist/ai-sdk.js +10 -1
- package/dist/client.d.ts +11 -1
- package/dist/client.js +232 -97
- package/dist/config.d.ts +1 -1
- package/dist/config.js +18 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/privacy.js +38 -20
- package/dist/safety.d.ts +8 -0
- package/dist/safety.js +179 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +196 -0
- package/dist/transport.d.ts +6 -0
- package/dist/transport.js +192 -32
- package/dist/types.d.ts +27 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ Reuse the client across server requests. Await stream completion before flushing
|
|
|
93
93
|
returning a streaming `Response` does not mean its stream has finished. The
|
|
94
94
|
[Next.js streaming recipe](https://docs.hue.run/integrations/opentelemetry#flush-streamed-responses-in-next-js)
|
|
95
95
|
shows how to keep completion and flushing within the request's background lifetime.
|
|
96
|
-
For a standalone script, put the operation in `try` and call `await hue.
|
|
96
|
+
For a standalone script, put the operation in `try` and call `await hue.shutdownSafe()`
|
|
97
97
|
in `finally`. Shut down a shared server client only when the application stops.
|
|
98
98
|
|
|
99
99
|
The integration creates real Vercel provider, streaming and tool spans and passes
|
|
@@ -117,8 +117,7 @@ names cannot be classified automatically; use them deliberately.
|
|
|
117
117
|
there is no SDK retention timer or automatic content expiry. To redact strings
|
|
118
118
|
before export, supply `redact(value, path)`; it applies to supported strings in
|
|
119
119
|
attributes, resources, event/link attributes and log bodies. Return a string.
|
|
120
|
-
|
|
121
|
-
counted as failed and the flush reports it. Shared resources are redacted once per
|
|
120
|
+
Invalid/oversized helper content is omitted with an instrumentation failure; the span can still be delivered. Export-time redactor failures reject the affected record and are reported by flush. Shared resources are redacted once per
|
|
122
121
|
export batch. Do not put user content or secrets in span names or scope names.
|
|
123
122
|
|
|
124
123
|
Manual helpers encode JSON values without converting null into absence. Unknown
|
|
@@ -165,7 +164,7 @@ flush. This is an in-memory queue, not durable storage.
|
|
|
165
164
|
|
|
166
165
|
`flush()` waits for the current trace and log export work. A partial rejection,
|
|
167
166
|
invalid acknowledgement, queue drop or failure throws `HueExportError`; its
|
|
168
|
-
`report` contains cumulative accepted/rejected/failed
|
|
167
|
+
`report` contains cumulative accepted/rejected/failed counts and current pending gauges. Accepted
|
|
169
168
|
means the collector acknowledged receipt, not that a complete trace has arrived.
|
|
170
169
|
A malformed response reports uncertain acceptance as failure. Partial successes
|
|
171
170
|
are not retried. Warning-only acknowledgements with zero rejected records remain
|
|
@@ -255,3 +254,11 @@ Use a 120-second host request limit for the default 90-second execution and
|
|
|
255
254
|
See the [managed-run guide](https://docs.hue.run/evaluations/managed-runs) and the
|
|
256
255
|
[full adapter contract](MANAGED_TARGETS.md) for registration, file handling,
|
|
257
256
|
existing-provider flush callbacks and recovery. Local/CI runners remain available.
|
|
257
|
+
|
|
258
|
+
## Serving safely
|
|
259
|
+
|
|
260
|
+
Use `createHueSafe(options)` for best-effort startup. Invalid initialization returns a disabled client with an instrumentation failure recorded. Pass `enabled: false` to disable Hue without a key; disabled helpers still execute the application callback. `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.
|
|
261
|
+
|
|
262
|
+
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. 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 safety](https://docs.hue.run/guides/production-safety).
|
|
263
|
+
|
|
264
|
+
Queued records snapshot supported telemetry values when a span ends or a log is emitted; later caller mutations cannot change queued data. Resource attributes still awaiting detection are omitted with a sanitized warning. Later records include them after detection finishes; await resource detection before instrumentation when those attributes are required.
|
package/dist/ai-sdk.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { OpenTelemetry } from "@ai-sdk/otel";
|
|
2
2
|
/** Use as the call's telemetry option; it does not change global AI SDK integrations. */
|
|
3
3
|
export function hueTelemetry(hue) {
|
|
4
|
+
// Core tracing also installs beside AI SDK 6. This adapter requires the v7
|
|
5
|
+
// per-call integration API; fail explicitly during configuration on v6.
|
|
6
|
+
if (hue.enabled) {
|
|
7
|
+
const { version } = createRequire(import.meta.url)("ai/package.json");
|
|
8
|
+
const [major, minor, patch] = version.split(".").map(Number);
|
|
9
|
+
if (major !== 7 || (minor === 0 && patch < 99))
|
|
10
|
+
throw new TypeError("hueTelemetry requires ai@^7.0.99; AI SDK 6 applications can use Hue core tracing with their existing OpenTelemetry integration");
|
|
11
|
+
}
|
|
4
12
|
return {
|
|
5
|
-
isEnabled:
|
|
13
|
+
isEnabled: hue.enabled,
|
|
6
14
|
recordInputs: hue.captureContent,
|
|
7
15
|
recordOutputs: hue.captureContent,
|
|
8
16
|
integrations: [new OpenTelemetry({ tracer: hue.tracer, usage: true })],
|
|
9
17
|
};
|
|
10
18
|
}
|
|
19
|
+
import { createRequire } from "node:module";
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Context, type Span, type Tracer } from "@opentelemetry/api";
|
|
2
2
|
import { HueTransport } from "./transport.js";
|
|
3
|
-
import type { ExportReport, FlushableLoggerProvider, FlushableTracerProvider, HueOptions, HueSpan, JsonValue, ProjectConnection, SpanOptions, VerifyTraceOptions, TraceVerification } from "./types.js";
|
|
3
|
+
import type { ExportReport, FlushableLoggerProvider, FlushableTracerProvider, HueOptions, HueSpan, JsonValue, ProjectConnection, SpanOptions, VerifyTraceOptions, TraceVerification, SafeLifecycleOptions, SafeLifecycleResult } from "./types.js";
|
|
4
4
|
export interface ExistingHueProviders {
|
|
5
5
|
transport: HueTransport;
|
|
6
6
|
tracerProvider: FlushableTracerProvider;
|
|
@@ -14,6 +14,7 @@ export declare class HueClient {
|
|
|
14
14
|
readonly transport: HueTransport;
|
|
15
15
|
readonly tracer: Tracer;
|
|
16
16
|
readonly captureContent: boolean;
|
|
17
|
+
readonly enabled: boolean;
|
|
17
18
|
private logger;
|
|
18
19
|
private storage;
|
|
19
20
|
private tracerProvider;
|
|
@@ -22,6 +23,8 @@ export declare class HueClient {
|
|
|
22
23
|
private closed;
|
|
23
24
|
private shutdownPromise?;
|
|
24
25
|
private flushPromise?;
|
|
26
|
+
private safeFlushPromise?;
|
|
27
|
+
private safeShutdownPromise?;
|
|
25
28
|
constructor(options: HueOptions | ExistingHueProviders);
|
|
26
29
|
verifyTrace(traceId: string, options?: VerifyTraceOptions): Promise<TraceVerification>;
|
|
27
30
|
getContext(): Context;
|
|
@@ -34,8 +37,15 @@ export declare class HueClient {
|
|
|
34
37
|
}, explicitContext?: Context): void;
|
|
35
38
|
private setContent;
|
|
36
39
|
checkConnection(): Promise<ProjectConnection>;
|
|
40
|
+
/** Production lifecycle path: never rejects; timeouts do not cancel borrowed provider work. */
|
|
41
|
+
flushSafe(options?: SafeLifecycleOptions): Promise<SafeLifecycleResult>;
|
|
42
|
+
/** Safe for finally blocks; preserves the application's result or original exception. */
|
|
43
|
+
shutdownSafe(options?: SafeLifecycleOptions): Promise<SafeLifecycleResult>;
|
|
44
|
+
private safeLifecycle;
|
|
37
45
|
flush(): Promise<ExportReport>;
|
|
38
46
|
private flushOnce;
|
|
39
47
|
shutdown(): Promise<ExportReport>;
|
|
40
48
|
}
|
|
41
49
|
export declare function createHue(options: HueOptions | ExistingHueProviders): HueClient;
|
|
50
|
+
/** Fail-open initialization for production; strict createHue remains available for setup/CI. */
|
|
51
|
+
export declare function createHueSafe(options: HueOptions | ExistingHueProviders): HueClient;
|
package/dist/client.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { context, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
2
|
+
import { context, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
3
3
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
4
4
|
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
5
5
|
import { TracerProvider } from "@opentelemetry/sdk-trace";
|
|
6
6
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
7
|
-
import {
|
|
7
|
+
import { encodeContent, noopSpan, safeSpan } from "./safety.js";
|
|
8
8
|
import { createHueTransport, HueExportError } from "./transport.js";
|
|
9
9
|
import { verifyTrace } from "./receipt.js";
|
|
10
10
|
export class HueConnectionError extends Error {
|
|
@@ -30,20 +30,32 @@ function identifier(value) {
|
|
|
30
30
|
class ContextualTracer {
|
|
31
31
|
source;
|
|
32
32
|
storage;
|
|
33
|
-
|
|
33
|
+
enabled;
|
|
34
|
+
failed;
|
|
35
|
+
constructor(source, storage, enabled, failed) {
|
|
34
36
|
this.source = source;
|
|
35
37
|
this.storage = storage;
|
|
38
|
+
this.enabled = enabled;
|
|
39
|
+
this.failed = failed;
|
|
36
40
|
}
|
|
37
41
|
startSpan(name, options = {}, parent) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
...
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
if (!this.enabled())
|
|
43
|
+
return noopSpan();
|
|
44
|
+
try {
|
|
45
|
+
const active = this.storage.getStore();
|
|
46
|
+
return safeSpan(this.source.startSpan(name, {
|
|
47
|
+
...options,
|
|
48
|
+
attributes: {
|
|
49
|
+
...options.attributes,
|
|
50
|
+
...(active?.sessionId ? { "gen_ai.conversation.id": active.sessionId } : {}),
|
|
51
|
+
...(active?.userId ? { "user.id": active.userId } : {}),
|
|
52
|
+
},
|
|
53
|
+
}, parent ?? active?.context ?? context.active()), this.failed);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
this.failed();
|
|
57
|
+
return noopSpan();
|
|
58
|
+
}
|
|
47
59
|
}
|
|
48
60
|
startActiveSpan(name, optionsOrFn, contextOrFn, fn) {
|
|
49
61
|
const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
@@ -54,17 +66,33 @@ class ContextualTracer {
|
|
|
54
66
|
: fn;
|
|
55
67
|
if (!callback)
|
|
56
68
|
throw new TypeError("A span callback is required");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
let parent = ROOT_CONTEXT;
|
|
70
|
+
try {
|
|
71
|
+
parent =
|
|
72
|
+
typeof contextOrFn === "object"
|
|
73
|
+
? contextOrFn
|
|
74
|
+
: (this.storage.getStore()?.context ?? context.active());
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
this.failed();
|
|
78
|
+
}
|
|
60
79
|
const span = this.startSpan(name, options, parent);
|
|
61
|
-
|
|
80
|
+
let active = ROOT_CONTEXT;
|
|
81
|
+
try {
|
|
82
|
+
active = trace.setSpan(parent, span);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
this.failed();
|
|
86
|
+
active = trace.setSpan(ROOT_CONTEXT, span);
|
|
87
|
+
}
|
|
88
|
+
return this.storage.run({ ...this.storage.getStore(), context: active }, () => callback(span));
|
|
62
89
|
}
|
|
63
90
|
}
|
|
64
91
|
export class HueClient {
|
|
65
92
|
transport;
|
|
66
93
|
tracer;
|
|
67
94
|
captureContent;
|
|
95
|
+
enabled;
|
|
68
96
|
logger;
|
|
69
97
|
storage = new AsyncLocalStorage();
|
|
70
98
|
tracerProvider;
|
|
@@ -73,6 +101,8 @@ export class HueClient {
|
|
|
73
101
|
closed = false;
|
|
74
102
|
shutdownPromise;
|
|
75
103
|
flushPromise;
|
|
104
|
+
safeFlushPromise;
|
|
105
|
+
safeShutdownPromise;
|
|
76
106
|
constructor(options) {
|
|
77
107
|
if ("transport" in options) {
|
|
78
108
|
this.transport = options.transport;
|
|
@@ -87,59 +117,87 @@ export class HueClient {
|
|
|
87
117
|
});
|
|
88
118
|
const tracer = new TracerProvider({
|
|
89
119
|
resource,
|
|
90
|
-
spanProcessors: [this.transport.spanProcessor],
|
|
120
|
+
spanProcessors: this.transport.options.enabled === false ? [] : [this.transport.spanProcessor],
|
|
91
121
|
});
|
|
92
122
|
const logger = new LoggerProvider({
|
|
93
123
|
resource,
|
|
94
|
-
processors: [this.transport.logRecordProcessor],
|
|
124
|
+
processors: this.transport.options.enabled === false ? [] : [this.transport.logRecordProcessor],
|
|
95
125
|
});
|
|
96
126
|
this.ownedProviders = { tracer, logger };
|
|
97
127
|
this.tracerProvider = tracer;
|
|
98
128
|
this.loggerProvider = logger;
|
|
99
129
|
}
|
|
100
130
|
this.captureContent = this.transport.options.captureContent;
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
131
|
+
this.enabled = this.transport.options.enabled !== false;
|
|
132
|
+
this.tracer = new ContextualTracer(this.tracerProvider.getTracer("@hue-run/sdk", "0.1.5"), this.storage, () => this.enabled && !this.closed, () => this.transport.instrumentationFailure());
|
|
133
|
+
this.logger = this.loggerProvider.getLogger("@hue-run/sdk", "0.1.5");
|
|
103
134
|
}
|
|
104
135
|
verifyTrace(traceId, options = {}) {
|
|
136
|
+
if (!this.enabled)
|
|
137
|
+
return Promise.reject(new HueConnectionError("Hue telemetry is disabled"));
|
|
105
138
|
return verifyTrace(this.transport.options, traceId, options);
|
|
106
139
|
}
|
|
107
140
|
getContext() {
|
|
108
|
-
|
|
141
|
+
try {
|
|
142
|
+
return this.storage.getStore()?.context ?? context.active();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
this.transport.instrumentationFailure();
|
|
146
|
+
return ROOT_CONTEXT;
|
|
147
|
+
}
|
|
109
148
|
}
|
|
110
149
|
async withSpan(name, callback, options = {}) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
let span = noopSpan();
|
|
151
|
+
let active = { context: this.getContext() };
|
|
152
|
+
if (this.enabled && !this.closed) {
|
|
153
|
+
try {
|
|
154
|
+
const inherited = this.storage.getStore();
|
|
155
|
+
active = {
|
|
156
|
+
context: options.parentContext ?? inherited?.context ?? context.active(),
|
|
157
|
+
sessionId: identifier(options.sessionId ?? inherited?.sessionId),
|
|
158
|
+
userId: identifier(options.userId ?? inherited?.userId),
|
|
159
|
+
};
|
|
160
|
+
span = this.storage.run(active, () => this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, active.context));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
this.transport.instrumentationFailure();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let spanContext;
|
|
167
|
+
try {
|
|
168
|
+
spanContext = trace.setSpan(active.context, span);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
this.transport.instrumentationFailure();
|
|
172
|
+
spanContext = trace.setSpan(ROOT_CONTEXT, span);
|
|
173
|
+
}
|
|
174
|
+
const handle = {
|
|
175
|
+
span,
|
|
176
|
+
context: spanContext,
|
|
177
|
+
traceId: span.spanContext().traceId,
|
|
178
|
+
spanId: span.spanContext().spanId,
|
|
179
|
+
setInput: (value) => this.setContent(span, "input.value", value),
|
|
180
|
+
setOutput: (value) => this.setContent(span, "output.value", value),
|
|
181
|
+
};
|
|
182
|
+
return this.storage.run({ ...active, context: spanContext }, async () => {
|
|
183
|
+
// Setup, capture and cleanup have separate failure boundaries from customer code.
|
|
184
|
+
try {
|
|
185
|
+
if (this.enabled && !this.closed && options.input !== undefined)
|
|
186
|
+
handle.setInput(options.input);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
this.transport.instrumentationFailure();
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
return await callback(handle);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
this.recordError(span, error);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
span.end();
|
|
200
|
+
}
|
|
143
201
|
});
|
|
144
202
|
}
|
|
145
203
|
async tool(name, input, execute) {
|
|
@@ -152,56 +210,66 @@ export class HueClient {
|
|
|
152
210
|
}, { attributes: { "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": name } });
|
|
153
211
|
}
|
|
154
212
|
recordError(span, error) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
213
|
+
if (!this.enabled || this.closed)
|
|
214
|
+
return;
|
|
215
|
+
try {
|
|
216
|
+
const type = error instanceof Error ? error.name : "Error";
|
|
217
|
+
span.setStatus({
|
|
218
|
+
code: SpanStatusCode.ERROR,
|
|
219
|
+
...(this.captureContent
|
|
220
|
+
? { message: error instanceof Error ? error.message : "Operation failed" }
|
|
221
|
+
: {}),
|
|
222
|
+
});
|
|
223
|
+
span.addEvent("exception", {
|
|
224
|
+
"exception.type": type,
|
|
225
|
+
...(this.captureContent && error instanceof Error
|
|
226
|
+
? {
|
|
227
|
+
"exception.message": error.message,
|
|
228
|
+
...(error.stack ? { "exception.stacktrace": error.stack } : {}),
|
|
229
|
+
}
|
|
230
|
+
: {}),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
this.transport.instrumentationFailure();
|
|
235
|
+
}
|
|
171
236
|
}
|
|
172
237
|
recordMessages(messages, explicitContext) {
|
|
173
|
-
if (this.closed)
|
|
174
|
-
throw new Error("Hue client is shut down");
|
|
175
|
-
if (!this.captureContent)
|
|
238
|
+
if (!this.enabled || this.closed || !this.captureContent)
|
|
176
239
|
return;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
240
|
+
try {
|
|
241
|
+
const active = explicitContext ?? this.getContext();
|
|
242
|
+
if (!trace.getSpanContext(active))
|
|
243
|
+
throw new Error("Message records require an active span or explicit span context");
|
|
244
|
+
const body = {};
|
|
245
|
+
if (messages.input !== undefined)
|
|
246
|
+
body["gen_ai.input.messages"] = messages.input;
|
|
247
|
+
if (messages.output !== undefined)
|
|
248
|
+
body["gen_ai.output.messages"] = messages.output;
|
|
249
|
+
this.logger.emit({
|
|
250
|
+
context: active,
|
|
251
|
+
severityNumber: SeverityNumber.INFO,
|
|
252
|
+
eventName: "gen_ai.client.inference.operation.details",
|
|
253
|
+
body: JSON.parse(encodeContent(body)),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
this.transport.instrumentationFailure("logs");
|
|
258
|
+
}
|
|
191
259
|
}
|
|
192
260
|
setContent(span, key, value) {
|
|
193
|
-
if (!this.captureContent)
|
|
261
|
+
if (!this.enabled || this.closed || !this.captureContent)
|
|
194
262
|
return;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
throw new RangeError("Captured content must be JSON and no more than 256 KiB");
|
|
202
|
-
span.setAttribute(key, encoded);
|
|
263
|
+
try {
|
|
264
|
+
span.setAttribute(key, encodeContent(value));
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
this.transport.instrumentationFailure();
|
|
268
|
+
}
|
|
203
269
|
}
|
|
204
270
|
async checkConnection() {
|
|
271
|
+
if (!this.enabled)
|
|
272
|
+
throw new HueConnectionError("Hue telemetry is disabled");
|
|
205
273
|
const options = this.transport.options;
|
|
206
274
|
let response;
|
|
207
275
|
try {
|
|
@@ -258,6 +326,62 @@ export class HueClient {
|
|
|
258
326
|
throw new HueConnectionError("Hue returned an invalid project response");
|
|
259
327
|
}
|
|
260
328
|
}
|
|
329
|
+
/** Production lifecycle path: never rejects; timeouts do not cancel borrowed provider work. */
|
|
330
|
+
flushSafe(options = {}) {
|
|
331
|
+
if (this.safeFlushPromise)
|
|
332
|
+
return this.safeFlushPromise;
|
|
333
|
+
const work = this.flushPromise ?? this.flush();
|
|
334
|
+
const result = this.safeLifecycle(() => work, options);
|
|
335
|
+
this.safeFlushPromise = result;
|
|
336
|
+
const clear = () => {
|
|
337
|
+
this.safeFlushPromise = undefined;
|
|
338
|
+
};
|
|
339
|
+
// Retain the timed-out result until its underlying drain settles. Repeated
|
|
340
|
+
// timeouts must not accumulate promises against a hung borrowed provider.
|
|
341
|
+
void work.then(clear, clear);
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
/** Safe for finally blocks; preserves the application's result or original exception. */
|
|
345
|
+
shutdownSafe(options = {}) {
|
|
346
|
+
if (this.safeShutdownPromise)
|
|
347
|
+
return this.safeShutdownPromise;
|
|
348
|
+
const work = this.shutdown();
|
|
349
|
+
const result = this.safeLifecycle(() => work, options);
|
|
350
|
+
this.safeShutdownPromise = result;
|
|
351
|
+
const clear = () => {
|
|
352
|
+
this.safeShutdownPromise = undefined;
|
|
353
|
+
};
|
|
354
|
+
void work.then(clear, clear);
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
async safeLifecycle(work, options) {
|
|
358
|
+
let timer;
|
|
359
|
+
try {
|
|
360
|
+
const timeout = options.timeoutMillis ?? 1000;
|
|
361
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 60000)
|
|
362
|
+
throw new TypeError("Invalid lifecycle budget");
|
|
363
|
+
const outcome = await Promise.race([
|
|
364
|
+
work().then(() => "complete"),
|
|
365
|
+
new Promise((resolve) => {
|
|
366
|
+
timer = setTimeout(() => resolve("timeout"), timeout);
|
|
367
|
+
}),
|
|
368
|
+
]);
|
|
369
|
+
const report = this.transport.getReport();
|
|
370
|
+
return {
|
|
371
|
+
ok: outcome === "complete" &&
|
|
372
|
+
this.transport.getFailureSequence() === 0 &&
|
|
373
|
+
report.pendingSpans + report.pendingLogs === 0,
|
|
374
|
+
timedOut: outcome === "timeout",
|
|
375
|
+
report,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return { ok: false, timedOut: false, report: this.transport.getReport() };
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
261
385
|
flush() {
|
|
262
386
|
// Each caller needs a drain after its own preceding span/log emissions.
|
|
263
387
|
// Joining an earlier drain can acknowledge records that were not in its batch.
|
|
@@ -281,8 +405,8 @@ export class HueClient {
|
|
|
281
405
|
}
|
|
282
406
|
async flushOnce() {
|
|
283
407
|
const results = await Promise.allSettled([
|
|
284
|
-
this.tracerProvider.forceFlush(),
|
|
285
|
-
this.loggerProvider.forceFlush(),
|
|
408
|
+
Promise.resolve().then(() => this.tracerProvider.forceFlush()),
|
|
409
|
+
Promise.resolve().then(() => this.loggerProvider.forceFlush()),
|
|
286
410
|
]);
|
|
287
411
|
for (const [index, result] of results.entries())
|
|
288
412
|
if (result.status === "rejected")
|
|
@@ -311,3 +435,14 @@ export class HueClient {
|
|
|
311
435
|
export function createHue(options) {
|
|
312
436
|
return new HueClient(options);
|
|
313
437
|
}
|
|
438
|
+
/** Fail-open initialization for production; strict createHue remains available for setup/CI. */
|
|
439
|
+
export function createHueSafe(options) {
|
|
440
|
+
try {
|
|
441
|
+
return new HueClient(options);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
const disabled = new HueClient({ enabled: false, captureContent: false });
|
|
445
|
+
disabled.transport.instrumentationFailure();
|
|
446
|
+
return disabled;
|
|
447
|
+
}
|
|
448
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { HueOptions } from "./types.js";
|
|
2
2
|
export declare const MAX_BODY_BYTES: number;
|
|
3
3
|
export declare const MAX_CONTENT_BYTES: number;
|
|
4
|
-
export declare function validateOptions(options: HueOptions): Required<Pick<HueOptions, "apiKey" | "serviceName" | "baseUrl" | "captureContent" | "timeoutMillis">> & HueOptions;
|
|
4
|
+
export declare function validateOptions(options: HueOptions): Required<Pick<HueOptions, "apiKey" | "serviceName" | "baseUrl" | "captureContent" | "timeoutMillis" | "maxQueueBytes">> & HueOptions;
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,18 @@ export const MAX_CONTENT_BYTES = 256 * 1024;
|
|
|
3
3
|
export function validateOptions(options) {
|
|
4
4
|
if (typeof options.captureContent !== "boolean")
|
|
5
5
|
throw new TypeError("Choose captureContent explicitly: true or false");
|
|
6
|
+
if (options.enabled !== undefined && typeof options.enabled !== "boolean")
|
|
7
|
+
throw new TypeError("enabled must be a boolean");
|
|
8
|
+
if (options.enabled === false)
|
|
9
|
+
return {
|
|
10
|
+
captureContent: options.captureContent,
|
|
11
|
+
enabled: false,
|
|
12
|
+
apiKey: "",
|
|
13
|
+
serviceName: "hue-disabled",
|
|
14
|
+
baseUrl: "https://app.hue.run",
|
|
15
|
+
timeoutMillis: 10000,
|
|
16
|
+
maxQueueBytes: 8 * 1024 * 1024,
|
|
17
|
+
};
|
|
6
18
|
if (typeof options.apiKey !== "string" ||
|
|
7
19
|
!options.apiKey ||
|
|
8
20
|
options.apiKey.length > 4096 ||
|
|
@@ -28,5 +40,10 @@ export function validateOptions(options) {
|
|
|
28
40
|
const timeoutMillis = options.timeoutMillis ?? 10000;
|
|
29
41
|
if (!Number.isInteger(timeoutMillis) || timeoutMillis < 100 || timeoutMillis > 60000)
|
|
30
42
|
throw new TypeError("timeoutMillis must be 100–60000");
|
|
31
|
-
|
|
43
|
+
const maxQueueBytes = options.maxQueueBytes ?? 8 * 1024 * 1024;
|
|
44
|
+
if (!Number.isSafeInteger(maxQueueBytes) ||
|
|
45
|
+
maxQueueBytes < 1024 ||
|
|
46
|
+
maxQueueBytes > 64 * 1024 * 1024)
|
|
47
|
+
throw new TypeError("maxQueueBytes must be 1024–67108864");
|
|
48
|
+
return { ...options, baseUrl: url.origin, timeoutMillis, maxQueueBytes };
|
|
32
49
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createHue, HueClient, HueConnectionError, type ExistingHueProviders } from "./client.js";
|
|
1
|
+
export { createHue, createHueSafe, HueClient, HueConnectionError, type ExistingHueProviders, } from "./client.js";
|
|
2
2
|
export { createHueTransport, HueTransport, HueExportError } from "./transport.js";
|
|
3
3
|
export { HueTraceVerificationError } from "./receipt.js";
|
|
4
4
|
export type * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createHue, HueClient, HueConnectionError } from "./client.js";
|
|
1
|
+
export { createHue, createHueSafe, HueClient, HueConnectionError, } from "./client.js";
|
|
2
2
|
export { createHueTransport, HueTransport, HueExportError } from "./transport.js";
|
|
3
3
|
export { HueTraceVerificationError } from "./receipt.js";
|