@narumitw/pi-langfuse 0.50.4 → 0.50.6

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/src/runtime.ts CHANGED
@@ -1,281 +1,264 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { LangfuseSpanProcessor } from "@langfuse/otel";
3
3
  import {
4
- type LangfuseObservation,
5
- type LangfuseObservationAttributes,
6
- LangfuseOtelSpanAttributes,
7
- setLangfuseTracerProvider,
8
- startObservation,
4
+ type LangfuseObservation,
5
+ type LangfuseObservationAttributes,
6
+ LangfuseOtelSpanAttributes,
7
+ setLangfuseTracerProvider,
8
+ startObservation,
9
9
  } from "@langfuse/tracing";
10
10
  import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
11
11
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
12
12
  import type { LangfuseConfig } from "./config.js";
13
- import type {
14
- Observation,
15
- ObservationAttributes,
16
- ObservationType,
17
- TraceBackend,
18
- } from "./tracing.js";
13
+ import type { Observation, ObservationAttributes, ObservationType, TraceBackend } from "./tracing.js";
19
14
 
20
15
  const RUNTIME_KEY = Symbol.for("@narumitw/pi-langfuse/runtime/v1");
21
16
 
22
17
  interface SharedRuntime {
23
- fingerprint: string;
24
- backend: ProductionTraceBackend;
25
- shutdown: boolean;
18
+ fingerprint: string;
19
+ backend: ProductionTraceBackend;
20
+ shutdown: boolean;
26
21
  }
27
22
 
28
23
  type GlobalWithRuntime = typeof globalThis & {
29
- [RUNTIME_KEY]?: Promise<SharedRuntime>;
24
+ [RUNTIME_KEY]?: Promise<SharedRuntime>;
30
25
  };
31
26
 
32
27
  export interface RuntimeFactories {
33
- createProcessor(config: LangfuseConfig): SpanProcessor;
34
- createProvider(processor: SpanProcessor): NodeTracerProvider;
35
- selectProvider(provider: NodeTracerProvider): void;
28
+ createProcessor(config: LangfuseConfig): SpanProcessor;
29
+ createProvider(processor: SpanProcessor): NodeTracerProvider;
30
+ selectProvider(provider: NodeTracerProvider): void;
36
31
  }
37
32
 
38
33
  class ProductionObservation implements Observation {
39
- constructor(readonly native: LangfuseObservation) {}
34
+ constructor(readonly native: LangfuseObservation) {}
40
35
 
41
- update(attributes: ObservationAttributes): Observation {
42
- const { sessionId, userId, ...observationAttributes } = attributes;
43
- this.native.updateOtelSpanAttributes(observationAttributes as LangfuseObservationAttributes);
44
- applySessionId(this.native, sessionId);
45
- applyUserId(this.native, userId);
46
- return this;
47
- }
36
+ update(attributes: ObservationAttributes): Observation {
37
+ const { sessionId, userId, ...observationAttributes } = attributes;
38
+ this.native.updateOtelSpanAttributes(observationAttributes as LangfuseObservationAttributes);
39
+ applySessionId(this.native, sessionId);
40
+ applyUserId(this.native, userId);
41
+ return this;
42
+ }
48
43
 
49
- updateTrace(attributes: ObservationAttributes): Observation {
50
- const { input, output, metadata, name, sessionId, userId, tags, version } = attributes;
51
- if (input !== undefined || output !== undefined) {
52
- this.native.setTraceIO({ input, output });
53
- }
54
- if (name !== undefined) {
55
- this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_NAME, name);
56
- }
57
- applySessionId(this.native, sessionId);
58
- applyUserId(this.native, userId);
59
- if (tags !== undefined) {
60
- this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_TAGS, tags);
61
- }
62
- if (version !== undefined) {
63
- this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.VERSION, version);
64
- }
65
- for (const [key, value] of Object.entries(metadata ?? {})) {
66
- this.native.otelSpan.setAttribute(
67
- `${LangfuseOtelSpanAttributes.TRACE_METADATA}.${key}`,
68
- String(value),
69
- );
70
- }
71
- return this;
72
- }
44
+ updateTrace(attributes: ObservationAttributes): Observation {
45
+ const { input, output, metadata, name, sessionId, userId, tags, version } = attributes;
46
+ if (input !== undefined || output !== undefined) {
47
+ this.native.setTraceIO({ input, output });
48
+ }
49
+ if (name !== undefined) {
50
+ this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_NAME, name);
51
+ }
52
+ applySessionId(this.native, sessionId);
53
+ applyUserId(this.native, userId);
54
+ if (tags !== undefined) {
55
+ this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_TAGS, tags);
56
+ }
57
+ if (version !== undefined) {
58
+ this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.VERSION, version);
59
+ }
60
+ for (const [key, value] of Object.entries(metadata ?? {})) {
61
+ this.native.otelSpan.setAttribute(`${LangfuseOtelSpanAttributes.TRACE_METADATA}.${key}`, String(value));
62
+ }
63
+ return this;
64
+ }
73
65
 
