pi-langfuse 1.4.3 → 1.4.4

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.
@@ -0,0 +1,115 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const REDACTED = "[REDACTED_SECRET]";
4
+
5
+ export interface RedactOptions {
6
+ maxDepth: number;
7
+ maxArrayItems: number;
8
+ maxObjectKeys: number;
9
+ maxStringLength: number;
10
+ }
11
+
12
+ const DEFAULT_OPTIONS: RedactOptions = {
13
+ maxDepth: 6,
14
+ maxArrayItems: 50,
15
+ maxObjectKeys: 80,
16
+ maxStringLength: 12_000,
17
+ };
18
+
19
+ const SECRET_ASSIGNMENT_RE =
20
+ /\b([A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASS|API[_-]?KEY|PRIVATE[_-]?KEY|AUTH|COOKIE)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi;
21
+ const PRIVATE_KEY_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
22
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi;
23
+ const KNOWN_TOKEN_RE =
24
+ /\b(?:sk-(?:lf|ant|proj|live|test)[A-Za-z0-9_-]*|pk-lf-[A-Za-z0-9_-]+|gh[pousr]_[A-Za-z0-9_]{20,}|npm_[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16})\b/g;
25
+ const ABSOLUTE_PATH_RE =
26
+ /(?:\/Users\/[^/\s]+|\/home\/[^/\s]+|\/private\/tmp|\/tmp|[A-Za-z]:\\Users\\[^\\\s]+)(?:[^\s"'`]*)/g;
27
+ const SENSITIVE_FIELD_RE =
28
+ /^(authorization|cookie|setcookie|xapikey|apikey|token|accesstoken|refreshtoken|secret|secretkey|password|passwd|privatekey)$/;
29
+
30
+ export function hashPath(path: string): string {
31
+ return `[PATH_HASH:${createHash("sha256").update(path).digest("hex").slice(0, 12)}]`;
32
+ }
33
+
34
+ function truncate(value: string, maxStringLength: number): string {
35
+ return value.length > maxStringLength ? `${value.slice(0, maxStringLength)}... [truncated]` : value;
36
+ }
37
+
38
+ export function redactString(value: string, options: Partial<RedactOptions> = {}): string {
39
+ const merged = { ...DEFAULT_OPTIONS, ...options };
40
+ const truncated = truncate(value, merged.maxStringLength);
41
+ return truncated
42
+ .replace(PRIVATE_KEY_RE, REDACTED)
43
+ .replace(BEARER_RE, REDACTED)
44
+ .replace(KNOWN_TOKEN_RE, REDACTED)
45
+ .replace(SECRET_ASSIGNMENT_RE, (_match, key: string) => `${key}=${REDACTED}`)
46
+ .replace(ABSOLUTE_PATH_RE, (path: string) => {
47
+ const envSuffix = path.match(/([/\\]\.env(?:\.[A-Za-z0-9_-]+)?)$/)?.[1];
48
+ return `${hashPath(envSuffix ? path.slice(0, -envSuffix.length) : path)}${envSuffix ?? ""}`;
49
+ });
50
+ }
51
+
52
+ function visit(value: unknown, options: RedactOptions, depth: number, seen: WeakSet<object>): unknown {
53
+ if (value === null || value === undefined || typeof value === "number" || typeof value === "boolean") {
54
+ return value;
55
+ }
56
+
57
+ if (typeof value === "bigint") {
58
+ return value.toString();
59
+ }
60
+
61
+ if (typeof value === "string") {
62
+ return redactString(value, options);
63
+ }
64
+
65
+ if (typeof value === "function" || typeof value === "symbol") {
66
+ return `[${typeof value}]`;
67
+ }
68
+
69
+ if (depth <= 0) {
70
+ return `[max depth ${options.maxDepth} reached]`;
71
+ }
72
+
73
+ if (value instanceof Error) {
74
+ return {
75
+ name: redactString(value.name, options),
76
+ message: redactString(value.message, options),
77
+ stack: value.stack ? redactString(value.stack, options) : undefined,
78
+ };
79
+ }
80
+
81
+ if (typeof value !== "object") {
82
+ return redactString(String(value), options);
83
+ }
84
+
85
+ if (seen.has(value)) {
86
+ return "[circular]";
87
+ }
88
+ seen.add(value);
89
+
90
+ if (Array.isArray(value)) {
91
+ const output = value
92
+ .slice(0, options.maxArrayItems)
93
+ .map((item) => visit(item, options, depth - 1, seen));
94
+ if (value.length > options.maxArrayItems) {
95
+ output.push(`[${value.length - options.maxArrayItems} truncated items]`);
96
+ }
97
+ return output;
98
+ }
99
+
100
+ const entries = Object.entries(value as Record<string, unknown>);
101
+ const output: Record<string, unknown> = {};
102
+ for (const [key, item] of entries.slice(0, options.maxObjectKeys)) {
103
+ const normalizedKey = key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
104
+ output[key] = SENSITIVE_FIELD_RE.test(normalizedKey) ? REDACTED : visit(item, options, depth - 1, seen);
105
+ }
106
+ if (entries.length > options.maxObjectKeys) {
107
+ output.__truncatedKeys = entries.length - options.maxObjectKeys;
108
+ }
109
+ return output;
110
+ }
111
+
112
+ export function redactValue(value: unknown, options: Partial<RedactOptions> = {}): unknown {
113
+ const merged: RedactOptions = { ...DEFAULT_OPTIONS, ...options };
114
+ return visit(value, merged, merged.maxDepth, new WeakSet<object>());
115
+ }
package/src/state.ts CHANGED
@@ -9,6 +9,7 @@ export interface SessionRunState {
9
9
  errorCount: number;
10
10
  turnCount: number;
11
11
  tracingDisabled: boolean;
12
+ setupAttemptedThisSession: boolean;
12
13
  }
13
14
 
14
15
  const DEFAULT_SESSION_ID = "__pi_langfuse_default_session__";
@@ -25,6 +26,7 @@ function createSessionRunState(): SessionRunState {
25
26
  errorCount: 0,
26
27
  turnCount: 0,
27
28
  tracingDisabled: false,
29
+ setupAttemptedThisSession: false,
28
30
  };
29
31
  }
30
32
 
@@ -59,7 +61,6 @@ export function runWithSession<T>(sessionId: string | undefined, fn: () => T): T
59
61
 
60
62
  export const state = {
61
63
  config: null as Config | null,
62
- setupAttemptedThisSession: false,
63
64
  sessionStates: new Map<string, SessionRunState>(),
64
65
 
65
66
  get currentSessionId() {
@@ -118,10 +119,23 @@ export const state = {
118
119
  set isTracingDisabled(disabled: boolean) {
119
120
  getSessionRunState().tracingDisabled = disabled;
120
121
  },
122
+
123
+ get setupAttemptedThisSession() {
124
+ return getSessionRunState().setupAttemptedThisSession;
125
+ },
126
+ set setupAttemptedThisSession(attempted: boolean) {
127
+ getSessionRunState().setupAttemptedThisSession = attempted;
128
+ },
121
129
  };
122
130
 
123
131
  export function resetRunState(sessionId = getActiveSessionId()) {
124
- state.sessionStates.set(normalizeSessionId(sessionId), createSessionRunState());
132
+ const normalizedSessionId = normalizeSessionId(sessionId);
133
+ const setupAttemptedThisSession =
134
+ state.sessionStates.get(normalizedSessionId)?.setupAttemptedThisSession ?? false;
135
+ state.sessionStates.set(normalizedSessionId, {
136
+ ...createSessionRunState(),
137
+ setupAttemptedThisSession,
138
+ });
125
139
  }
126
140
 
127
141
  export function clearAllSessionStates() {
package/src/types.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import type { CapturePolicy } from "./capture-policy.js";
2
+
1
3
  export interface Config {
2
4
  publicKey: string;
3
5
  secretKey: string;
4
6
  host: string;
7
+ capturePolicy?: CapturePolicy;
5
8
  }
6
9
 
7
10
  export interface LangfuseObservation {
package/src/utils.ts CHANGED
@@ -6,6 +6,13 @@ import {
6
6
  MAX_STRING_LENGTH,
7
7
  MAX_TOOL_PAYLOAD_LENGTH,
8
8
  } from "./constants.js";
9
+ import { createCapturePolicy, type CapturePolicy } from "./capture-policy.js";
10
+ import { redactValue } from "./redaction.js";
11
+ import { state } from "./state.js";
12
+
13
+ export function getCapturePolicy(): CapturePolicy {
14
+ return state.config?.capturePolicy ?? createCapturePolicy();
15
+ }
9
16
 
10
17
  export function truncate(value: string, maxLength = MAX_STRING_LENGTH): string {
11
18
  return value.length > maxLength ? `${value.slice(0, maxLength)}... [truncated]` : value;
@@ -26,7 +33,10 @@ export function tryParseJson(value: string): unknown {
26
33
 
27
34
  const PAYLOAD_TOO_LARGE = "[payload too large]";
28
35
 
29
- export function shapePayload(value: unknown, options: { maxString?: number; depth?: number; maxNodes?: number } = {}): unknown {
36
+ export function shapePayload(
37
+ value: unknown,
38
+ options: { maxString?: number; depth?: number; maxNodes?: number; redact?: boolean } = {},
39
+ ): unknown {
30
40
  const maxString = options.maxString ?? MAX_STRING_LENGTH;
31
41
  const depth = options.depth ?? MAX_DEPTH;
32
42
  const maxNodes = options.maxNodes ?? MAX_PAYLOAD_NODES;
@@ -117,7 +127,15 @@ export function shapePayload(value: unknown, options: { maxString?: number; dept
117
127
  return String(item);
118
128
  }
119
129
 
120
- return visit(value, depth, new WeakSet<object>());
130
+ const shaped = visit(value, depth, new WeakSet<object>());
131
+ return options.redact === false
132
+ ? shaped
133
+ : redactValue(shaped, {
134
+ maxDepth: depth,
135
+ maxStringLength: maxString,
136
+ maxArrayItems: MAX_ARRAY_ITEMS,
137
+ maxObjectKeys: MAX_OBJECT_KEYS,
138
+ });
121
139
  }
122
140
 
123
141
  export function safeSerialize(value: unknown, maxLength = MAX_TOOL_PAYLOAD_LENGTH): string {