@raquezha/notrace 0.0.4 → 0.0.6

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +123 -23
  3. package/assets/notrace-logo.svg +20 -0
  4. package/assets/notrace-mark.svg +18 -0
  5. package/assets/notrace-wordmark.svg +4 -0
  6. package/dist/notrace/adapters.d.ts +32 -0
  7. package/dist/notrace/adapters.js +83 -0
  8. package/dist/notrace/index.d.ts +2 -0
  9. package/dist/notrace/index.js +295 -0
  10. package/dist/notrace/renderer.d.ts +4 -0
  11. package/dist/notrace/renderer.js +605 -0
  12. package/dist/notrace/types.d.ts +93 -0
  13. package/dist/notrace/types.js +1 -0
  14. package/extensions/notrace/adapters.ts +88 -0
  15. package/extensions/notrace/index.ts +359 -0
  16. package/extensions/notrace/renderer.ts +616 -0
  17. package/extensions/notrace/types.ts +108 -0
  18. package/package.json +4 -3
  19. package/templates/README.md +24 -0
  20. package/templates/dashboard.sample.html +361 -0
  21. package/templates/dashboard.sample.json +113 -0
  22. package/templates/notrace-logo.preview.png +0 -0
  23. package/templates/render-samples.mjs +44 -0
  24. package/templates/session.sample.html +431 -0
  25. package/templates/session.sample.json +127 -0
  26. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.html +313 -0
  27. package/templates/sessions/019ecec4-1c48-7b47-bdbf-cec3500978ed/notrace.json +129 -0
  28. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.html +313 -0
  29. package/templates/sessions/019ecfa1-7ac2-7c00-bbe9-541f37751201/notrace.json +129 -0
  30. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.html +426 -0
  31. package/templates/sessions/019ed2ee-1000-76ee-b353-000000000001/notrace.json +134 -0
  32. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.html +425 -0
  33. package/templates/sessions/019ed2ee-1001-76ee-b353-000000000002/notrace.json +133 -0
  34. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.html +426 -0
  35. package/templates/sessions/019ed2ee-1002-76ee-b353-000000000003/notrace.json +134 -0
  36. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.html +423 -0
  37. package/templates/sessions/019ed2ee-1003-76ee-b353-000000000004/notrace.json +130 -0
  38. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.html +423 -0
  39. package/templates/sessions/019ed2ee-1004-76ee-b353-000000000005/notrace.json +130 -0
  40. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.html +423 -0
  41. package/templates/sessions/019ed2ee-1005-76ee-b353-000000000006/notrace.json +130 -0
  42. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.html +423 -0
  43. package/templates/sessions/019ed2ee-1006-76ee-b353-000000000007/notrace.json +130 -0
  44. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.html +423 -0
  45. package/templates/sessions/019ed2ee-1007-76ee-b353-000000000008/notrace.json +130 -0
  46. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.html +423 -0
  47. package/templates/sessions/019ed2ee-1008-76ee-b353-000000000009/notrace.json +130 -0
  48. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.html +423 -0
  49. package/templates/sessions/019ed2ee-1009-76ee-b353-000000000010/notrace.json +130 -0
  50. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.html +423 -0
  51. package/templates/sessions/019ed2ee-1010-76ee-b353-000000000011/notrace.json +130 -0
  52. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.html +423 -0
  53. package/templates/sessions/019ed2ee-1011-76ee-b353-000000000012/notrace.json +130 -0
  54. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.html +423 -0
  55. package/templates/sessions/019ed2ee-1012-76ee-b353-000000000013/notrace.json +130 -0
  56. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.html +423 -0
  57. package/templates/sessions/019ed2ee-1013-76ee-b353-000000000014/notrace.json +130 -0
  58. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.html +423 -0
  59. package/templates/sessions/019ed2ee-1014-76ee-b353-000000000015/notrace.json +130 -0
  60. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.html +423 -0
  61. package/templates/sessions/019ed2ee-1015-76ee-b353-000000000016/notrace.json +130 -0
  62. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.html +423 -0
  63. package/templates/sessions/019ed2ee-1016-76ee-b353-000000000017/notrace.json +130 -0
  64. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.html +423 -0
  65. package/templates/sessions/019ed2ee-1017-76ee-b353-000000000018/notrace.json +130 -0
  66. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.html +423 -0
  67. package/templates/sessions/019ed2ee-1018-76ee-b353-000000000019/notrace.json +130 -0
  68. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.html +423 -0
  69. package/templates/sessions/019ed2ee-1019-76ee-b353-000000000020/notrace.json +130 -0
  70. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.html +423 -0
  71. package/templates/sessions/019ed2ee-1020-76ee-b353-000000000021/notrace.json +130 -0
  72. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.html +423 -0
  73. package/templates/sessions/019ed2ee-1021-76ee-b353-000000000022/notrace.json +130 -0
  74. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.html +423 -0
  75. package/templates/sessions/019ed2ee-1022-76ee-b353-000000000023/notrace.json +130 -0
  76. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.html +423 -0
  77. package/templates/sessions/019ed2ee-1023-76ee-b353-000000000024/notrace.json +130 -0
  78. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.html +423 -0
  79. package/templates/sessions/019ed2ee-1024-76ee-b353-000000000025/notrace.json +130 -0
  80. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.html +423 -0
  81. package/templates/sessions/019ed2ee-1025-76ee-b353-000000000026/notrace.json +130 -0
  82. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.html +423 -0
  83. package/templates/sessions/019ed2ee-1026-76ee-b353-000000000027/notrace.json +130 -0
  84. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.html +423 -0
  85. package/templates/sessions/019ed2ee-1027-76ee-b353-000000000028/notrace.json +130 -0
  86. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.html +423 -0
  87. package/templates/sessions/019ed2ee-1028-76ee-b353-000000000029/notrace.json +130 -0
  88. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.html +423 -0
  89. package/templates/sessions/019ed2ee-1029-76ee-b353-000000000030/notrace.json +130 -0
  90. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.html +313 -0
  91. package/templates/sessions/019ed2ee-5252-76ee-b353-ad925a6bad31/notrace.json +129 -0
  92. package/tsconfig.json +1 -1
  93. package/dist/notrace.d.ts +0 -9
  94. package/dist/notrace.js +0 -785
  95. package/extensions/notrace.ts +0 -802
