@narumitw/pi-langfuse 0.50.5 → 0.51.0

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,447 @@
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
- import type { LangfuseConfig } from "./config.js";
13
- import type {
14
- Observation,
15
- ObservationAttributes,
16
- ObservationType,
17
- TraceBackend,
18
- } from "./tracing.js";
12
+ import { DEFAULT_BASE_URL, type LangfuseConfig } from "./config.js";
13
+ import {
14
+ type CreateLangfuseRuntimeOptions,
15
+ createLangfuseRuntimeFromBackend,
16
+ getLangfuseRuntimeInternal,
17
+ type LangfuseRuntime,
18
+ type LangfuseRuntimeInternal,
19
+ } from "./runtime-core.js";
20
+ import type { Observation, ObservationAttributes, ObservationType, TraceBackend } from "./tracing.js";
21
+
22
+ export type {
23
+ CreateLangfuseRuntimeOptions,
24
+ LangfuseRuntime,
25
+ LangfuseRuntimeConfig,
26
+ } from "./runtime-core.js";
19
27
 
20
- const RUNTIME_KEY = Symbol.for("@narumitw/pi-langfuse/runtime/v1");
28
+ const RUNTIME_KEY = Symbol.for("@narumitw/pi-langfuse/runtime/v2");
29
+ const LEGACY_RUNTIME_KEY = Symbol.for("@narumitw/pi-langfuse/runtime/v1");
30
+
31
+ interface ResolvedRuntimeConfig {
32
+ publicKey: string;
33
+ secretKey: string;
34
+ baseUrl: string;
35
+ environment?: string;
36
+ release?: string;
37
+ }
21
38
 
22
39
  interface SharedRuntime {
23
- fingerprint: string;
24
- backend: ProductionTraceBackend;
25
- shutdown: boolean;
40
+ fingerprint: string;
41
+ runtime: LangfuseRuntimeInternal;
42
+ shutdown: true;
26
43
  }
27
44
 
28
45
  type GlobalWithRuntime = typeof globalThis & {
29
- [RUNTIME_KEY]?: Promise<SharedRuntime>;
46
+ [RUNTIME_KEY]?: Promise<SharedRuntime>;
47
+ [LEGACY_RUNTIME_KEY]?: unknown;
30
48
  };
31
49
 
32
50
  export interface RuntimeFactories {
33
- createProcessor(config: LangfuseConfig): SpanProcessor;
34
- createProvider(processor: SpanProcessor): NodeTracerProvider;
35
- selectProvider(provider: NodeTracerProvider): void;
51
+ createProcessor(config: ResolvedRuntimeConfig): SpanProcessor;
52
+ createProvider(processor: SpanProcessor): NodeTracerProvider;
53
+ selectProvider(provider: NodeTracerProvider): void;
36
54
  }
37
55
 
38
56
  class ProductionObservation implements Observation {
39
- constructor(readonly native: LangfuseObservation) {}
40
-
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
- }
48
-
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
- }
73
-
74
- end(endTime?: number): Observation {
75
- this.native.end(endTime);
76
- return this;
77
- }
57
+ readonly traceId: string;
58
+
59
+ constructor(
60
+ readonly native: LangfuseObservation,
61
+ private readonly secrets: readonly string[],
62
+ ) {
63
+ this.traceId = native.traceId;
64
+ }
65
+
66
+ update(attributes: ObservationAttributes): Observation {
67
+ const { sessionId, userId, ...observationAttributes } = maskObservationAttributes(attributes, this.secrets);
68
+ this.native.updateOtelSpanAttributes(observationAttributes as LangfuseObservationAttributes);
69
+ applySessionId(this.native, sessionId);
70
+ applyUserId(this.native, userId);
71
+ return this;
72
+ }
73
+
74
+ updateTrace(attributes: ObservationAttributes): Observation {
75
+ const { input, output, metadata, name, sessionId, userId, tags, version } = maskObservationAttributes(
76
+ attributes,
77
+ this.secrets,
78
+ );
79
+ if (input !== undefined || output !== undefined) {
80
+ this.native.setTraceIO({ input, output });
81
+ }
82
+ if (name !== undefined) {
83
+ this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_NAME, name);
84
+ }
85
+ applySessionId(this.native, sessionId);
86
+ applyUserId(this.native, userId);
87
+ if (tags !== undefined) {
88
+ this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_TAGS, tags);
89
+ }
90
+ if (version !== undefined) {
91
+ this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.VERSION, version);
92
+ }
93
+ for (const [key, value] of Object.entries(metadata ?? {})) {
94
+ const serialized = serializeMetadataValue(value);
95
+ if (serialized !== undefined) {
96
+ this.native.otelSpan.setAttribute(`${LangfuseOtelSpanAttributes.TRACE_METADATA}.${key}`, serialized);
97
+ }
98
+ }
99
+ return this;
100
+ }
101
+
102
+ end(endTime?: number): Observation {
103
+ this.native.end(endTime);
104
+ return this;
105
+ }
78
106
  }
