pi-langfuse 1.4.4 → 1.4.7
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/index.ts +15 -0
- package/package.json +4 -2
- package/src/commands.ts +284 -0
- package/src/config.ts +5 -4
- package/src/langfuse.ts +62 -14
package/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
13
13
|
import { state, resetRunState, runWithSession, setCurrentSession } from "./src/state.js";
|
|
14
14
|
import { ensureConfig, promptForConfig, loadConfig } from "./src/config.js";
|
|
15
15
|
import { shutdownRuntime } from "./src/langfuse.js";
|
|
16
|
+
import { handleLangfusePrivacyCommand, handleLangfuseTestCommand } from "./src/commands.js";
|
|
16
17
|
import { getMessageFromEvent, extractAssistantOutput, getCapturePolicy } from "./src/utils.js";
|
|
17
18
|
import { applyCapturePolicy } from "./src/capture-policy.js";
|
|
18
19
|
import { startAgentRun, finishAgentRun } from "./src/handlers/agent.js";
|
|
@@ -52,6 +53,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
52
53
|
},
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
pi.registerCommand("langfuse-test", {
|
|
57
|
+
description: "Send a test trace to Langfuse to verify configuration",
|
|
58
|
+
handler: async (args, ctx) => {
|
|
59
|
+
await handleLangfuseTestCommand(String(args ?? ""), ctx);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
pi.registerCommand("langfuse-privacy", {
|
|
64
|
+
description: "View or set Langfuse telemetry privacy preset",
|
|
65
|
+
handler: async (args, ctx) => {
|
|
66
|
+
await handleLangfusePrivacyCommand(String(args ?? ""), ctx);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
55
70
|
const getSessionId = (ctx?: any) => {
|
|
56
71
|
try {
|
|
57
72
|
const sessionFile = ctx?.sessionManager?.getSessionFile?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-langfuse",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
4
4
|
"description": "Langfuse extension for Pi coding agent",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"tsconfig.json"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
-
"typecheck": "tsc --noEmit"
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "tsx --test test/*.test.ts"
|
|
28
29
|
},
|
|
29
30
|
"keywords": [
|
|
30
31
|
"pi-package",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"node": ">=22"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
64
|
+
"tsx": "^4.19.0",
|
|
63
65
|
"typescript": "^6.0.3"
|
|
64
66
|
}
|
|
65
67
|
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { CONFIG_PATH } from "./constants.js";
|
|
4
|
+
import { loadConfig, saveConfig, ensureConfig } from "./config.js";
|
|
5
|
+
import { createCapturePolicy, type PrivacyPreset, type CapturePolicy } from "./capture-policy.js";
|
|
6
|
+
import { getRuntime, forceShutdownRuntime as shutdownLangfuseRuntime } from "./langfuse.js";
|
|
7
|
+
import { state } from "./state.js";
|
|
8
|
+
import type { LangfuseRuntime } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const PRIVACY_PRESETS = ["metadata-only", "prompts-only", "conversations", "full-debug"] as const;
|
|
11
|
+
|
|
12
|
+
export interface CommandContextLike {
|
|
13
|
+
hasUI?: boolean;
|
|
14
|
+
ui?: {
|
|
15
|
+
notify?: (message: string, level?: "info" | "warning" | "error") => void;
|
|
16
|
+
select?: (title: string, options: string[]) => Promise<string | undefined>;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CommandDeps {
|
|
21
|
+
configPath?: string;
|
|
22
|
+
getRuntime?: () => Promise<LangfuseRuntime>;
|
|
23
|
+
forceShutdownRuntime?: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function notify(ctx: CommandContextLike, message: string, level: "info" | "warning" | "error" = "info") {
|
|
27
|
+
if (ctx.hasUI && ctx.ui?.notify) {
|
|
28
|
+
ctx.ui.notify(message, level);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const prefix = level === "error" ? "❌" : level === "warning" ? "⚠️" : "📊";
|
|
32
|
+
console.log(`${prefix} Langfuse: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseCommandArgs(args: string): { values: Record<string, string>; positional: string[]; malformed: string[] } {
|
|
36
|
+
const values: Record<string, string> = {};
|
|
37
|
+
const positional: string[] = [];
|
|
38
|
+
const malformed: string[] = [];
|
|
39
|
+
|
|
40
|
+
for (const part of args.trim().split(/\s+/)) {
|
|
41
|
+
if (!part) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const eq = part.indexOf("=");
|
|
45
|
+
if (eq === -1) {
|
|
46
|
+
positional.push(part);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (eq === 0) {
|
|
50
|
+
malformed.push(part);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
values[part.slice(0, eq)] = part.slice(eq + 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { values, positional, malformed };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isPrivacyPreset(value: string | undefined): value is PrivacyPreset {
|
|
60
|
+
return PRIVACY_PRESETS.includes(value as PrivacyPreset);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function inferPreset(policy: CapturePolicy): PrivacyPreset | "custom" {
|
|
64
|
+
const entries: Array<[PrivacyPreset, CapturePolicy]> = [
|
|
65
|
+
[
|
|
66
|
+
"metadata-only",
|
|
67
|
+
{
|
|
68
|
+
captureInputs: false,
|
|
69
|
+
captureOutputs: false,
|
|
70
|
+
captureToolIo: false,
|
|
71
|
+
captureSystemPrompt: false,
|
|
72
|
+
captureCwd: false,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"prompts-only",
|
|
77
|
+
{
|
|
78
|
+
captureInputs: true,
|
|
79
|
+
captureOutputs: false,
|
|
80
|
+
captureToolIo: false,
|
|
81
|
+
captureSystemPrompt: false,
|
|
82
|
+
captureCwd: false,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
"conversations",
|
|
87
|
+
{
|
|
88
|
+
captureInputs: true,
|
|
89
|
+
captureOutputs: true,
|
|
90
|
+
captureToolIo: false,
|
|
91
|
+
captureSystemPrompt: false,
|
|
92
|
+
captureCwd: false,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
"full-debug",
|
|
97
|
+
{
|
|
98
|
+
captureInputs: true,
|
|
99
|
+
captureOutputs: true,
|
|
100
|
+
captureToolIo: true,
|
|
101
|
+
captureSystemPrompt: true,
|
|
102
|
+
captureCwd: true,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const [preset, presetPolicy] of entries) {
|
|
108
|
+
if (
|
|
109
|
+
policy.captureInputs === presetPolicy.captureInputs &&
|
|
110
|
+
policy.captureOutputs === presetPolicy.captureOutputs &&
|
|
111
|
+
policy.captureToolIo === presetPolicy.captureToolIo &&
|
|
112
|
+
policy.captureSystemPrompt === presetPolicy.captureSystemPrompt &&
|
|
113
|
+
policy.captureCwd === presetPolicy.captureCwd
|
|
114
|
+
) {
|
|
115
|
+
return preset;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return "custom";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function describePolicy(policy: CapturePolicy) {
|
|
122
|
+
return [
|
|
123
|
+
`captureInputs: ${policy.captureInputs}`,
|
|
124
|
+
`captureOutputs: ${policy.captureOutputs}`,
|
|
125
|
+
`captureToolIo: ${policy.captureToolIo}`,
|
|
126
|
+
`captureSystemPrompt: ${policy.captureSystemPrompt}`,
|
|
127
|
+
`captureCwd: ${policy.captureCwd}`,
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readPersistedConfig(path: string) {
|
|
132
|
+
if (!existsSync(path)) {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
|
|
137
|
+
} catch {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hasActiveAgentObservation() {
|
|
143
|
+
for (const sessionState of state.sessionStates.values()) {
|
|
144
|
+
if (sessionState.agentState?.root) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function savePrivacyPreset(
|
|
152
|
+
requestedPreset: PrivacyPreset,
|
|
153
|
+
ctx: CommandContextLike,
|
|
154
|
+
configPath: string,
|
|
155
|
+
): boolean {
|
|
156
|
+
const existing = readPersistedConfig(configPath);
|
|
157
|
+
const loaded = state.config ?? loadConfig(process.env, configPath);
|
|
158
|
+
|
|
159
|
+
const publicKey = existing.publicKey ?? loaded?.publicKey;
|
|
160
|
+
const secretKey = existing.secretKey ?? loaded?.secretKey;
|
|
161
|
+
const host = existing.host ?? loaded?.host;
|
|
162
|
+
|
|
163
|
+
if (!publicKey || !secretKey || !host) {
|
|
164
|
+
notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup before changing privacy settings.", "warning");
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const nextConfig = {
|
|
169
|
+
publicKey: String(publicKey),
|
|
170
|
+
secretKey: String(secretKey),
|
|
171
|
+
host: String(host),
|
|
172
|
+
privacyPreset: requestedPreset,
|
|
173
|
+
};
|
|
174
|
+
saveConfig(nextConfig, configPath);
|
|
175
|
+
state.config = loadConfig(process.env, configPath);
|
|
176
|
+
|
|
177
|
+
notify(ctx, `Langfuse privacy preset saved: ${requestedPreset}\n${describePolicy(state.config?.capturePolicy ?? createCapturePolicy())}`);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function handleLangfusePrivacyCommand(
|
|
182
|
+
args: string,
|
|
183
|
+
ctx: CommandContextLike,
|
|
184
|
+
deps: CommandDeps = {},
|
|
185
|
+
): Promise<boolean> {
|
|
186
|
+
const configPath = deps.configPath ?? CONFIG_PATH;
|
|
187
|
+
const parsed = parseCommandArgs(args);
|
|
188
|
+
if (parsed.malformed.length > 0) {
|
|
189
|
+
notify(ctx, `Couldn't understand '${parsed.malformed[0]}'. Use /langfuse-privacy preset=metadata-only.`, "warning");
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const requestedPreset = parsed.values.preset ?? parsed.positional[0];
|
|
194
|
+
if (!requestedPreset) {
|
|
195
|
+
state.config = state.config ?? loadConfig(process.env, configPath);
|
|
196
|
+
const policy = state.config?.capturePolicy ?? createCapturePolicy();
|
|
197
|
+
if (ctx.hasUI && ctx.ui?.select) {
|
|
198
|
+
const currentPreset = inferPreset(policy);
|
|
199
|
+
const selectedPreset = await ctx.ui.select(
|
|
200
|
+
`Langfuse privacy preset (current: ${currentPreset})`,
|
|
201
|
+
[...PRIVACY_PRESETS],
|
|
202
|
+
);
|
|
203
|
+
if (!selectedPreset) {
|
|
204
|
+
notify(ctx, `Current Langfuse privacy preset: ${currentPreset}\n${describePolicy(policy)}`);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
if (!isPrivacyPreset(selectedPreset)) {
|
|
208
|
+
notify(ctx, `Unknown privacy preset '${selectedPreset}'. Use one of: ${PRIVACY_PRESETS.join(", ")}.`, "warning");
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return savePrivacyPreset(selectedPreset, ctx, configPath);
|
|
212
|
+
}
|
|
213
|
+
notify(ctx, `Current Langfuse privacy preset: ${inferPreset(policy)}\n${describePolicy(policy)}`);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!isPrivacyPreset(requestedPreset)) {
|
|
218
|
+
notify(
|
|
219
|
+
ctx,
|
|
220
|
+
`Unknown privacy preset '${requestedPreset}'. Use one of: ${PRIVACY_PRESETS.join(", ")}.`,
|
|
221
|
+
"warning",
|
|
222
|
+
);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return savePrivacyPreset(requestedPreset, ctx, configPath);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function handleLangfuseTestCommand(
|
|
230
|
+
_args: string,
|
|
231
|
+
ctx: CommandContextLike,
|
|
232
|
+
deps: CommandDeps = {},
|
|
233
|
+
): Promise<boolean> {
|
|
234
|
+
if (!state.config && !(await ensureConfig(ctx))) {
|
|
235
|
+
notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup first.", "warning");
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (hasActiveAgentObservation()) {
|
|
240
|
+
notify(ctx, "Langfuse test skipped because an agent run is active. Try again after the run finishes.", "warning");
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let runtimeInitialized = false;
|
|
245
|
+
try {
|
|
246
|
+
const rt = await (deps.getRuntime ?? getRuntime)();
|
|
247
|
+
runtimeInitialized = true;
|
|
248
|
+
rt.propagateAttributes(
|
|
249
|
+
{
|
|
250
|
+
traceName: "pi-langfuse-test",
|
|
251
|
+
metadata: {
|
|
252
|
+
source: "pi-langfuse",
|
|
253
|
+
command: "langfuse-test",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
() => {
|
|
257
|
+
const observation = rt.startObservation(
|
|
258
|
+
"pi-langfuse-test",
|
|
259
|
+
{
|
|
260
|
+
input: { command: "/langfuse-test" },
|
|
261
|
+
output: "ok",
|
|
262
|
+
metadata: {
|
|
263
|
+
source: "pi-langfuse",
|
|
264
|
+
command: "langfuse-test",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{ asType: "span" },
|
|
268
|
+
);
|
|
269
|
+
observation.end();
|
|
270
|
+
return observation;
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
notify(ctx, `Langfuse test succeeded. Test trace sent to ${state.config?.host}.`);
|
|
274
|
+
return true;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
277
|
+
notify(ctx, `Langfuse test failed: ${message}`, "error");
|
|
278
|
+
return false;
|
|
279
|
+
} finally {
|
|
280
|
+
if (runtimeInitialized) {
|
|
281
|
+
await (deps.forceShutdownRuntime ?? shutdownLangfuseRuntime)();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
2
3
|
import type { Config } from "./types.js";
|
|
3
|
-
import {
|
|
4
|
+
import { CONFIG_PATH, DEFAULT_LANGFUSE_HOST } from "./constants.js";
|
|
4
5
|
import { state } from "./state.js";
|
|
5
6
|
import { forceShutdownRuntime } from "./langfuse.js";
|
|
6
7
|
import { createCapturePolicy, type EnvLike } from "./capture-policy.js";
|
|
@@ -50,9 +51,9 @@ export function loadConfig(env: EnvLike = process.env as EnvLike, path = CONFIG_
|
|
|
50
51
|
return loadConfigFromFile(path, env) || loadConfigFromEnv(env);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export function saveConfig(config: Config) {
|
|
54
|
-
mkdirSync(
|
|
55
|
-
writeFileSync(
|
|
54
|
+
export function saveConfig(config: Config, path = CONFIG_PATH) {
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
async function collectConfigFromUI(ctx: any, reason: string): Promise<Config | null> {
|
package/src/langfuse.ts
CHANGED
|
@@ -45,6 +45,9 @@ interface RestFallbackStore {
|
|
|
45
45
|
|
|
46
46
|
const OTEL_VISIBILITY_TIMEOUT_MS = 1_500;
|
|
47
47
|
const OTEL_VISIBILITY_POLL_INTERVAL_MS = 200;
|
|
48
|
+
const DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS = 2_000;
|
|
49
|
+
|
|
50
|
+
let shutdownStepTimeoutMs = DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS;
|
|
48
51
|
|
|
49
52
|
function nowIso() {
|
|
50
53
|
return new Date().toISOString();
|
|
@@ -54,6 +57,29 @@ function delay(ms: number) {
|
|
|
54
57
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
async function withTimeout<T>(label: string, operation: Promise<T> | undefined): Promise<T | undefined> {
|
|
61
|
+
if (!operation) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
66
|
+
try {
|
|
67
|
+
return await Promise.race([
|
|
68
|
+
operation,
|
|
69
|
+
new Promise<undefined>((resolve) => {
|
|
70
|
+
timeout = setTimeout(() => {
|
|
71
|
+
console.log(`📊 Langfuse: ${label} timed out after ${shutdownStepTimeoutMs}ms`);
|
|
72
|
+
resolve(undefined);
|
|
73
|
+
}, shutdownStepTimeoutMs);
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
} finally {
|
|
77
|
+
if (timeout) {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
57
83
|
function toIso(value: unknown): string | undefined {
|
|
58
84
|
if (!value) {
|
|
59
85
|
return undefined;
|
|
@@ -185,7 +211,10 @@ async function traceExists(rt: LangfuseRuntime, traceId: string): Promise<boolea
|
|
|
185
211
|
if (!getTrace) {
|
|
186
212
|
return false;
|
|
187
213
|
}
|
|
188
|
-
await getTrace(traceId);
|
|
214
|
+
const trace = await withTimeout("Trace visibility check", getTrace(traceId));
|
|
215
|
+
if (!trace) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
189
218
|
return true;
|
|
190
219
|
} catch {
|
|
191
220
|
return false;
|
|
@@ -272,21 +301,34 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
|
272
301
|
});
|
|
273
302
|
}
|
|
274
303
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
304
|
+
const ingestionBatch = rt.scoreClient.api?.ingestion?.batch;
|
|
305
|
+
if (!ingestionBatch) {
|
|
306
|
+
console.log("📊 Langfuse: REST fallback ingestion is unavailable");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const response = await withTimeout(
|
|
311
|
+
"REST fallback ingestion",
|
|
312
|
+
ingestionBatch({
|
|
313
|
+
batch,
|
|
314
|
+
metadata: {
|
|
315
|
+
source: "pi-langfuse",
|
|
316
|
+
fallback: "rest-ingestion",
|
|
317
|
+
reason: "otel-trace-not-visible-after-flush",
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (!response) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
283
325
|
|
|
284
326
|
const responseBody = response as { errors?: unknown[] } | undefined;
|
|
285
327
|
const errors = Array.isArray(responseBody?.errors) ? responseBody.errors : [];
|
|
286
328
|
if (errors.length > 0) {
|
|
287
329
|
console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
|
|
288
330
|
} else {
|
|
289
|
-
console.
|
|
331
|
+
console.log(`📊 Langfuse: OTel trace ${trace.id} was not visible; wrote fallback trace via REST ingestion`);
|
|
290
332
|
}
|
|
291
333
|
}
|
|
292
334
|
|
|
@@ -356,11 +398,11 @@ function doShutdownRuntime(): Promise<void> {
|
|
|
356
398
|
runtime = null;
|
|
357
399
|
|
|
358
400
|
try {
|
|
359
|
-
await rt.tracerProvider?.forceFlush?.();
|
|
401
|
+
await withTimeout("OTel force flush", rt.tracerProvider?.forceFlush?.());
|
|
360
402
|
await fallbackToRestIngestion(rt);
|
|
361
|
-
await rt.scoreClient.flush?.();
|
|
362
|
-
await rt.scoreClient.shutdown?.();
|
|
363
|
-
await rt.tracerProvider?.shutdown?.();
|
|
403
|
+
await withTimeout("Langfuse score flush", rt.scoreClient.flush?.());
|
|
404
|
+
await withTimeout("Langfuse client shutdown", rt.scoreClient.shutdown?.());
|
|
405
|
+
await withTimeout("OTel tracer shutdown", rt.tracerProvider?.shutdown?.());
|
|
364
406
|
} catch (e) {
|
|
365
407
|
console.warn("📊 Langfuse: Failed to flush/shutdown cleanly", e);
|
|
366
408
|
} finally {
|
|
@@ -400,6 +442,12 @@ export async function forceShutdownRuntime(): Promise<void> {
|
|
|
400
442
|
await doShutdownRuntime();
|
|
401
443
|
}
|
|
402
444
|
|
|
445
|
+
export function __setRuntimeForTest(rt: LangfuseRuntime | null, timeoutMs = DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS): void {
|
|
446
|
+
runtime = rt;
|
|
447
|
+
shutdownStepTimeoutMs = timeoutMs;
|
|
448
|
+
activeSessions.clear();
|
|
449
|
+
}
|
|
450
|
+
|
|
403
451
|
export async function sendScore(name: string, value: number, options: { traceId?: string; observationId?: string } = {}) {
|
|
404
452
|
try {
|
|
405
453
|
const rt = await getRuntime();
|