@neutrome/open-ai-router 0.6.3 → 0.6.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/package.json +3 -3
- package/src/app/factory.test.ts +2 -2
- package/src/index.ts +1 -0
- package/src/router/error-codec.ts +19 -1
- package/src/router/errors.ts +10 -0
- package/src/router/execution-resolution.ts +28 -2
- package/src/router/provider-invoker.ts +4 -10
- package/src/router/provider-stream.ts +9 -14
- package/src/router/provider-telemetry.ts +13 -4
- package/src/telemetry/chat-normalization.ts +108 -0
- package/src/telemetry/index.ts +1 -0
- package/src/telemetry/posthog.ts +28 -2
- package/src/telemetry/router.ts +175 -224
- package/src/telemetry/timeline.ts +204 -0
- package/src/telemetry/types.ts +18 -0
- package/src/telemetry.test.ts +135 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/open-ai-router",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"hono": "^4.12.18",
|
|
12
12
|
"posthog-node": "^5.41.0",
|
|
13
13
|
"zod": "^4.2.1",
|
|
14
|
-
"@neutrome/lil-engine": "0.4.
|
|
15
|
-
"@neutrome/lilsdk": "0.4.
|
|
14
|
+
"@neutrome/lil-engine": "0.4.2",
|
|
15
|
+
"@neutrome/lilsdk": "0.4.3"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/node": "^25.9.3",
|
package/src/app/factory.test.ts
CHANGED
|
@@ -217,8 +217,8 @@ describe("createRouterApp trace finalization", () => {
|
|
|
217
217
|
expect(waited).toHaveLength(1);
|
|
218
218
|
await waited[0];
|
|
219
219
|
expect(onExecutionSummary).toHaveBeenCalledTimes(1);
|
|
220
|
-
const events = onExecutionSummary.mock.calls[0]?.[0].spans.
|
|
221
|
-
(
|
|
220
|
+
const events = onExecutionSummary.mock.calls[0]?.[0].spans.flatMap((span) =>
|
|
221
|
+
span.timeline.map((entry) => entry.event),
|
|
222
222
|
);
|
|
223
223
|
expect(events).toContain("admission.authenticate");
|
|
224
224
|
expect(events).toContain("upstream_auth.resolve_target");
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,13 @@ export async function toProviderError(
|
|
|
23
23
|
const summary =
|
|
24
24
|
firstError(program)?.message ||
|
|
25
25
|
`Provider ${provider.name} returned ${response.status}`;
|
|
26
|
-
return new ProviderHttpError(
|
|
26
|
+
return new ProviderHttpError(
|
|
27
|
+
summary,
|
|
28
|
+
response.status,
|
|
29
|
+
program,
|
|
30
|
+
errorBody(body),
|
|
31
|
+
body.byteLength,
|
|
32
|
+
);
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export function errorResponse(
|
|
@@ -168,3 +174,15 @@ export function toBody(bytes: Uint8Array): ArrayBuffer {
|
|
|
168
174
|
bytes.byteOffset + bytes.byteLength,
|
|
169
175
|
) as ArrayBuffer;
|
|
170
176
|
}
|
|
177
|
+
|
|
178
|
+
function errorBody(body: Uint8Array): Record<string, unknown> {
|
|
179
|
+
const text = new TextDecoder().decode(body);
|
|
180
|
+
try {
|
|
181
|
+
const value: unknown = JSON.parse(text);
|
|
182
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
183
|
+
? { json: value }
|
|
184
|
+
: { text };
|
|
185
|
+
} catch {
|
|
186
|
+
return { text };
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/router/errors.ts
CHANGED
|
@@ -28,6 +28,14 @@ export type ExecutionErrorDetails = {
|
|
|
28
28
|
transform?: string;
|
|
29
29
|
target?: string;
|
|
30
30
|
source?: "provider" | "executor";
|
|
31
|
+
upstream?: {
|
|
32
|
+
status: number;
|
|
33
|
+
source?: string;
|
|
34
|
+
kind?: string;
|
|
35
|
+
code?: unknown;
|
|
36
|
+
responseBytes?: number;
|
|
37
|
+
body?: Record<string, unknown>;
|
|
38
|
+
};
|
|
31
39
|
};
|
|
32
40
|
|
|
33
41
|
export class ExecutionError extends Error {
|
|
@@ -74,6 +82,8 @@ export class ProviderHttpError extends Error {
|
|
|
74
82
|
message: string,
|
|
75
83
|
readonly status: number,
|
|
76
84
|
readonly program: import("@neutrome/lil-engine").Program,
|
|
85
|
+
readonly body: Record<string, unknown>,
|
|
86
|
+
readonly responseBytes: number,
|
|
77
87
|
) {
|
|
78
88
|
super(message);
|
|
79
89
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import { firstError, type Program } from "@neutrome/lil-engine";
|
|
2
|
+
import {
|
|
3
|
+
ExecutionError,
|
|
4
|
+
isExecutionError,
|
|
5
|
+
ProviderHttpError,
|
|
6
|
+
} from "./errors.ts";
|
|
3
7
|
import { executionTargetId } from "./targets.ts";
|
|
4
8
|
import type {
|
|
5
9
|
ExecutionRuntimeOptions,
|
|
@@ -95,6 +99,28 @@ export function normalizeExecutionError(
|
|
|
95
99
|
maxDepth: number,
|
|
96
100
|
): ExecutionError {
|
|
97
101
|
if (isExecutionError(error)) return error;
|
|
102
|
+
if (error instanceof ProviderHttpError) {
|
|
103
|
+
const upstream = firstError(error.program);
|
|
104
|
+
return new ExecutionError("provider", error.message, {
|
|
105
|
+
stage: fallbackStage,
|
|
106
|
+
details: {
|
|
107
|
+
depth,
|
|
108
|
+
maxDepth,
|
|
109
|
+
target: executionTargetId(target),
|
|
110
|
+
upstream: {
|
|
111
|
+
status: error.status,
|
|
112
|
+
...(upstream?.source ? { source: upstream.source } : {}),
|
|
113
|
+
...(upstream?.kind ? { kind: upstream.kind } : {}),
|
|
114
|
+
...(upstream?.data.code === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { code: upstream.data.code }),
|
|
117
|
+
responseBytes: error.responseBytes,
|
|
118
|
+
body: error.body,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
cause: error,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
98
124
|
if (isAbortLike(error)) {
|
|
99
125
|
return new ExecutionError("cancellation", "Execution aborted", {
|
|
100
126
|
stage: fallbackStage,
|
|
@@ -17,6 +17,7 @@ import type { RequestContext } from "../upstream-auth/types.ts";
|
|
|
17
17
|
import { streamProvider } from "./provider-stream.ts";
|
|
18
18
|
import type { RouterRuntime } from "./runtime.ts";
|
|
19
19
|
import { observeTimedExecution } from "./execution-events.ts";
|
|
20
|
+
import { normalizeChatRequest } from "../telemetry/chat-normalization.ts";
|
|
20
21
|
|
|
21
22
|
const DEFAULT_UPSTREAM_TIMEOUT_MS = 120_000;
|
|
22
23
|
|
|
@@ -54,6 +55,8 @@ export function createFetchProviderInvoker(
|
|
|
54
55
|
provider: provider.name,
|
|
55
56
|
model: providerCtx.target.model,
|
|
56
57
|
bytes: reqBody.byteLength,
|
|
58
|
+
requestBytes: reqBody.byteLength,
|
|
59
|
+
input: normalizeChatRequest(resolvedRequest),
|
|
57
60
|
},
|
|
58
61
|
});
|
|
59
62
|
const reqBytes = reqBody.byteLength;
|
|
@@ -108,12 +111,11 @@ export function createFetchProviderInvoker(
|
|
|
108
111
|
const response = parseProviderResponse(provider.style, bytes);
|
|
109
112
|
emitProviderUsage(
|
|
110
113
|
providerCtx,
|
|
114
|
+
resolvedRequest,
|
|
111
115
|
response,
|
|
112
116
|
provider.name,
|
|
113
117
|
reqBytes,
|
|
114
118
|
bytes.byteLength,
|
|
115
|
-
jsonBody(reqBody),
|
|
116
|
-
jsonBody(bytes),
|
|
117
119
|
);
|
|
118
120
|
return response;
|
|
119
121
|
} catch (error) {
|
|
@@ -134,11 +136,3 @@ export function createFetchProviderInvoker(
|
|
|
134
136
|
},
|
|
135
137
|
};
|
|
136
138
|
}
|
|
137
|
-
|
|
138
|
-
function jsonBody(body: Uint8Array): unknown {
|
|
139
|
-
try {
|
|
140
|
-
return JSON.parse(new TextDecoder().decode(body));
|
|
141
|
-
} catch {
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
@@ -21,6 +21,7 @@ import { isSse } from "../util/headers.ts";
|
|
|
21
21
|
import { eventDataLines, splitSseEvents } from "../util/sse.ts";
|
|
22
22
|
import type { RouterRuntime } from "./runtime.ts";
|
|
23
23
|
import { observeTimedExecution } from "./execution-events.ts";
|
|
24
|
+
import { normalizeChatRequest } from "../telemetry/chat-normalization.ts";
|
|
24
25
|
|
|
25
26
|
const encoder = new TextEncoder();
|
|
26
27
|
const decoder = new TextDecoder();
|
|
@@ -58,6 +59,8 @@ export async function* streamProvider(
|
|
|
58
59
|
provider: provider.name,
|
|
59
60
|
model: providerCtx.target.model,
|
|
60
61
|
bytes: reqBody.byteLength,
|
|
62
|
+
requestBytes: reqBody.byteLength,
|
|
63
|
+
input: normalizeChatRequest(resolvedRequest),
|
|
61
64
|
},
|
|
62
65
|
});
|
|
63
66
|
const reqBytes = reqBody.byteLength;
|
|
@@ -102,7 +105,7 @@ export async function* streamProvider(
|
|
|
102
105
|
let firstByteObserved = false;
|
|
103
106
|
const streamReadStartedMs = Date.now();
|
|
104
107
|
let lastChunkWithUsage: Program | null = null;
|
|
105
|
-
const responseChunks:
|
|
108
|
+
const responseChunks: Program[] = [];
|
|
106
109
|
let respBytes = 0;
|
|
107
110
|
while (true) {
|
|
108
111
|
const { done, value } = await reader.read();
|
|
@@ -137,21 +140,21 @@ export async function* streamProvider(
|
|
|
137
140
|
if (data === "[DONE]") {
|
|
138
141
|
emitProviderUsage(
|
|
139
142
|
providerCtx,
|
|
143
|
+
resolvedRequest,
|
|
140
144
|
lastChunkWithUsage,
|
|
141
145
|
provider.name,
|
|
142
146
|
reqBytes,
|
|
143
147
|
respBytes,
|
|
144
|
-
jsonBody(reqBody),
|
|
145
148
|
responseChunks,
|
|
146
149
|
);
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
152
|
if (data[0] !== "{") continue;
|
|
150
|
-
responseChunks.push(jsonBody(encoder.encode(data)));
|
|
151
153
|
const chunk = parseProviderStreamChunk(
|
|
152
154
|
provider.style,
|
|
153
155
|
encoder.encode(data),
|
|
154
156
|
);
|
|
157
|
+
responseChunks.push(chunk);
|
|
155
158
|
if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
|
|
156
159
|
yield chunk;
|
|
157
160
|
}
|
|
@@ -163,32 +166,32 @@ export async function* streamProvider(
|
|
|
163
166
|
if (data === "[DONE]") {
|
|
164
167
|
emitProviderUsage(
|
|
165
168
|
providerCtx,
|
|
169
|
+
resolvedRequest,
|
|
166
170
|
lastChunkWithUsage,
|
|
167
171
|
provider.name,
|
|
168
172
|
reqBytes,
|
|
169
173
|
respBytes,
|
|
170
|
-
jsonBody(reqBody),
|
|
171
174
|
responseChunks,
|
|
172
175
|
);
|
|
173
176
|
return;
|
|
174
177
|
}
|
|
175
178
|
if (data[0] !== "{") continue;
|
|
176
|
-
responseChunks.push(jsonBody(encoder.encode(data)));
|
|
177
179
|
const chunk = parseProviderStreamChunk(
|
|
178
180
|
provider.style,
|
|
179
181
|
encoder.encode(data),
|
|
180
182
|
);
|
|
183
|
+
responseChunks.push(chunk);
|
|
181
184
|
if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
|
|
182
185
|
yield chunk;
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
188
|
emitProviderUsage(
|
|
186
189
|
providerCtx,
|
|
190
|
+
resolvedRequest,
|
|
187
191
|
lastChunkWithUsage,
|
|
188
192
|
provider.name,
|
|
189
193
|
reqBytes,
|
|
190
194
|
respBytes,
|
|
191
|
-
jsonBody(reqBody),
|
|
192
195
|
responseChunks,
|
|
193
196
|
);
|
|
194
197
|
} catch (error) {
|
|
@@ -199,11 +202,3 @@ export async function* streamProvider(
|
|
|
199
202
|
timed.cleanup();
|
|
200
203
|
}
|
|
201
204
|
}
|
|
202
|
-
|
|
203
|
-
function jsonBody(body: Uint8Array): unknown {
|
|
204
|
-
try {
|
|
205
|
-
return JSON.parse(new TextDecoder().decode(body));
|
|
206
|
-
} catch {
|
|
207
|
-
return undefined;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { usageObject, type Program } from "@neutrome/lil-engine";
|
|
2
2
|
import { createExecutionEvent } from "@neutrome/lilsdk";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
normalizeChatRequest,
|
|
6
|
+
normalizeChatResponse,
|
|
7
|
+
normalizeChatStream,
|
|
8
|
+
} from "../telemetry/chat-normalization.ts";
|
|
4
9
|
import type { ProviderInvocationContext } from "./execution-types.ts";
|
|
5
10
|
|
|
6
11
|
const encoder = new TextEncoder();
|
|
@@ -9,12 +14,12 @@ const TokenCountSchema = z.number().finite().nonnegative();
|
|
|
9
14
|
|
|
10
15
|
export function emitProviderUsage(
|
|
11
16
|
providerCtx: ProviderInvocationContext,
|
|
17
|
+
request: Program,
|
|
12
18
|
response: Program | null,
|
|
13
19
|
provider: string,
|
|
14
20
|
requestBytes: number,
|
|
15
21
|
responseBytes: number,
|
|
16
|
-
|
|
17
|
-
output?: unknown,
|
|
22
|
+
streamChunks?: readonly Program[],
|
|
18
23
|
): void {
|
|
19
24
|
const usage = response ? usageObject(response) : undefined;
|
|
20
25
|
const u = (usage && typeof usage === "object" ? usage : {}) as Record<
|
|
@@ -43,8 +48,12 @@ export function emitProviderUsage(
|
|
|
43
48
|
cachedTokensInput,
|
|
44
49
|
requestBytes,
|
|
45
50
|
responseBytes,
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
input: normalizeChatRequest(request),
|
|
52
|
+
output: streamChunks
|
|
53
|
+
? normalizeChatStream(streamChunks)
|
|
54
|
+
: response
|
|
55
|
+
? normalizeChatResponse(response)
|
|
56
|
+
: undefined,
|
|
48
57
|
},
|
|
49
58
|
}),
|
|
50
59
|
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeStreamToolDelta,
|
|
3
|
+
emitChatCompletionsRequest,
|
|
4
|
+
emitChatCompletionsResponse,
|
|
5
|
+
type Program,
|
|
6
|
+
Opcode,
|
|
7
|
+
usageObject,
|
|
8
|
+
} from "@neutrome/lil-engine";
|
|
9
|
+
|
|
10
|
+
const decoder = new TextDecoder();
|
|
11
|
+
|
|
12
|
+
/** The trace wire format is always a non-streaming Chat Completions payload. */
|
|
13
|
+
export function normalizeChatRequest(
|
|
14
|
+
program: Program,
|
|
15
|
+
): Record<string, unknown> {
|
|
16
|
+
const request = json(emitChatCompletionsRequest(program));
|
|
17
|
+
delete request.stream;
|
|
18
|
+
return request;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeChatResponse(
|
|
22
|
+
program: Program,
|
|
23
|
+
): Record<string, unknown> {
|
|
24
|
+
return json(emitChatCompletionsResponse(program));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizeChatStream(
|
|
28
|
+
chunks: readonly Program[],
|
|
29
|
+
): Record<string, unknown> {
|
|
30
|
+
const content: string[] = [];
|
|
31
|
+
const reasoning: string[] = [];
|
|
32
|
+
const tools = new Map<
|
|
33
|
+
number,
|
|
34
|
+
{ id?: string; name?: string; arguments: string }
|
|
35
|
+
>();
|
|
36
|
+
let id: string | undefined;
|
|
37
|
+
let model: string | undefined;
|
|
38
|
+
let finishReason = "stop";
|
|
39
|
+
let usage: unknown;
|
|
40
|
+
|
|
41
|
+
for (const chunk of chunks) {
|
|
42
|
+
const chunkUsage = usageObject(chunk);
|
|
43
|
+
if (chunkUsage !== undefined) usage = chunkUsage;
|
|
44
|
+
for (const instruction of chunk.code) {
|
|
45
|
+
if (
|
|
46
|
+
instruction.opcode === Opcode.RESP_ID &&
|
|
47
|
+
instruction.value.kind === "string"
|
|
48
|
+
)
|
|
49
|
+
id = instruction.value.value;
|
|
50
|
+
if (
|
|
51
|
+
instruction.opcode === Opcode.RESP_MODEL &&
|
|
52
|
+
instruction.value.kind === "string"
|
|
53
|
+
)
|
|
54
|
+
model = instruction.value.value;
|
|
55
|
+
if (
|
|
56
|
+
instruction.opcode === Opcode.RESP_DONE &&
|
|
57
|
+
instruction.value.kind === "string"
|
|
58
|
+
)
|
|
59
|
+
finishReason = instruction.value.value;
|
|
60
|
+
if (
|
|
61
|
+
instruction.opcode === Opcode.STREAM_DELTA &&
|
|
62
|
+
instruction.value.kind === "string"
|
|
63
|
+
)
|
|
64
|
+
content.push(instruction.value.value);
|
|
65
|
+
if (
|
|
66
|
+
instruction.opcode === Opcode.STREAM_THINK_DELTA &&
|
|
67
|
+
instruction.value.kind === "string"
|
|
68
|
+
)
|
|
69
|
+
reasoning.push(instruction.value.value);
|
|
70
|
+
if (instruction.opcode !== Opcode.STREAM_TOOL_DELTA) continue;
|
|
71
|
+
const delta = decodeStreamToolDelta(instruction);
|
|
72
|
+
if (!delta) continue;
|
|
73
|
+
const tool = tools.get(delta.index) ?? { arguments: "" };
|
|
74
|
+
if (delta.id !== undefined) tool.id = delta.id;
|
|
75
|
+
if (delta.name !== undefined) tool.name = delta.name;
|
|
76
|
+
if (delta.arguments !== undefined) tool.arguments += delta.arguments;
|
|
77
|
+
tools.set(delta.index, tool);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const message: Record<string, unknown> = {
|
|
82
|
+
role: "assistant",
|
|
83
|
+
content: content.join("") || null,
|
|
84
|
+
};
|
|
85
|
+
if (reasoning.length > 0) message.reasoning_content = reasoning.join("");
|
|
86
|
+
if (tools.size > 0) {
|
|
87
|
+
message.tool_calls = [...tools.entries()].map(([index, tool]) => ({
|
|
88
|
+
index,
|
|
89
|
+
...(tool.id ? { id: tool.id } : {}),
|
|
90
|
+
type: "function",
|
|
91
|
+
function: {
|
|
92
|
+
...(tool.name ? { name: tool.name } : {}),
|
|
93
|
+
arguments: tool.arguments,
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
object: "chat.completion",
|
|
99
|
+
...(id ? { id } : {}),
|
|
100
|
+
...(model ? { model } : {}),
|
|
101
|
+
choices: [{ index: 0, message, finish_reason: finishReason }],
|
|
102
|
+
...(usage === undefined ? {} : { usage }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function json(bytes: Uint8Array): Record<string, unknown> {
|
|
107
|
+
return JSON.parse(decoder.decode(bytes)) as Record<string, unknown>;
|
|
108
|
+
}
|
package/src/telemetry/index.ts
CHANGED
package/src/telemetry/posthog.ts
CHANGED
|
@@ -33,6 +33,14 @@ export class PosthogTraceTelemetry implements TraceTelemetryDriver {
|
|
|
33
33
|
...(generation.model ? { model: generation.model } : {}),
|
|
34
34
|
input: generation.input ?? null,
|
|
35
35
|
output: generation.output ?? null,
|
|
36
|
+
...(generation.httpStatus === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { httpStatus: generation.httpStatus }),
|
|
39
|
+
...(generation.error
|
|
40
|
+
? {
|
|
41
|
+
error: namedError(generation.error.name, generation.error.message),
|
|
42
|
+
}
|
|
43
|
+
: {}),
|
|
36
44
|
latency: elapsedSeconds(generation.startedAt, generation.finishedAt),
|
|
37
45
|
...(generation.timeToFirstTokenMs === undefined
|
|
38
46
|
? {}
|
|
@@ -48,6 +56,12 @@ export class PosthogTraceTelemetry implements TraceTelemetryDriver {
|
|
|
48
56
|
"lil.cached_input_tokens": generation.cachedTokensInput || undefined,
|
|
49
57
|
"lil.request_bytes": generation.requestBytes || undefined,
|
|
50
58
|
"lil.response_bytes": generation.responseBytes || undefined,
|
|
59
|
+
"lil.error_kind": generation.error?.kind,
|
|
60
|
+
"lil.error_stage": generation.error?.stage,
|
|
61
|
+
"lil.error_status": generation.error?.status,
|
|
62
|
+
"lil.error_code": generation.error?.code,
|
|
63
|
+
"lil.error_details": generation.error?.details,
|
|
64
|
+
"lil.upstream_error_body": generation.error?.details?.body,
|
|
51
65
|
}),
|
|
52
66
|
captureImmediate: true,
|
|
53
67
|
});
|
|
@@ -62,12 +76,18 @@ export class PosthogErrorTelemetry implements ErrorTelemetryDriver {
|
|
|
62
76
|
|
|
63
77
|
async recordError(error: TelemetryError): Promise<void> {
|
|
64
78
|
await this.client.captureExceptionImmediate(
|
|
65
|
-
|
|
79
|
+
namedError(error.name ?? "RouterError", error.message),
|
|
66
80
|
this.distinctId,
|
|
67
81
|
{
|
|
68
82
|
"lil.trace_id": error.traceId,
|
|
69
83
|
"lil.request_id": error.requestId,
|
|
70
84
|
"lil.execution_id": error.executionId,
|
|
85
|
+
"lil.error_kind": error.kind,
|
|
86
|
+
"lil.error_stage": error.stage,
|
|
87
|
+
"lil.error_status": error.status,
|
|
88
|
+
"lil.error_code": error.code,
|
|
89
|
+
"lil.error_details": error.details,
|
|
90
|
+
"lil.upstream_error_body": error.details?.body,
|
|
71
91
|
},
|
|
72
92
|
);
|
|
73
93
|
}
|
|
@@ -101,8 +121,14 @@ function elapsedSeconds(startedAt: string, finishedAt: string): number {
|
|
|
101
121
|
return Math.max(0, Date.parse(finishedAt) - Date.parse(startedAt)) / 1_000;
|
|
102
122
|
}
|
|
103
123
|
|
|
104
|
-
function compact(values: Record<string,
|
|
124
|
+
function compact(values: Record<string, unknown>) {
|
|
105
125
|
return Object.fromEntries(
|
|
106
126
|
Object.entries(values).filter(([, value]) => value !== undefined),
|
|
107
127
|
);
|
|
108
128
|
}
|
|
129
|
+
|
|
130
|
+
function namedError(name: string, message: string): Error {
|
|
131
|
+
const error = new Error(message);
|
|
132
|
+
error.name = name;
|
|
133
|
+
return error;
|
|
134
|
+
}
|