79
107
 
80
108
  class ProductionTraceBackend implements TraceBackend {
81
- constructor(
82
- private readonly provider: NodeTracerProvider,
83
- private readonly processor: SpanProcessor,
84
- ) {}
85
-
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
- }
101
-
102
- async forceFlush(): Promise<void> {
103
- await this.processor.forceFlush();
104
- }
105
-
106
- async shutdown(): Promise<void> {
107
- await this.provider.shutdown();
108
- }
109
+ constructor(
110
+ private readonly provider: NodeTracerProvider,
111
+ private readonly processor: SpanProcessor,
112
+ private readonly secrets: readonly string[],
113
+ ) {}
114
+
115
+ start(
116
+ name: string,
117
+ attributes: ObservationAttributes,
118
+ options: { asType: ObservationType; parent?: Observation; startTime?: Date },
119
+ ): Observation {
120
+ const maskedName = maskSecretString(name, this.secrets);
121
+ const { sessionId, userId, ...observationAttributes } = maskObservationAttributes(attributes, this.secrets);
122
+ const parent = options.parent;
123
+ const native =
124
+ parent instanceof ProductionObservation
125
+ ? startChild(parent.native, maskedName, observationAttributes, options.asType, options.startTime)
126
+ : startRoot(maskedName, observationAttributes, options.asType, options.startTime);
127
+ applySessionId(native, sessionId);
128
+ applyUserId(native, userId);
129
+ return new ProductionObservation(native, this.secrets);
130
+ }
131
+
132
+ async forceFlush(): Promise<void> {
133
+ await this.processor.forceFlush();
134
+ }
135
+
136
+ async shutdown(): Promise<void> {
137
+ await this.provider.shutdown();
138
+ }
139
+ }
140
+
141
+ export async function createLangfuseRuntime(options: CreateLangfuseRuntimeOptions = {}): Promise<LangfuseRuntime> {
142
+ return createSharedRuntime(resolveLangfuseRuntimeConfig(options), defaultFactories);
109
143
  }
110
144
 
111
145
  export async function createProductionBackend(
112
- config: LangfuseConfig,
113
- factoryOverrides: Partial<RuntimeFactories> = {},
146
+ config: LangfuseConfig,
147
+ factoryOverrides: Partial<RuntimeFactories> = {},
114
148
  ): 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
- }
128
-
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
- }
149
+ const runtime = await createSharedRuntime(resolveLangfuseRuntimeConfig({ config, env: false }), {
150
+ ...defaultFactories,
151
+ ...factoryOverrides,
152
+ });
153
+ return getLangfuseRuntimeInternal(runtime).backend;
154
+ }
155
+
156
+ async function createSharedRuntime(
157
+ config: ResolvedRuntimeConfig,
158
+ factories: RuntimeFactories,
159
+ ): Promise<LangfuseRuntime> {
160
+ const globalRuntime = globalThis as GlobalWithRuntime;
161
+ const fingerprint = configFingerprint(config);
162
+ const existing = globalRuntime[RUNTIME_KEY];
163
+ const legacyRuntime = globalRuntime[LEGACY_RUNTIME_KEY];
164
+ if (legacyRuntime !== undefined && legacyRuntime !== existing) {
165
+ throw new Error("An older Langfuse runtime is already loaded; restart the process before enabling this version.");
166
+ }
167
+ if (existing) {
168
+ if (legacyRuntime === undefined) globalRuntime[LEGACY_RUNTIME_KEY] = existing;
169
+ const shared = await existing;
170
+ if (shared.runtime.closed) {
171
+ throw new Error("Langfuse tracing was already shut down; restart the process to enable it again.");
172
+ }
173
+ if (shared.fingerprint !== fingerprint) {
174
+ throw new Error("Langfuse configuration changed; restart the process to apply the new credentials.");
175
+ }
176
+ return shared.runtime;
177
+ }
178
+
179
+ const initializing = initializeRuntime(config, fingerprint, factories);
180
+ globalRuntime[RUNTIME_KEY] = initializing;
181
+ globalRuntime[LEGACY_RUNTIME_KEY] = initializing;
182
+ try {
183
+ return (await initializing).runtime;
184
+ } catch (error) {
185
+ if (globalRuntime[RUNTIME_KEY] === initializing) delete globalRuntime[RUNTIME_KEY];
186
+ if (globalRuntime[LEGACY_RUNTIME_KEY] === initializing) delete globalRuntime[LEGACY_RUNTIME_KEY];
187
+ throw error;
188
+ }
138
189
  }