74
- end(endTime?: number): Observation {
75
- this.native.end(endTime);
76
- return this;
77
- }
66
+ end(endTime?: number): Observation {
67
+ this.native.end(endTime);
68
+ return this;
69
+ }
78
70
  }
79
71
 
80
72
  class ProductionTraceBackend implements TraceBackend {
81
- constructor(
82
- private readonly provider: NodeTracerProvider,
83
- private readonly processor: SpanProcessor,
84
- ) {}
73
+ constructor(
74
+ private readonly provider: NodeTracerProvider,
75
+ private readonly processor: SpanProcessor,
76
+ ) {}
85
77
 
86
- start(
87
- name: string,
88
- attributes: ObservationAttributes,
89
- options: { asType: ObservationType; parent?: Observation },
90
- ): Observation {
91
- const { sessionId, userId, ...observationAttributes } = attributes;
92
- const parent = options.parent;
93
- const native =
94
- parent instanceof ProductionObservation
95
- ? startChild(parent.native, name, observationAttributes, options.asType)
96
- : startRoot(name, observationAttributes, options.asType);
97
- applySessionId(native, sessionId);
98
- applyUserId(native, userId);
99
- return new ProductionObservation(native);
100
- }
78
+ start(
79
+ name: string,
80
+ attributes: ObservationAttributes,
81
+ options: { asType: ObservationType; parent?: Observation },
82
+ ): Observation {
83
+ const { sessionId, userId, ...observationAttributes } = attributes;
84
+ const parent = options.parent;
85
+ const native =
86
+ parent instanceof ProductionObservation
87
+ ? startChild(parent.native, name, observationAttributes, options.asType)
88
+ : startRoot(name, observationAttributes, options.asType);
89
+ applySessionId(native, sessionId);
90
+ applyUserId(native, userId);
91
+ return new ProductionObservation(native);
92
+ }
101
93
 
102
- async forceFlush(): Promise<void> {
103
- await this.processor.forceFlush();
104
- }
94
+ async forceFlush(): Promise<void> {
95
+ await this.processor.forceFlush();
96
+ }
105
97
 
106
- async shutdown(): Promise<void> {
107
- await this.provider.shutdown();
108
- }
98
+ async shutdown(): Promise<void> {
99
+ await this.provider.shutdown();
100
+ }
109
101
  }
110
102
 
111
103
  export async function createProductionBackend(
112
- config: LangfuseConfig,
113
- factoryOverrides: Partial<RuntimeFactories> = {},
104
+ config: LangfuseConfig,
105
+ factoryOverrides: Partial<RuntimeFactories> = {},
114
106
  ): Promise<TraceBackend> {
115
- const globalRuntime = globalThis as GlobalWithRuntime;
116
- const fingerprint = configFingerprint(config);
117
- const existing = globalRuntime[RUNTIME_KEY];
118
- if (existing) {
119
- const runtime = await existing;
120
- if (runtime.shutdown) {
121
- throw new Error("Langfuse tracing was already shut down; restart Pi to enable it again.");
122
- }
123
- if (runtime.fingerprint !== fingerprint) {
124
- throw new Error("Langfuse configuration changed; restart Pi to apply the new credentials.");
125
- }
126
- return runtime.backend;
127
- }
107
+ const globalRuntime = globalThis as GlobalWithRuntime;
108
+ const fingerprint = configFingerprint(config);
109
+ const existing = globalRuntime[RUNTIME_KEY];
110
+ if (existing) {
111
+ const runtime = await existing;
112
+ if (runtime.shutdown) {
113
+ throw new Error("Langfuse tracing was already shut down; restart Pi to enable it again.");
114
+ }
115
+ if (runtime.fingerprint !== fingerprint) {
116
+ throw new Error("Langfuse configuration changed; restart Pi to apply the new credentials.");
117
+ }
118
+ return runtime.backend;
119
+ }
128
120
 
129
- const factories = { ...defaultFactories, ...factoryOverrides };
130
- const initializing = initializeRuntime(config, fingerprint, factories);
131
- globalRuntime[RUNTIME_KEY] = initializing;
132
- try {
133
- return (await initializing).backend;
134
- } catch (error) {
135
- delete globalRuntime[RUNTIME_KEY];
136
- throw error;
137
- }
121
+ const factories = { ...defaultFactories, ...factoryOverrides };
122
+ const initializing = initializeRuntime(config, fingerprint, factories);
123
+ globalRuntime[RUNTIME_KEY] = initializing;
124
+ try {
125
+ return (await initializing).backend;
126
+ } catch (error) {
127
+ delete globalRuntime[RUNTIME_KEY];
128
+ throw error;
129
+ }
138
130
  }
