@narumitw/pi-langfuse 0.49.4 → 0.50.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/README.md CHANGED
@@ -61,13 +61,14 @@ You can also create the file manually:
61
61
  "baseUrl": "https://us.cloud.langfuse.com",
62
62
  "environment": "development",
63
63
  "release": "local",
64
+ "userId": "your-user-id",
64
65
  "captureContent": true
65
66
  }
66
67
  ```
67
68
 
68
69
  `publicKey` and `secretKey` are required literal strings. Environment-variable and command interpolation are intentionally unsupported. `baseUrl` defaults to `https://us.cloud.langfuse.com`; regional and self-hosted HTTP or HTTPS endpoints are supported. Prefer HTTPS because HTTP sends Langfuse credentials and trace content without transport encryption.
69
70
 
70
- `environment` and `release` are optional Langfuse trace attributes. An environment must match Langfuse's contract: at most 40 lowercase letters, numbers, hyphens, or underscores, and it cannot start with `langfuse`. Set `captureContent` to `false` to trace timing, model, usage, cost, status, and bounded diagnostic metadata without sending prompts, provider-request snapshots, responses, or tool content.
71
+ `environment`, `release`, and `userId` are optional Langfuse trace attributes. An environment must match Langfuse's contract: at most 40 lowercase letters, numbers, hyphens, or underscores, and it cannot start with `langfuse`. `userId` populates the Langfuse user dimension, which is what the Sessions and Traces views group by; Langfuse accepts at most 200 characters, and leaving it unset reports no user. Set `captureContent` to `false` to trace timing, model, usage, cost, status, and bounded diagnostic metadata without sending prompts, provider-request snapshots, responses, or tool content.
71
72
 
72
73
  The extension automatically restricts an existing config file to mode `0600` and refuses to load credentials if that protection cannot be enforced. You can also set it explicitly:
73
74
 
@@ -75,7 +76,7 @@ The extension automatically restricts an existing config file to mode `0600` and
75
76
  chmod 600 ~/.pi/agent/pi-langfuse.json
