copilot-tracer 1.0.5 → 1.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.
- package/README.md +29 -8
- package/dist/otlpReceiver.js +192 -4
- package/dist/setup.js +47 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# copilot-tracer
|
|
2
2
|
|
|
3
|
-
Real-time tracing and prompt-refinement companion for **GitHub Copilot CLI** and **VS Code Copilot extension**.
|
|
3
|
+
Real-time tracing and prompt-refinement companion for **GitHub Copilot CLI**, **Claude Code**, and **VS Code Copilot extension**.
|
|
4
4
|
|
|
5
5
|
Captures every prompt, response, token usage, AI credits, tool calls, and duration — all in one place. Runs as a background daemon that collects data from all your projects automatically. Includes a web dashboard with project overview and per-project live tracing.
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ Captures every prompt, response, token usage, AI credits, tool calls, and durati
|
|
|
11
11
|
- **Daemon mode** — install once, run forever. Collects traces from all projects automatically
|
|
12
12
|
- **Auto project detection** — detects project from `github.copilot.git.repository` in OTLP spans
|
|
13
13
|
- **Zero-intrusion capture** — uses Copilot's built-in OTel support. Set env vars, done.
|
|
14
|
-
- **Works everywhere** — captures
|
|
14
|
+
- **Works everywhere** — captures GitHub Copilot CLI, Claude Code, and VS Code Copilot Chat
|
|
15
15
|
- **Dashboard** — overview of all projects with token usage, credits, and session counts
|
|
16
16
|
- **Live tracer** — real-time trace table per project with detail panel
|
|
17
17
|
- **Prompt refinement** — rewrites prompts with stronger instructions and less noise
|
|
@@ -31,7 +31,8 @@ This will:
|
|
|
31
31
|
1. Detect your Copilot CLI and VS Code installation
|
|
32
32
|
2. Patch `~/.zshrc` with OTEL env vars
|
|
33
33
|
3. Patch VS Code `settings.json` with terminal env vars
|
|
34
|
-
4.
|
|
34
|
+
4. Enable Claude Code OTLP logs/events and enhanced beta traces
|
|
35
|
+
5. Start the daemon on port 4747
|
|
35
36
|
|
|
36
37
|
Then apply env vars in your current shell:
|
|
37
38
|
|
|
@@ -39,7 +40,9 @@ Then apply env vars in your current shell:
|
|
|
39
40
|
source ~/.zshrc
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
Restart VS Code once. After that, the daemon collects traces from all your Copilot
|
|
43
|
+
Restart VS Code once. After that, the daemon collects traces from all your Copilot
|
|
44
|
+
and Claude Code sessions automatically. Claude Code content flags are enabled by
|
|
45
|
+
setup so prompts and responses can be displayed in the local dashboard.
|
|
43
46
|
|
|
44
47
|
Open **http://localhost:4747** to see the dashboard.
|
|
45
48
|
|
|
@@ -51,20 +54,20 @@ Open **http://localhost:4747** to see the dashboard.
|
|
|
51
54
|
┌─────────────────────────────────────────────────────────┐
|
|
52
55
|
│ copilot-tracer --daemon (runs once, stays running) │
|
|
53
56
|
│ │
|
|
54
|
-
│ OTLP Receiver ← Copilot CLI + VS Code │
|
|
57
|
+
│ OTLP Receiver ← Copilot CLI + Claude Code + VS Code │
|
|
55
58
|
│ (auto-detects project from github.copilot.git.repository)│
|
|
56
59
|
│ │
|
|
57
60
|
│ SQLite DB → Dashboard + Live Tracer (Socket.io) │
|
|
58
61
|
└─────────────────────────────────────────────────────────┘
|
|
59
62
|
|
|
60
|
-
Copilot CLI / VS Code Copilot Chat
|
|
63
|
+
Copilot CLI / Claude Code / VS Code Copilot Chat
|
|
61
64
|
│
|
|
62
65
|
│ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4747
|
|
63
66
|
↓
|
|
64
|
-
POST /v1/traces (OpenTelemetry OTLP JSON)
|
|
67
|
+
POST /v1/traces and /v1/logs (OpenTelemetry OTLP JSON)
|
|
65
68
|
│
|
|
66
69
|
↓
|
|
67
|
-
copilot-tracer parses spans → tokens, credits, tool calls
|
|
70
|
+
copilot-tracer parses spans and events → tokens, credits, tool calls
|
|
68
71
|
│
|
|
69
72
|
↓
|
|
70
73
|
SQLite DB (~/.copilot-tracer/traces.db)
|
|
@@ -121,6 +124,7 @@ Open http://localhost:4747 after starting the daemon.
|
|
|
121
124
|
- **Summary cards** — total projects, sessions, tokens, credits
|
|
122
125
|
- **Project cards** — each project shows path, session count, tokens, credits, last active
|
|
123
126
|
- **Click a project** → opens live tracer filtered to that project
|
|
127
|
+
<img width="737" height="410" alt="image" src="https://github.com/user-attachments/assets/dc20653b-8774-46b3-9a42-6e7bb934aded" />
|
|
124
128
|
|
|
125
129
|
---
|
|
126
130
|
|
|
@@ -138,6 +142,10 @@ Real-time trace table for a specific project.
|
|
|
138
142
|
- Click Skills / Agents / MCPs → filtered call list
|
|
139
143
|
- Real-time updates via Socket.io
|
|
140
144
|
|
|
145
|
+
<img width="1504" height="742" alt="image" src="https://github.com/user-attachments/assets/f334108f-c587-4228-8120-7dac2f85f90b" />
|
|
146
|
+
|
|
147
|
+
<img width="1061" height="696" alt="image" src="https://github.com/user-attachments/assets/1555d5a2-dbca-4f1d-b102-3d2865dcee68" />
|
|
148
|
+
|
|
141
149
|
---
|
|
142
150
|
|
|
143
151
|
## Prompt Refinement
|
|
@@ -173,8 +181,21 @@ Add to `~/.zshrc`:
|
|
|
173
181
|
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4747
|
|
174
182
|
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
|
|
175
183
|
export COPILOT_OTEL_ENABLED=true
|
|
184
|
+
|
|
185
|
+
# Claude Code telemetry
|
|
186
|
+
export CLAUDE_CODE_ENABLE_TELEMETRY=1
|
|
187
|
+
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
|
|
188
|
+
export OTEL_LOGS_EXPORTER=otlp
|
|
189
|
+
export OTEL_TRACES_EXPORTER=otlp
|
|
190
|
+
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
|
191
|
+
export OTEL_LOG_USER_PROMPTS=1
|
|
192
|
+
export OTEL_LOG_ASSISTANT_RESPONSES=1
|
|
176
193
|
```
|
|
177
194
|
|
|
195
|
+
Claude Code exports standard OTLP logs/events and optional beta traces. See
|
|
196
|
+
[Anthropic's monitoring documentation](https://code.claude.com/docs/en/monitoring-usage)
|
|
197
|
+
for protocol and content controls.
|
|
198
|
+
|
|
178
199
|
For VS Code, add to `~/Library/Application Support/Code/User/settings.json`:
|
|
179
200
|
|
|
180
201
|
```json
|
package/dist/otlpReceiver.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* chat <model> span attrs: same as above
|
|
15
15
|
* User message event: github.copilot.user.message
|
|
16
16
|
*/
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
17
18
|
import { upsertTrace, createSession, deleteTrace, ensureProject, ensureProjectByRepo } from './db.js';
|
|
18
19
|
import { traceEvents } from './proxy.js';
|
|
19
20
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -36,6 +37,27 @@ function nanoToMs(nano) {
|
|
|
36
37
|
function nanoToIso(nano) {
|
|
37
38
|
return new Date(nanoToMs(nano)).toISOString();
|
|
38
39
|
}
|
|
40
|
+
function getStringAttr(attrs, ...keys) {
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
const value = getAttr(attrs, key);
|
|
43
|
+
if (value !== undefined && String(value).trim())
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function getBodyText(body) {
|
|
49
|
+
if (!body)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (body.stringValue !== undefined)
|
|
52
|
+
return body.stringValue;
|
|
53
|
+
if (body.intValue !== undefined)
|
|
54
|
+
return String(body.intValue);
|
|
55
|
+
if (body.doubleValue !== undefined)
|
|
56
|
+
return String(body.doubleValue);
|
|
57
|
+
if (body.boolValue !== undefined)
|
|
58
|
+
return String(body.boolValue);
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
39
61
|
const inFlight = new Map(); // traceId → InFlight
|
|
40
62
|
// Sessions are created lazily. Always upsert so project_id gets backfilled
|
|
41
63
|
// when the session was created earlier without a project.
|
|
@@ -53,7 +75,7 @@ function resolveProjectId(repoUrl, workingDir, defaultProjectId) {
|
|
|
53
75
|
return defaultProjectId;
|
|
54
76
|
}
|
|
55
77
|
function detectWorkingDir(attrs) {
|
|
56
|
-
for (const key of ['process.working_directory', 'github.copilot.working_dir']) {
|
|
78
|
+
for (const key of ['process.working_directory', 'github.copilot.working_dir', 'claude_code.working_dir']) {
|
|
57
79
|
const v = getAttr(attrs, key);
|
|
58
80
|
if (v && String(v).trim())
|
|
59
81
|
return String(v).trim();
|
|
@@ -65,6 +87,116 @@ function detectWorkingDir(attrs) {
|
|
|
65
87
|
const pendingChatIds = new Map(); // `chat:${traceId}` → list of entryIds
|
|
66
88
|
// Buffer tool calls that arrive before invoke_agent
|
|
67
89
|
const pendingToolCalls = new Map(); // traceId → tool calls
|
|
90
|
+
const claudePromptEntries = new Map(); // prompt.id → trace entry
|
|
91
|
+
const claudeInteractionEntries = new Map(); // traceId → interaction entry
|
|
92
|
+
const CLAUDE_STATE_LIMIT = 1000;
|
|
93
|
+
function rememberClaudeEntry(map, key, entry) {
|
|
94
|
+
map.set(key, entry);
|
|
95
|
+
if (map.size > CLAUDE_STATE_LIMIT) {
|
|
96
|
+
const oldest = map.keys().next().value;
|
|
97
|
+
if (oldest)
|
|
98
|
+
map.delete(oldest);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function claudeEventId(record, attrs, eventName) {
|
|
102
|
+
const messageId = getStringAttr(attrs, 'message.uuid', 'tool_use_id');
|
|
103
|
+
const promptId = getStringAttr(attrs, 'prompt.id');
|
|
104
|
+
return `claude:${messageId ?? promptId ?? record.traceId ?? randomUUID()}:${eventName}`;
|
|
105
|
+
}
|
|
106
|
+
function processClaudeLogRecord(record, resourceAttrs, sessionId, projectId, workingDir) {
|
|
107
|
+
const attrs = record.attributes ?? [];
|
|
108
|
+
const eventName = getStringAttr(attrs, 'event.name') ?? getBodyText(record.body);
|
|
109
|
+
if (!eventName || !eventName.startsWith('claude_code.'))
|
|
110
|
+
return;
|
|
111
|
+
const eventTime = getStringAttr(attrs, 'event.timestamp');
|
|
112
|
+
const dateTime = eventTime ?? (record.timeUnixNano ? nanoToIso(record.timeUnixNano) : new Date().toISOString());
|
|
113
|
+
const promptId = getStringAttr(attrs, 'prompt.id');
|
|
114
|
+
const resolvedProjectId = resolveProjectId(getStringAttr(resourceAttrs, 'github.copilot.git.repository', 'vcs.repository.url'), getStringAttr(attrs, 'process.working_directory', 'github.copilot.working_dir', 'claude_code.working_dir') ?? workingDir, projectId);
|
|
115
|
+
const eventSessionId = getStringAttr(attrs, 'session.id') ?? sessionId;
|
|
116
|
+
if (eventName === 'claude_code.user_prompt') {
|
|
117
|
+
const entry = {
|
|
118
|
+
id: claudeEventId(record, attrs, 'prompt'),
|
|
119
|
+
sessionId: eventSessionId,
|
|
120
|
+
dateTime,
|
|
121
|
+
prompt: getStringAttr(attrs, 'prompt') ?? '[Claude Code prompt]',
|
|
122
|
+
tokens: { input: 0, output: 0, cached: 0, reasoning: 0, written: 0, total: 0 },
|
|
123
|
+
aiCredits: 0,
|
|
124
|
+
durationMs: 0,
|
|
125
|
+
toolCalls: [],
|
|
126
|
+
skillCount: 0,
|
|
127
|
+
agentCount: 0,
|
|
128
|
+
mcpCount: 0,
|
|
129
|
+
status: 'running',
|
|
130
|
+
};
|
|
131
|
+
if (promptId)
|
|
132
|
+
rememberClaudeEntry(claudePromptEntries, promptId, entry);
|
|
133
|
+
ensureSession(eventSessionId, resolvedProjectId);
|
|
134
|
+
upsertTrace(entry);
|
|
135
|
+
traceEvents.emit('trace:update', entry);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (eventName === 'claude_code.assistant_response') {
|
|
139
|
+
const existingEntry = promptId ? claudePromptEntries.get(promptId) : undefined;
|
|
140
|
+
const entry = existingEntry ?? {
|
|
141
|
+
id: claudeEventId(record, attrs, 'response'),
|
|
142
|
+
sessionId: eventSessionId,
|
|
143
|
+
dateTime,
|
|
144
|
+
prompt: '[Claude Code response]',
|
|
145
|
+
tokens: { input: 0, output: 0, cached: 0, reasoning: 0, written: 0, total: 0 },
|
|
146
|
+
aiCredits: 0,
|
|
147
|
+
durationMs: 0,
|
|
148
|
+
toolCalls: [],
|
|
149
|
+
skillCount: 0,
|
|
150
|
+
agentCount: 0,
|
|
151
|
+
mcpCount: 0,
|
|
152
|
+
status: 'running',
|
|
153
|
+
};
|
|
154
|
+
entry.response = getStringAttr(attrs, 'response');
|
|
155
|
+
entry.status = 'done';
|
|
156
|
+
entry.durationMs = Number(getStringAttr(attrs, 'duration_ms') ?? 0);
|
|
157
|
+
ensureSession(eventSessionId, resolvedProjectId);
|
|
158
|
+
upsertTrace(entry);
|
|
159
|
+
traceEvents.emit('trace:update', entry);
|
|
160
|
+
traceEvents.emit('trace:done', entry);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (eventName === 'claude_code.tool_result') {
|
|
164
|
+
const entry = promptId ? claudePromptEntries.get(promptId) : undefined;
|
|
165
|
+
if (!entry)
|
|
166
|
+
return;
|
|
167
|
+
const toolName = getStringAttr(attrs, 'tool_name') ?? 'Claude Code tool';
|
|
168
|
+
const toolId = getStringAttr(attrs, 'tool_use_id') ?? randomUUID();
|
|
169
|
+
if (entry.toolCalls.some(call => call.id === toolId))
|
|
170
|
+
return;
|
|
171
|
+
entry.toolCalls.push({
|
|
172
|
+
id: toolId,
|
|
173
|
+
name: toolName,
|
|
174
|
+
type: detectToolType(toolName),
|
|
175
|
+
input: {},
|
|
176
|
+
startedAt: Date.parse(dateTime),
|
|
177
|
+
endedAt: Date.parse(dateTime),
|
|
178
|
+
durationMs: Number(getStringAttr(attrs, 'duration_ms') ?? 0),
|
|
179
|
+
error: getStringAttr(attrs, 'error'),
|
|
180
|
+
});
|
|
181
|
+
upsertTrace(entry);
|
|
182
|
+
traceEvents.emit('trace:update', entry);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function processClaudeLogs(payload, defaultSessionId, projectId) {
|
|
186
|
+
if (!payload || !Array.isArray(payload.resourceLogs)) {
|
|
187
|
+
throw new Error('OTLP logs payload must contain resourceLogs');
|
|
188
|
+
}
|
|
189
|
+
for (const resourceLogs of payload.resourceLogs ?? []) {
|
|
190
|
+
const resourceAttrs = resourceLogs.resource?.attributes ?? [];
|
|
191
|
+
const sessionId = getStringAttr(resourceAttrs, 'session.id') ?? defaultSessionId;
|
|
192
|
+
const workingDir = detectWorkingDir(resourceAttrs);
|
|
193
|
+
for (const scopeLogs of resourceLogs.scopeLogs ?? []) {
|
|
194
|
+
for (const record of scopeLogs.logRecords ?? []) {
|
|
195
|
+
processClaudeLogRecord(record, resourceAttrs, sessionId, projectId, workingDir);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
68
200
|
function processSpans(spans, sessionId, projectId, workingDir) {
|
|
69
201
|
for (const span of spans) {
|
|
70
202
|
// DEBUG — log raw span to stderr when COPILOT_TRACER_DEBUG=1
|
|
@@ -75,6 +207,53 @@ function processSpans(spans, sessionId, projectId, workingDir) {
|
|
|
75
207
|
const spanName = span.name;
|
|
76
208
|
const traceId = span.traceId;
|
|
77
209
|
const spanId = span.spanId;
|
|
210
|
+
if (spanName === 'claude_code.interaction') {
|
|
211
|
+
const attrs = span.attributes ?? [];
|
|
212
|
+
const inputTokens = Number(getAttr(attrs, 'input_tokens') ?? 0);
|
|
213
|
+
const outputTokens = Number(getAttr(attrs, 'output_tokens') ?? 0);
|
|
214
|
+
const entry = {
|
|
215
|
+
id: spanId,
|
|
216
|
+
sessionId: getStringAttr(attrs, 'session.id') ?? sessionId,
|
|
217
|
+
dateTime: nanoToIso(span.startTimeUnixNano),
|
|
218
|
+
prompt: getStringAttr(attrs, 'user_prompt') ?? '[Claude Code interaction]',
|
|
219
|
+
tokens: { input: inputTokens, output: outputTokens, cached: Number(getAttr(attrs, 'cache_read_tokens') ?? 0), reasoning: 0, written: outputTokens, total: inputTokens + outputTokens },
|
|
220
|
+
aiCredits: Number(getAttr(attrs, 'cost_usd') ?? 0),
|
|
221
|
+
durationMs: Number(getAttr(attrs, 'interaction.duration_ms')
|
|
222
|
+
?? (nanoToMs(span.endTimeUnixNano) - nanoToMs(span.startTimeUnixNano))),
|
|
223
|
+
toolCalls: [],
|
|
224
|
+
skillCount: 0,
|
|
225
|
+
agentCount: 0,
|
|
226
|
+
mcpCount: 0,
|
|
227
|
+
status: (span.status?.code ?? 0) === 2 ? 'error' : 'done',
|
|
228
|
+
error: span.status?.message,
|
|
229
|
+
};
|
|
230
|
+
ensureSession(entry.sessionId, resolveProjectId(getAttr(attrs, 'vcs.repository.url'), detectWorkingDir(attrs) ?? workingDir, projectId));
|
|
231
|
+
upsertTrace(entry);
|
|
232
|
+
rememberClaudeEntry(claudeInteractionEntries, traceId, entry);
|
|
233
|
+
traceEvents.emit('trace:update', entry);
|
|
234
|
+
traceEvents.emit('trace:done', entry);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (spanName === 'claude_code.llm_request') {
|
|
238
|
+
const inputTokens = Number(getAttr(attrs, 'input_tokens') ?? 0);
|
|
239
|
+
const outputTokens = Number(getAttr(attrs, 'output_tokens') ?? 0);
|
|
240
|
+
const cachedTokens = Number(getAttr(attrs, 'cache_read_tokens') ?? 0);
|
|
241
|
+
const entry = claudeInteractionEntries.get(traceId);
|
|
242
|
+
if (entry) {
|
|
243
|
+
entry.tokens = {
|
|
244
|
+
input: entry.tokens.input + inputTokens,
|
|
245
|
+
output: entry.tokens.output + outputTokens,
|
|
246
|
+
cached: entry.tokens.cached + cachedTokens,
|
|
247
|
+
reasoning: 0,
|
|
248
|
+
written: entry.tokens.written + outputTokens,
|
|
249
|
+
total: entry.tokens.total + inputTokens + outputTokens,
|
|
250
|
+
};
|
|
251
|
+
entry.aiCredits += Number(getAttr(attrs, 'cost_usd') ?? 0);
|
|
252
|
+
upsertTrace(entry);
|
|
253
|
+
traceEvents.emit('trace:update', entry);
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
78
257
|
// ── invoke_agent span = top-level agent turn ──────────────────────────
|
|
79
258
|
if (spanName === 'invoke_agent') {
|
|
80
259
|
const startMs = nanoToMs(span.startTimeUnixNano);
|
|
@@ -426,8 +605,17 @@ export function registerOtlpRoutes(app, defaultSessionId, projectId) {
|
|
|
426
605
|
res.status(400).json({ error: 'invalid payload' });
|
|
427
606
|
}
|
|
428
607
|
});
|
|
429
|
-
|
|
608
|
+
app.post('/v1/logs', (req, res) => {
|
|
609
|
+
try {
|
|
610
|
+
processClaudeLogs(req.body, defaultSessionId, projectId);
|
|
611
|
+
res.status(200).json({ partialSuccess: {} });
|
|
612
|
+
}
|
|
613
|
+
catch (e) {
|
|
614
|
+
console.error('[OTLP] log parse error:', e);
|
|
615
|
+
res.status(400).json({ error: 'invalid payload' });
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
// Metrics remain accepted for Claude Code dashboards but are not trace entries.
|
|
430
619
|
app.post('/v1/metrics', (_req, res) => res.status(200).json({ partialSuccess: {} }));
|
|
431
|
-
|
|
432
|
-
console.log(' 📡 OTLP receiver ready on /v1/traces');
|
|
620
|
+
console.log(' 📡 OTLP receiver ready on /v1/traces and /v1/logs');
|
|
433
621
|
}
|
package/dist/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* copilot-tracer setup — auto-detect
|
|
2
|
+
* copilot-tracer setup — auto-detect Copilot/VS Code and inject OTLP env config
|
|
3
3
|
*
|
|
4
4
|
* What it does:
|
|
5
5
|
* 1. Detect copilot CLI (which copilot)
|
|
@@ -16,12 +16,26 @@ import os from 'os';
|
|
|
16
16
|
const OTEL_ENDPOINT_KEY = 'OTEL_EXPORTER_OTLP_ENDPOINT';
|
|
17
17
|
const OTEL_CONTENT_KEY = 'OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT';
|
|
18
18
|
const OTEL_ENABLED_KEY = 'COPILOT_OTEL_ENABLED';
|
|
19
|
+
const CLAUDE_TELEMETRY_KEY = 'CLAUDE_CODE_ENABLE_TELEMETRY';
|
|
20
|
+
const CLAUDE_TRACES_KEY = 'CLAUDE_CODE_ENHANCED_TELEMETRY_BETA';
|
|
21
|
+
const OTEL_LOGS_EXPORTER_KEY = 'OTEL_LOGS_EXPORTER';
|
|
22
|
+
const OTEL_TRACES_EXPORTER_KEY = 'OTEL_TRACES_EXPORTER';
|
|
23
|
+
const OTEL_PROTOCOL_KEY = 'OTEL_EXPORTER_OTLP_PROTOCOL';
|
|
24
|
+
const OTEL_LOG_PROMPTS_KEY = 'OTEL_LOG_USER_PROMPTS';
|
|
25
|
+
const OTEL_LOG_RESPONSES_KEY = 'OTEL_LOG_ASSISTANT_RESPONSES';
|
|
19
26
|
function otelEnvBlock(port) {
|
|
20
27
|
return [
|
|
21
28
|
`# >>> copilot-tracer OTLP config (auto-added) >>>`,
|
|
22
29
|
`export ${OTEL_ENDPOINT_KEY}=http://localhost:${port}`,
|
|
23
30
|
`export ${OTEL_CONTENT_KEY}=true`,
|
|
24
31
|
`export ${OTEL_ENABLED_KEY}=true`,
|
|
32
|
+
`export ${CLAUDE_TELEMETRY_KEY}=1`,
|
|
33
|
+
`export ${CLAUDE_TRACES_KEY}=1`,
|
|
34
|
+
`export ${OTEL_LOGS_EXPORTER_KEY}=otlp`,
|
|
35
|
+
`export ${OTEL_TRACES_EXPORTER_KEY}=otlp`,
|
|
36
|
+
`export ${OTEL_PROTOCOL_KEY}=http/json`,
|
|
37
|
+
`export ${OTEL_LOG_PROMPTS_KEY}=1`,
|
|
38
|
+
`export ${OTEL_LOG_RESPONSES_KEY}=1`,
|
|
25
39
|
``,
|
|
26
40
|
`# Tag every copilot prompt with the terminal folder it ran from, so the`,
|
|
27
41
|
`# tracer can attribute it to the right project. OTLP carries no working-dir,`,
|
|
@@ -42,6 +56,22 @@ function otelEnvBlock(port) {
|
|
|
42
56
|
` export OTEL_RESOURCE_ATTRIBUTES`,
|
|
43
57
|
` command copilot "\$@"`,
|
|
44
58
|
`}`,
|
|
59
|
+
``,
|
|
60
|
+
`claude() {`,
|
|
61
|
+
` local _wd`,
|
|
62
|
+
` if command -v python3 >/dev/null 2>&1; then`,
|
|
63
|
+
` _wd="$(pwd | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read().strip(), safe="/"))')"`,
|
|
64
|
+
` else`,
|
|
65
|
+
` _wd="$(pwd)"`,
|
|
66
|
+
` fi`,
|
|
67
|
+
` if [ -n "\${OTEL_RESOURCE_ATTRIBUTES:-}" ]; then`,
|
|
68
|
+
` OTEL_RESOURCE_ATTRIBUTES="\${OTEL_RESOURCE_ATTRIBUTES},claude_code.working_dir=\${_wd}"`,
|
|
69
|
+
` else`,
|
|
70
|
+
` OTEL_RESOURCE_ATTRIBUTES="claude_code.working_dir=\${_wd}"`,
|
|
71
|
+
` fi`,
|
|
72
|
+
` export OTEL_RESOURCE_ATTRIBUTES`,
|
|
73
|
+
` command claude "\$@"`,
|
|
74
|
+
`}`,
|
|
45
75
|
`# <<< copilot-tracer <<<`,
|
|
46
76
|
].join('\n');
|
|
47
77
|
}
|
|
@@ -50,6 +80,13 @@ function vscodeEnvBlock(port) {
|
|
|
50
80
|
[OTEL_ENDPOINT_KEY]: `http://localhost:${port}`,
|
|
51
81
|
[OTEL_CONTENT_KEY]: 'true',
|
|
52
82
|
[OTEL_ENABLED_KEY]: 'true',
|
|
83
|
+
[CLAUDE_TELEMETRY_KEY]: '1',
|
|
84
|
+
[CLAUDE_TRACES_KEY]: '1',
|
|
85
|
+
[OTEL_LOGS_EXPORTER_KEY]: 'otlp',
|
|
86
|
+
[OTEL_TRACES_EXPORTER_KEY]: 'otlp',
|
|
87
|
+
[OTEL_PROTOCOL_KEY]: 'http/json',
|
|
88
|
+
[OTEL_LOG_PROMPTS_KEY]: '1',
|
|
89
|
+
[OTEL_LOG_RESPONSES_KEY]: '1',
|
|
53
90
|
};
|
|
54
91
|
}
|
|
55
92
|
// ── Detection helpers ─────────────────────────────────────────────────────────
|
|
@@ -193,13 +230,13 @@ export function runSetup(port, silent = false) {
|
|
|
193
230
|
const label = path.basename(profilePath);
|
|
194
231
|
if (result.action === 'added') {
|
|
195
232
|
console.log(`\n${CHECK} Shell profile patched: ${label}`);
|
|
196
|
-
console.log(` ${ARROW} Added
|
|
233
|
+
console.log(` ${ARROW} Added Copilot + Claude Code OTLP env vars`);
|
|
197
234
|
console.log(` ${ARROW} Added copilot() wrapper — tags each prompt with the terminal folder`);
|
|
198
235
|
console.log(` ${ARROW} Run: source ${profilePath}`);
|
|
199
236
|
}
|
|
200
237
|
else if (result.action === 'updated') {
|
|
201
238
|
console.log(`\n${CHECK} Shell profile updated: ${label}`);
|
|
202
|
-
console.log(` ${ARROW} Updated port to ${port} +
|
|
239
|
+
console.log(` ${ARROW} Updated port to ${port} + Claude Code/Copilot OTLP config`);
|
|
203
240
|
console.log(` ${ARROW} Run: source ${profilePath}`);
|
|
204
241
|
}
|
|
205
242
|
else {
|
|
@@ -231,6 +268,13 @@ export function runSetup(port, silent = false) {
|
|
|
231
268
|
process.env[OTEL_ENDPOINT_KEY] = `http://localhost:${port}`;
|
|
232
269
|
process.env[OTEL_CONTENT_KEY] = 'true';
|
|
233
270
|
process.env[OTEL_ENABLED_KEY] = 'true';
|
|
271
|
+
process.env[CLAUDE_TELEMETRY_KEY] = '1';
|
|
272
|
+
process.env[CLAUDE_TRACES_KEY] = '1';
|
|
273
|
+
process.env[OTEL_LOGS_EXPORTER_KEY] = 'otlp';
|
|
274
|
+
process.env[OTEL_TRACES_EXPORTER_KEY] = 'otlp';
|
|
275
|
+
process.env[OTEL_PROTOCOL_KEY] = 'http/json';
|
|
276
|
+
process.env[OTEL_LOG_PROMPTS_KEY] = '1';
|
|
277
|
+
process.env[OTEL_LOG_RESPONSES_KEY] = '1';
|
|
234
278
|
// 6. Summary
|
|
235
279
|
if (!silent) {
|
|
236
280
|
const profileBase = profilePath ? path.basename(profilePath) : '.zshrc';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-tracer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/
|
|
17
|
+
"url": "https://github.com/chuongxl/copilot-tracer.git"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=18"
|