@hue-run/sdk 0.1.2
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/EVALUATIONS.md +141 -0
- package/LICENSE +18 -0
- package/README.md +196 -0
- package/dist/ai-sdk.d.ts +4 -0
- package/dist/ai-sdk.js +10 -0
- package/dist/client.d.ts +40 -0
- package/dist/client.js +309 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +32 -0
- package/dist/evals/checkpoint.d.ts +9 -0
- package/dist/evals/checkpoint.js +89 -0
- package/dist/evals/client.d.ts +96 -0
- package/dist/evals/client.js +195 -0
- package/dist/evals/json.d.ts +6 -0
- package/dist/evals/json.js +61 -0
- package/dist/evals/runner.d.ts +49 -0
- package/dist/evals/runner.js +369 -0
- package/dist/evals/schema-worker.d.ts +1 -0
- package/dist/evals/schema-worker.js +14 -0
- package/dist/evals/scorers.d.ts +21 -0
- package/dist/evals/scorers.js +212 -0
- package/dist/evals/types.d.ts +301 -0
- package/dist/evals/types.js +1 -0
- package/dist/evals.d.ts +7 -0
- package/dist/evals.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/privacy.d.ts +7 -0
- package/dist/privacy.js +138 -0
- package/dist/transport.d.ts +44 -0
- package/dist/transport.js +320 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +1 -0
- package/package.json +79 -0
package/dist/privacy.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
2
|
+
import { MAX_CONTENT_BYTES } from "./config.js";
|
|
3
|
+
const contentPrefixes = [
|
|
4
|
+
"gen_ai.input.messages",
|
|
5
|
+
"gen_ai.output.messages",
|
|
6
|
+
"gen_ai.system_instructions",
|
|
7
|
+
"gen_ai.prompt",
|
|
8
|
+
"gen_ai.completion",
|
|
9
|
+
"gen_ai.tool.call.arguments",
|
|
10
|
+
"gen_ai.tool.call.result",
|
|
11
|
+
"gen_ai.tool.definitions",
|
|
12
|
+
"gen_ai.event.content",
|
|
13
|
+
"llm.input_messages",
|
|
14
|
+
"llm.output_messages",
|
|
15
|
+
"llm.prompts",
|
|
16
|
+
"llm.completions",
|
|
17
|
+
"llm.invocation_parameters",
|
|
18
|
+
"input.value",
|
|
19
|
+
"output.value",
|
|
20
|
+
"ai.prompt",
|
|
21
|
+
"ai.response.text",
|
|
22
|
+
"ai.response.object",
|
|
23
|
+
"ai.response.toolCalls",
|
|
24
|
+
"ai.response.body",
|
|
25
|
+
"ai.toolCall.args",
|
|
26
|
+
"ai.toolCall.result",
|
|
27
|
+
"ai.value",
|
|
28
|
+
"ai.values",
|
|
29
|
+
"ai.embedding",
|
|
30
|
+
"ai.embeddings",
|
|
31
|
+
"traceloop.entity.input",
|
|
32
|
+
"traceloop.entity.output",
|
|
33
|
+
"tool.parameters",
|
|
34
|
+
"exception.message",
|
|
35
|
+
"exception.stacktrace",
|
|
36
|
+
];
|
|
37
|
+
// Kept as a function so metadata-only filtering is applied consistently to every exporter location.
|
|
38
|
+
function isContentKey(key) {
|
|
39
|
+
return contentPrefixes.some((prefix) => key === prefix || key.startsWith(`${prefix}.`));
|
|
40
|
+
}
|
|
41
|
+
function redactValue(value, path, options, depth = 0) {
|
|
42
|
+
if (depth > 32)
|
|
43
|
+
throw new Error("Telemetry value exceeds the supported nesting limit");
|
|
44
|
+
if (typeof value === "string") {
|
|
45
|
+
const result = options.redact ? options.redact(value, path) : value;
|
|
46
|
+
if (typeof result !== "string" || !result.isWellFormed() || result.includes("\u0000"))
|
|
47
|
+
throw new Error("Redaction produced unsupported text");
|
|
48
|
+
if (Buffer.byteLength(result) > MAX_CONTENT_BYTES)
|
|
49
|
+
throw new Error("Telemetry text exceeds 256 KiB");
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(value))
|
|
53
|
+
return value.map((item, index) => redactValue(item, `${path}.${index}`, options, depth + 1));
|
|
54
|
+
if (value instanceof Uint8Array) {
|
|
55
|
+
if (value.byteLength > MAX_CONTENT_BYTES)
|
|
56
|
+
throw new Error("Telemetry bytes exceed 256 KiB");
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
if (value !== null && typeof value === "object")
|
|
60
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
61
|
+
key,
|
|
62
|
+
redactValue(item, `${path}.${key}`, options, depth + 1),
|
|
63
|
+
]));
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function attributes(source, options, path) {
|
|
67
|
+
return Object.fromEntries(Object.entries(source).flatMap(([key, value]) => !options.captureContent && isContentKey(key)
|
|
68
|
+
? []
|
|
69
|
+
: [[key, redactValue(value, `${path}.${key}`, options)]]));
|
|
70
|
+
}
|
|
71
|
+
function redactResource(resource, options, cache) {
|
|
72
|
+
let result = cache.get(resource);
|
|
73
|
+
if (!result) {
|
|
74
|
+
result = resourceFromAttributes(attributes(resource.attributes, options, "resource.attributes"), { schemaUrl: resource.schemaUrl });
|
|
75
|
+
cache.set(resource, result);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
export function redactSpan(span, options, cache) {
|
|
80
|
+
return {
|
|
81
|
+
name: span.name,
|
|
82
|
+
kind: span.kind,
|
|
83
|
+
spanContext: () => span.spanContext(),
|
|
84
|
+
parentSpanContext: span.parentSpanContext,
|
|
85
|
+
startTime: span.startTime,
|
|
86
|
+
endTime: span.endTime,
|
|
87
|
+
duration: span.duration,
|
|
88
|
+
ended: span.ended,
|
|
89
|
+
status: {
|
|
90
|
+
code: span.status.code,
|
|
91
|
+
...(options.captureContent && span.status.message !== undefined
|
|
92
|
+
? { message: String(redactValue(span.status.message, "status.message", options)) }
|
|
93
|
+
: {}),
|
|
94
|
+
},
|
|
95
|
+
attributes: attributes(span.attributes, options, "attributes"),
|
|
96
|
+
events: span.events
|
|
97
|
+
.filter((event) => options.captureContent ||
|
|
98
|
+
!/^gen_ai\.(?:system|user|assistant|tool|choice)/.test(event.name))
|
|
99
|
+
.map((event) => ({
|
|
100
|
+
...event,
|
|
101
|
+
attributes: attributes(event.attributes ?? {}, options, `events.${event.name}`),
|
|
102
|
+
})),
|
|
103
|
+
links: span.links.map((link) => ({
|
|
104
|
+
...link,
|
|
105
|
+
attributes: attributes(link.attributes ?? {}, options, "links.attributes"),
|
|
106
|
+
})),
|
|
107
|
+
resource: redactResource(span.resource, options, cache),
|
|
108
|
+
instrumentationScope: span.instrumentationScope,
|
|
109
|
+
droppedAttributesCount: span.droppedAttributesCount,
|
|
110
|
+
droppedEventsCount: span.droppedEventsCount,
|
|
111
|
+
droppedLinksCount: span.droppedLinksCount,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function redactLog(log, options, cache) {
|
|
115
|
+
const body = options.captureContent ? redactValue(log.body, "body", options) : undefined;
|
|
116
|
+
if (body !== undefined && Buffer.byteLength(JSON.stringify(body)) > MAX_CONTENT_BYTES)
|
|
117
|
+
throw new Error("Telemetry log body exceeds 256 KiB");
|
|
118
|
+
return {
|
|
119
|
+
hrTime: log.hrTime,
|
|
120
|
+
hrTimeObserved: log.hrTimeObserved,
|
|
121
|
+
spanContext: log.spanContext,
|
|
122
|
+
severityText: log.severityText,
|
|
123
|
+
severityNumber: log.severityNumber,
|
|
124
|
+
eventName: log.eventName,
|
|
125
|
+
body: body,
|
|
126
|
+
attributes: attributes(log.attributes, options, "attributes"),
|
|
127
|
+
resource: redactResource(log.resource, options, cache),
|
|
128
|
+
instrumentationScope: {
|
|
129
|
+
...log.instrumentationScope,
|
|
130
|
+
...(log.instrumentationScope.attributes
|
|
131
|
+
? {
|
|
132
|
+
attributes: attributes(log.instrumentationScope.attributes, options, "scope.attributes"),
|
|
133
|
+
}
|
|
134
|
+
: {}),
|
|
135
|
+
},
|
|
136
|
+
droppedAttributesCount: log.droppedAttributesCount,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ReadableSpan, type SpanProcessor } from "@opentelemetry/sdk-trace";
|
|
2
|
+
import { type LogRecordProcessor, type ReadableLogRecord } from "@opentelemetry/sdk-logs";
|
|
3
|
+
import { validateOptions } from "./config.js";
|
|
4
|
+
import type { ExportIssue, ExportReport, HueOptions, Signal } from "./types.js";
|
|
5
|
+
type RecordValue = ReadableSpan | ReadableLogRecord;
|
|
6
|
+
export declare class HueExportError extends Error {
|
|
7
|
+
readonly issues: ExportIssue[];
|
|
8
|
+
readonly report: ExportReport;
|
|
9
|
+
constructor(issues: ExportIssue[], report: ExportReport);
|
|
10
|
+
}
|
|
11
|
+
/** Owned transport components; attach processors during provider construction. */
|
|
12
|
+
export declare class HueTransport {
|
|
13
|
+
readonly options: ReturnType<typeof validateOptions>;
|
|
14
|
+
readonly spanProcessor: SpanProcessor;
|
|
15
|
+
readonly logRecordProcessor: LogRecordProcessor;
|
|
16
|
+
private sequence;
|
|
17
|
+
private observedSequence;
|
|
18
|
+
private failureSequence;
|
|
19
|
+
private issues;
|
|
20
|
+
private accepted;
|
|
21
|
+
private rejected;
|
|
22
|
+
private failed;
|
|
23
|
+
private spans;
|
|
24
|
+
private logs;
|
|
25
|
+
private traceExporter;
|
|
26
|
+
private logExporter;
|
|
27
|
+
private closed;
|
|
28
|
+
private shutdownPromise?;
|
|
29
|
+
private flushPromise?;
|
|
30
|
+
constructor(options: HueOptions);
|
|
31
|
+
private enqueue;
|
|
32
|
+
finish(signal: Signal, records: RecordValue[]): void;
|
|
33
|
+
acceptedRecords(signal: Signal, count: number): void;
|
|
34
|
+
issue(signal: Signal, kind: ExportIssue["kind"], count: number, message: string, status?: number): void;
|
|
35
|
+
getReport(): ExportReport;
|
|
36
|
+
getIssues(): ExportIssue[];
|
|
37
|
+
/** Monotonic failure marker, retained even when the bounded issue history rolls over. */
|
|
38
|
+
getFailureSequence(): number;
|
|
39
|
+
flush(): Promise<ExportReport>;
|
|
40
|
+
private flushOnce;
|
|
41
|
+
shutdown(): Promise<ExportReport>;
|
|
42
|
+
}
|
|
43
|
+
export declare function createHueTransport(options: HueOptions): HueTransport;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { ExportResultCode } from "@opentelemetry/core";
|
|
2
|
+
import { CompressionAlgorithm, OTLPExporterBase, OTLPExporterError, } from "@opentelemetry/otlp-exporter-base";
|
|
3
|
+
import { convertLegacyHttpOptions, createOtlpHttpExportDelegate, } from "@opentelemetry/otlp-exporter-base/node-http";
|
|
4
|
+
import { LogsExporterMetricsHelper, ProtobufLogsSerializer, ProtobufTraceSerializer, TraceExporterMetricsHelper, } from "@opentelemetry/otlp-transformer";
|
|
5
|
+
import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace";
|
|
6
|
+
import { BatchLogRecordProcessor, } from "@opentelemetry/sdk-logs";
|
|
7
|
+
import { MAX_BODY_BYTES, validateOptions } from "./config.js";
|
|
8
|
+
import { redactLog, redactSpan } from "./privacy.js";
|
|
9
|
+
export class HueExportError extends Error {
|
|
10
|
+
issues;
|
|
11
|
+
report;
|
|
12
|
+
constructor(issues, report) {
|
|
13
|
+
super("Hue could not accept all telemetry. Inspect issues and report for sanitized counts.");
|
|
14
|
+
this.issues = issues;
|
|
15
|
+
this.report = report;
|
|
16
|
+
this.name = "HueExportError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Owned transport components; attach processors during provider construction. */
|
|
20
|
+
export class HueTransport {
|
|
21
|
+
options;
|
|
22
|
+
spanProcessor;
|
|
23
|
+
logRecordProcessor;
|
|
24
|
+
sequence = 0;
|
|
25
|
+
observedSequence = 0;
|
|
26
|
+
failureSequence = 0;
|
|
27
|
+
issues = [];
|
|
28
|
+
accepted = { traces: 0, logs: 0 };
|
|
29
|
+
rejected = { traces: 0, logs: 0 };
|
|
30
|
+
failed = { traces: 0, logs: 0 };
|
|
31
|
+
spans = new Set();
|
|
32
|
+
logs = new Set();
|
|
33
|
+
traceExporter;
|
|
34
|
+
logExporter;
|
|
35
|
+
closed = false;
|
|
36
|
+
shutdownPromise;
|
|
37
|
+
flushPromise;
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.options = validateOptions(options);
|
|
40
|
+
Object.defineProperty(this, "options", { enumerable: false });
|
|
41
|
+
this.traceExporter = new ReportingExporter(this, "traces", ProtobufTraceSerializer, TraceExporterMetricsHelper, (span, cache) => redactSpan(span, this.options, cache));
|
|
42
|
+
this.logExporter = new ReportingExporter(this, "logs", ProtobufLogsSerializer, LogsExporterMetricsHelper, (log, cache) => redactLog(log, this.options, cache));
|
|
43
|
+
const batching = {
|
|
44
|
+
maxQueueSize: 2048,
|
|
45
|
+
maxExportBatchSize: 128,
|
|
46
|
+
scheduledDelayMillis: 1000,
|
|
47
|
+
exportTimeoutMillis: this.options.timeoutMillis * 16 + 1000,
|
|
48
|
+
};
|
|
49
|
+
const spans = new BatchSpanProcessor({ exporter: this.traceExporter, ...batching });
|
|
50
|
+
const logs = new BatchLogRecordProcessor({ exporter: this.logExporter, ...batching });
|
|
51
|
+
this.spanProcessor = {
|
|
52
|
+
onStart: (span, parent) => spans.onStart(span, parent),
|
|
53
|
+
onEnd: (span) => {
|
|
54
|
+
if (!(span.spanContext().traceFlags & 1))
|
|
55
|
+
return;
|
|
56
|
+
if (!this.enqueue("traces", span))
|
|
57
|
+
return;
|
|
58
|
+
spans.onEnd(span);
|
|
59
|
+
},
|
|
60
|
+
forceFlush: () => spans.forceFlush(),
|
|
61
|
+
shutdown: () => spans.shutdown(),
|
|
62
|
+
};
|
|
63
|
+
this.logRecordProcessor = {
|
|
64
|
+
onEmit: (log) => {
|
|
65
|
+
if (!this.enqueue("logs", log))
|
|
66
|
+
return;
|
|
67
|
+
logs.onEmit(log);
|
|
68
|
+
},
|
|
69
|
+
forceFlush: () => logs.forceFlush(),
|
|
70
|
+
shutdown: () => logs.shutdown(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
enqueue(signal, record) {
|
|
74
|
+
const pending = signal === "traces" ? this.spans : this.logs;
|
|
75
|
+
if (this.closed || pending.size >= 2048) {
|
|
76
|
+
this.issue(signal, "dropped", 1, this.closed
|
|
77
|
+
? "Telemetry emitted after transport shutdown"
|
|
78
|
+
: "Telemetry queue reached 2048 records");
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (signal === "traces")
|
|
82
|
+
this.spans.add(record);
|
|
83
|
+
else
|
|
84
|
+
this.logs.add(record);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
finish(signal, records) {
|
|
88
|
+
for (const record of records) {
|
|
89
|
+
if (signal === "traces")
|
|
90
|
+
this.spans.delete(record);
|
|
91
|
+
else
|
|
92
|
+
this.logs.delete(record);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
acceptedRecords(signal, count) {
|
|
96
|
+
this.accepted[signal] += count;
|
|
97
|
+
}
|
|
98
|
+
issue(signal, kind, count, message, status) {
|
|
99
|
+
if (kind === "rejected")
|
|
100
|
+
this.rejected[signal] += count;
|
|
101
|
+
else if (kind !== "warning")
|
|
102
|
+
this.failed[signal] += count;
|
|
103
|
+
const issue = {
|
|
104
|
+
sequence: ++this.sequence,
|
|
105
|
+
signal,
|
|
106
|
+
kind,
|
|
107
|
+
count,
|
|
108
|
+
message,
|
|
109
|
+
...(status !== undefined ? { status } : {}),
|
|
110
|
+
};
|
|
111
|
+
if (kind !== "warning")
|
|
112
|
+
this.failureSequence = issue.sequence;
|
|
113
|
+
this.issues.push(issue);
|
|
114
|
+
if (this.issues.length > 128)
|
|
115
|
+
this.issues.shift();
|
|
116
|
+
try {
|
|
117
|
+
this.options.onExportIssue?.({ ...issue });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* Diagnostics callbacks cannot interrupt the customer's application. */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
getReport() {
|
|
124
|
+
return {
|
|
125
|
+
acceptedSpans: this.accepted.traces,
|
|
126
|
+
acceptedLogs: this.accepted.logs,
|
|
127
|
+
rejectedSpans: this.rejected.traces,
|
|
128
|
+
rejectedLogs: this.rejected.logs,
|
|
129
|
+
failedSpans: this.failed.traces,
|
|
130
|
+
failedLogs: this.failed.logs,
|
|
131
|
+
pendingSpans: this.spans.size,
|
|
132
|
+
pendingLogs: this.logs.size,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
getIssues() {
|
|
136
|
+
return this.issues.map((issue) => ({ ...issue }));
|
|
137
|
+
}
|
|
138
|
+
/** Monotonic failure marker, retained even when the bounded issue history rolls over. */
|
|
139
|
+
getFailureSequence() {
|
|
140
|
+
return this.failureSequence;
|
|
141
|
+
}
|
|
142
|
+
flush() {
|
|
143
|
+
const from = this.observedSequence;
|
|
144
|
+
const next = (this.flushPromise ?? Promise.resolve()).then(() => this.flushOnce(from), () => this.flushOnce(from));
|
|
145
|
+
this.flushPromise = next;
|
|
146
|
+
const clear = () => {
|
|
147
|
+
if (this.flushPromise === next)
|
|
148
|
+
this.flushPromise = undefined;
|
|
149
|
+
};
|
|
150
|
+
void next.then(clear, clear);
|
|
151
|
+
return next;
|
|
152
|
+
}
|
|
153
|
+
async flushOnce(from) {
|
|
154
|
+
const processors = await Promise.allSettled([
|
|
155
|
+
this.spanProcessor.forceFlush(),
|
|
156
|
+
this.logRecordProcessor.forceFlush(),
|
|
157
|
+
]);
|
|
158
|
+
await Promise.all([this.traceExporter.forceFlush(), this.logExporter.forceFlush()]);
|
|
159
|
+
for (const [index, result] of processors.entries())
|
|
160
|
+
if (result.status === "rejected") {
|
|
161
|
+
const signal = index === 0 ? "traces" : "logs";
|
|
162
|
+
if (!this.issues.some((issue) => issue.sequence > from && issue.signal === signal))
|
|
163
|
+
this.issue(signal, "failed", 0, "Telemetry processor flush failed");
|
|
164
|
+
}
|
|
165
|
+
const issues = this.issues.filter((issue) => issue.sequence > from && issue.kind !== "warning");
|
|
166
|
+
this.observedSequence = this.sequence;
|
|
167
|
+
const report = this.getReport();
|
|
168
|
+
if (this.failureSequence > from)
|
|
169
|
+
throw new HueExportError(issues, report);
|
|
170
|
+
return report;
|
|
171
|
+
}
|
|
172
|
+
shutdown() {
|
|
173
|
+
this.shutdownPromise ??= (async () => {
|
|
174
|
+
this.closed = true;
|
|
175
|
+
try {
|
|
176
|
+
return await this.flush();
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await Promise.allSettled([
|
|
180
|
+
this.spanProcessor.shutdown(),
|
|
181
|
+
this.logRecordProcessor.shutdown(),
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
return this.shutdownPromise;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
class ReportingExporter {
|
|
189
|
+
transport;
|
|
190
|
+
signal;
|
|
191
|
+
serializer;
|
|
192
|
+
metrics;
|
|
193
|
+
redact;
|
|
194
|
+
pending = new Set();
|
|
195
|
+
constructor(transport, signal, serializer, metrics, redact) {
|
|
196
|
+
this.transport = transport;
|
|
197
|
+
this.signal = signal;
|
|
198
|
+
this.serializer = serializer;
|
|
199
|
+
this.metrics = metrics;
|
|
200
|
+
this.redact = redact;
|
|
201
|
+
}
|
|
202
|
+
export(records, callback) {
|
|
203
|
+
const work = this.exportRecords(records)
|
|
204
|
+
.then(() => callback({ code: ExportResultCode.SUCCESS }), () => callback({
|
|
205
|
+
code: ExportResultCode.FAILED,
|
|
206
|
+
error: new Error("Hue telemetry export failed; inspect SDK export issues"),
|
|
207
|
+
}))
|
|
208
|
+
.finally(() => {
|
|
209
|
+
this.transport.finish(this.signal, records);
|
|
210
|
+
this.pending.delete(work);
|
|
211
|
+
});
|
|
212
|
+
this.pending.add(work);
|
|
213
|
+
}
|
|
214
|
+
async exportRecords(records) {
|
|
215
|
+
const accepted = [];
|
|
216
|
+
const cache = new WeakMap();
|
|
217
|
+
let failed = false;
|
|
218
|
+
for (const record of records) {
|
|
219
|
+
try {
|
|
220
|
+
await record.resource.waitForAsyncAttributes?.();
|
|
221
|
+
accepted.push(this.redact(record, cache));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
failed = true;
|
|
225
|
+
this.transport.issue(this.signal, "invalid", 1, "Telemetry record could not be redacted or exceeds supported content limits");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let batch = [];
|
|
229
|
+
for (const record of accepted) {
|
|
230
|
+
const candidate = [...batch, record];
|
|
231
|
+
// Leave room for gzip headers/blocks when otherwise incompressible data is near the wire cap.
|
|
232
|
+
if ((this.serializer.serializeRequest(candidate)?.byteLength ?? 0) <= MAX_BODY_BYTES - 1024) {
|
|
233
|
+
batch = candidate;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (batch.length && !(await this.send(batch)))
|
|
237
|
+
failed = true;
|
|
238
|
+
batch = [];
|
|
239
|
+
if ((this.serializer.serializeRequest([record])?.byteLength ?? 0) > MAX_BODY_BYTES - 1024) {
|
|
240
|
+
failed = true;
|
|
241
|
+
this.transport.issue(this.signal, "invalid", 1, "Telemetry record exceeds the 1 MiB request limit");
|
|
242
|
+
}
|
|
243
|
+
else
|
|
244
|
+
batch = [record];
|
|
245
|
+
}
|
|
246
|
+
if (batch.length && !(await this.send(batch)))
|
|
247
|
+
failed = true;
|
|
248
|
+
if (failed)
|
|
249
|
+
throw new Error("Hue telemetry export failed");
|
|
250
|
+
}
|
|
251
|
+
async send(records) {
|
|
252
|
+
const options = this.transport.options;
|
|
253
|
+
let rejected = 0;
|
|
254
|
+
let validResponse = true;
|
|
255
|
+
const serializer = {
|
|
256
|
+
serializeRequest: (data) => this.serializer.serializeRequest(data),
|
|
257
|
+
deserializeResponse: (bytes) => {
|
|
258
|
+
try {
|
|
259
|
+
const response = this.serializer.deserializeResponse(bytes);
|
|
260
|
+
const partial = response.partialSuccess;
|
|
261
|
+
const count = Number(partial?.[this.signal === "traces" ? "rejectedSpans" : "rejectedLogRecords"] ?? 0);
|
|
262
|
+
if (!Number.isSafeInteger(count) || count < 0 || count > records.length)
|
|
263
|
+
throw new Error("Invalid rejection count");
|
|
264
|
+
rejected = count;
|
|
265
|
+
if (count || partial?.errorMessage)
|
|
266
|
+
this.transport.issue(this.signal, count ? "rejected" : "warning", count, count
|
|
267
|
+
? "Hue rejected telemetry records; inspect the project ingestion settings and supported limits"
|
|
268
|
+
: "Hue returned an ingestion warning");
|
|
269
|
+
// Do not pass backend error text or raw response bytes into the global OTel diagnostic logger.
|
|
270
|
+
return {};
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
validResponse = false;
|
|
274
|
+
this.transport.issue(this.signal, "failed", records.length, "Hue returned an invalid OTLP acknowledgement; acceptance is uncertain");
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
const endpoint = `${options.baseUrl}/api/v1/otlp/v1/${this.signal}`;
|
|
280
|
+
const delegate = createOtlpHttpExportDelegate(convertLegacyHttpOptions({
|
|
281
|
+
url: endpoint,
|
|
282
|
+
headers: { Authorization: `Bearer ${options.apiKey}` },
|
|
283
|
+
timeoutMillis: options.timeoutMillis,
|
|
284
|
+
concurrencyLimit: 1,
|
|
285
|
+
compression: CompressionAlgorithm.GZIP,
|
|
286
|
+
}, this.signal === "traces" ? "TRACES" : "LOGS", `v1/${this.signal}`, { "Content-Type": "application/x-protobuf" }), serializer, this.signal === "traces" ? "otlp_http_span_exporter" : "otlp_http_log_exporter", this.metrics, undefined);
|
|
287
|
+
const exporter = new OTLPExporterBase(delegate);
|
|
288
|
+
try {
|
|
289
|
+
const result = await new Promise((resolve) => exporter.export(records, resolve));
|
|
290
|
+
if (result.code === ExportResultCode.SUCCESS) {
|
|
291
|
+
if (validResponse)
|
|
292
|
+
this.transport.acceptedRecords(this.signal, records.length - rejected);
|
|
293
|
+
return validResponse;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const status = result.error instanceof OTLPExporterError && Number.isInteger(result.error.code)
|
|
297
|
+
? result.error.code
|
|
298
|
+
: undefined;
|
|
299
|
+
this.transport.issue(this.signal, "failed", records.length, "Hue telemetry request failed", status);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
this.transport.issue(this.signal, "failed", records.length, "Hue telemetry request failed");
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
await exporter.shutdown();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async forceFlush() {
|
|
312
|
+
await Promise.all(this.pending);
|
|
313
|
+
}
|
|
314
|
+
async shutdown() {
|
|
315
|
+
await this.forceFlush();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export function createHueTransport(options) {
|
|
319
|
+
return new HueTransport(options);
|
|
320
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Attributes, Context, Span, SpanKind, TracerProvider } from "@opentelemetry/api";
|
|
2
|
+
import type { LoggerProvider } from "@opentelemetry/api-logs";
|
|
3
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
4
|
+
[key: string]: JsonValue;
|
|
5
|
+
};
|
|
6
|
+
export type Signal = "traces" | "logs";
|
|
7
|
+
export interface HueOptions {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
serviceName: string;
|
|
10
|
+
captureContent: boolean;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
serviceVersion?: string;
|
|
13
|
+
/** Runs on string values before Hue export, including custom attribute values. */
|
|
14
|
+
redact?: (value: string, path: string) => string;
|
|
15
|
+
onExportIssue?: (issue: ExportIssue) => void;
|
|
16
|
+
timeoutMillis?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ExportIssue {
|
|
19
|
+
sequence: number;
|
|
20
|
+
signal: Signal;
|
|
21
|
+
kind: "rejected" | "failed" | "dropped" | "invalid" | "warning";
|
|
22
|
+
count: number;
|
|
23
|
+
status?: number;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ExportReport {
|
|
27
|
+
acceptedSpans: number;
|
|
28
|
+
acceptedLogs: number;
|
|
29
|
+
rejectedSpans: number;
|
|
30
|
+
rejectedLogs: number;
|
|
31
|
+
failedSpans: number;
|
|
32
|
+
failedLogs: number;
|
|
33
|
+
pendingSpans: number;
|
|
34
|
+
pendingLogs: number;
|
|
35
|
+
}
|
|
36
|
+
export interface SpanOptions {
|
|
37
|
+
kind?: SpanKind;
|
|
38
|
+
attributes?: Attributes;
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
userId?: string;
|
|
41
|
+
input?: JsonValue;
|
|
42
|
+
parentContext?: Context;
|
|
43
|
+
}
|
|
44
|
+
export interface HueSpan {
|
|
45
|
+
span: Span;
|
|
46
|
+
context: Context;
|
|
47
|
+
traceId: string;
|
|
48
|
+
spanId: string;
|
|
49
|
+
setInput(value: JsonValue): void;
|
|
50
|
+
setOutput(value: JsonValue): void;
|
|
51
|
+
}
|
|
52
|
+
export type FlushableTracerProvider = TracerProvider & {
|
|
53
|
+
forceFlush(): Promise<void>;
|
|
54
|
+
};
|
|
55
|
+
export type FlushableLoggerProvider = LoggerProvider & {
|
|
56
|
+
forceFlush(): Promise<void>;
|
|
57
|
+
};
|
|
58
|
+
export interface ProjectConnection {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
organizationId: string;
|
|
62
|
+
slug: string;
|
|
63
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hue-run/sdk",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public",
|
|
8
|
+
"registry": "https://registry.npmjs.org/",
|
|
9
|
+
"provenance": false
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://docs.hue.run",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/hue-run/hue-sdk.git",
|
|
15
|
+
"directory": "packages/sdk-typescript"
|
|
16
|
+
},
|
|
17
|
+
"description": "Hue OpenTelemetry helpers for Node.js agent applications",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"EVALUATIONS.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./ai-sdk": {
|
|
31
|
+
"types": "./dist/ai-sdk.d.ts",
|
|
32
|
+
"import": "./dist/ai-sdk.js"
|
|
33
|
+
},
|
|
34
|
+
"./evals": {
|
|
35
|
+
"types": "./dist/evals.d.ts",
|
|
36
|
+
"import": "./dist/evals.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
42
|
+
"test": "bun test tests"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@opentelemetry/api-logs": "0.222.0",
|
|
46
|
+
"@opentelemetry/core": "2.11.0",
|
|
47
|
+
"@opentelemetry/otlp-exporter-base": "0.222.0",
|
|
48
|
+
"@opentelemetry/otlp-transformer": "0.222.0",
|
|
49
|
+
"@opentelemetry/resources": "2.11.0",
|
|
50
|
+
"@opentelemetry/sdk-logs": "0.222.0",
|
|
51
|
+
"@opentelemetry/sdk-trace": "2.11.0",
|
|
52
|
+
"ajv": "8.20.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@ai-sdk/otel": "1.0.99",
|
|
56
|
+
"@opentelemetry/api": "1.9.1",
|
|
57
|
+
"@types/bun": "1.4.2",
|
|
58
|
+
"@types/node": "24.10.1",
|
|
59
|
+
"ai": "7.0.99",
|
|
60
|
+
"protobufjs": "8.8.0",
|
|
61
|
+
"typescript": "7.0.2"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@ai-sdk/otel": "^1.0.99",
|
|
65
|
+
"@opentelemetry/api": "^1.9.1",
|
|
66
|
+
"ai": "^7.0.99"
|
|
67
|
+
},
|
|
68
|
+
"peerDependenciesMeta": {
|
|
69
|
+
"@ai-sdk/otel": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"ai": {
|
|
73
|
+
"optional": true
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"engines": {
|
|
77
|
+
"node": ">=24"
|
|
78
|
+
}
|
|
79
|
+
}
|