139
131
 
140
132
  async function initializeRuntime(
141
- config: LangfuseConfig,
142
- fingerprint: string,
143
- factories: RuntimeFactories,
133
+ config: LangfuseConfig,
134
+ fingerprint: string,
135
+ factories: RuntimeFactories,
144
136
  ): Promise<SharedRuntime> {
145
- const processor = factories.createProcessor(config);
146
- let provider: NodeTracerProvider | undefined;
147
- try {
148
- provider = factories.createProvider(processor);
149
- factories.selectProvider(provider);
150
- } catch (error) {
151
- await (provider?.shutdown() ?? processor.shutdown()).catch(() => undefined);
152
- throw error;
153
- }
154
- const backend = new ProductionTraceBackend(provider, processor);
155
- const runtime: SharedRuntime = { fingerprint, backend, shutdown: false };
156
- const originalShutdown = backend.shutdown.bind(backend);
157
- backend.shutdown = async () => {
158
- if (runtime.shutdown) return;
159
- runtime.shutdown = true;
160
- await originalShutdown();
161
- };
162
- return runtime;
137
+ const processor = factories.createProcessor(config);
138
+ let provider: NodeTracerProvider | undefined;
139
+ try {
140
+ provider = factories.createProvider(processor);
141
+ factories.selectProvider(provider);
142
+ } catch (error) {
143
+ await (provider?.shutdown() ?? processor.shutdown()).catch(() => undefined);
144
+ throw error;
145
+ }
146
+ const backend = new ProductionTraceBackend(provider, processor);
147
+ const runtime: SharedRuntime = { fingerprint, backend, shutdown: false };
148
+ const originalShutdown = backend.shutdown.bind(backend);
149
+ backend.shutdown = async () => {
150
+ if (runtime.shutdown) return;
151
+ runtime.shutdown = true;
152
+ await originalShutdown();
153
+ };
154
+ return runtime;
163
155
  }
164
156
 
165
157
  const defaultFactories: RuntimeFactories = {
166
- createProcessor: (config) => {
167
- const secrets = [config.secretKey, config.publicKey];
168
- return new LangfuseSpanProcessor({
169
- publicKey: config.publicKey,
170
- secretKey: config.secretKey,
171
- baseUrl: config.baseUrl,
172
- environment: config.environment ?? "",
173
- release: config.release ?? "",
174
- flushAt: 512,
175
- flushInterval: 5,
176
- timeout: 5,
177
- mask: ({ data }) => maskSecrets(data, secrets),
178
- shouldExportSpan: ({ otelSpan }) =>
179
- typeof otelSpan.attributes["langfuse.observation.type"] === "string",
180
- });
181
- },
182
- createProvider: (processor) => new NodeTracerProvider({ spanProcessors: [processor] }),
183
- selectProvider: setLangfuseTracerProvider,
158
+ createProcessor: (config) => {
159
+ const secrets = [config.secretKey, config.publicKey];
160
+ return new LangfuseSpanProcessor({
161
+ publicKey: config.publicKey,
162
+ secretKey: config.secretKey,
163
+ baseUrl: config.baseUrl,
164
+ environment: config.environment ?? "",
165
+ release: config.release ?? "",
166
+ flushAt: 512,
167
+ flushInterval: 5,
168
+ timeout: 5,
169
+ mask: ({ data }) => maskSecrets(data, secrets),
170
+ shouldExportSpan: ({ otelSpan }) => typeof otelSpan.attributes["langfuse.observation.type"] === "string",
171
+ });
172
+ },
173
+ createProvider: (processor) => new NodeTracerProvider({ spanProcessors: [processor] }),
174
+ selectProvider: setLangfuseTracerProvider,
184
175
  };
185
176
 
186
177
  function applySessionId(observation: LangfuseObservation, sessionId: string | undefined): void {
187
- if (sessionId !== undefined) {
188
- observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId);
189
- }
178
+ if (sessionId !== undefined) {
179
+ observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId);
180
+ }
190
181
  }
191
182
 
192
183
  function applyUserId(observation: LangfuseObservation, userId: string | undefined): void {
193
- if (userId !== undefined) {
194
- observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId);
195
- }
184
+ if (userId !== undefined) {
185
+ observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId);
186
+ }
196
187
  }
197
188
 
