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