package/dist/notrace.js DELETED
@@ -1,785 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
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
- return SENSITIVE_KEY_RE.test(key.replace(/[^a-z0-9]/gi, ""));
18
- }
19
- function redactString(value) {
20
- const redacted = value.replace(SENSITIVE_VALUE_RE, REDACTED);
21
- if (redacted.length <= MAX_STRING_LENGTH)
22
- return redacted;
23
- return `${redacted.slice(0, MAX_STRING_LENGTH)}\n…[truncated ${redacted.length - MAX_STRING_LENGTH} chars by notrace]`;
24
- }
25
- function sanitizeTraceValue(value, depth = 0, seen = new WeakSet()) {
26
- if (getCaptureMode() === "full")
27
- return value;
28
- if (value == null || typeof value === "number" || typeof value === "boolean")
29
- return value;
30
- if (typeof value === "string")
31
- return redactString(value);
32
- if (typeof value === "bigint")
33
- return value.toString();
34
- if (typeof value === "function" || typeof value === "symbol")
35
- return `[${typeof value}]`;
36
- if (depth >= MAX_DEPTH)
37
- return "[Max depth reached by notrace]";
38
- if (typeof value !== "object")
39
- return String(value);
40
- if (seen.has(value))
41
- return "[Circular]";
42
- seen.add(value);
43
- if (Array.isArray(value)) {
44
- const items = value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeTraceValue(item, depth + 1, seen));
45
- if (value.length > MAX_ARRAY_ITEMS)
46
- items.push(`…[truncated ${value.length - MAX_ARRAY_ITEMS} items by notrace]`);
47
- return items;
48
- }
49
- const output = {};
50
- const entries = Object.entries(value);
51
- for (const [key, item] of entries.slice(0, MAX_OBJECT_KEYS)) {
52
- output[key] = isSensitiveKey(key) ? REDACTED : sanitizeTraceValue(item, depth + 1, seen);
53
- }
54
- if (entries.length > MAX_OBJECT_KEYS)
55
- output.__notrace_truncated__ = `${entries.length - MAX_OBJECT_KEYS} keys`;
56
- return output;
57
- }
58
- function safeResolveUnder(baseDir, candidate) {
59
- const base = path.resolve(baseDir);
60
- const resolved = path.resolve(base, candidate);
61
- const relative = path.relative(base, resolved);
62
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved : null;
63
- }
64
- function escapeHtml(value) {
65
- return String(value).replace(/[&<>'"]/g, (char) => {
66
- switch (char) {
67
- case "&": return "&amp;";
68
- case "<": return "&lt;";
69
- case ">": return "&gt;";
70
- case "'": return "&#39;";
71
- case '"': return "&quot;";
72
- default: return char;
73
- }
74
- });
75
- }
76
- /**
77
- * html-observability extension
78
- *
79
- * Captures execution traces from the pi coding agent sessions and writes
80
- * a self-contained, interactive, beautiful HTML report to the active task's
81
- * workspace at the end of the session.
82
- */
83
- export default function (pi) {
84
- const events = [];
85
- const sessionStartTime = Date.now();
86
- let traceId = "";
87
- let activeLlmPayload = null;
88
- let llmStartTime = 0;
89
- const activeToolTimes = {};
90
- // Helper to extract active task path from .workflow/active_task.json.
91
- // Reports are constrained to cwd/.workflow to avoid task metadata causing writes elsewhere.
92
- function getActiveTaskDir(cwd) {
93
- const workflowDir = path.resolve(cwd, ".workflow");
94
- try {
95
- const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
96
- if (existsSync(activeTaskJsonPath)) {
97
- const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
98
- const candidate = typeof content.taskPath === "string"
99
- ? safeResolveUnder(cwd, content.taskPath)
100
- : typeof content.active_task === "string"
101
- ? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
102
- : null;
103
- if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
104
- return candidate;
105
- }
106
- }
107
- }
108
- catch {
109
- // fallback
110
- }
111
- if (!existsSync(workflowDir)) {
112
- try {
113
- mkdirSync(workflowDir, { recursive: true, mode: 0o700 });
114
- }
115
- catch { }
116
- }
117
- return workflowDir;
118
- }
119
- // 1. Session start
120
- pi.on("session_start", async (event, ctx) => {
121
- traceId = ctx.sessionManager.getSessionId() || `session-${Date.now()}`;
122
- events.push({
123
- type: "session_start",
124
- timestamp: Date.now(),
125
- reason: event.reason,
126
- sessionFile: ctx.sessionManager.getSessionFile() || ""
127
- });
128
- });
129
- // 2. Turn start
130
- pi.on("turn_start", async (event, ctx) => {
131
- events.push({
132
- type: "turn_start",
133
- timestamp: Date.now()
134
- });
135
- });
136
- // 3. Turn end
137
- pi.on("turn_end", async (event, ctx) => {
138
- events.push({
139
- type: "turn_end",
140
- timestamp: Date.now()
141
- });
142
- });
143
- // 4. Tool start
144
- pi.on("tool_execution_start", async (event, ctx) => {
145
- const { toolCallId, toolName, args } = event;
146
- activeToolTimes[toolCallId] = Date.now();
147
- events.push({
148
- type: "tool_start",
149
- toolCallId,
150
- toolName,
151
- args: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(args),
152
- timestamp: Date.now()
153
- });
154
- });
155
- // 5. Tool end
156
- pi.on("tool_execution_end", async (event, ctx) => {
157
- const { toolCallId, toolName, result, isError } = event;
158
- const startTime = activeToolTimes[toolCallId] || Date.now();
159
- const durationMs = Date.now() - startTime;
160
- events.push({
161
- type: "tool_end",
162
- toolCallId,
163
- toolName,
164
- result: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(result),
165
- isError,
166
- durationMs,
167
- timestamp: Date.now()
168
- });
169
- delete activeToolTimes[toolCallId];
170
- });
171
- // 6. LLM call start (capture payload)
172
- pi.on("before_provider_request", async (event, ctx) => {
173
- activeLlmPayload = getCaptureMode() === "metadata" ? null : sanitizeTraceValue(event.payload);
174
- llmStartTime = Date.now();
175
- });
176
- // 7. LLM call end (capture generation / usage)
177
- pi.on("message_end", async (event, ctx) => {
178
- const { message } = event;
179
- if (message.role !== "assistant")
180
- return;
181
- const durationMs = llmStartTime > 0 ? Date.now() - llmStartTime : 0;
182
- events.push({
183
- type: "llm_completion",
184
- model: message.model || "unknown",
185
- provider: message.provider || "unknown",
186
- inputPayload: activeLlmPayload,
187
- outputContent: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(message.content),
188
- usage: sanitizeTraceValue(message.usage),
189
- durationMs,
190
- timestamp: Date.now()
191
- });
192
- activeLlmPayload = null;
193
- llmStartTime = 0;
194
- });
195
- // 8. Shutdown: Compile the HTML report and write to disk
196
- pi.on("session_shutdown", async (event, ctx) => {
197
- const sessionEndTime = Date.now();
198
- const totalDurationMs = sessionEndTime - sessionStartTime;
199
- const taskDir = getActiveTaskDir(ctx.cwd);
200
- const reportPath = path.join(taskDir, "notrace.html");
201
- // Gather statistics
202
- let totalTokens = 0;
203
- let inputTokens = 0;
204
- let outputTokens = 0;
205
- let totalCost = 0;
206
- let toolCallCount = 0;
207
- let llmCallCount = 0;
208
- events.forEach((e) => {
209
- if (e.type === "llm_completion") {
210
- llmCallCount++;
211
- if (e.usage) {
212
- inputTokens += e.usage.input || 0;
213
- outputTokens += e.usage.output || 0;
214
- totalTokens += e.usage.totalTokens || 0;
215
- totalCost += e.usage.cost?.total || 0;
216
- }
217
- }
218
- else if (e.type === "tool_start") {
219
- toolCallCount++;
220
- }
221
- });
222
- const htmlContent = generateHtmlReport({
223
- traceId,
224
- projectName: process.env.PHOENIX_PROJECT_NAME || "pi-coding-agent",
225
- startTime: new Date(sessionStartTime).toISOString(),
226
- endTime: new Date(sessionEndTime).toISOString(),
227
- durationMs: totalDurationMs,
228
- metrics: {
229
- totalTokens,
230
- inputTokens,
231
- outputTokens,
232
- totalCost: totalCost.toFixed(5),
233
- toolCallCount,
234
- llmCallCount
235
- },
236
- events
237
- });
238
- try {
239
- mkdirSync(taskDir, { recursive: true, mode: 0o700 });
240
- writeFileSync(reportPath, htmlContent, { encoding: "utf-8", mode: 0o600 });
241
- // Output a nice clickable file:// link to the console for the user
242
- console.log(`\n📊 [notrace] Observability report generated:`);
243
- console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m\n`);
244
- }
245
- catch (err) {
246
- console.warn(`[notrace] Failed to write HTML report: ${err?.message || err}`);
247
- }
248
- });
249
- }
250
- function safeJsonForScript(value) {
251
- return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (char) => {
252
- switch (char) {
253
- case "<": return "\\u003c";
254
- case ">": return "\\u003e";
255
- case "&": return "\\u0026";
256
- case "\u2028": return "\\u2028";
257
- case "\u2029": return "\\u2029";
258
- default: return char;
259
- }
260
- });
261
- }
262
- // Returns a self-contained premium HTML template incorporating the design tokens
263
- function generateHtmlReport(data) {
264
- const serializedData = safeJsonForScript(data);
265
- const escapedTraceId = escapeHtml(data.traceId);
266
- return `<!DOCTYPE html>
267
- <html lang="en">
268
- <head>
269
- <meta charset="UTF-8">
270
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
271
- <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'">
272
- <meta name="referrer" content="no-referrer">
273
- <title>notrace - ${escapedTraceId}</title>
274
- <style>
275
- :root {
276
- --bg: #0b0b0e;
277
- --bg-panel: rgba(22, 22, 28, 0.45);
278
- --bg-card: rgba(30, 30, 38, 0.6);
279
- --border: rgba(255, 255, 255, 0.08);
280
- --border-hover: rgba(255, 255, 255, 0.15);
281
- --text: #e2e2e9;
282
- --text-muted: #9f9fa9;
283
- --accent: #8b5cf6;
284
- --accent-gradient: linear-gradient(135deg, #a78bfa, #8b5cf6);
285
- --success: #10b981;
286
- --error: #ef4444;
287
- --warning: #f59e0b;
288
- --info: #3b82f6;
289
- }
290
-
291
- * {
292
- box-sizing: border-box;
293
- margin: 0;
294
- padding: 0;
295
- }
296
-
297
- body {
298
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
299
- background-color: var(--bg);
300
- color: var(--text);
301
- line-height: 1.5;
302
- padding: 2rem;
303
- min-height: 100vh;
304
- background-image:
305
- radial-gradient(circle at 10% 20%, rgba(139, 92, 246, 0.08) 0%, transparent 40%),
306
- radial-gradient(circle at 90% 80%, rgba(59, 130, 246, 0.06) 0%, transparent 40%);
307
- background-attachment: fixed;
308
- }
309
-
310
- .container {
311
- max-width: 1100px;
312
- margin: 0 auto;
313
- }
314
-
315
- header {
316
- display: flex;
317
- justify-content: space-between;
318
- align-items: center;
319
- margin-bottom: 2rem;
320
- padding-bottom: 1.5rem;
321
- border-bottom: 1px solid var(--border);
322
- }
323
-
324
- .brand {
325
- display: flex;
326
- align-items: center;
327
- gap: 0.75rem;
328
- }
329
-
330
- .brand h1 {
331
- font-size: 1.75rem;
332
- font-weight: 700;
333
- background: var(--accent-gradient);
334
- -webkit-background-clip: text;
335
- -webkit-text-fill-color: transparent;
336
- }
337
-
338
- .brand-tag {
339
- font-size: 0.75rem;
340
- text-transform: uppercase;
341
- letter-spacing: 0.05em;
342
- background: rgba(139, 92, 246, 0.15);
343
- color: #c084fc;
344
- padding: 0.2rem 0.5rem;
345
- border-radius: 4px;
346
- border: 1px solid rgba(167, 139, 250, 0.3);
347
- }
348
-
349
- .meta-time {
350
- font-size: 0.875rem;
351
- color: var(--text-muted);
352
- text-align: right;
353
- }
354
-
355
- .metrics-grid {
356
- display: grid;
357
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
358
- gap: 1rem;
359
- margin-bottom: 2.5rem;
360
- }
361
-
362
- .metric-card {
363
- background: var(--bg-panel);
364
- border: 1px solid var(--border);
365
- padding: 1.25rem;
366
- border-radius: 12px;
367
- backdrop-filter: blur(12px);
368
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
369
- }
370
-
371
- .metric-card:hover {
372
- border-color: var(--border-hover);
373
- transform: translateY(-2px);
374
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
375
- }
376
-
377
- .metric-label {
378
- font-size: 0.75rem;
379
- text-transform: uppercase;
380
- letter-spacing: 0.05em;
381
- color: var(--text-muted);
382
- margin-bottom: 0.5rem;
383
- }
384
-
385
- .metric-value {
386
- font-size: 1.5rem;
387
- font-weight: 600;
388
- color: #fff;
389
- }
390
-
391
- .timeline {
392
- position: relative;
393
- padding-left: 2rem;
394
- border-left: 2px solid var(--border);
395
- margin-left: 1rem;
396
- }
397
-
398
- .timeline-event {
399
- position: relative;
400
- margin-bottom: 2rem;
401
- animation: slideIn 0.4s ease-out;
402
- }
403
-
404
- @keyframes slideIn {
405
- from { opacity: 0; transform: translateX(-10px); }
406
- to { opacity: 1; transform: translateX(0); }
407
- }
408
-
409
- .timeline-dot {
410
- position: absolute;
411
- left: calc(-2rem - 6px);
412
- top: 1.25rem;
413
- width: 10px;
414
- height: 10px;
415
- border-radius: 50%;
416
- background: var(--text-muted);
417
- box-shadow: 0 0 0 4px var(--bg);
418
- transition: all 0.3s ease;
419
- }
420
-
421
- /* Event color mappings */
422
- .timeline-event.session-start .timeline-dot { background: var(--info); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--info); }
423
- .timeline-event.turn-start .timeline-dot { background: var(--accent); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--accent); }
424
- .timeline-event.tool-call .timeline-dot { background: var(--warning); box-shadow: 0 0 0 4px var(--bg); }
425
- .timeline-event.tool-call.error .timeline-dot { background: var(--error); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--error); }
426
- .timeline-event.llm-call .timeline-dot { background: var(--success); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--success); }
427
-
428
- .card {
429
- background: var(--bg-panel);
430
- border: 1px solid var(--border);
431
- border-radius: 12px;
432
- padding: 1.25rem;
433
- backdrop-filter: blur(12px);
434
- transition: border-color 0.2s;
435
- }
436
-
437
- .card:hover {
438
- border-color: var(--border-hover);
439
- }
440
-
441
- .card-header {
442
- display: flex;
443
- justify-content: space-between;
444
- align-items: center;
445
- cursor: pointer;
446
- user-select: none;
447
- }
448
-
449
- .card-title {
450
- display: flex;
451
- align-items: center;
452
- gap: 0.75rem;
453
- font-weight: 600;
454
- }
455
-
456
- .card-badge {
457
- font-size: 0.75rem;
458
- font-weight: 500;
459
- padding: 0.15rem 0.5rem;
460
- border-radius: 4px;
461
- }
462
-
463
- .badge-session { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
464
- .badge-turn { background: rgba(139, 92, 246, 0.15); color: #c084fc; }
465
- .badge-tool { background: rgba(245, 158, 11, 0.15); color: #fbbf24; }
466
- .badge-tool.error { background: rgba(239, 68, 68, 0.15); color: #f87171; }
467
- .badge-llm { background: rgba(16, 185, 129, 0.15); color: #34d399; }
468
-
469
- .card-time {
470
- font-size: 0.8rem;
471
- color: var(--text-muted);
472
- display: flex;
473
- align-items: center;
474
- gap: 0.5rem;
475
- }
476
-
477
- .arrow-icon {
478
- width: 16px;
479
- height: 16px;
480
- fill: none;
481
- stroke: currentColor;
482
- stroke-width: 2;
483
- stroke-linecap: round;
484
- stroke-linejoin: round;
485
- transition: transform 0.2s;
486
- }
487
-
488
- .expanded .arrow-icon {
489
- transform: rotate(90deg);
490
- }
491
-
492
- .card-body {
493
- margin-top: 1rem;
494
- padding-top: 1rem;
495
- border-top: 1px solid var(--border);
496
- display: none;
497
- }
498
-
499
- .expanded .card-body {
500
- display: block;
501
- }
502
-
503
- .code-block {
504
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
505
- font-size: 0.875rem;
506
- background: rgba(0, 0, 0, 0.35);
507
- border: 1px solid var(--border);
508
- padding: 1rem;
509
- border-radius: 8px;
510
- overflow-x: auto;
511
- margin-top: 0.5rem;
512
- color: #e2e2e9;
513
- white-space: pre-wrap;
514
- }
515
-
516
- .messages-container {
517
- display: flex;
518
- flex-direction: column;
519
- gap: 0.75rem;
520
- }
521
-
522
- .msg-row {
523
- display: flex;
524
- flex-direction: column;
525
- gap: 0.25rem;
526
- padding: 0.75rem;
527
- background: rgba(255, 255, 255, 0.02);
528
- border-radius: 6px;
529
- border-left: 3px solid var(--border);
530
- }
531
-
532
- .msg-row.user { border-left-color: var(--info); }
533
- .msg-row.assistant { border-left-color: var(--success); }
534
- .msg-row.system { border-left-color: var(--accent); }
535
-
536
- .msg-role {
537
- font-size: 0.75rem;
538
- font-weight: 600;
539
- text-transform: uppercase;
540
- letter-spacing: 0.05em;
541
- }
542
-
543
- .msg-row.user .msg-role { color: #60a5fa; }
544
- .msg-row.assistant .msg-role { color: #34d399; }
545
- .msg-row.system .msg-role { color: #c084fc; }
546
-
547
- .msg-text {
548
- font-size: 0.9rem;
549
- white-space: pre-wrap;
550
- }
551
-
552
- .duration-pill {
553
- font-size: 0.75rem;
554
- background: rgba(255, 255, 255, 0.06);
555
- padding: 0.1rem 0.4rem;
556
- border-radius: 4px;
557
- color: var(--text-muted);
558
- }
559
- </style>
560
- </head>
561
- <body>
562
- <div class="container">
563
- <header>
564
- <div class="brand">
565
- <h1>notrace</h1>
566
- <span class="brand-tag">Local Observability</span>
567
- </div>
568
- <div class="meta-time">
569
- <div>Session: <span id="sess-id"></span></div>
570
- <div id="sess-time" style="font-size: 0.8rem;"></div>
571
- </div>
572
- </header>
573
-
574
- <div class="metrics-grid">
575
- <div class="metric-card">
576
- <div class="metric-label">Duration</div>
577
- <div class="metric-value" id="val-duration">-</div>
578
- </div>
579
- <div class="metric-card">
580
- <div class="metric-label">Total Tokens</div>
581
- <div class="metric-value" id="val-tokens">-</div>
582
- </div>
583
- <div class="metric-card">
584
- <div class="metric-label">LLM Calls</div>
585
- <div class="metric-value" id="val-llms">-</div>
586
- </div>
587
- <div class="metric-card">
588
- <div class="metric-label">Tool Calls</div>
589
- <div class="metric-value" id="val-tools">-</div>
590
- </div>
591
- <div class="metric-card">
592
- <div class="metric-label">Cost (USD)</div>
593
- <div class="metric-value" id="val-cost">-</div>
594
- </div>
595
- </div>
596
-
597
- <h2 style="margin-bottom: 1.5rem; font-weight: 600; font-size: 1.25rem; color: #fff;">Activity Flow</h2>
598
- <div class="timeline" id="timeline-container">
599
- <!-- Injected by JS -->
600
- </div>
601
- </div>
602
-
603
- <script>
604
- const traceData = ${serializedData};
605
-
606
- function escapeHtml(value) {
607
- return String(value ?? "").replace(/[&<>'"]/g, (char) => ({
608
- "&": "&amp;",
609
- "<": "&lt;",
610
- ">": "&gt;",
611
- "'": "&#39;",
612
- '"': "&quot;"
613
- }[char]));
614
- }
615
-
616
- function safeClassName(value, fallback = "unknown") {
617
- const normalized = String(value ?? fallback).toLowerCase().replace(/[^a-z0-9_-]/g, "-");
618
- return normalized || fallback;
619
- }
620
-
621
- function jsonText(value) {
622
- return escapeHtml(typeof value === "string" ? value : JSON.stringify(value, null, 2));
623
- }
624
-
625
- // Render Metrics
626
- document.getElementById("sess-id").textContent = traceData.traceId;
627
- document.getElementById("sess-time").textContent = new Date(traceData.startTime).toLocaleString();
628
- document.getElementById("val-duration").textContent = (traceData.durationMs / 1000).toFixed(2) + "s";
629
- document.getElementById("val-tokens").textContent = traceData.metrics.totalTokens.toLocaleString();
630
- document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
631
- document.getElementById("val-tools").textContent = traceData.metrics.toolCallCount;
632
- document.getElementById("val-cost").textContent = "$" + traceData.metrics.totalCost;
633
-
634
- // Process & Render Timeline
635
- const container = document.getElementById("timeline-container");
636
- const events = traceData.events;
637
-
638
- // Group start and end of tools to display them as single cards
639
- const toolExecutions = {};
640
- const renderedEvents = [];
641
-
642
- events.forEach(e => {
643
- if (e.type === "session_start") {
644
- renderedEvents.push({
645
- type: "session",
646
- title: "Session Initialized",
647
- time: new Date(e.timestamp).toLocaleTimeString(),
648
- body: \`Reason: \${e.reason}\\nFile: \${e.sessionFile}\`
649
- });
650
- } else if (e.type === "turn_start") {
651
- renderedEvents.push({
652
- type: "turn",
653
- title: "User Turn Started",
654
- time: new Date(e.timestamp).toLocaleTimeString(),
655
- body: "Agent waiting for query execution loop."
656
- });
657
- } else if (e.type === "tool_start") {
658
- toolExecutions[e.toolCallId] = {
659
- type: "tool",
660
- title: \`Tool Call: \${e.toolName}\`,
661
- toolName: e.toolName,
662
- time: new Date(e.timestamp).toLocaleTimeString(),
663
- args: e.args,
664
- startTime: e.timestamp
665
- };
666
- } else if (e.type === "tool_end") {
667
- const start = toolExecutions[e.toolCallId];
668
- if (start) {
669
- start.result = e.result;
670
- start.isError = e.isError;
671
- start.durationMs = e.durationMs;
672
- renderedEvents.push(start);
673
- delete toolExecutions[e.toolCallId];
674
- }
675
- } else if (e.type === "llm_completion") {
676
- renderedEvents.push({
677
- type: "llm",
678
- title: \`LLM Call: \${e.model}\`,
679
- model: e.model,
680
- provider: e.provider,
681
- time: new Date(e.timestamp).toLocaleTimeString(),
682
- durationMs: e.durationMs,
683
- usage: e.usage,
684
- payload: e.inputPayload,
685
- output: e.outputContent
686
- });
687
- }
688
- });
689
-
690
- // Render cards
691
- renderedEvents.forEach((ev, index) => {
692
- const evDiv = document.createElement("div");
693
- const eventType = safeClassName(ev.type);
694
- evDiv.className = \`timeline-event \${eventType}-start \${ev.isError ? "error" : ""}\`;
695
-
696
- let cardHtml = \`
697
- <div class="timeline-dot"></div>
698
- <div class="card" id="card-\${index}">
699
- <div class="card-header" onclick="toggleCard(\${index})">
700
- <div class="card-title">
701
- <span class="card-badge badge-\${eventType} \${ev.isError ? "error" : ""}">\${escapeHtml(eventType.toUpperCase())}</span>
702
- <span>\${escapeHtml(ev.title)}</span>
703
- \${ev.durationMs ? \`<span class="duration-pill">\${escapeHtml((ev.durationMs / 1000).toFixed(2))}s</span>\` : ""}
704
- </div>
705
- <div class="card-time">
706
- <span>\${escapeHtml(ev.time)}</span>
707
- <svg class="arrow-icon" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
708
- </div>
709
- </div>
710
- <div class="card-body">
711
- \`;
712
-
713
- if (ev.type === "session" || ev.type === "turn") {
714
- cardHtml += \`<div class="code-block">\${escapeHtml(ev.body)}</div>\`;
715
- } else if (ev.type === "tool") {
716
- cardHtml += \`
717
- <strong>Arguments:</strong>
718
- <div class="code-block">\${jsonText(ev.args)}</div>
719
- <strong style="margin-top: 1rem; display: block;">Result (\${ev.isError ? "Error" : "Success"}):</strong>
720
- <div class="code-block">\${jsonText(ev.result)}</div>
721
- \`;
722
- } else if (ev.type === "llm") {
723
- // Render system prompt and input messages if present
724
- let messagesHtml = '<div class="messages-container">';
725
- if (ev.payload) {
726
- if (ev.payload.system_instruction) {
727
- const instr = typeof ev.payload.system_instruction === "string"
728
- ? ev.payload.system_instruction
729
- : JSON.stringify(ev.payload.system_instruction);
730
- messagesHtml += \`
731
- <div class="msg-row system">
732
- <span class="msg-role">System Instruction</span>
733
- <span class="msg-text">\${escapeHtml(instr)}</span>
734
- </div>
735
- \`;
736
- }
737
- if (ev.payload.messages && Array.isArray(ev.payload.messages)) {
738
- ev.payload.messages.forEach(m => {
739
- const contentText = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
740
- const role = safeClassName(m.role, "message");
741
- messagesHtml += \`
742
- <div class="msg-row \${role}">
743
- <span class="msg-role">\${escapeHtml(m.role)}</span>
744
- <span class="msg-text">\${escapeHtml(contentText)}</span>
745
- </div>
746
- \`;
747
- });
748
- }
749
- }
750
- messagesHtml += '</div>';
751
-
752
- cardHtml += \`
753
- <strong>Context Messages:</strong>
754
- \${messagesHtml}
755
- <strong style="margin-top: 1.25rem; display: block;">Generated Response:</strong>
756
- <div class="code-block">\${jsonText(ev.output)}</div>
757
- \${ev.usage ? \`
758
- <div style="margin-top: 1rem; font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 1.5rem;">
759
- <span>Input Tokens: <strong>\${escapeHtml(ev.usage.input ?? 0)}</strong></span>
760
- <span>Output Tokens: <strong>\${escapeHtml(ev.usage.output ?? 0)}</strong></span>
761
- <span>Total Tokens: <strong>\${escapeHtml(ev.usage.totalTokens ?? 0)}</strong></span>
762
- <span>Cost: <strong>\$\${escapeHtml(ev.usage.cost?.total?.toFixed?.(5) || "0.00")}</strong></span>
763
- </div>
764
- \` : ""}
765
- \`;
766
- }
767
-
768
- cardHtml += \`
769
- </div>
770
- </div>
771
- \`;
772
-
773
- evDiv.innerHTML = cardHtml;
774
- container.appendChild(evDiv);
775
- });
776
-
777
- // Expand/Collapse controller
778
- function toggleCard(index) {
779
- const card = document.getElementById(\`card-\${index}\`);
780
- card?.classList.toggle("expanded");
781
- }
782
- </script>
783
- </body>
784
- </html>`;
785
- }