@raquezha/notrace 0.0.3 → 0.0.5
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/CHANGELOG.md +13 -0
- package/README.md +70 -7
- package/dist/notrace.js +584 -360
- package/extensions/notrace.ts +618 -358
- package/package.json +1 -1
package/extensions/notrace.ts
CHANGED
|
@@ -2,6 +2,302 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
+
const REDACTED = "[REDACTED by notrace]";
|
|
6
|
+
const MAX_STRING_LENGTH = 20_000;
|
|
7
|
+
const MAX_ARRAY_ITEMS = 200;
|
|
8
|
+
const MAX_OBJECT_KEYS = 200;
|
|
9
|
+
const MAX_DEPTH = 8;
|
|
10
|
+
|
|
11
|
+
const SENSITIVE_KEY_RE = /(authorization|cookie|setcookie|password|passwd|pwd|secret|token|apikey|accesskey|accesskeyid|accessid|accesstoken|privatekey|session|credential|refreshtoken|idtoken)/i;
|
|
12
|
+
const SENSITIVE_VALUE_RE = /(bearer\s+[a-z0-9._~+/=-]{12,}|sk-[a-z0-9_-]{16,}|gh[pousr]_[a-z0-9_]{16,}|xox[baprs]-[a-z0-9-]{16,}|AKIA[0-9A-Z]{16})/gi;
|
|
13
|
+
|
|
14
|
+
type CaptureMode = "metadata" | "redacted" | "full";
|
|
15
|
+
|
|
16
|
+
function getCaptureMode(): CaptureMode {
|
|
17
|
+
const mode = process.env.NOTRACE_CAPTURE?.toLowerCase();
|
|
18
|
+
if (mode === "metadata" || mode === "full") return mode;
|
|
19
|
+
return "redacted";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isSensitiveKey(key: string): boolean {
|
|
23
|
+
const normalized = key.replace(/[^a-z0-9]/gi, "");
|
|
24
|
+
if (/^(inputtokens|outputtokens|totaltokens|prompttokens|completiontokens|reasoningtokens|cachedtokens|cachecreationinputtokens|cachereadinputtokens|cost|total|input|output|prompt|completion|reasoning|read|write)$/i.test(normalized)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return SENSITIVE_KEY_RE.test(normalized);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sanitizeUsageValue(usage: unknown): unknown {
|
|
31
|
+
if (getCaptureMode() === "full") return usage;
|
|
32
|
+
if (!usage || typeof usage !== "object") return sanitizeTraceValue(usage);
|
|
33
|
+
|
|
34
|
+
const source = usage as Record<string, unknown>;
|
|
35
|
+
const output: Record<string, unknown> = {};
|
|
36
|
+
|
|
37
|
+
for (const [key, value] of Object.entries(source)) {
|
|
38
|
+
if (typeof value === "number" || typeof value === "boolean" || value == null) {
|
|
39
|
+
output[key] = value;
|
|
40
|
+
} else if (typeof value === "object" && value) {
|
|
41
|
+
output[key] = sanitizeUsageValue(value);
|
|
42
|
+
} else {
|
|
43
|
+
output[key] = sanitizeTraceValue(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function redactString(value: string): string {
|
|
51
|
+
const redacted = value.replace(SENSITIVE_VALUE_RE, REDACTED);
|
|
52
|
+
if (redacted.length <= MAX_STRING_LENGTH) return redacted;
|
|
53
|
+
return `${redacted.slice(0, MAX_STRING_LENGTH)}\n…[truncated ${redacted.length - MAX_STRING_LENGTH} chars by notrace]`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sanitizeTraceValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
|
|
57
|
+
if (getCaptureMode() === "full") return value;
|
|
58
|
+
if (value == null || typeof value === "number" || typeof value === "boolean") return value;
|
|
59
|
+
if (typeof value === "string") return redactString(value);
|
|
60
|
+
if (typeof value === "bigint") return value.toString();
|
|
61
|
+
if (typeof value === "function" || typeof value === "symbol") return `[${typeof value}]`;
|
|
62
|
+
if (depth >= MAX_DEPTH) return "[Max depth reached by notrace]";
|
|
63
|
+
if (typeof value !== "object") return String(value);
|
|
64
|
+
if (seen.has(value)) return "[Circular]";
|
|
65
|
+
seen.add(value);
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
const items = value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeTraceValue(item, depth + 1, seen));
|
|
69
|
+
if (value.length > MAX_ARRAY_ITEMS) items.push(`…[truncated ${value.length - MAX_ARRAY_ITEMS} items by notrace]`);
|
|
70
|
+
return items;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const output: Record<string, unknown> = {};
|
|
74
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
75
|
+
for (const [key, item] of entries.slice(0, MAX_OBJECT_KEYS)) {
|
|
76
|
+
output[key] = isSensitiveKey(key) ? REDACTED : sanitizeTraceValue(item, depth + 1, seen);
|
|
77
|
+
}
|
|
78
|
+
if (entries.length > MAX_OBJECT_KEYS) output.__notrace_truncated__ = `${entries.length - MAX_OBJECT_KEYS} keys`;
|
|
79
|
+
return output;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function safeResolveUnder(baseDir: string, candidate: string): string | null {
|
|
83
|
+
const base = path.resolve(baseDir);
|
|
84
|
+
const resolved = path.resolve(base, candidate);
|
|
85
|
+
const relative = path.relative(base, resolved);
|
|
86
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function escapeHtml(value: unknown): string {
|
|
90
|
+
return String(value).replace(/[&<>'"]/g, (char) => {
|
|
91
|
+
switch (char) {
|
|
92
|
+
case "&": return "&";
|
|
93
|
+
case "<": return "<";
|
|
94
|
+
case ">": return ">";
|
|
95
|
+
case "'": return "'";
|
|
96
|
+
case '"': return """;
|
|
97
|
+
default: return char;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type NotraceLocation = {
|
|
103
|
+
workflowDir: string;
|
|
104
|
+
notraceDir: string;
|
|
105
|
+
outputDir: string;
|
|
106
|
+
taskDir: string | null;
|
|
107
|
+
taskId: string | null;
|
|
108
|
+
taskPath: string | null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type NotraceMetrics = {
|
|
112
|
+
totalTokens: number;
|
|
113
|
+
inputTokens: number;
|
|
114
|
+
outputTokens: number;
|
|
115
|
+
totalCost: number;
|
|
116
|
+
toolCallCount: number;
|
|
117
|
+
toolErrorCount: number;
|
|
118
|
+
llmCallCount: number;
|
|
119
|
+
turnCount: number;
|
|
120
|
+
models: string[];
|
|
121
|
+
providers: string[];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function ensureWorkflowDir(workflowDir: string): void {
|
|
125
|
+
if (existsSync(workflowDir)) return;
|
|
126
|
+
try {
|
|
127
|
+
mkdirSync(workflowDir, { recursive: true, mode: 0o700 });
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function safePathSegment(value: string): string {
|
|
132
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 160) || `session-${Date.now()}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getNotraceLocation(cwd: string, sessionId: string): NotraceLocation {
|
|
136
|
+
const workflowDir = path.resolve(cwd, ".workflow");
|
|
137
|
+
const notraceDir = path.resolve(cwd, ".notrace");
|
|
138
|
+
const outputDir = path.join(notraceDir, "sessions", safePathSegment(sessionId));
|
|
139
|
+
ensureWorkflowDir(workflowDir);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
|
|
143
|
+
if (existsSync(activeTaskJsonPath)) {
|
|
144
|
+
const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
|
|
145
|
+
const candidate = typeof content.taskPath === "string"
|
|
146
|
+
? safeResolveUnder(cwd, content.taskPath)
|
|
147
|
+
: typeof content.active_task === "string"
|
|
148
|
+
? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
|
|
149
|
+
: null;
|
|
150
|
+
|
|
151
|
+
if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
|
|
152
|
+
return {
|
|
153
|
+
workflowDir,
|
|
154
|
+
notraceDir,
|
|
155
|
+
outputDir,
|
|
156
|
+
taskDir: candidate,
|
|
157
|
+
taskId: typeof content.active_task === "string" ? content.active_task : path.basename(candidate),
|
|
158
|
+
taskPath: path.relative(cwd, candidate)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// fallback
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
workflowDir,
|
|
168
|
+
notraceDir,
|
|
169
|
+
outputDir,
|
|
170
|
+
taskDir: null,
|
|
171
|
+
taskId: null,
|
|
172
|
+
taskPath: null
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function updateNotraceIndex(cwd: string, location: NotraceLocation, entry: Record<string, unknown>): void {
|
|
177
|
+
const indexPath = path.join(location.notraceDir, "index.json");
|
|
178
|
+
const base = {
|
|
179
|
+
schemaVersion: 1,
|
|
180
|
+
kind: "notrace-index",
|
|
181
|
+
workdir: ".",
|
|
182
|
+
sessions: [] as Record<string, unknown>[]
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
mkdirSync(location.notraceDir, { recursive: true, mode: 0o700 });
|
|
187
|
+
const existing = existsSync(indexPath)
|
|
188
|
+
? JSON.parse(readFileSync(indexPath, "utf-8"))
|
|
189
|
+
: base;
|
|
190
|
+
const sessions = Array.isArray(existing.sessions) ? existing.sessions : [];
|
|
191
|
+
const sessionId = entry.sessionId;
|
|
192
|
+
const nextSessions = sessions.filter((item: any) => item?.sessionId !== sessionId);
|
|
193
|
+
nextSessions.push(entry);
|
|
194
|
+
const next = {
|
|
195
|
+
...base,
|
|
196
|
+
...existing,
|
|
197
|
+
schemaVersion: 1,
|
|
198
|
+
kind: "notrace-index",
|
|
199
|
+
workdir: ".",
|
|
200
|
+
sessions: nextSessions
|
|
201
|
+
};
|
|
202
|
+
writeFileSync(indexPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
203
|
+
} catch {
|
|
204
|
+
// keep notrace non-fatal if index update fails
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function appendWorkLogEntry(taskDir: string, message: string): void {
|
|
209
|
+
const workMd = path.join(taskDir, "WORK.md");
|
|
210
|
+
if (!existsSync(workMd)) return;
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const text = readFileSync(workMd, "utf-8");
|
|
214
|
+
const entry = `- ${new Date().toISOString()}: ${message}`;
|
|
215
|
+
|
|
216
|
+
if (!/^(## )?\[LOG\]\s*$/m.test(text)) {
|
|
217
|
+
writeFileSync(workMd, `${text.trimEnd()}\n\n## [LOG]\n${entry}\n`, { encoding: "utf-8" });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const lines = text.split("\n");
|
|
222
|
+
const logIndex = lines.findIndex((line) => /^(## )?\[LOG\]\s*$/.test(line));
|
|
223
|
+
if (logIndex === -1) return;
|
|
224
|
+
|
|
225
|
+
let nextSectionIndex = lines.length;
|
|
226
|
+
for (let i = logIndex + 1; i < lines.length; i++) {
|
|
227
|
+
if (/^(## )?\[[A-Z0-9_-]+\]\s*$/.test(lines[i])) {
|
|
228
|
+
nextSectionIndex = i;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const before = lines.slice(0, nextSectionIndex);
|
|
234
|
+
const after = lines.slice(nextSectionIndex);
|
|
235
|
+
while (before.length > logIndex + 1 && before[before.length - 1]?.trim() === "") {
|
|
236
|
+
before.pop();
|
|
237
|
+
}
|
|
238
|
+
before.push(entry);
|
|
239
|
+
|
|
240
|
+
writeFileSync(workMd, `${[...before, ...after].join("\n").replace(/\n*$/, "\n")}`, { encoding: "utf-8" });
|
|
241
|
+
} catch {
|
|
242
|
+
// keep notrace non-fatal if WORK.md append fails
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function collectMetrics(events: any[]): NotraceMetrics {
|
|
247
|
+
let totalTokens = 0;
|
|
248
|
+
let inputTokens = 0;
|
|
249
|
+
let outputTokens = 0;
|
|
250
|
+
let totalCost = 0;
|
|
251
|
+
let toolCallCount = 0;
|
|
252
|
+
let toolErrorCount = 0;
|
|
253
|
+
let llmCallCount = 0;
|
|
254
|
+
let turnCount = 0;
|
|
255
|
+
const models = new Set<string>();
|
|
256
|
+
const providers = new Set<string>();
|
|
257
|
+
|
|
258
|
+
for (const event of events) {
|
|
259
|
+
if (event.type === "turn_start") {
|
|
260
|
+
turnCount++;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (event.type === "tool_start") {
|
|
265
|
+
toolCallCount++;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (event.type === "tool_end") {
|
|
270
|
+
if (event.isError) toolErrorCount++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (event.type === "llm_completion") {
|
|
275
|
+
llmCallCount++;
|
|
276
|
+
if (typeof event.model === "string" && event.model) models.add(event.model);
|
|
277
|
+
if (typeof event.provider === "string" && event.provider) providers.add(event.provider);
|
|
278
|
+
if (event.usage) {
|
|
279
|
+
inputTokens += event.usage.input || 0;
|
|
280
|
+
outputTokens += event.usage.output || 0;
|
|
281
|
+
totalTokens += event.usage.totalTokens || 0;
|
|
282
|
+
totalCost += event.usage.cost?.total || 0;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
totalTokens,
|
|
289
|
+
inputTokens,
|
|
290
|
+
outputTokens,
|
|
291
|
+
totalCost,
|
|
292
|
+
toolCallCount,
|
|
293
|
+
toolErrorCount,
|
|
294
|
+
llmCallCount,
|
|
295
|
+
turnCount,
|
|
296
|
+
models: [...models],
|
|
297
|
+
providers: [...providers]
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
5
301
|
/**
|
|
6
302
|
* html-observability extension
|
|
7
303
|
*
|
|
@@ -17,28 +313,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
17
313
|
let llmStartTime = 0;
|
|
18
314
|
const activeToolTimes: Record<string, number> = {};
|
|
19
315
|
|
|
20
|
-
// Helper to extract active task path from .workflow/active_task.json
|
|
21
|
-
function getActiveTaskDir(cwd: string): string {
|
|
22
|
-
try {
|
|
23
|
-
const activeTaskJsonPath = path.join(cwd, ".workflow", "active_task.json");
|
|
24
|
-
if (existsSync(activeTaskJsonPath)) {
|
|
25
|
-
const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
|
|
26
|
-
if (content.taskPath) {
|
|
27
|
-
return path.resolve(cwd, content.taskPath);
|
|
28
|
-
} else if (content.active_task) {
|
|
29
|
-
return path.resolve(cwd, ".workflow", "tasks", content.active_task);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
// fallback
|
|
34
|
-
}
|
|
35
|
-
const defaultDir = path.join(cwd, ".workflow");
|
|
36
|
-
if (!existsSync(defaultDir)) {
|
|
37
|
-
try { mkdirSync(defaultDir, { recursive: true }); } catch {}
|
|
38
|
-
}
|
|
39
|
-
return defaultDir;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
316
|
// 1. Session start
|
|
43
317
|
pi.on("session_start", async (event, ctx) => {
|
|
44
318
|
traceId = ctx.sessionManager.getSessionId() || `session-${Date.now()}`;
|
|
@@ -74,7 +348,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
74
348
|
type: "tool_start",
|
|
75
349
|
toolCallId,
|
|
76
350
|
toolName,
|
|
77
|
-
args,
|
|
351
|
+
args: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(args),
|
|
78
352
|
timestamp: Date.now()
|
|
79
353
|
});
|
|
80
354
|
});
|
|
@@ -89,7 +363,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
89
363
|
type: "tool_end",
|
|
90
364
|
toolCallId,
|
|
91
365
|
toolName,
|
|
92
|
-
result,
|
|
366
|
+
result: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(result),
|
|
93
367
|
isError,
|
|
94
368
|
durationMs,
|
|
95
369
|
timestamp: Date.now()
|
|
@@ -99,7 +373,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
373
|
|
|
100
374
|
// 6. LLM call start (capture payload)
|
|
101
375
|
pi.on("before_provider_request", async (event, ctx) => {
|
|
102
|
-
activeLlmPayload = event.payload;
|
|
376
|
+
activeLlmPayload = getCaptureMode() === "metadata" ? null : sanitizeTraceValue(event.payload);
|
|
103
377
|
llmStartTime = Date.now();
|
|
104
378
|
});
|
|
105
379
|
|
|
@@ -115,8 +389,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
115
389
|
model: message.model || "unknown",
|
|
116
390
|
provider: message.provider || "unknown",
|
|
117
391
|
inputPayload: activeLlmPayload,
|
|
118
|
-
outputContent: message.content,
|
|
119
|
-
usage: message.usage,
|
|
392
|
+
outputContent: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(message.content),
|
|
393
|
+
usage: sanitizeUsageValue(message.usage),
|
|
120
394
|
durationMs,
|
|
121
395
|
timestamp: Date.now()
|
|
122
396
|
});
|
|
@@ -129,56 +403,103 @@ export default function (pi: ExtensionAPI) {
|
|
|
129
403
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
130
404
|
const sessionEndTime = Date.now();
|
|
131
405
|
const totalDurationMs = sessionEndTime - sessionStartTime;
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let totalCost = 0;
|
|
141
|
-
let toolCallCount = 0;
|
|
142
|
-
let llmCallCount = 0;
|
|
143
|
-
|
|
144
|
-
events.forEach((e) => {
|
|
145
|
-
if (e.type === "llm_completion") {
|
|
146
|
-
llmCallCount++;
|
|
147
|
-
if (e.usage) {
|
|
148
|
-
inputTokens += e.usage.input || 0;
|
|
149
|
-
outputTokens += e.usage.output || 0;
|
|
150
|
-
totalTokens += e.usage.totalTokens || 0;
|
|
151
|
-
totalCost += e.usage.cost?.total || 0;
|
|
152
|
-
}
|
|
153
|
-
} else if (e.type === "tool_start") {
|
|
154
|
-
toolCallCount++;
|
|
155
|
-
}
|
|
156
|
-
});
|
|
406
|
+
const projectName = process.env.PHOENIX_PROJECT_NAME || "pi-coding-agent";
|
|
407
|
+
const captureMode = getCaptureMode();
|
|
408
|
+
const location = getNotraceLocation(ctx.cwd, traceId);
|
|
409
|
+
const reportPath = path.join(location.outputDir, "notrace.html");
|
|
410
|
+
const recordPath = path.join(location.outputDir, "notrace.json");
|
|
411
|
+
const reviewPath = path.join(location.outputDir, "notrace.review.json");
|
|
412
|
+
const metrics = collectMetrics(events);
|
|
413
|
+
const sessionFile = ctx.sessionManager.getSessionFile() || null;
|
|
157
414
|
|
|
158
415
|
const htmlContent = generateHtmlReport({
|
|
159
416
|
traceId,
|
|
160
|
-
projectName
|
|
417
|
+
projectName,
|
|
161
418
|
startTime: new Date(sessionStartTime).toISOString(),
|
|
162
419
|
endTime: new Date(sessionEndTime).toISOString(),
|
|
163
420
|
durationMs: totalDurationMs,
|
|
164
421
|
metrics: {
|
|
165
|
-
totalTokens,
|
|
166
|
-
inputTokens,
|
|
167
|
-
outputTokens,
|
|
168
|
-
totalCost: totalCost.toFixed(5),
|
|
169
|
-
toolCallCount,
|
|
170
|
-
llmCallCount
|
|
422
|
+
totalTokens: metrics.totalTokens,
|
|
423
|
+
inputTokens: metrics.inputTokens,
|
|
424
|
+
outputTokens: metrics.outputTokens,
|
|
425
|
+
totalCost: metrics.totalCost.toFixed(5),
|
|
426
|
+
toolCallCount: metrics.toolCallCount,
|
|
427
|
+
llmCallCount: metrics.llmCallCount
|
|
171
428
|
},
|
|
172
429
|
events
|
|
173
430
|
});
|
|
174
431
|
|
|
432
|
+
const runRecord = {
|
|
433
|
+
schemaVersion: 1,
|
|
434
|
+
kind: "notrace-run",
|
|
435
|
+
traceId,
|
|
436
|
+
runtime: "pi",
|
|
437
|
+
projectName,
|
|
438
|
+
captureMode,
|
|
439
|
+
session: {
|
|
440
|
+
id: traceId,
|
|
441
|
+
cwd: ctx.cwd,
|
|
442
|
+
file: sessionFile
|
|
443
|
+
},
|
|
444
|
+
task: location.taskId || location.taskPath
|
|
445
|
+
? {
|
|
446
|
+
id: location.taskId,
|
|
447
|
+
path: location.taskPath
|
|
448
|
+
}
|
|
449
|
+
: null,
|
|
450
|
+
conditions: {
|
|
451
|
+
models: metrics.models,
|
|
452
|
+
providers: metrics.providers
|
|
453
|
+
},
|
|
454
|
+
activity: {
|
|
455
|
+
startTime: new Date(sessionStartTime).toISOString(),
|
|
456
|
+
endTime: new Date(sessionEndTime).toISOString(),
|
|
457
|
+
durationMs: totalDurationMs,
|
|
458
|
+
turnCount: metrics.turnCount,
|
|
459
|
+
llmCallCount: metrics.llmCallCount,
|
|
460
|
+
toolCallCount: metrics.toolCallCount,
|
|
461
|
+
toolErrorCount: metrics.toolErrorCount,
|
|
462
|
+
totals: {
|
|
463
|
+
totalTokens: metrics.totalTokens,
|
|
464
|
+
inputTokens: metrics.inputTokens,
|
|
465
|
+
outputTokens: metrics.outputTokens,
|
|
466
|
+
totalCostUsd: Number(metrics.totalCost.toFixed(5))
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
artifacts: {
|
|
470
|
+
htmlReportPath: path.relative(ctx.cwd, reportPath),
|
|
471
|
+
recordPath: path.relative(ctx.cwd, recordPath),
|
|
472
|
+
reviewPath: path.relative(ctx.cwd, reviewPath)
|
|
473
|
+
},
|
|
474
|
+
evidence: {
|
|
475
|
+
events
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
175
479
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
480
|
+
mkdirSync(location.outputDir, { recursive: true, mode: 0o700 });
|
|
481
|
+
writeFileSync(reportPath, htmlContent, { encoding: "utf-8", mode: 0o600 });
|
|
482
|
+
writeFileSync(recordPath, `${JSON.stringify(runRecord, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
483
|
+
updateNotraceIndex(ctx.cwd, location, {
|
|
484
|
+
sessionId: traceId,
|
|
485
|
+
startedAt: new Date(sessionStartTime).toISOString(),
|
|
486
|
+
endedAt: new Date(sessionEndTime).toISOString(),
|
|
487
|
+
taskId: location.taskId,
|
|
488
|
+
taskPath: location.taskPath,
|
|
489
|
+
artifacts: {
|
|
490
|
+
record: path.relative(ctx.cwd, recordPath),
|
|
491
|
+
html: path.relative(ctx.cwd, reportPath),
|
|
492
|
+
review: path.relative(ctx.cwd, reviewPath)
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
if (location.taskDir && (location.taskId || location.taskPath)) {
|
|
496
|
+
appendWorkLogEntry(location.taskDir, `notrace captured artifacts: ${path.relative(location.taskDir, reportPath)}, ${path.relative(location.taskDir, recordPath)}`);
|
|
497
|
+
}
|
|
498
|
+
console.log(`\n📊 [notrace] Observability artifacts generated:`);
|
|
499
|
+
console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m`);
|
|
500
|
+
console.log(`🧾 \x1b[36mfile://${recordPath}\x1b[0m\n`);
|
|
180
501
|
} catch (err: any) {
|
|
181
|
-
console.warn(`[notrace] Failed to write
|
|
502
|
+
console.warn(`[notrace] Failed to write notrace artifacts: ${err?.message || err}`);
|
|
182
503
|
}
|
|
183
504
|
});
|
|
184
505
|
}
|
|
@@ -199,313 +520,234 @@ function safeJsonForScript(value: any): string {
|
|
|
199
520
|
// Returns a self-contained premium HTML template incorporating the design tokens
|
|
200
521
|
function generateHtmlReport(data: any): string {
|
|
201
522
|
const serializedData = safeJsonForScript(data);
|
|
523
|
+
const escapedTraceId = escapeHtml(data.traceId);
|
|
202
524
|
|
|
203
525
|
return `<!DOCTYPE html>
|
|
204
526
|
<html lang="en">
|
|
205
527
|
<head>
|
|
206
528
|
<meta charset="UTF-8">
|
|
207
529
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
208
|
-
<
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap" rel="stylesheet">
|
|
530
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'; connect-src 'none'">
|
|
531
|
+
<meta name="referrer" content="no-referrer">
|
|
532
|
+
<title>notrace - ${escapedTraceId}</title>
|
|
212
533
|
<style>
|
|
213
534
|
:root {
|
|
214
|
-
--bg: #
|
|
215
|
-
--bg-
|
|
216
|
-
--
|
|
217
|
-
--
|
|
218
|
-
--
|
|
219
|
-
--
|
|
220
|
-
--text
|
|
221
|
-
--
|
|
222
|
-
--
|
|
223
|
-
--
|
|
224
|
-
--
|
|
225
|
-
--
|
|
226
|
-
--
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
535
|
+
--bg: #191613;
|
|
536
|
+
--bg-glow: #2a221c;
|
|
537
|
+
--panel: rgba(244, 237, 228, 0.055);
|
|
538
|
+
--panel-strong: rgba(244, 237, 228, 0.075);
|
|
539
|
+
--paper: #f6efe7;
|
|
540
|
+
--paper-soft: #e7dbce;
|
|
541
|
+
--text: #f5eee7;
|
|
542
|
+
--text-soft: #ddd0c2;
|
|
543
|
+
--text-muted: #b9ab9d;
|
|
544
|
+
--border: rgba(255, 255, 255, 0.09);
|
|
545
|
+
--border-strong: rgba(255, 255, 255, 0.16);
|
|
546
|
+
--accent: #d88462;
|
|
547
|
+
--accent-soft: rgba(216, 132, 98, 0.14);
|
|
548
|
+
--session: #8ab7ff;
|
|
549
|
+
--turn: #e0b46b;
|
|
550
|
+
--tool: #86cca4;
|
|
551
|
+
--error: #f08e8e;
|
|
552
|
+
--shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
|
|
553
|
+
--card-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
234
557
|
|
|
235
558
|
body {
|
|
236
|
-
font-family:
|
|
237
|
-
background
|
|
559
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
560
|
+
background:
|
|
561
|
+
radial-gradient(900px 420px at 50% -10%, rgba(216, 132, 98, 0.16), transparent 60%),
|
|
562
|
+
radial-gradient(680px 360px at 10% 0%, rgba(255, 255, 255, 0.03), transparent 60%),
|
|
563
|
+
linear-gradient(180deg, #171411 0%, #1b1714 100%);
|
|
238
564
|
color: var(--text);
|
|
239
|
-
line-height: 1.
|
|
240
|
-
padding:
|
|
565
|
+
line-height: 1.65;
|
|
566
|
+
padding: 14px 12px 40px;
|
|
241
567
|
min-height: 100vh;
|
|
242
|
-
|
|
243
|
-
radial-gradient(circle at 10% 20%, rgba(139, 92, 246, 0.08) 0%, transparent 40%),
|
|
244
|
-
radial-gradient(circle at 90% 80%, rgba(59, 130, 246, 0.06) 0%, transparent 40%);
|
|
245
|
-
background-attachment: fixed;
|
|
568
|
+
letter-spacing: 0.01em;
|
|
246
569
|
}
|
|
247
570
|
|
|
248
|
-
.container {
|
|
249
|
-
|
|
250
|
-
margin: 0 auto;
|
|
251
|
-
}
|
|
571
|
+
.container { max-width: 920px; margin: 0 auto; }
|
|
572
|
+
header { margin-bottom: 24px; }
|
|
252
573
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
.brand {
|
|
263
|
-
display: flex;
|
|
264
|
-
align-items: center;
|
|
265
|
-
gap: 0.75rem;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
.brand h1 {
|
|
269
|
-
font-size: 1.75rem;
|
|
270
|
-
font-weight: 700;
|
|
271
|
-
background: var(--accent-gradient);
|
|
272
|
-
-webkit-background-clip: text;
|
|
273
|
-
-webkit-text-fill-color: transparent;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
.brand-tag {
|
|
277
|
-
font-size: 0.75rem;
|
|
278
|
-
text-transform: uppercase;
|
|
279
|
-
letter-spacing: 0.05em;
|
|
280
|
-
background: rgba(139, 92, 246, 0.15);
|
|
281
|
-
color: #c084fc;
|
|
282
|
-
padding: 0.2rem 0.5rem;
|
|
283
|
-
border-radius: 4px;
|
|
284
|
-
border: 1px solid rgba(167, 139, 250, 0.3);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
.meta-time {
|
|
288
|
-
font-size: 0.875rem;
|
|
289
|
-
color: var(--text-muted);
|
|
290
|
-
text-align: right;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
.metrics-grid {
|
|
294
|
-
display: grid;
|
|
295
|
-
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
296
|
-
gap: 1rem;
|
|
297
|
-
margin-bottom: 2.5rem;
|
|
574
|
+
.hero {
|
|
575
|
+
position: relative;
|
|
576
|
+
overflow: hidden;
|
|
577
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.025));
|
|
578
|
+
border: 1px solid var(--border);
|
|
579
|
+
border-radius: 22px;
|
|
580
|
+
padding: 18px 16px 16px;
|
|
581
|
+
box-shadow: var(--shadow);
|
|
582
|
+
backdrop-filter: blur(10px);
|
|
298
583
|
}
|
|
299
584
|
|
|
585
|
+
.hero::after {
|
|
586
|
+
content: "";
|
|
587
|
+
position: absolute;
|
|
588
|
+
inset: auto -10% -30% auto;
|
|
589
|
+
width: 260px;
|
|
590
|
+
height: 260px;
|
|
591
|
+
background: radial-gradient(circle, rgba(216,132,98,0.14), transparent 70%);
|
|
592
|
+
pointer-events: none;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; position: relative; z-index: 1; }
|
|
596
|
+
.brand h1 { font-size: 2rem; line-height: 1; font-weight: 680; color: var(--text); letter-spacing: -0.03em; }
|
|
597
|
+
.brand-tag { font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: #f4ccb9; background: var(--accent-soft); border: 1px solid rgba(216, 132, 98, 0.24); padding: 0.35rem 0.6rem; border-radius: 999px; }
|
|
598
|
+
.hero-subtitle { position: relative; z-index: 1; color: var(--text-soft); margin-bottom: 20px; max-width: 720px; font-size: 1rem; }
|
|
599
|
+
.hero-meta { position: relative; z-index: 1; display: flex; flex-wrap: wrap; gap: 10px; }
|
|
600
|
+
.meta-pill { display: inline-flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.04); border: 1px solid var(--border); border-radius: 999px; padding: 0.48rem 0.82rem; font-size: 0.86rem; color: var(--text-muted); }
|
|
601
|
+
.meta-pill strong { color: var(--text); font-weight: 570; }
|
|
602
|
+
|
|
603
|
+
.metrics-grid { display: grid; grid-template-columns: 1fr; gap: 10px; margin: 16px 0 24px; }
|
|
300
604
|
.metric-card {
|
|
301
|
-
background:
|
|
605
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.028));
|
|
302
606
|
border: 1px solid var(--border);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
transition:
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
.metric-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
607
|
+
border-radius: 20px;
|
|
608
|
+
padding: 18px 16px;
|
|
609
|
+
box-shadow: var(--card-shadow);
|
|
610
|
+
transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
|
|
611
|
+
}
|
|
612
|
+
.metric-card:hover { border-color: var(--border-strong); transform: translateY(-1px); background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.03)); }
|
|
613
|
+
.metric-label { font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); margin-bottom: 7px; }
|
|
614
|
+
.metric-value { font-size: 1.42rem; font-weight: 640; color: var(--text); letter-spacing: -0.03em; }
|
|
615
|
+
|
|
616
|
+
.section-title {
|
|
617
|
+
font-size: 0.95rem;
|
|
618
|
+
font-weight: 620;
|
|
619
|
+
color: var(--paper-soft);
|
|
620
|
+
margin-bottom: 16px;
|
|
621
|
+
padding-left: 2px;
|
|
622
|
+
letter-spacing: 0.04em;
|
|
317
623
|
text-transform: uppercase;
|
|
318
|
-
letter-spacing: 0.05em;
|
|
319
|
-
color: var(--text-muted);
|
|
320
|
-
margin-bottom: 0.5rem;
|
|
321
624
|
}
|
|
322
625
|
|
|
323
|
-
.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
.timeline {
|
|
330
|
-
position: relative;
|
|
331
|
-
padding-left: 2rem;
|
|
332
|
-
border-left: 2px solid var(--border);
|
|
333
|
-
margin-left: 1rem;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.timeline-event {
|
|
337
|
-
position: relative;
|
|
338
|
-
margin-bottom: 2rem;
|
|
339
|
-
animation: slideIn 0.4s ease-out;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
@keyframes slideIn {
|
|
343
|
-
from { opacity: 0; transform: translateX(-10px); }
|
|
344
|
-
to { opacity: 1; transform: translateX(0); }
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
.timeline-dot {
|
|
348
|
-
position: absolute;
|
|
349
|
-
left: calc(-2rem - 6px);
|
|
350
|
-
top: 1.25rem;
|
|
351
|
-
width: 10px;
|
|
352
|
-
height: 10px;
|
|
353
|
-
border-radius: 50%;
|
|
354
|
-
background: var(--text-muted);
|
|
355
|
-
box-shadow: 0 0 0 4px var(--bg);
|
|
356
|
-
transition: all 0.3s ease;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/* Event color mappings */
|
|
360
|
-
.timeline-event.session-start .timeline-dot { background: var(--info); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--info); }
|
|
361
|
-
.timeline-event.turn-start .timeline-dot { background: var(--accent); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--accent); }
|
|
362
|
-
.timeline-event.tool-call .timeline-dot { background: var(--warning); box-shadow: 0 0 0 4px var(--bg); }
|
|
363
|
-
.timeline-event.tool-call.error .timeline-dot { background: var(--error); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--error); }
|
|
364
|
-
.timeline-event.llm-call .timeline-dot { background: var(--success); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--success); }
|
|
626
|
+
.timeline { display: flex; flex-direction: column; gap: 16px; }
|
|
627
|
+
.timeline-event { position: relative; animation: slideIn 0.28s ease-out; }
|
|
628
|
+
@keyframes slideIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
629
|
+
.timeline-dot { display: none; }
|
|
365
630
|
|
|
366
631
|
.card {
|
|
367
|
-
|
|
632
|
+
position: relative;
|
|
633
|
+
overflow: hidden;
|
|
634
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.024));
|
|
368
635
|
border: 1px solid var(--border);
|
|
369
|
-
border-radius:
|
|
370
|
-
padding:
|
|
371
|
-
|
|
372
|
-
transition: border-color 0.
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
.card:hover {
|
|
376
|
-
border-color: var(--border-hover);
|
|
636
|
+
border-radius: 18px;
|
|
637
|
+
padding: 14px 14px 13px;
|
|
638
|
+
box-shadow: var(--card-shadow);
|
|
639
|
+
transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
|
|
377
640
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
.card-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.badge-
|
|
402
|
-
.badge-
|
|
403
|
-
.
|
|
404
|
-
.
|
|
405
|
-
.
|
|
406
|
-
|
|
407
|
-
.card-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
display: flex;
|
|
411
|
-
align-items: center;
|
|
412
|
-
gap: 0.5rem;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.arrow-icon {
|
|
416
|
-
width: 16px;
|
|
417
|
-
height: 16px;
|
|
418
|
-
fill: none;
|
|
419
|
-
stroke: currentColor;
|
|
420
|
-
stroke-width: 2;
|
|
421
|
-
stroke-linecap: round;
|
|
422
|
-
stroke-linejoin: round;
|
|
423
|
-
transition: transform 0.2s;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
.expanded .arrow-icon {
|
|
427
|
-
transform: rotate(90deg);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
.card-body {
|
|
431
|
-
margin-top: 1rem;
|
|
432
|
-
padding-top: 1rem;
|
|
433
|
-
border-top: 1px solid var(--border);
|
|
434
|
-
display: none;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
.expanded .card-body {
|
|
438
|
-
display: block;
|
|
439
|
-
}
|
|
440
|
-
|
|
641
|
+
.card::before {
|
|
642
|
+
content: "";
|
|
643
|
+
position: absolute;
|
|
644
|
+
left: 0;
|
|
645
|
+
top: 0;
|
|
646
|
+
bottom: 0;
|
|
647
|
+
width: 3px;
|
|
648
|
+
background: rgba(255,255,255,0.1);
|
|
649
|
+
}
|
|
650
|
+
.timeline-event.session-start .card::before { background: var(--session); }
|
|
651
|
+
.timeline-event.turn-start .card::before { background: var(--turn); }
|
|
652
|
+
.timeline-event.tool-start .card::before { background: var(--tool); }
|
|
653
|
+
.timeline-event.tool-start.error .card::before { background: var(--error); }
|
|
654
|
+
.timeline-event.llm-start .card::before { background: var(--accent); }
|
|
655
|
+
.card:hover { border-color: var(--border-strong); background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.028)); transform: translateY(-1px); }
|
|
656
|
+
|
|
657
|
+
.card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; cursor: pointer; user-select: none; }
|
|
658
|
+
.card-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-weight: 600; color: var(--text); }
|
|
659
|
+
.card-title span:last-child { font-size: 1rem; letter-spacing: -0.01em; }
|
|
660
|
+
.card-badge { font-size: 0.69rem; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.34rem 0.62rem; border-radius: 999px; border: 1px solid transparent; }
|
|
661
|
+
.badge-session { background: rgba(138, 183, 255, 0.14); color: var(--session); border-color: rgba(138, 183, 255, 0.24); }
|
|
662
|
+
.badge-turn { background: rgba(224, 180, 107, 0.14); color: var(--turn); border-color: rgba(224, 180, 107, 0.22); }
|
|
663
|
+
.badge-tool { background: rgba(134, 204, 164, 0.14); color: var(--tool); border-color: rgba(134, 204, 164, 0.22); }
|
|
664
|
+
.badge-tool.error { background: rgba(240, 142, 142, 0.15); color: var(--error); border-color: rgba(240, 142, 142, 0.24); }
|
|
665
|
+
.badge-llm { background: rgba(216, 132, 98, 0.14); color: #f2c2ae; border-color: rgba(216, 132, 98, 0.22); }
|
|
666
|
+
.card-time { flex-shrink: 0; font-size: 0.82rem; color: var(--text-muted); display: flex; align-items: center; gap: 8px; padding-top: 3px; }
|
|
667
|
+
.arrow-icon { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.2s; }
|
|
668
|
+
.expanded .arrow-icon { transform: rotate(90deg); }
|
|
669
|
+
.card-body { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); display: none; }
|
|
670
|
+
.expanded .card-body { display: block; }
|
|
671
|
+
|
|
672
|
+
.detail-label { font-size: 0.76rem; font-weight: 650; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin: 14px 0 8px; display: block; }
|
|
441
673
|
.code-block {
|
|
442
|
-
font-family:
|
|
443
|
-
font-size: 0.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
674
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
675
|
+
font-size: 0.84rem;
|
|
676
|
+
line-height: 1.62;
|
|
677
|
+
background: rgba(16, 14, 12, 0.64);
|
|
678
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
679
|
+
padding: 14px;
|
|
680
|
+
border-radius: 16px;
|
|
448
681
|
overflow-x: auto;
|
|
449
|
-
margin-top: 0.
|
|
450
|
-
color: #
|
|
682
|
+
margin-top: 0.35rem;
|
|
683
|
+
color: #f1e9df;
|
|
451
684
|
white-space: pre-wrap;
|
|
452
685
|
}
|
|
453
686
|
|
|
454
|
-
.messages-container {
|
|
455
|
-
display: flex;
|
|
456
|
-
flex-direction: column;
|
|
457
|
-
gap: 0.75rem;
|
|
458
|
-
}
|
|
459
|
-
|
|
687
|
+
.messages-container { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
|
|
460
688
|
.msg-row {
|
|
461
689
|
display: flex;
|
|
462
690
|
flex-direction: column;
|
|
463
|
-
gap:
|
|
464
|
-
padding:
|
|
465
|
-
background: rgba(255,
|
|
466
|
-
border
|
|
467
|
-
border-
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
.msg-row.
|
|
471
|
-
.msg-row.
|
|
472
|
-
.msg-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
letter-spacing: 0.05em;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
.msg-row.user .msg-role { color: #60a5fa; }
|
|
482
|
-
.msg-row.assistant .msg-role { color: #34d399; }
|
|
483
|
-
.msg-row.system .msg-role { color: #c084fc; }
|
|
484
|
-
|
|
485
|
-
.msg-text {
|
|
486
|
-
font-size: 0.9rem;
|
|
487
|
-
white-space: pre-wrap;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
.duration-pill {
|
|
491
|
-
font-size: 0.75rem;
|
|
492
|
-
background: rgba(255, 255, 255, 0.06);
|
|
493
|
-
padding: 0.1rem 0.4rem;
|
|
494
|
-
border-radius: 4px;
|
|
691
|
+
gap: 4px;
|
|
692
|
+
padding: 12px 14px;
|
|
693
|
+
background: rgba(255,255,255,0.038);
|
|
694
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
695
|
+
border-radius: 18px;
|
|
696
|
+
}
|
|
697
|
+
.msg-row.user { background: rgba(138, 183, 255, 0.08); }
|
|
698
|
+
.msg-row.assistant { background: rgba(216, 132, 98, 0.08); }
|
|
699
|
+
.msg-row.system { background: rgba(224, 180, 107, 0.08); }
|
|
700
|
+
.msg-role { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
|
|
701
|
+
.msg-text { font-size: 0.93rem; color: var(--text-soft); white-space: pre-wrap; }
|
|
702
|
+
|
|
703
|
+
.usage-row {
|
|
704
|
+
margin-top: 12px;
|
|
705
|
+
font-size: 0.84rem;
|
|
495
706
|
color: var(--text-muted);
|
|
707
|
+
display: flex;
|
|
708
|
+
flex-wrap: wrap;
|
|
709
|
+
gap: 12px;
|
|
710
|
+
padding: 10px 12px;
|
|
711
|
+
border-radius: 14px;
|
|
712
|
+
background: rgba(255,255,255,0.03);
|
|
713
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
714
|
+
}
|
|
715
|
+
.usage-row strong { color: var(--text); font-weight: 620; }
|
|
716
|
+
.duration-pill { font-size: 0.74rem; background: rgba(255,255,255,0.06); padding: 0.2rem 0.46rem; border-radius: 999px; color: var(--text-muted); border: 1px solid rgba(255,255,255,0.06); }
|
|
717
|
+
|
|
718
|
+
@media (min-width: 640px) {
|
|
719
|
+
body { padding: 28px 18px 56px; }
|
|
720
|
+
.hero { border-radius: 28px; padding: 28px 28px 24px; }
|
|
721
|
+
.metrics-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 18px 0 30px; }
|
|
722
|
+
.card { border-radius: 22px; padding: 18px 18px 16px; }
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
@media (max-width: 720px) {
|
|
726
|
+
.hero, .card, .metric-card { border-radius: 18px; }
|
|
727
|
+
.card-header { flex-direction: column; }
|
|
728
|
+
.card-time { width: 100%; justify-content: space-between; }
|
|
729
|
+
.brand { align-items: flex-start; flex-direction: column; gap: 8px; }
|
|
730
|
+
.brand h1 { font-size: 1.8rem; }
|
|
731
|
+
.hero-meta { flex-direction: column; align-items: stretch; }
|
|
732
|
+
.meta-pill { width: 100%; justify-content: space-between; }
|
|
733
|
+
.usage-row { flex-direction: column; gap: 6px; }
|
|
734
|
+
.code-block { font-size: 0.8rem; padding: 12px; }
|
|
496
735
|
}
|
|
497
736
|
</style>
|
|
498
737
|
</head>
|
|
499
738
|
<body>
|
|
500
739
|
<div class="container">
|
|
501
740
|
<header>
|
|
502
|
-
<div class="
|
|
503
|
-
<
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
<
|
|
508
|
-
<div
|
|
741
|
+
<div class="hero">
|
|
742
|
+
<div class="brand">
|
|
743
|
+
<h1>notrace</h1>
|
|
744
|
+
<span class="brand-tag">retrospective</span>
|
|
745
|
+
</div>
|
|
746
|
+
<p class="hero-subtitle">A local evidence view of the run: messages, tools, model calls, timing, and token cost.</p>
|
|
747
|
+
<div class="hero-meta">
|
|
748
|
+
<span class="meta-pill">Session <strong id="sess-id"></strong></span>
|
|
749
|
+
<span class="meta-pill">Started <strong id="sess-time"></strong></span>
|
|
750
|
+
</div>
|
|
509
751
|
</div>
|
|
510
752
|
</header>
|
|
511
753
|
|
|
@@ -532,7 +774,7 @@ function generateHtmlReport(data: any): string {
|
|
|
532
774
|
</div>
|
|
533
775
|
</div>
|
|
534
776
|
|
|
535
|
-
<h2
|
|
777
|
+
<h2 class="section-title">Activity flow</h2>
|
|
536
778
|
<div class="timeline" id="timeline-container">
|
|
537
779
|
<!-- Injected by JS -->
|
|
538
780
|
</div>
|
|
@@ -541,18 +783,34 @@ function generateHtmlReport(data: any): string {
|
|
|
541
783
|
<script>
|
|
542
784
|
const traceData = ${serializedData};
|
|
543
785
|
|
|
786
|
+
function escapeHtml(value) {
|
|
787
|
+
return String(value ?? "").replace(/[&<>'"]/g, (char) => ({
|
|
788
|
+
"&": "&",
|
|
789
|
+
"<": "<",
|
|
790
|
+
">": ">",
|
|
791
|
+
"'": "'",
|
|
792
|
+
'"': """
|
|
793
|
+
}[char]));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function safeClassName(value, fallback = "unknown") {
|
|
797
|
+
const normalized = String(value ?? fallback).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
798
|
+
return normalized || fallback;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function jsonText(value) {
|
|
802
|
+
return escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
803
|
+
}
|
|
804
|
+
|
|
544
805
|
// Render Metrics
|
|
545
806
|
document.getElementById("sess-id").textContent = traceData.traceId;
|
|
546
807
|
document.getElementById("sess-time").textContent = new Date(traceData.startTime).toLocaleString();
|
|
547
808
|
document.getElementById("val-duration").textContent = (traceData.durationMs / 1000).toFixed(2) + "s";
|
|
548
809
|
document.getElementById("val-tokens").textContent = traceData.metrics.totalTokens.toLocaleString();
|
|
549
|
-
document.getElementById("val-llms").textContent = traceData.metrics.
|
|
810
|
+
document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
|
|
550
811
|
document.getElementById("val-tools").textContent = traceData.metrics.toolCallCount;
|
|
551
812
|
document.getElementById("val-cost").textContent = "$" + traceData.metrics.totalCost;
|
|
552
813
|
|
|
553
|
-
// Correct the labels mapping if names swapped
|
|
554
|
-
document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
|
|
555
|
-
|
|
556
814
|
// Process & Render Timeline
|
|
557
815
|
const container = document.getElementById("timeline-container");
|
|
558
816
|
const events = traceData.events;
|
|
@@ -612,19 +870,20 @@ function generateHtmlReport(data: any): string {
|
|
|
612
870
|
// Render cards
|
|
613
871
|
renderedEvents.forEach((ev, index) => {
|
|
614
872
|
const evDiv = document.createElement("div");
|
|
615
|
-
|
|
873
|
+
const eventType = safeClassName(ev.type);
|
|
874
|
+
evDiv.className = \`timeline-event \${eventType}-start \${ev.isError ? "error" : ""}\`;
|
|
616
875
|
|
|
617
876
|
let cardHtml = \`
|
|
618
877
|
<div class="timeline-dot"></div>
|
|
619
878
|
<div class="card" id="card-\${index}">
|
|
620
879
|
<div class="card-header" onclick="toggleCard(\${index})">
|
|
621
880
|
<div class="card-title">
|
|
622
|
-
<span class="card-badge badge-\${
|
|
623
|
-
<span>\${ev.title}</span>
|
|
624
|
-
\${ev.durationMs ? \`<span class="duration-pill">\${(ev.durationMs / 1000).toFixed(2)}s</span>\` : ""}
|
|
881
|
+
<span class="card-badge badge-\${eventType} \${ev.isError ? "error" : ""}">\${escapeHtml(eventType.toUpperCase())}</span>
|
|
882
|
+
<span>\${escapeHtml(ev.title)}</span>
|
|
883
|
+
\${ev.durationMs ? \`<span class="duration-pill">\${escapeHtml((ev.durationMs / 1000).toFixed(2))}s</span>\` : ""}
|
|
625
884
|
</div>
|
|
626
885
|
<div class="card-time">
|
|
627
|
-
<span>\${ev.time}</span>
|
|
886
|
+
<span>\${escapeHtml(ev.time)}</span>
|
|
628
887
|
<svg class="arrow-icon" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
|
629
888
|
</div>
|
|
630
889
|
</div>
|
|
@@ -632,13 +891,13 @@ function generateHtmlReport(data: any): string {
|
|
|
632
891
|
\`;
|
|
633
892
|
|
|
634
893
|
if (ev.type === "session" || ev.type === "turn") {
|
|
635
|
-
cardHtml += \`<div class="code-block">\${ev.body}</div>\`;
|
|
894
|
+
cardHtml += \`<div class="code-block">\${escapeHtml(ev.body)}</div>\`;
|
|
636
895
|
} else if (ev.type === "tool") {
|
|
637
896
|
cardHtml += \`
|
|
638
|
-
<
|
|
639
|
-
<div class="code-block">\${
|
|
640
|
-
<
|
|
641
|
-
<div class="code-block">\${
|
|
897
|
+
<span class="detail-label">Arguments</span>
|
|
898
|
+
<div class="code-block">\${jsonText(ev.args)}</div>
|
|
899
|
+
<span class="detail-label">Result (\${ev.isError ? "Error" : "Success"})</span>
|
|
900
|
+
<div class="code-block">\${jsonText(ev.result)}</div>
|
|
642
901
|
\`;
|
|
643
902
|
} else if (ev.type === "llm") {
|
|
644
903
|
// Render system prompt and input messages if present
|
|
@@ -651,17 +910,18 @@ function generateHtmlReport(data: any): string {
|
|
|
651
910
|
messagesHtml += \`
|
|
652
911
|
<div class="msg-row system">
|
|
653
912
|
<span class="msg-role">System Instruction</span>
|
|
654
|
-
<span class="msg-text">\${instr}</span>
|
|
913
|
+
<span class="msg-text">\${escapeHtml(instr)}</span>
|
|
655
914
|
</div>
|
|
656
915
|
\`;
|
|
657
916
|
}
|
|
658
917
|
if (ev.payload.messages && Array.isArray(ev.payload.messages)) {
|
|
659
918
|
ev.payload.messages.forEach(m => {
|
|
660
919
|
const contentText = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
|
|
920
|
+
const role = safeClassName(m.role, "message");
|
|
661
921
|
messagesHtml += \`
|
|
662
|
-
<div class="msg-row \${
|
|
663
|
-
<span class="msg-role">\${m.role}</span>
|
|
664
|
-
<span class="msg-text">\${contentText}</span>
|
|
922
|
+
<div class="msg-row \${role}">
|
|
923
|
+
<span class="msg-role">\${escapeHtml(m.role)}</span>
|
|
924
|
+
<span class="msg-text">\${escapeHtml(contentText)}</span>
|
|
665
925
|
</div>
|
|
666
926
|
\`;
|
|
667
927
|
});
|
|
@@ -670,16 +930,16 @@ function generateHtmlReport(data: any): string {
|
|
|
670
930
|
messagesHtml += '</div>';
|
|
671
931
|
|
|
672
932
|
cardHtml += \`
|
|
673
|
-
<
|
|
933
|
+
<span class="detail-label">Context messages</span>
|
|
674
934
|
\${messagesHtml}
|
|
675
|
-
<
|
|
676
|
-
<div class="code-block">\${
|
|
935
|
+
<span class="detail-label">Generated response</span>
|
|
936
|
+
<div class="code-block">\${jsonText(ev.output)}</div>
|
|
677
937
|
\${ev.usage ? \`
|
|
678
|
-
<div
|
|
679
|
-
<span>Input
|
|
680
|
-
<span>Output
|
|
681
|
-
<span>Total
|
|
682
|
-
<span>Cost
|
|
938
|
+
<div class="usage-row">
|
|
939
|
+
<span>Input tokens <strong>\${escapeHtml(ev.usage.input ?? 0)}</strong></span>
|
|
940
|
+
<span>Output tokens <strong>\${escapeHtml(ev.usage.output ?? 0)}</strong></span>
|
|
941
|
+
<span>Total tokens <strong>\${escapeHtml(ev.usage.totalTokens ?? 0)}</strong></span>
|
|
942
|
+
<span>Cost <strong>\$\${escapeHtml(ev.usage.cost?.total?.toFixed?.(5) || "0.00")}</strong></span>
|
|
683
943
|
</div>
|
|
684
944
|
\` : ""}
|
|
685
945
|
\`;
|
|
@@ -697,7 +957,7 @@ function generateHtmlReport(data: any): string {
|
|
|
697
957
|
// Expand/Collapse controller
|
|
698
958
|
function toggleCard(index) {
|
|
699
959
|
const card = document.getElementById(\`card-\${index}\`);
|
|
700
|
-
card
|
|
960
|
+
card?.classList.toggle("expanded");
|
|
701
961
|
}
|
|
702
962
|
</script>
|
|
703
963
|
</body>
|