139
190
 
140
191
  async function initializeRuntime(
141
- config: LangfuseConfig,
142
- fingerprint: string,
143
- factories: RuntimeFactories,
192
+ config: ResolvedRuntimeConfig,
193
+ fingerprint: string,
194
+ factories: RuntimeFactories,
144
195
  ): 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;
196
+ const processor = factories.createProcessor(config);
197
+ let provider: NodeTracerProvider | undefined;
198
+ try {
199
+ provider = factories.createProvider(processor);
200
+ factories.selectProvider(provider);
201
+ } catch (error) {
202
+ await (provider?.shutdown() ?? processor.shutdown()).catch(() => undefined);
203
+ throw error;
204
+ }
205
+ const runtime = getLangfuseRuntimeInternal(
206
+ createLangfuseRuntimeFromBackend(
207
+ new ProductionTraceBackend(provider, processor, [config.publicKey, config.secretKey]),
208
+ ),
209
+ );
210
+ return { fingerprint, runtime, shutdown: true };
163
211
  }
164
212
 
165
213
  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,
214
+ createProcessor: (config) => {
215
+ const secrets = [config.secretKey, config.publicKey];
216
+ return new LangfuseSpanProcessor({
217
+ publicKey: config.publicKey,
218
+ secretKey: config.secretKey,
219
+ baseUrl: config.baseUrl,
220
+ environment: config.environment ?? "",
221
+ release: config.release ?? "",
222
+ flushAt: 512,
223
+ flushInterval: 5,
224
+ timeout: 5,
225
+ mask: ({ data }) => maskSecrets(data, secrets),
226
+ shouldExportSpan: ({ otelSpan }) => typeof otelSpan.attributes["langfuse.observation.type"] === "string",
227
+ });
228
+ },
229
+ createProvider: (processor) => new NodeTracerProvider({ spanProcessors: [processor] }),
230
+ selectProvider: setLangfuseTracerProvider,
184
231
  };
185
232
 
