@braintrust/pi-extension 0.1.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/LICENSE +21 -0
- package/README.md +126 -0
- package/package.json +71 -0
- package/src/client.ts +244 -0
- package/src/config.test.ts +313 -0
- package/src/config.ts +461 -0
- package/src/index.integration.test.ts +598 -0
- package/src/index.test.ts +409 -0
- package/src/index.ts +861 -0
- package/src/state.test.ts +131 -0
- package/src/state.ts +197 -0
- package/src/types.ts +179 -0
- package/src/utils.test.ts +163 -0
- package/src/utils.ts +384 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { createLogger, loadConfig } from "./config.ts";
|
|
6
|
+
import type { TraceConfig } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const ENV_KEYS = [
|
|
9
|
+
"HOME",
|
|
10
|
+
"BRAINTRUST_API_KEY",
|
|
11
|
+
"BRAINTRUST_API_URL",
|
|
12
|
+
"BRAINTRUST_APP_URL",
|
|
13
|
+
"BRAINTRUST_ORG_NAME",
|
|
14
|
+
"BRAINTRUST_PROJECT",
|
|
15
|
+
"TRACE_TO_BRAINTRUST",
|
|
16
|
+
"BRAINTRUST_DEBUG",
|
|
17
|
+
"BRAINTRUST_LOG_FILE",
|
|
18
|
+
"BRAINTRUST_STATE_DIR",
|
|
19
|
+
"PI_PARENT_SPAN_ID",
|
|
20
|
+
"PI_ROOT_SPAN_ID",
|
|
21
|
+
"BRAINTRUST_ADDITIONAL_METADATA",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
const originalEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
25
|
+
const tempDirs: string[] = [];
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
for (const key of ENV_KEYS) {
|
|
29
|
+
delete process.env[key];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
for (const key of ENV_KEYS) {
|
|
35
|
+
const value = originalEnv[key];
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
delete process.env[key];
|
|
38
|
+
} else {
|
|
39
|
+
process.env[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
while (tempDirs.length > 0) {
|
|
44
|
+
rmSync(tempDirs.pop()!, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function makeTempDir(prefix: string): string {
|
|
49
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
50
|
+
tempDirs.push(dir);
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeJson(path: string, value: unknown): void {
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("loadConfig", () => {
|
|
60
|
+
it("applies global config, project config, then env overrides", () => {
|
|
61
|
+
const home = makeTempDir("pi-extension-home-");
|
|
62
|
+
const cwd = join(home, "workspace");
|
|
63
|
+
const projectStateDir = join(home, "project-state");
|
|
64
|
+
const envStateDir = join(home, "env-state");
|
|
65
|
+
|
|
66
|
+
process.env.HOME = home;
|
|
67
|
+
process.env.BRAINTRUST_PROJECT = "from-env";
|
|
68
|
+
process.env.TRACE_TO_BRAINTRUST = "false";
|
|
69
|
+
process.env.BRAINTRUST_ADDITIONAL_METADATA = '{"origin":"env","team":"platform"}';
|
|
70
|
+
process.env.BRAINTRUST_STATE_DIR = envStateDir;
|
|
71
|
+
|
|
72
|
+
writeJson(join(home, ".pi", "agent", "braintrust.json"), {
|
|
73
|
+
api_key: "global-key",
|
|
74
|
+
api_url: "https://global.example",
|
|
75
|
+
project: "from-global",
|
|
76
|
+
trace_to_braintrust: false,
|
|
77
|
+
debug: true,
|
|
78
|
+
additional_metadata: { origin: "global" },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
writeJson(join(cwd, ".pi", "braintrust.json"), {
|
|
82
|
+
api_url: "https://project.example",
|
|
83
|
+
project: "from-project",
|
|
84
|
+
trace_to_braintrust: true,
|
|
85
|
+
state_dir: projectStateDir,
|
|
86
|
+
additional_metadata: { origin: "project" },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const config = loadConfig(cwd);
|
|
90
|
+
|
|
91
|
+
expect(config.apiKey).toBe("global-key");
|
|
92
|
+
expect(config.apiUrl).toBe("https://project.example");
|
|
93
|
+
expect(config.projectName).toBe("from-env");
|
|
94
|
+
expect(config.enabled).toBe(false);
|
|
95
|
+
expect(config.additionalMetadata).toEqual({ origin: "env", team: "platform" });
|
|
96
|
+
expect(config.stateDir).toBe(envStateDir);
|
|
97
|
+
expect(config.configIssues).toEqual([]);
|
|
98
|
+
expect(existsSync(envStateDir)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("records config file parse errors without throwing away other valid config sources", () => {
|
|
102
|
+
const home = makeTempDir("pi-extension-home-");
|
|
103
|
+
const cwd = join(home, "workspace");
|
|
104
|
+
|
|
105
|
+
process.env.HOME = home;
|
|
106
|
+
|
|
107
|
+
mkdirSync(dirname(join(home, ".pi", "agent", "braintrust.json")), { recursive: true });
|
|
108
|
+
writeFileSync(
|
|
109
|
+
join(home, ".pi", "agent", "braintrust.json"),
|
|
110
|
+
'{\n "trace_to_braintrust": true,\n "api_key": "bad-json",\n}\n',
|
|
111
|
+
"utf8",
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
writeJson(join(cwd, ".pi", "braintrust.json"), {
|
|
115
|
+
trace_to_braintrust: true,
|
|
116
|
+
project: "from-project",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const config = loadConfig(cwd);
|
|
120
|
+
|
|
121
|
+
expect(config.enabled).toBe(true);
|
|
122
|
+
expect(config.projectName).toBe("from-project");
|
|
123
|
+
expect(config.configIssues).toEqual(
|
|
124
|
+
expect.arrayContaining([
|
|
125
|
+
expect.objectContaining({
|
|
126
|
+
path: join(home, ".pi", "agent", "braintrust.json"),
|
|
127
|
+
severity: "error",
|
|
128
|
+
message: expect.stringContaining("JSON"),
|
|
129
|
+
}),
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
path: "BRAINTRUST_API_KEY",
|
|
132
|
+
severity: "warning",
|
|
133
|
+
}),
|
|
134
|
+
]),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("ignores malformed config value types without crashing", () => {
|
|
139
|
+
const home = makeTempDir("pi-extension-home-");
|
|
140
|
+
const cwd = join(home, "workspace");
|
|
141
|
+
|
|
142
|
+
process.env.HOME = home;
|
|
143
|
+
process.env.BRAINTRUST_ADDITIONAL_METADATA = "not-json";
|
|
144
|
+
|
|
145
|
+
writeJson(join(home, ".pi", "agent", "braintrust.json"), {
|
|
146
|
+
api_key: { nested: true },
|
|
147
|
+
project: ["wrong-type"],
|
|
148
|
+
trace_to_braintrust: "definitely",
|
|
149
|
+
debug: { nope: true },
|
|
150
|
+
state_dir: { bad: true },
|
|
151
|
+
additional_metadata: ["bad"],
|
|
152
|
+
parent_span_id: { bad: true },
|
|
153
|
+
root_span_id: ["bad"],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const config = loadConfig(cwd);
|
|
157
|
+
|
|
158
|
+
expect(config.apiKey).toBe("");
|
|
159
|
+
expect(config.projectName).toBe("pi");
|
|
160
|
+
expect(config.enabled).toBe(false);
|
|
161
|
+
expect(config.debug).toBe(false);
|
|
162
|
+
expect(config.additionalMetadata).toEqual({});
|
|
163
|
+
expect(config.parentSpanId).toBeUndefined();
|
|
164
|
+
expect(config.rootSpanId).toBeUndefined();
|
|
165
|
+
expect(config.configIssues).toEqual(
|
|
166
|
+
expect.arrayContaining([
|
|
167
|
+
expect.objectContaining({
|
|
168
|
+
path: join(home, ".pi", "agent", "braintrust.json"),
|
|
169
|
+
message: expect.stringContaining("additional_metadata"),
|
|
170
|
+
severity: "error",
|
|
171
|
+
}),
|
|
172
|
+
expect.objectContaining({
|
|
173
|
+
path: "BRAINTRUST_ADDITIONAL_METADATA",
|
|
174
|
+
severity: "error",
|
|
175
|
+
}),
|
|
176
|
+
]),
|
|
177
|
+
);
|
|
178
|
+
expect(config.stateDir.endsWith(join(".pi", "agent", "state", "braintrust-pi-extension"))).toBe(
|
|
179
|
+
true,
|
|
180
|
+
);
|
|
181
|
+
expect(existsSync(config.stateDir)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("mirrors the parent span id to the root span id when only parent is provided", () => {
|
|
185
|
+
const home = makeTempDir("pi-extension-home-");
|
|
186
|
+
process.env.HOME = home;
|
|
187
|
+
process.env.BRAINTRUST_STATE_DIR = join(home, "state");
|
|
188
|
+
process.env.PI_PARENT_SPAN_ID = "parent-123";
|
|
189
|
+
|
|
190
|
+
const config = loadConfig(home);
|
|
191
|
+
|
|
192
|
+
expect(config.parentSpanId).toBe("parent-123");
|
|
193
|
+
expect(config.rootSpanId).toBe("parent-123");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("mirrors the root span id to the parent span id when only root is provided", () => {
|
|
197
|
+
const home = makeTempDir("pi-extension-home-");
|
|
198
|
+
process.env.HOME = home;
|
|
199
|
+
process.env.BRAINTRUST_STATE_DIR = join(home, "state");
|
|
200
|
+
process.env.PI_ROOT_SPAN_ID = "root-123";
|
|
201
|
+
|
|
202
|
+
const config = loadConfig(home);
|
|
203
|
+
|
|
204
|
+
expect(config.rootSpanId).toBe("root-123");
|
|
205
|
+
expect(config.parentSpanId).toBe("root-123");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("keeps lower-precedence URLs when higher-precedence URL values are invalid", () => {
|
|
209
|
+
const home = makeTempDir("pi-extension-home-");
|
|
210
|
+
const cwd = join(home, "workspace");
|
|
211
|
+
|
|
212
|
+
process.env.HOME = home;
|
|
213
|
+
process.env.BRAINTRUST_APP_URL = "ftp://braintrust.example";
|
|
214
|
+
|
|
215
|
+
writeJson(join(home, ".pi", "agent", "braintrust.json"), {
|
|
216
|
+
api_url: "https://global.example",
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
writeJson(join(cwd, ".pi", "braintrust.json"), {
|
|
220
|
+
api_url: "not-a-url",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const config = loadConfig(cwd);
|
|
224
|
+
|
|
225
|
+
expect(config.apiUrl).toBe("https://global.example");
|
|
226
|
+
expect(config.appUrl).toBe("https://www.braintrust.dev");
|
|
227
|
+
expect(config.configIssues).toEqual(
|
|
228
|
+
expect.arrayContaining([
|
|
229
|
+
expect.objectContaining({
|
|
230
|
+
path: join(cwd, ".pi", "braintrust.json"),
|
|
231
|
+
message: "api_url must be a valid http(s) URL",
|
|
232
|
+
severity: "error",
|
|
233
|
+
}),
|
|
234
|
+
expect.objectContaining({
|
|
235
|
+
path: "BRAINTRUST_APP_URL",
|
|
236
|
+
message: "BRAINTRUST_APP_URL must be a valid http(s) URL",
|
|
237
|
+
severity: "error",
|
|
238
|
+
}),
|
|
239
|
+
]),
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("warns when parent_span_id and root_span_id are both explicitly set to the same value", () => {
|
|
244
|
+
const home = makeTempDir("pi-extension-home-");
|
|
245
|
+
process.env.HOME = home;
|
|
246
|
+
process.env.BRAINTRUST_STATE_DIR = join(home, "state");
|
|
247
|
+
process.env.PI_PARENT_SPAN_ID = "span-123";
|
|
248
|
+
process.env.PI_ROOT_SPAN_ID = "span-123";
|
|
249
|
+
|
|
250
|
+
const config = loadConfig(home);
|
|
251
|
+
|
|
252
|
+
expect(config.configIssues).toContainEqual({
|
|
253
|
+
path: "parent_span_id/root_span_id",
|
|
254
|
+
message:
|
|
255
|
+
"parent_span_id and root_span_id are identical; set only one unless the parent span is also the trace root",
|
|
256
|
+
severity: "warning",
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("warns when tracing is enabled without an API key", () => {
|
|
261
|
+
const home = makeTempDir("pi-extension-home-");
|
|
262
|
+
process.env.HOME = home;
|
|
263
|
+
process.env.BRAINTRUST_STATE_DIR = join(home, "state");
|
|
264
|
+
process.env.TRACE_TO_BRAINTRUST = "true";
|
|
265
|
+
|
|
266
|
+
const config = loadConfig(home);
|
|
267
|
+
|
|
268
|
+
expect(config.enabled).toBe(true);
|
|
269
|
+
expect(config.configIssues).toContainEqual({
|
|
270
|
+
path: "BRAINTRUST_API_KEY",
|
|
271
|
+
message: "TRACE_TO_BRAINTRUST is enabled but BRAINTRUST_API_KEY is not set",
|
|
272
|
+
severity: "warning",
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("createLogger", () => {
|
|
278
|
+
it("writes json log lines to the default log file when debug is enabled", async () => {
|
|
279
|
+
const stateDir = makeTempDir("pi-extension-state-");
|
|
280
|
+
const config: TraceConfig = {
|
|
281
|
+
enabled: true,
|
|
282
|
+
apiKey: "key",
|
|
283
|
+
apiUrl: undefined,
|
|
284
|
+
appUrl: "https://www.braintrust.dev",
|
|
285
|
+
orgName: undefined,
|
|
286
|
+
projectName: "pi",
|
|
287
|
+
debug: true,
|
|
288
|
+
logFile: undefined,
|
|
289
|
+
stateDir,
|
|
290
|
+
additionalMetadata: {},
|
|
291
|
+
parentSpanId: undefined,
|
|
292
|
+
rootSpanId: undefined,
|
|
293
|
+
configIssues: [],
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const logger = createLogger(config);
|
|
297
|
+
logger.debug("debug message", { nested: { value: 1 } });
|
|
298
|
+
logger.warn("warn message");
|
|
299
|
+
await logger.flush();
|
|
300
|
+
|
|
301
|
+
const lines = readFileSync(logger.filePath, "utf8").trim().split("\n");
|
|
302
|
+
expect(lines).toHaveLength(2);
|
|
303
|
+
expect(JSON.parse(lines[0])).toMatchObject({
|
|
304
|
+
level: "debug",
|
|
305
|
+
message: "debug message",
|
|
306
|
+
data: { nested: { value: 1 } },
|
|
307
|
+
});
|
|
308
|
+
expect(JSON.parse(lines[1])).toMatchObject({
|
|
309
|
+
level: "warn",
|
|
310
|
+
message: "warn message",
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|