198
- function startRoot(
199
- name: string,
200
- attributes: ObservationAttributes,
201
- type: ObservationType,
202
- ): LangfuseObservation {
203
- if (type === "agent") {
204
- return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "agent" });
205
- }
206
- if (type === "generation") {
207
- return startObservation(name, attributes as LangfuseObservationAttributes, {
208
- asType: "generation",
209
- });
210
- }
211
- if (type === "tool") {
212
- return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool" });
213
- }
214
- return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span" });
189
+ function startRoot(name: string, attributes: ObservationAttributes, type: ObservationType): LangfuseObservation {
190
+ if (type === "agent") {
191
+ return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "agent" });
192
+ }
193
+ if (type === "generation") {
194
+ return startObservation(name, attributes as LangfuseObservationAttributes, {
195
+ asType: "generation",
196
+ });
197
+ }
198
+ if (type === "tool") {
199
+ return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool" });
200
+ }
201
+ return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span" });
215
202
  }
216
203
 
217
204
  function startChild(
218
- parent: LangfuseObservation,
219
- name: string,
220
- attributes: ObservationAttributes,
221
- type: ObservationType,
205
+ parent: LangfuseObservation,
206
+ name: string,
207
+ attributes: ObservationAttributes,
208
+ type: ObservationType,
222
209
  ): LangfuseObservation {
223
- if (type === "agent") {
224
- return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
225
- asType: "agent",
226
- });
227
- }
228
- if (type === "generation") {
229
- return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
230
- asType: "generation",
231
- });
232
- }
233
- if (type === "tool") {
234
- return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
235
- asType: "tool",
236
- });
237
- }
238
- return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
239
- asType: "span",
240
- });
210
+ if (type === "agent") {
211
+ return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
212
+ asType: "agent",
213
+ });
214
+ }
215
+ if (type === "generation") {
216
+ return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
217
+ asType: "generation",
218
+ });
219
+ }
220
+ if (type === "tool") {
221
+ return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
222
+ asType: "tool",
223
+ });
224
+ }
225
+ return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
226
+ asType: "span",
227
+ });
241
228
  }
242
229
 
243
230
  function configFingerprint(config: LangfuseConfig): string {
244
- return createHash("sha256")
245
- .update(
246
- `${config.publicKey}\0${config.secretKey}\0${config.baseUrl}\0${config.environment ?? ""}\0${config.release ?? ""}`,
247
- )
248
- .digest("hex");
231
+ return createHash("sha256")
232
+ .update(
233
+ `${config.publicKey}\0${config.secretKey}\0${config.baseUrl}\0${config.environment ?? ""}\0${config.release ?? ""}`,
234
+ )
235
+ .digest("hex");
249
236
  }
250
237
 
251
238
  export function maskSecrets(data: unknown, secrets: readonly string[]): unknown {
252
- return maskSecretValue(data, secrets, new WeakSet<object>());
239
+ return maskSecretValue(data, secrets, new WeakSet<object>());
253
240
  }
254
241
 
255
- function maskSecretValue(
256
- data: unknown,
257
- secrets: readonly string[],
258
- active: WeakSet<object>,
259
- ): unknown {
260
- if (typeof data === "string") {
261
- let masked = data.replace(/\b(?:sk|pk)-lf-[A-Za-z0-9_-]+\b/g, "[LANGFUSE_KEY_REDACTED]");
262
- for (const secret of secrets) {
263
- if (secret) masked = masked.replaceAll(secret, "[LANGFUSE_KEY_REDACTED]");
264
- }
265
- return masked;
266
- }
267
- if (!data || typeof data !== "object") return data;
268
- if (active.has(data)) return "[circular]";
242
+ function maskSecretValue(data: unknown, secrets: readonly string[], active: WeakSet<object>): unknown {
243
+ if (typeof data === "string") {
244
+ let masked = data.replace(/\b(?:sk|pk)-lf-[A-Za-z0-9_-]+\b/g, "[LANGFUSE_KEY_REDACTED]");
245
+ for (const secret of secrets) {
246
+ if (secret) masked = masked.replaceAll(secret, "[LANGFUSE_KEY_REDACTED]");
247
+ }
248
+ return masked;
249
+ }
250
+ if (!data || typeof data !== "object") return data;
251
+ if (active.has(data)) return "[circular]";
269
252
 
270
- active.add(data);
271
- try {
272
- if (Array.isArray(data)) {
273
- return data.map((item) => maskSecretValue(item, secrets, active));
274
- }
275
- return Object.fromEntries(
276
- Object.entries(data).map(([key, value]) => [key, maskSecretValue(value, secrets, active)]),
277
- );
278
- } finally {
279
- active.delete(data);
280
- }
253
+ active.add(data);
254
+ try {
255
+ if (Array.isArray(data)) {
256
+ return data.map((item) => maskSecretValue(item, secrets, active));
257
+ }
258
+ return Object.fromEntries(
259
+ Object.entries(data).map(([key, value]) => [key, maskSecretValue(value, secrets, active)]),
260
+ );
261
+ } finally {
262
+ active.delete(data);
263
+ }
281
264
  }