233
+ export function resolveLangfuseRuntimeConfig(options: CreateLangfuseRuntimeOptions): ResolvedRuntimeConfig {
234
+ const env = options.env === false ? undefined : (options.env ?? process.env);
235
+ const publicKey = requiredSetting("publicKey", options.config?.publicKey, env?.LANGFUSE_PUBLIC_KEY);
236
+ const secretKey = requiredSetting("secretKey", options.config?.secretKey, env?.LANGFUSE_SECRET_KEY);
237
+ const explicitBaseUrl = options.config?.baseUrl;
238
+ if (explicitBaseUrl !== undefined && !normalizeString(explicitBaseUrl)) {
239
+ throw new Error("Langfuse baseUrl must be a non-empty string.");
240
+ }
241
+ const rawBaseUrl = selectedSetting(explicitBaseUrl, env?.LANGFUSE_BASE_URL) ?? DEFAULT_BASE_URL;
242
+ const baseUrl = normalizeBaseUrl(rawBaseUrl);
243
+ if (!baseUrl) {
244
+ throw new Error("Langfuse baseUrl must use HTTP or HTTPS without credentials, a query, or a fragment.");
245
+ }
246
+ const environment = optionalSetting("environment", options.config?.environment, env?.LANGFUSE_TRACING_ENVIRONMENT);
247
+ if (environment && (environment.length > 40 || !/^(?!langfuse)[a-z0-9_-]+$/u.test(environment))) {
248
+ throw new Error(
249
+ "Langfuse environment must be at most 40 lowercase letters, numbers, hyphens, or underscores and must not start with langfuse.",
250
+ );
251
+ }
252
+ const release = optionalSetting("release", options.config?.release, env?.LANGFUSE_RELEASE);
253
+ return { publicKey, secretKey, baseUrl, ...(environment ? { environment } : {}), ...(release ? { release } : {}) };
254
+ }
255
+
256
+ function requiredSetting(name: string, explicit: string | undefined, environment: string | undefined): string {
257
+ const value = selectedSetting(explicit, environment);
258
+ if (!value) throw new Error(`Langfuse ${name} is required.`);
259
+ return value;
260
+ }
261
+
262
+ function optionalSetting(
263
+ name: string,
264
+ explicit: string | undefined,
265
+ environment: string | undefined,
266
+ ): string | undefined {
267
+ const selected = explicit !== undefined ? explicit : environment;
268
+ if (selected === undefined) return undefined;
269
+ const value = normalizeString(selected);
270
+ if (!value) throw new Error(`Langfuse ${name} must be a non-empty string.`);
271
+ return value;
272
+ }
273
+
274
+ function selectedSetting(explicit: string | undefined, environment: string | undefined): string | undefined {
275
+ if (explicit !== undefined) return normalizeString(explicit);
276
+ return normalizeString(environment);
277
+ }
278
+
279
+ function normalizeBaseUrl(value: string): string | undefined {
280
+ try {
281
+ const url = new URL(value);
282
+ if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
283
+ if (url.username || url.password || url.search || url.hash) return undefined;
284
+ return url.toString().replace(/\/+$/, "");
285
+ } catch {
286
+ return undefined;
287
+ }
288
+ }
289
+
290
+ function normalizeString(value: unknown): string | undefined {
291
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
292
+ }
293
+
294
+ function serializeMetadataValue(value: unknown): string | undefined {
295
+ return typeof value === "string" ? value : JSON.stringify(value);
296
+ }
297
+
298
+ function maskObservationAttributes(
299
+ attributes: ObservationAttributes,
300
+ secrets: readonly string[],
301
+ ): ObservationAttributes {
302
+ return {
303
+ ...attributes,
304
+ ...(attributes.metadata ? { metadata: maskSecrets(attributes.metadata, secrets) as Record<string, unknown> } : {}),
305
+ ...(attributes.name ? { name: maskSecretString(attributes.name, secrets) } : {}),
306
+ ...(attributes.sessionId ? { sessionId: maskSecretString(attributes.sessionId, secrets) } : {}),
307
+ ...(attributes.userId ? { userId: maskSecretString(attributes.userId, secrets) } : {}),
308
+ ...(attributes.tags ? { tags: attributes.tags.map((tag) => maskSecretString(tag, secrets)) } : {}),
309
+ ...(attributes.statusMessage ? { statusMessage: maskSecretString(attributes.statusMessage, secrets) } : {}),
310
+ ...(attributes.model ? { model: maskSecretString(attributes.model, secrets) } : {}),
311
+ ...(attributes.version ? { version: maskSecretString(attributes.version, secrets) } : {}),
312
+ ...(attributes.modelParameters
313
+ ? {
314
+ modelParameters: Object.fromEntries(
315
+ Object.entries(attributes.modelParameters).map(([key, value]) => [
316
+ maskSecretString(key, secrets),
317
+ typeof value === "string" ? maskSecretString(value, secrets) : value,
318
+ ]),
319
+ ),
320
+ }
321
+ : {}),
322
+ };
323
+ }
324
+
325
+ function maskSecretString(value: string, secrets: readonly string[]): string {
326
+ return maskSecrets(value, secrets) as string;
327
+ }
328
+
186
329
  function applySessionId(observation: LangfuseObservation, sessionId: string | undefined): void {
187
- if (sessionId !== undefined) {
188
- observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId);
189
- }
330
+ if (sessionId !== undefined) {
331
+ observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId);
332
+ }
190
333
  }
191
334
 
192
335
  function applyUserId(observation: LangfuseObservation, userId: string | undefined): void {
193
- if (userId !== undefined) {
194
- observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId);
195
- }
336
+ if (userId !== undefined) {
337
+ observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId);
338
+ }
196
339
  }
197
340
 