76
77
  ```
77
78
 
78
- Restart Pi after changing credentials, endpoint, environment, release, or `captureContent`. The isolated OpenTelemetry tracer provider is initialized once per Pi process and selected only for Langfuse; it does not replace Pi's process-global provider or send Langfuse observations to another extension's exporter.
79
+ Restart Pi after changing credentials, endpoint, environment, release, or `captureContent`. Changes to `userId` apply to new sessions without restarting Pi. The isolated OpenTelemetry tracer provider is initialized once per Pi process and selected only for Langfuse; it does not replace Pi's process-global provider or send Langfuse observations to another extension's exporter.
79
80
 
80
81
  ## 🔭 What is traced
81
82
 
@@ -166,7 +167,7 @@ includes current tracing state and the manual configuration path.
166
167
 
167
168
  With content capture enabled, traces can contain user prompts, model responses, tool arguments, and tool results. These may include source code, file contents, shell output, or other sensitive project data. Review your Langfuse retention and access controls before enabling this extension.
168
169
 
169
- Git branch names, commit ids, working directory, session/leaf ids, model identity, usage/cost, aggregate counts, and allowlisted response-header values are metadata. They remain exported when `captureContent` is `false`; branch names and diagnostic header values can themselves contain operational details.
170
+ Git branch names, commit ids, working directory, session/leaf ids, the configured `userId`, model identity, usage/cost, aggregate counts, and allowlisted response-header values are metadata. They remain exported when `captureContent` is `false`; branch names and diagnostic header values can themselves contain operational details, and a `userId` may itself be personally identifying if you set it to an email address or a real name. Choose a pseudonymous value when that matters, or leave `userId` unset to export no user at all.
170
171
 
171
172
  The built-in mask specifically protects Langfuse credentials; it is not a general secret scanner. Set `"captureContent": false` in `pi-langfuse.json` when prompts, provider-request snapshots, responses, and tool content must remain local. Compaction summaries, tool partial results, opaque continuation signatures, authorization headers, cookies, and unapproved response headers are never exported in either mode.
172
173
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-langfuse",
3
- "version": "0.49.4",
3
+ "version": "0.50.0",
4
4
  "description": "Pi extension that traces LLM generations and tool activity to Langfuse.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@langfuse/otel": "^5.10.0",
45
45
  "@langfuse/tracing": "^5.10.0",
46
- "@narumitw/pi-tui-kit": "^0.49.1",
46
+ "@narumitw/pi-tui-kit": "^0.55.0",
47
47
  "@opentelemetry/api": "^1.9.1",
48
48
  "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
49
49
  "@opentelemetry/sdk-trace-base": "^2.10.0",
package/src/config.ts CHANGED
@@ -12,6 +12,7 @@ export interface LangfuseConfig {
12
12
  baseUrl: string;
13
13
  environment?: string;
14
14
  release?: string;
15
+ userId?: string;
15
16
  captureContent: boolean;
16
17
  }
17
18
 
@@ -61,6 +62,7 @@ async function writeLangfuseConfigNow(
61
62
  baseUrl: _baseUrl,
62
63
  environment: _environment,
63
64
  release: _release,
65
+ userId: _userId,
64
66
  captureContent: _captureContent,
65
67
  ...unknownFields
66
68
  } = currentDocument;
@@ -209,6 +211,11 @@ export function normalizeLangfuseConfig(
209
211
  }
210
212
  const release = optionalString(input.release, "release");
211
213
  if (!release.ok) return release;
214
+ const userId = optionalString(input.userId, "userId");
215
+ if (!userId.ok) return userId;
216
+ if (userId.value && userId.value.length > 200) {
217
+ return { ok: false, reason: "pi-langfuse.json userId must be at most 200 characters." };
218
+ }
212
219
 
213
220
  return {
214
221
  ok: true,
@@ -218,6 +225,7 @@ export function normalizeLangfuseConfig(
218
225
  baseUrl,
219
226
  ...(environment.value ? { environment: environment.value } : {}),
220
227
  ...(release.value ? { release: release.value } : {}),
228
+ ...(userId.value ? { userId: userId.value } : {}),
221
229
  captureContent: input.captureContent ?? true,
222
230
  },
223
231
  };
package/src/langfuse.ts CHANGED
@@ -201,6 +201,7 @@ export function createLangfuseExtension(
201
201
  activeConfig = result.config;
202
202
  recorder = new TraceRecorder(backend, {
203
203
  sessionId: ctx.sessionManager.getSessionId(),
204
+ ...(result.config.userId ? { userId: result.config.userId } : {}),
204
205
  cwd: ctx.cwd,
205
206
  mode: ctx.mode,
206
207
  captureContent: result.config.captureContent,
package/src/runtime.ts CHANGED
@@ -39,14 +39,15 @@ class ProductionObservation implements Observation {
39
39
  constructor(readonly native: LangfuseObservation) {}
40
40
 
41
41
  update(attributes: ObservationAttributes): Observation {
42
- const { sessionId, ...observationAttributes } = attributes;
42
+ const { sessionId, userId, ...observationAttributes } = attributes;
43
43
  this.native.updateOtelSpanAttributes(observationAttributes as LangfuseObservationAttributes);
44
44
  applySessionId(this.native, sessionId);
45
+ applyUserId(this.native, userId);
45
46
  return this;
46
47
  }
47
48
 
48
49
  updateTrace(attributes: ObservationAttributes): Observation {
49
- const { input, output, metadata, name, sessionId, tags, version } = attributes;
50
+ const { input, output, metadata, name, sessionId, userId, tags, version } = attributes;
50
51
  if (input !== undefined || output !== undefined) {
51
52
  this.native.setTraceIO({ input, output });
52
53
  }
@@ -54,6 +55,7 @@ class ProductionObservation implements Observation {
54
55
  this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_NAME, name);
55
56
  }
56
57
  applySessionId(this.native, sessionId);
58
+ applyUserId(this.native, userId);
57
59
  if (tags !== undefined) {
58
60
  this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_TAGS, tags);
59
61
  }
@@ -86,13 +88,14 @@ class ProductionTraceBackend implements TraceBackend {
86
88
  attributes: ObservationAttributes,
87
89
  options: { asType: ObservationType; parent?: Observation },
88
90
  ): Observation {
89
- const { sessionId, ...observationAttributes } = attributes;
91
+ const { sessionId, userId, ...observationAttributes } = attributes;
90
92
  const parent = options.parent;
91
93
  const native =
92
94
  parent instanceof ProductionObservation
93
95
  ? startChild(parent.native, name, observationAttributes, options.asType)
94
96
  : startRoot(name, observationAttributes, options.asType);
95
97
  applySessionId(native, sessionId);
98
+ applyUserId(native, userId);
96
99
  return new ProductionObservation(native);
97
100
  }
98
101
 
@@ -186,6 +189,12 @@ function applySessionId(observation: LangfuseObservation, sessionId: string | un
186
189
  }
187
190
  }
188
191
 
192
+ function applyUserId(observation: LangfuseObservation, userId: string | undefined): void {
193
+ if (userId !== undefined) {
194
+ observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId);
195
+ }
196
+ }
197
+
189
198
  function startRoot(
190
199
  name: string,
191
200
  attributes: ObservationAttributes,
package/src/tracing.ts CHANGED
@@ -40,6 +40,7 @@ export interface ObservationAttributes {
40
40
  version?: string;
41
41
  name?: string;
42
42
  sessionId?: string;
43
+ userId?: string;
43
44
  tags?: string[];
44
45
  }
45
46
 
@@ -63,6 +64,7 @@ export interface TraceBackend {
63
64
 
64
65
  interface RecorderContext {
65
66
  sessionId: string;
67
+ userId?: string;
66
68
  cwd: string;
67
69
  mode: string;
68
70
  captureContent: boolean;
@@ -239,7 +241,12 @@ export class TraceRecorder {
239
241
  attributes: ObservationAttributes,
240
242
  options: { asType: ObservationType; parent?: Observation },
241
243
  ): Observation {
242
- return this.backend.start(name, { ...attributes, sessionId: this.context.sessionId }, options);
244
+ const { sessionId, userId } = this.context;
245
+ return this.backend.start(
246
+ name,
247
+ { ...attributes, sessionId, ...(userId ? { userId } : {}) },
248
+ options,
249
+ );
243
250
  }
244
251
 
245
252
  hasActiveTrace(): boolean {
@@ -287,6 +294,7 @@ export class TraceRecorder {
287
294
  this.root.updateTrace?.({
288
295
  name: "pi.trace",
289
296
  sessionId: this.context.sessionId,
297
+ ...(this.context.userId ? { userId: this.context.userId } : {}),
290
298
  version: TRACE_SCHEMA_VERSION,
291
299
  input: traceInput,
292
300
  metadata,