pi-langfuse 1.5.2 → 1.5.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.
- package/README.md +47 -0
- package/package.json +1 -1
- package/src/handlers/agent.ts +5 -0
- package/src/handlers/generation.ts +2 -2
- package/src/langfuse.ts +2 -1
- package/src/source-metadata.ts +160 -0
- package/src/types.ts +1 -0
- package/src/utils.ts +3 -0
package/README.md
CHANGED
|
@@ -150,6 +150,53 @@ The package also includes a Langfuse CLI skill, so Langfuse data can be queried
|
|
|
150
150
|
/pi-langfuse-langfuse <your-query>
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
## Source Metadata
|
|
154
|
+
|
|
155
|
+
Local prototype note: source metadata support in this installed package is a local prototype patch. A durable solution should be shipped through an upstream PR, a fork, or a maintained package version so reinstalling the extension does not lose the behavior.
|
|
156
|
+
|
|
157
|
+
For Git-backed runs, the extension attaches safe source metadata to traces:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"source_type": "git-repo",
|
|
162
|
+
"repo_identity": "owner/repo",
|
|
163
|
+
"repo_owner": "owner",
|
|
164
|
+
"repo_name": "repo",
|
|
165
|
+
"repo_root_name": "repo",
|
|
166
|
+
"git_branch": "main",
|
|
167
|
+
"git_commit": "abc123",
|
|
168
|
+
"git_remote_host": "github.com",
|
|
169
|
+
"git_remote_path": "owner/repo",
|
|
170
|
+
"metadata_source": "git-detection"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`repo_identity` is `owner/repo`. `repo_name` is the repo name only and must not contain a slash.
|
|
175
|
+
|
|
176
|
+
A Git repo may optionally provide `.pi-langfuse.metadata.json`. Overrides are whitelist-only; unknown keys are ignored. Allowed keys are:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
repo_identity
|
|
180
|
+
repo_owner
|
|
181
|
+
repo_name
|
|
182
|
+
source_type
|
|
183
|
+
service_name
|
|
184
|
+
project_slug
|
|
185
|
+
environment
|
|
186
|
+
observability_owner
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Repo-local overrides are used only after the working directory is confirmed to be inside a usable Git repo. If Git detection fails for any reason, including a missing Git command, corrupted repo, or non-Git folder, the extension ignores repo-local identity files and emits only:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"source_type": "non-git",
|
|
194
|
+
"metadata_source": "non-git"
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The extension must not upload raw absolute local paths, credentialed remotes, tokens, unknown override keys, or folder names for non-Git folders.
|
|
199
|
+
|
|
153
200
|
## Troubleshooting
|
|
154
201
|
|
|
155
202
|
### No traces appearing?
|
package/package.json
CHANGED
package/src/handlers/agent.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { ensureConfig } from "../config.js";
|
|
|
4
4
|
import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput, getCapturePolicy } from "../utils.js";
|
|
5
5
|
import { closeDanglingObservations } from "./tool.js";
|
|
6
6
|
import { applyCapturePolicy } from "../capture-policy.js";
|
|
7
|
+
import { collectSourceMetadata } from "../source-metadata.js";
|
|
7
8
|
|
|
8
9
|
function stringMetadata(metadata: Record<string, unknown> | undefined): Record<string, string> | undefined {
|
|
9
10
|
if (!metadata) {
|
|
@@ -67,11 +68,13 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
67
68
|
images: event.images,
|
|
68
69
|
context: event.context ?? event.attachments,
|
|
69
70
|
});
|
|
71
|
+
const sourceMetadata = collectSourceMetadata(cwd);
|
|
70
72
|
const captured = applyCapturePolicy(
|
|
71
73
|
{
|
|
72
74
|
input: rawPromptInput,
|
|
73
75
|
metadata: {
|
|
74
76
|
cwd,
|
|
77
|
+
...sourceMetadata,
|
|
75
78
|
...(state.currentModel ? { model: state.currentModel } : {}),
|
|
76
79
|
...(state.currentProvider ? { provider: state.currentProvider } : {}),
|
|
77
80
|
sessionId: state.currentSessionId || undefined,
|
|
@@ -88,6 +91,7 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
88
91
|
activeGenerations: new Map(),
|
|
89
92
|
generationOrder: [],
|
|
90
93
|
activeTools: new Map(),
|
|
94
|
+
sourceMetadata,
|
|
91
95
|
providerMetadataByRequest: new Map(),
|
|
92
96
|
};
|
|
93
97
|
|
|
@@ -133,6 +137,7 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
133
137
|
output: rawOutput,
|
|
134
138
|
metadata: {
|
|
135
139
|
cwd: state.agentState.cwd,
|
|
140
|
+
...(state.agentState.sourceMetadata ?? {}),
|
|
136
141
|
completed: true,
|
|
137
142
|
model: state.currentModel || undefined,
|
|
138
143
|
provider: state.currentProvider || undefined,
|
|
@@ -184,7 +184,7 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
184
184
|
model: model || undefined,
|
|
185
185
|
modelParameters,
|
|
186
186
|
usageDetails,
|
|
187
|
-
costDetails,
|
|
187
|
+
...(costDetails ? { costDetails } : {}),
|
|
188
188
|
metadata: {
|
|
189
189
|
...generation.metadata,
|
|
190
190
|
finishReason: message.finishReason ?? message.stopReason ?? event.finishReason,
|
|
@@ -232,7 +232,7 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
232
232
|
model: model || undefined,
|
|
233
233
|
modelParameters,
|
|
234
234
|
usageDetails,
|
|
235
|
-
costDetails,
|
|
235
|
+
...(costDetails ? { costDetails } : {}),
|
|
236
236
|
metadata: captured.metadata,
|
|
237
237
|
},
|
|
238
238
|
asType: "generation",
|
package/src/langfuse.ts
CHANGED
|
@@ -335,7 +335,8 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
const responseBody = response as { errors?: unknown[] } | undefined;
|
|
338
|
-
const
|
|
338
|
+
const responseErrors = responseBody?.errors;
|
|
339
|
+
const errors = Array.isArray(responseErrors) ? responseErrors : [];
|
|
339
340
|
if (errors.length > 0) {
|
|
340
341
|
console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
|
|
341
342
|
} else {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export type SourceMetadata = Record<string, string>;
|
|
6
|
+
|
|
7
|
+
const OVERRIDE_KEYS = new Set([
|
|
8
|
+
"repo_identity",
|
|
9
|
+
"repo_owner",
|
|
10
|
+
"repo_name",
|
|
11
|
+
"source_type",
|
|
12
|
+
"service_name",
|
|
13
|
+
"project_slug",
|
|
14
|
+
"environment",
|
|
15
|
+
"observability_owner",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function nonGitMetadata(): SourceMetadata {
|
|
19
|
+
return {
|
|
20
|
+
source_type: "non-git",
|
|
21
|
+
metadata_source: "non-git",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runGit(cwd: string, args: string[]): string {
|
|
26
|
+
return execFileSync("git", args, {
|
|
27
|
+
cwd,
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
30
|
+
timeout: 2000,
|
|
31
|
+
}).trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function firstLine(value: string): string {
|
|
35
|
+
return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function sanitizeGitRemote(remoteUrl: string): Partial<Pick<SourceMetadata, "git_remote_host" | "git_remote_path">> {
|
|
39
|
+
const raw = firstLine(remoteUrl).replace(/\.git$/i, "");
|
|
40
|
+
if (!raw) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const parsed = new URL(raw);
|
|
46
|
+
const path = parsed.pathname.replace(/^\/+/, "").replace(/\.git$/i, "");
|
|
47
|
+
return parsed.hostname && path ? { git_remote_host: parsed.hostname, git_remote_path: path } : {};
|
|
48
|
+
} catch {
|
|
49
|
+
// Continue with scp-like SSH syntax, for example git@github.com:owner/repo.git.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const scpLike = raw.match(/^(?:[^@\s/:]+@)?([^:\s]+):(.+)$/);
|
|
53
|
+
if (scpLike) {
|
|
54
|
+
const host = scpLike[1];
|
|
55
|
+
const path = scpLike[2].replace(/^\/+/, "").replace(/\.git$/i, "");
|
|
56
|
+
return host && path && !path.includes("@") ? { git_remote_host: host, git_remote_path: path } : {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function deriveIdentity(remotePath: string | undefined): Partial<Pick<SourceMetadata, "repo_identity" | "repo_owner" | "repo_name">> {
|
|
63
|
+
if (!remotePath) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
const parts = remotePath.split("/").filter(Boolean);
|
|
67
|
+
if (parts.length < 2) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
const repoName = parts[parts.length - 1];
|
|
71
|
+
const owner = parts[parts.length - 2];
|
|
72
|
+
if (!repoName || !owner || repoName.includes("/")) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
repo_identity: `${owner}/${repoName}`,
|
|
77
|
+
repo_owner: owner,
|
|
78
|
+
repo_name: repoName,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findRepoMetadataFile(cwd: string, gitRoot: string): string | undefined {
|
|
83
|
+
let current = cwd;
|
|
84
|
+
const root = gitRoot;
|
|
85
|
+
while (true) {
|
|
86
|
+
const candidate = join(current, ".pi-langfuse.metadata.json");
|
|
87
|
+
if (existsSync(candidate)) {
|
|
88
|
+
return candidate;
|
|
89
|
+
}
|
|
90
|
+
if (current === root) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const parent = dirname(current);
|
|
94
|
+
if (parent === current) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
current = parent;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readWhitelistedOverrides(cwd: string, gitRoot: string): SourceMetadata {
|
|
103
|
+
const path = findRepoMetadataFile(cwd, gitRoot);
|
|
104
|
+
if (!path) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
109
|
+
const output: SourceMetadata = {};
|
|
110
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
111
|
+
if (OVERRIDE_KEYS.has(key) && typeof value === "string" && value.trim()) {
|
|
112
|
+
output[key] = value.trim();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (output.repo_name?.includes("/")) {
|
|
116
|
+
delete output.repo_name;
|
|
117
|
+
}
|
|
118
|
+
return output;
|
|
119
|
+
} catch {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function collectSourceMetadata(cwd: string): SourceMetadata {
|
|
125
|
+
try {
|
|
126
|
+
const inside = runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
127
|
+
if (inside !== "true") {
|
|
128
|
+
return nonGitMetadata();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const gitRoot = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
132
|
+
const commit = runGit(cwd, ["rev-parse", "HEAD"]);
|
|
133
|
+
const branch = runGit(cwd, ["branch", "--show-current"]);
|
|
134
|
+
const remote = runGit(cwd, ["config", "--get", "remote.origin.url"]);
|
|
135
|
+
const remoteMetadata = sanitizeGitRemote(remote);
|
|
136
|
+
const derivedIdentity = deriveIdentity(remoteMetadata.git_remote_path);
|
|
137
|
+
const overrides = readWhitelistedOverrides(cwd, gitRoot);
|
|
138
|
+
|
|
139
|
+
const metadata: SourceMetadata = {
|
|
140
|
+
source_type: "git-repo",
|
|
141
|
+
repo_root_name: basename(gitRoot),
|
|
142
|
+
...(branch ? { git_branch: branch } : {}),
|
|
143
|
+
git_commit: commit,
|
|
144
|
+
...remoteMetadata,
|
|
145
|
+
...derivedIdentity,
|
|
146
|
+
...overrides,
|
|
147
|
+
metadata_source: Object.keys(overrides).length > 0 ? "repo-file" : "git-detection",
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (metadata.repo_name?.includes("/")) {
|
|
151
|
+
delete metadata.repo_name;
|
|
152
|
+
}
|
|
153
|
+
if (!metadata.repo_identity && metadata.repo_owner && metadata.repo_name) {
|
|
154
|
+
metadata.repo_identity = `${metadata.repo_owner}/${metadata.repo_name}`;
|
|
155
|
+
}
|
|
156
|
+
return metadata;
|
|
157
|
+
} catch {
|
|
158
|
+
return nonGitMetadata();
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -107,5 +107,6 @@ export interface AgentState {
|
|
|
107
107
|
generationOrder: string[];
|
|
108
108
|
activeTools: Map<string, ToolState>;
|
|
109
109
|
latestAssistantOutput?: unknown;
|
|
110
|
+
sourceMetadata?: Record<string, unknown>;
|
|
110
111
|
providerMetadataByRequest: Map<string, Record<string, unknown>>;
|
|
111
112
|
}
|
package/src/utils.ts
CHANGED
|
@@ -400,6 +400,9 @@ export function extractCostDetails(messageOrEvent: Record<string, unknown>): Rec
|
|
|
400
400
|
const input = Number(cost.input ?? cost.inputCost ?? 0);
|
|
401
401
|
const output = Number(cost.output ?? cost.outputCost ?? 0);
|
|
402
402
|
const total = Number(cost.total ?? cost.totalCost ?? input + output);
|
|
403
|
+
if (input === 0 && output === 0 && total === 0) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
403
406
|
|
|
404
407
|
return { input, output, total };
|
|
405
408
|
}
|