198
341
  function startRoot(
199
- name: string,
200
- attributes: ObservationAttributes,
201
- type: ObservationType,
342
+ name: string,
343
+ attributes: ObservationAttributes,
344
+ type: ObservationType,
345
+ startTime?: Date,
202
346
  ): 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" });
347
+ if (type === "agent") {
348
+ return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "agent", startTime });
349
+ }
350
+ if (type === "generation") {
351
+ return startObservation(name, attributes as LangfuseObservationAttributes, {
352
+ asType: "generation",
353
+ startTime,
354
+ });
355
+ }
356
+ if (type === "tool") {
357
+ return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool", startTime });
358
+ }
359
+ return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span", startTime });
215
360
  }
216
361
 
217
362
  function startChild(
218
- parent: LangfuseObservation,
219
- name: string,
220
- attributes: ObservationAttributes,
221
- type: ObservationType,
363
+ parent: LangfuseObservation,
364
+ name: string,
365
+ attributes: ObservationAttributes,
366
+ type: ObservationType,
367
+ startTime?: Date,
222
368
  ): 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
- });
369
+ if (startTime) {
370
+ const parentSpanContext = parent.otelSpan.spanContext();
371
+ if (type === "agent") {
372
+ return startObservation(name, attributes as LangfuseObservationAttributes, {
373
+ asType: "agent",
374
+ parentSpanContext,
375
+ startTime,
376
+ });
377
+ }
378
+ if (type === "generation") {
379
+ return startObservation(name, attributes as LangfuseObservationAttributes, {
380
+ asType: "generation",
381
+ parentSpanContext,
382
+ startTime,
383
+ });
384
+ }
385
+ if (type === "tool") {
386
+ return startObservation(name, attributes as LangfuseObservationAttributes, {
387
+ asType: "tool",
388
+ parentSpanContext,
389
+ startTime,
390
+ });
391
+ }
392
+ return startObservation(name, attributes as LangfuseObservationAttributes, {
393
+ asType: "span",
394
+ parentSpanContext,
395
+ startTime,
396
+ });
397
+ }
398
+ if (type === "agent") {
399
+ return parent.startObservation(name, attributes as LangfuseObservationAttributes, { asType: "agent" });
400
+ }
401
+ if (type === "generation") {
402
+ return parent.startObservation(name, attributes as LangfuseObservationAttributes, { asType: "generation" });
403
+ }
404
+ if (type === "tool") {
405
+ return parent.startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool" });
406
+ }
407
+ return parent.startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span" });
241
408
  }
242
409
 
243
- 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");
410
+ function configFingerprint(config: ResolvedRuntimeConfig): string {
411
+ return createHash("sha256")
412
+ .update(
413
+ `${config.publicKey}\0${config.secretKey}\0${config.baseUrl}\0${config.environment ?? ""}\0${config.release ?? ""}`,
414
+ )
415
+ .digest("hex");
249
416
  }
250
417
 
251
418
  export function maskSecrets(data: unknown, secrets: readonly string[]): unknown {
252
- return maskSecretValue(data, secrets, new WeakSet<object>());
419
+ return maskSecretValue(data, secrets, new WeakSet<object>());
253
420
  }
254
421
 
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]";
269
-
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
- }
422
+ function maskSecretValue(data: unknown, secrets: readonly string[], active: WeakSet<object>): unknown {
423
+ if (typeof data === "string") {
424
+ let masked = data.replace(/\b(?:sk|pk)-lf-[A-Za-z0-9_-]+\b/g, "[LANGFUSE_KEY_REDACTED]");
425
+ for (const secret of secrets) {
426
+ if (secret) masked = masked.replaceAll(secret, "[LANGFUSE_KEY_REDACTED]");
427
+ }
428
+ return masked;
429
+ }
430
+ if (!data || typeof data !== "object") return data;
431
+ if (active.has(data)) return "[circular]";
432
+
433
+ active.add(data);
434
+ try {
435
+ if (Array.isArray(data)) {
436
+ return data.map((item) => maskSecretValue(item, secrets, active));
437
+ }
438
+ return Object.fromEntries(
439
+ Object.entries(data).map(([key, value]) => [
440
+ maskSecretString(key, secrets),
441
+ maskSecretValue(value, secrets, active),
442
+ ]),
443
+ );
444
+ } finally {
445
+ active.delete(data);
446
+ }
281
447
  }