copilot-tracer 1.0.4 → 1.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/README.md +114 -158
- package/dist/cli.js +95 -76
- package/dist/db.js +166 -3
- package/dist/otlpReceiver.js +31 -12
- package/dist/setup.js +35 -12
- package/dist/webServer.js +39 -8
- package/package.json +1 -1
- package/web/index.html +296 -73
package/dist/db.js
CHANGED
|
@@ -8,10 +8,20 @@ if (!fs.existsSync(DB_DIR))
|
|
|
8
8
|
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
9
9
|
const db = new Database(DB_PATH);
|
|
10
10
|
db.exec(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
path TEXT NOT NULL UNIQUE,
|
|
14
|
+
repo_url TEXT,
|
|
15
|
+
local_path TEXT,
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
updated_at TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
|
|
11
20
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
12
21
|
id TEXT PRIMARY KEY,
|
|
13
22
|
started_at TEXT NOT NULL,
|
|
14
|
-
ended_at TEXT
|
|
23
|
+
ended_at TEXT,
|
|
24
|
+
project_id TEXT REFERENCES projects(id)
|
|
15
25
|
);
|
|
16
26
|
|
|
17
27
|
CREATE TABLE IF NOT EXISTS traces (
|
|
@@ -38,8 +48,113 @@ db.exec(`
|
|
|
38
48
|
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
|
39
49
|
);
|
|
40
50
|
`);
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
// Migrate existing DBs
|
|
52
|
+
try {
|
|
53
|
+
db.prepare('ALTER TABLE projects ADD COLUMN repo_url TEXT').run();
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
try {
|
|
57
|
+
db.prepare('ALTER TABLE projects ADD COLUMN local_path TEXT').run();
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
try {
|
|
61
|
+
db.prepare('ALTER TABLE sessions ADD COLUMN project_id TEXT REFERENCES projects(id)').run();
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
export function ensureProject(projectPath, repoUrl) {
|
|
65
|
+
const id = 'project:' + projectPath;
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
db.prepare('INSERT OR IGNORE INTO projects (id, path, repo_url, local_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, projectPath, repoUrl ?? null, projectPath, now, now);
|
|
68
|
+
if (repoUrl) {
|
|
69
|
+
db.prepare('UPDATE projects SET repo_url = ?, updated_at = ? WHERE id = ?').run(repoUrl, now, id);
|
|
70
|
+
}
|
|
71
|
+
db.prepare('UPDATE projects SET updated_at = ? WHERE id = ?').run(now, id);
|
|
72
|
+
return id;
|
|
73
|
+
}
|
|
74
|
+
export function ensureProjectByRepo(repoUrl) {
|
|
75
|
+
// Try to find existing project by repo URL
|
|
76
|
+
const existing = db.prepare('SELECT id FROM projects WHERE repo_url = ?').get(repoUrl);
|
|
77
|
+
if (existing) {
|
|
78
|
+
db.prepare('UPDATE projects SET updated_at = ? WHERE id = ?').run(new Date().toISOString(), existing.id);
|
|
79
|
+
return existing.id;
|
|
80
|
+
}
|
|
81
|
+
// Create new project with repo URL as path (local_path set later)
|
|
82
|
+
const id = 'project:' + repoUrl;
|
|
83
|
+
const now = new Date().toISOString();
|
|
84
|
+
db.prepare('INSERT OR IGNORE INTO projects (id, path, repo_url, local_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, repoUrl, repoUrl, null, now, now);
|
|
85
|
+
return id;
|
|
86
|
+
}
|
|
87
|
+
export function updateProjectLocalPath(projectId, localPath) {
|
|
88
|
+
db.prepare('UPDATE projects SET local_path = ?, updated_at = ? WHERE id = ?').run(localPath, new Date().toISOString(), projectId);
|
|
89
|
+
}
|
|
90
|
+
export function findProjectByRepo(repoUrl) {
|
|
91
|
+
const row = db.prepare('SELECT id, path, local_path FROM projects WHERE repo_url = ?').get(repoUrl);
|
|
92
|
+
return row ? { id: row.id, path: row.path, local_path: row.local_path } : null;
|
|
93
|
+
}
|
|
94
|
+
export function createSession(id, projectId) {
|
|
95
|
+
// Backfill-safe upsert: insert if missing, set project_id only when a project is
|
|
96
|
+
// provided AND the session currently has none (never null out an existing link).
|
|
97
|
+
db.prepare(`
|
|
98
|
+
INSERT INTO sessions (id, started_at, project_id) VALUES (?, ?, ?)
|
|
99
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
100
|
+
project_id = CASE
|
|
101
|
+
WHEN excluded.project_id IS NOT NULL THEN COALESCE(sessions.project_id, excluded.project_id)
|
|
102
|
+
ELSE sessions.project_id
|
|
103
|
+
END
|
|
104
|
+
`).run(id, new Date().toISOString(), projectId ?? null);
|
|
105
|
+
}
|
|
106
|
+
export function getDashboard() {
|
|
107
|
+
const projects = db.prepare(`
|
|
108
|
+
SELECT
|
|
109
|
+
p.id, p.path, p.repo_url, p.local_path,
|
|
110
|
+
COUNT(DISTINCT s.id) as session_count,
|
|
111
|
+
COALESCE(SUM(t.tokens_total), 0) as total_tokens,
|
|
112
|
+
COALESCE(SUM(t.ai_credits), 0) as total_credits,
|
|
113
|
+
MAX(s.started_at) as last_active_at
|
|
114
|
+
FROM projects p
|
|
115
|
+
LEFT JOIN sessions s ON s.project_id = p.id
|
|
116
|
+
LEFT JOIN traces t ON t.session_id = s.id
|
|
117
|
+
GROUP BY p.id
|
|
118
|
+
ORDER BY last_active_at DESC
|
|
119
|
+
`).all();
|
|
120
|
+
const totals = db.prepare(`
|
|
121
|
+
SELECT
|
|
122
|
+
(SELECT COUNT(*) FROM projects) as projects,
|
|
123
|
+
(SELECT COUNT(*) FROM sessions) as sessions,
|
|
124
|
+
COALESCE((SELECT SUM(tokens_total) FROM traces), 0) as tokens,
|
|
125
|
+
COALESCE((SELECT SUM(ai_credits) FROM traces), 0) as credits
|
|
126
|
+
`).get();
|
|
127
|
+
const enriched = projects.map(p => {
|
|
128
|
+
const lastSession = db.prepare(`
|
|
129
|
+
SELECT s.id,
|
|
130
|
+
COALESCE((SELECT SUM(tokens_total) FROM traces WHERE session_id = s.id), 0) as tokens,
|
|
131
|
+
COALESCE((SELECT SUM(ai_credits) FROM traces WHERE session_id = s.id), 0) as credits
|
|
132
|
+
FROM sessions s
|
|
133
|
+
WHERE s.project_id = ?
|
|
134
|
+
ORDER BY s.started_at DESC
|
|
135
|
+
LIMIT 1
|
|
136
|
+
`).get(p.id);
|
|
137
|
+
return {
|
|
138
|
+
id: p.id,
|
|
139
|
+
path: p.path,
|
|
140
|
+
repoUrl: p.repo_url,
|
|
141
|
+
localPath: p.local_path,
|
|
142
|
+
sessionCount: p.session_count || 0,
|
|
143
|
+
totalTokens: p.total_tokens || 0,
|
|
144
|
+
totalCredits: p.total_credits || 0,
|
|
145
|
+
lastActiveAt: p.last_active_at,
|
|
146
|
+
lastSession: lastSession ?? null,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
projects: enriched,
|
|
151
|
+
totals: {
|
|
152
|
+
projects: totals.projects,
|
|
153
|
+
sessions: totals.sessions,
|
|
154
|
+
tokens: totals.tokens,
|
|
155
|
+
credits: totals.credits,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
43
158
|
}
|
|
44
159
|
export function upsertTrace(entry) {
|
|
45
160
|
db.prepare(`
|
|
@@ -156,3 +271,51 @@ function rowToEntry(row) {
|
|
|
156
271
|
export function deleteTrace(id) {
|
|
157
272
|
db.prepare('DELETE FROM traces WHERE id = ?').run(id);
|
|
158
273
|
}
|
|
274
|
+
export function getProjectTraces(projectId, limit = 200) {
|
|
275
|
+
const rows = db.prepare(`
|
|
276
|
+
SELECT t.* FROM traces t
|
|
277
|
+
JOIN sessions s ON s.id = t.session_id
|
|
278
|
+
WHERE s.project_id = ?
|
|
279
|
+
ORDER BY t.date_time DESC
|
|
280
|
+
LIMIT ?
|
|
281
|
+
`).all(projectId, limit);
|
|
282
|
+
return rows.map((r) => rowToEntry(r));
|
|
283
|
+
}
|
|
284
|
+
export function getProjectSessionSummary(projectId) {
|
|
285
|
+
const stats = db.prepare(`
|
|
286
|
+
SELECT
|
|
287
|
+
COUNT(*) as entries,
|
|
288
|
+
SUM(t.tokens_input) as input,
|
|
289
|
+
SUM(t.tokens_output) as output,
|
|
290
|
+
SUM(t.tokens_cached) as cached,
|
|
291
|
+
SUM(t.tokens_reasoning) as reasoning,
|
|
292
|
+
SUM(t.tokens_written) as written,
|
|
293
|
+
SUM(t.tokens_total) as total,
|
|
294
|
+
SUM(t.ai_credits) as credits,
|
|
295
|
+
SUM(t.duration_ms) as duration,
|
|
296
|
+
SUM(t.skill_count) as skills,
|
|
297
|
+
SUM(t.agent_count) as agents,
|
|
298
|
+
SUM(t.mcp_count) as mcps
|
|
299
|
+
FROM traces t
|
|
300
|
+
JOIN sessions s ON s.id = t.session_id
|
|
301
|
+
WHERE s.project_id = ?
|
|
302
|
+
`).get(projectId);
|
|
303
|
+
return {
|
|
304
|
+
sessionId: projectId,
|
|
305
|
+
startedAt: '',
|
|
306
|
+
totalEntries: stats.entries || 0,
|
|
307
|
+
totalTokens: {
|
|
308
|
+
input: stats.input || 0,
|
|
309
|
+
output: stats.output || 0,
|
|
310
|
+
cached: stats.cached || 0,
|
|
311
|
+
reasoning: stats.reasoning || 0,
|
|
312
|
+
written: stats.written || 0,
|
|
313
|
+
total: stats.total || 0,
|
|
314
|
+
},
|
|
315
|
+
totalCredits: stats.credits || 0,
|
|
316
|
+
totalDurationMs: stats.duration || 0,
|
|
317
|
+
totalSkillCalls: stats.skills || 0,
|
|
318
|
+
totalAgentCalls: stats.agents || 0,
|
|
319
|
+
totalMcpCalls: stats.mcps || 0,
|
|
320
|
+
};
|
|
321
|
+
}
|
package/dist/otlpReceiver.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* chat <model> span attrs: same as above
|
|
15
15
|
* User message event: github.copilot.user.message
|
|
16
16
|
*/
|
|
17
|
-
import { upsertTrace, createSession, deleteTrace } from './db.js';
|
|
17
|
+
import { upsertTrace, createSession, deleteTrace, ensureProject, ensureProjectByRepo } from './db.js';
|
|
18
18
|
import { traceEvents } from './proxy.js';
|
|
19
19
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
20
20
|
function getAttr(attrs, key) {
|
|
@@ -37,20 +37,35 @@ function nanoToIso(nano) {
|
|
|
37
37
|
return new Date(nanoToMs(nano)).toISOString();
|
|
38
38
|
}
|
|
39
39
|
const inFlight = new Map(); // traceId → InFlight
|
|
40
|
-
// Sessions
|
|
41
|
-
|
|
42
|
-
function ensureSession(sessionId) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
// Sessions are created lazily. Always upsert so project_id gets backfilled
|
|
41
|
+
// when the session was created earlier without a project.
|
|
42
|
+
function ensureSession(sessionId, projectId) {
|
|
43
|
+
createSession(sessionId, projectId);
|
|
44
|
+
}
|
|
45
|
+
// ── Project resolution ────────────────────────────────────────────────────────
|
|
46
|
+
// Precedence: repo URL > working dir > default (CLI) project.
|
|
47
|
+
// If none match, the session keeps no project (traces still appear on live page).
|
|
48
|
+
function resolveProjectId(repoUrl, workingDir, defaultProjectId) {
|
|
49
|
+
if (repoUrl)
|
|
50
|
+
return ensureProjectByRepo(String(repoUrl));
|
|
51
|
+
if (workingDir)
|
|
52
|
+
return ensureProject(workingDir);
|
|
53
|
+
return defaultProjectId;
|
|
54
|
+
}
|
|
55
|
+
function detectWorkingDir(attrs) {
|
|
56
|
+
for (const key of ['process.working_directory', 'github.copilot.working_dir']) {
|
|
57
|
+
const v = getAttr(attrs, key);
|
|
58
|
+
if (v && String(v).trim())
|
|
59
|
+
return String(v).trim();
|
|
46
60
|
}
|
|
61
|
+
return undefined;
|
|
47
62
|
}
|
|
48
63
|
// ── Process one batch of spans ────────────────────────────────────────────────
|
|
49
64
|
// Track standalone chat entries so invoke_agent can replace them (multiple chat spans per traceId)
|
|
50
65
|
const pendingChatIds = new Map(); // `chat:${traceId}` → list of entryIds
|
|
51
66
|
// Buffer tool calls that arrive before invoke_agent
|
|
52
67
|
const pendingToolCalls = new Map(); // traceId → tool calls
|
|
53
|
-
function processSpans(spans, sessionId) {
|
|
68
|
+
function processSpans(spans, sessionId, projectId, workingDir) {
|
|
54
69
|
for (const span of spans) {
|
|
55
70
|
// DEBUG — log raw span to stderr when COPILOT_TRACER_DEBUG=1
|
|
56
71
|
if (process.env.COPILOT_TRACER_DEBUG === '1') {
|
|
@@ -65,6 +80,9 @@ function processSpans(spans, sessionId) {
|
|
|
65
80
|
const startMs = nanoToMs(span.startTimeUnixNano);
|
|
66
81
|
const endMs = nanoToMs(span.endTimeUnixNano);
|
|
67
82
|
const durationMs = endMs - startMs;
|
|
83
|
+
// Extract repo URL for auto project detection
|
|
84
|
+
const repoUrl = getAttr(attrs, 'github.copilot.git.repository');
|
|
85
|
+
const resolvedProjectId = resolveProjectId(repoUrl, workingDir, projectId);
|
|
68
86
|
// Extract prompt from gen_ai.input.messages (real attr name from copilot)
|
|
69
87
|
let promptText = '';
|
|
70
88
|
let responseText = '';
|
|
@@ -213,7 +231,7 @@ function processSpans(spans, sessionId) {
|
|
|
213
231
|
inf.entry.mcpCount = mcps;
|
|
214
232
|
pendingToolCalls.delete(traceId);
|
|
215
233
|
}
|
|
216
|
-
ensureSession(sessionId);
|
|
234
|
+
ensureSession(sessionId, resolvedProjectId);
|
|
217
235
|
upsertTrace(entry);
|
|
218
236
|
traceEvents.emit('trace:update', entry);
|
|
219
237
|
traceEvents.emit('trace:done', entry);
|
|
@@ -276,7 +294,7 @@ function processSpans(spans, sessionId) {
|
|
|
276
294
|
skillCount: 0, agentCount: 0, mcpCount: 0,
|
|
277
295
|
status: 'done',
|
|
278
296
|
};
|
|
279
|
-
ensureSession(sessionId);
|
|
297
|
+
ensureSession(sessionId, resolveProjectId(getAttr(attrs, 'github.copilot.git.repository'), workingDir, projectId));
|
|
280
298
|
upsertTrace(entry);
|
|
281
299
|
const chatKey = `chat:${traceId}`;
|
|
282
300
|
const existing = pendingChatIds.get(chatKey) ?? [];
|
|
@@ -383,7 +401,7 @@ function detectToolType(name) {
|
|
|
383
401
|
return 'builtin';
|
|
384
402
|
}
|
|
385
403
|
// ── Register OTLP HTTP routes on the Express app ──────────────────────────────
|
|
386
|
-
export function registerOtlpRoutes(app, defaultSessionId) {
|
|
404
|
+
export function registerOtlpRoutes(app, defaultSessionId, projectId) {
|
|
387
405
|
// OTLP traces — copilot sends JSON (http/json protocol)
|
|
388
406
|
// body already parsed by express.json() middleware registered before this call
|
|
389
407
|
app.post('/v1/traces', (req, res) => {
|
|
@@ -396,8 +414,9 @@ export function registerOtlpRoutes(app, defaultSessionId) {
|
|
|
396
414
|
?? getAttr(resAttrs, 'session.id')
|
|
397
415
|
?? getAttr(resAttrs, 'github.copilot.conversation_id');
|
|
398
416
|
const sessionId = String(sessionFromOtel ?? defaultSessionId);
|
|
417
|
+
const workingDir = detectWorkingDir(resAttrs);
|
|
399
418
|
for (const ss of rs.scopeSpans ?? []) {
|
|
400
|
-
processSpans(ss.spans ?? [], sessionId);
|
|
419
|
+
processSpans(ss.spans ?? [], sessionId, projectId, workingDir);
|
|
401
420
|
}
|
|
402
421
|
}
|
|
403
422
|
res.status(200).json({ partialSuccess: {} });
|
package/dist/setup.js
CHANGED
|
@@ -22,6 +22,26 @@ function otelEnvBlock(port) {
|
|
|
22
22
|
`export ${OTEL_ENDPOINT_KEY}=http://localhost:${port}`,
|
|
23
23
|
`export ${OTEL_CONTENT_KEY}=true`,
|
|
24
24
|
`export ${OTEL_ENABLED_KEY}=true`,
|
|
25
|
+
``,
|
|
26
|
+
`# Tag every copilot prompt with the terminal folder it ran from, so the`,
|
|
27
|
+
`# tracer can attribute it to the right project. OTLP carries no working-dir,`,
|
|
28
|
+
`# so we inject it via OTEL_RESOURCE_ATTRIBUTES (percent-encoded).`,
|
|
29
|
+
`copilot() {`,
|
|
30
|
+
` local _wd`,
|
|
31
|
+
` if command -v python3 >/dev/null 2>&1; then`,
|
|
32
|
+
` _wd="$(pwd | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read().strip(), safe="/"))')"`,
|
|
33
|
+
` else`,
|
|
34
|
+
` _wd="$(pwd)"`,
|
|
35
|
+
` fi`,
|
|
36
|
+
` if [ -n "\${OTEL_RESOURCE_ATTRIBUTES:-}" ]; then`,
|
|
37
|
+
` # Drop any stale working_dir entry, then append the current folder`,
|
|
38
|
+
` OTEL_RESOURCE_ATTRIBUTES="$(printf '%s' "\$OTEL_RESOURCE_ATTRIBUTES" | sed -E 's/(^|,)github\.copilot\.working_dir=[^,]*/\\1/g; s/^,//')"`,
|
|
39
|
+
` [ -z "\$OTEL_RESOURCE_ATTRIBUTES" ] || OTEL_RESOURCE_ATTRIBUTES="\${OTEL_RESOURCE_ATTRIBUTES},"`,
|
|
40
|
+
` fi`,
|
|
41
|
+
` OTEL_RESOURCE_ATTRIBUTES="\${OTEL_RESOURCE_ATTRIBUTES}github.copilot.working_dir=\${_wd}"`,
|
|
42
|
+
` export OTEL_RESOURCE_ATTRIBUTES`,
|
|
43
|
+
` command copilot "\$@"`,
|
|
44
|
+
`}`,
|
|
25
45
|
`# <<< copilot-tracer <<<`,
|
|
26
46
|
].join('\n');
|
|
27
47
|
}
|
|
@@ -130,7 +150,7 @@ function patchVSCodeSettings(settingsPath, port) {
|
|
|
130
150
|
return { action: wasSet ? 'updated' : 'added' };
|
|
131
151
|
}
|
|
132
152
|
// ── Main setup ────────────────────────────────────────────────────────────────
|
|
133
|
-
export function runSetup(port) {
|
|
153
|
+
export function runSetup(port, silent = false) {
|
|
134
154
|
const CHECK = '✅';
|
|
135
155
|
const WARN = '⚠️ ';
|
|
136
156
|
const INFO = '📍';
|
|
@@ -174,11 +194,12 @@ export function runSetup(port) {
|
|
|
174
194
|
if (result.action === 'added') {
|
|
175
195
|
console.log(`\n${CHECK} Shell profile patched: ${label}`);
|
|
176
196
|
console.log(` ${ARROW} Added OTEL env vars (endpoint, content capture)`);
|
|
197
|
+
console.log(` ${ARROW} Added copilot() wrapper — tags each prompt with the terminal folder`);
|
|
177
198
|
console.log(` ${ARROW} Run: source ${profilePath}`);
|
|
178
199
|
}
|
|
179
200
|
else if (result.action === 'updated') {
|
|
180
201
|
console.log(`\n${CHECK} Shell profile updated: ${label}`);
|
|
181
|
-
console.log(` ${ARROW} Updated port to ${port}`);
|
|
202
|
+
console.log(` ${ARROW} Updated port to ${port} + copilot() wrapper`);
|
|
182
203
|
console.log(` ${ARROW} Run: source ${profilePath}`);
|
|
183
204
|
}
|
|
184
205
|
else {
|
|
@@ -211,14 +232,16 @@ export function runSetup(port) {
|
|
|
211
232
|
process.env[OTEL_CONTENT_KEY] = 'true';
|
|
212
233
|
process.env[OTEL_ENABLED_KEY] = 'true';
|
|
213
234
|
// 6. Summary
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
235
|
+
if (!silent) {
|
|
236
|
+
const profileBase = profilePath ? path.basename(profilePath) : '.zshrc';
|
|
237
|
+
console.log('\n────────────────────────────────────────────────');
|
|
238
|
+
console.log(' One manual step required:\n');
|
|
239
|
+
console.log(` source ~/${profileBase}`);
|
|
240
|
+
console.log(` (opens a new terminal already? — env is already active there)`);
|
|
241
|
+
if (vscode.found)
|
|
242
|
+
console.log('\n Restart VS Code once to pick up the new terminal env.');
|
|
243
|
+
console.log('\n ✨ Starting tracer web UI now...');
|
|
244
|
+
console.log(` Open: http://localhost:${port}/`);
|
|
245
|
+
console.log('────────────────────────────────────────────────\n');
|
|
246
|
+
}
|
|
224
247
|
}
|
package/dist/webServer.js
CHANGED
|
@@ -4,11 +4,11 @@ import { Server } from 'socket.io';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
-
import { getTraces, getTrace, getSessionSummary } from './db.js';
|
|
7
|
+
import { getTraces, getTrace, getSessionSummary, getDashboard, updateProjectLocalPath, getProjectTraces, getProjectSessionSummary } from './db.js';
|
|
8
8
|
import { traceEvents } from './proxy.js';
|
|
9
9
|
import { registerOtlpRoutes } from './otlpReceiver.js';
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
export function startWebServer(port = 4747, sessionId) {
|
|
11
|
+
export function startWebServer(port = 4747, sessionId, projectId) {
|
|
12
12
|
const app = express();
|
|
13
13
|
const httpServer = createServer(app);
|
|
14
14
|
const io = new Server(httpServer, { cors: { origin: '*' } });
|
|
@@ -16,7 +16,7 @@ export function startWebServer(port = 4747, sessionId) {
|
|
|
16
16
|
app.use(express.static(path.join(__dirname, '../web')));
|
|
17
17
|
// Register OTLP receiver routes
|
|
18
18
|
app.use(express.json({ limit: '10mb' }));
|
|
19
|
-
registerOtlpRoutes(app, sessionId ?? 'default');
|
|
19
|
+
registerOtlpRoutes(app, sessionId ?? 'default', projectId);
|
|
20
20
|
// API
|
|
21
21
|
app.get('/api/traces', (req, res) => {
|
|
22
22
|
const sid = req.query.sessionId || undefined; // undefined = all sessions
|
|
@@ -81,14 +81,45 @@ ${prompt.trim()}`;
|
|
|
81
81
|
return res.json(null);
|
|
82
82
|
res.json(getSessionSummary(sid));
|
|
83
83
|
});
|
|
84
|
+
app.get('/api/dashboard', (_req, res) => {
|
|
85
|
+
res.json(getDashboard());
|
|
86
|
+
});
|
|
87
|
+
// Project registration — CLI registers its local path for a project
|
|
88
|
+
app.post('/api/projects', (req, res) => {
|
|
89
|
+
const { projectId, localPath } = req.body;
|
|
90
|
+
if (!projectId || !localPath) {
|
|
91
|
+
res.status(400).json({ error: 'projectId and localPath required' });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
updateProjectLocalPath(projectId, localPath);
|
|
95
|
+
res.json({ ok: true });
|
|
96
|
+
});
|
|
97
|
+
// Project-scoped traces
|
|
98
|
+
app.get('/api/projects/:id/traces', (req, res) => {
|
|
99
|
+
const traces = getProjectTraces(req.params.id, 200);
|
|
100
|
+
res.json(traces);
|
|
101
|
+
});
|
|
102
|
+
// Project-scoped summary
|
|
103
|
+
app.get('/api/projects/:id/summary', (req, res) => {
|
|
104
|
+
res.json(getProjectSessionSummary(req.params.id));
|
|
105
|
+
});
|
|
84
106
|
// Socket.io — push real-time updates
|
|
85
107
|
io.on('connection', (socket) => {
|
|
108
|
+
const projectId = socket.handshake.query.projectId;
|
|
86
109
|
// Send current state on connect
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
if (projectId) {
|
|
111
|
+
socket.emit('init', {
|
|
112
|
+
traces: getProjectTraces(projectId, 200),
|
|
113
|
+
summary: getProjectSessionSummary(projectId),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const sid = sessionId;
|
|
118
|
+
socket.emit('init', {
|
|
119
|
+
traces: getTraces(sid, 200),
|
|
120
|
+
summary: sid ? getSessionSummary(sid) : null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
92
123
|
const onUpdate = (entry) => socket.emit('trace:update', entry);
|
|
93
124
|
const onDone = (entry) => socket.emit('trace:done', entry);
|
|
94
125
|
traceEvents.on('trace:update', onUpdate);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-tracer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Real-time tracing and prompt-refinement companion for GitHub Copilot CLI and VS Code Copilot — tracks tokens, AI credits, tool calls, and refined prompts with console and web UI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"copilot",
|