copilot-metrics 0.1.0
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 +22 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/RELEASE.md +74 -0
- package/bin/copilot-metrics.js +16 -0
- package/package.json +48 -0
- package/scripts/manual-copilot-cli-flow.js +134 -0
- package/skills/copilot-metrics/SKILL.md +36 -0
- package/src/cli.js +288 -0
- package/src/hook-logger.js +79 -0
- package/src/ingest.js +71 -0
- package/src/jsonl.js +28 -0
- package/src/label-extractors.js +120 -0
- package/src/labels.js +56 -0
- package/src/otel.js +126 -0
- package/src/paths.js +51 -0
- package/src/pricing.js +66 -0
- package/src/reports.js +245 -0
- package/src/setup.js +196 -0
- package/src/sqlite-store.js +290 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const initSqlJs = require('sql.js');
|
|
6
|
+
|
|
7
|
+
let sqlModulePromise;
|
|
8
|
+
|
|
9
|
+
function getSqlModule() {
|
|
10
|
+
if (!sqlModulePromise) sqlModulePromise = initSqlJs();
|
|
11
|
+
return sqlModulePromise;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function openDatabase(dbPath) {
|
|
15
|
+
const SQL = await getSqlModule();
|
|
16
|
+
if (fs.existsSync(dbPath)) {
|
|
17
|
+
return new SQL.Database(fs.readFileSync(dbPath));
|
|
18
|
+
}
|
|
19
|
+
return new SQL.Database();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function persistDatabase(dbPath, db) {
|
|
23
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true, mode: 0o700 });
|
|
24
|
+
fs.writeFileSync(dbPath, Buffer.from(db.export()), { mode: 0o600 });
|
|
25
|
+
try {
|
|
26
|
+
fs.chmodSync(dbPath, 0o600);
|
|
27
|
+
} catch {
|
|
28
|
+
// Best-effort on non-POSIX filesystems.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function initStore(dbPath) {
|
|
33
|
+
const db = await openDatabase(dbPath);
|
|
34
|
+
db.run(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS raw_records (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
imported_at TEXT NOT NULL,
|
|
38
|
+
source TEXT NOT NULL,
|
|
39
|
+
line INTEGER NOT NULL,
|
|
40
|
+
payload_json TEXT NOT NULL
|
|
41
|
+
);
|
|
42
|
+
CREATE TABLE IF NOT EXISTS usage_records (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
imported_at TEXT NOT NULL,
|
|
45
|
+
source TEXT NOT NULL,
|
|
46
|
+
raw_line INTEGER NOT NULL,
|
|
47
|
+
span_id TEXT,
|
|
48
|
+
trace_id TEXT,
|
|
49
|
+
parent_span_id TEXT,
|
|
50
|
+
timestamp TEXT,
|
|
51
|
+
surface TEXT,
|
|
52
|
+
conversation_id TEXT,
|
|
53
|
+
session_id TEXT,
|
|
54
|
+
requested_model TEXT,
|
|
55
|
+
resolved_model TEXT,
|
|
56
|
+
repo TEXT,
|
|
57
|
+
branch TEXT,
|
|
58
|
+
cwd TEXT,
|
|
59
|
+
commit_sha TEXT,
|
|
60
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
62
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
|
64
|
+
reasoning_tokens INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
estimated_usd REAL,
|
|
66
|
+
estimated_ai_credits REAL,
|
|
67
|
+
estimate_label TEXT NOT NULL,
|
|
68
|
+
warnings_json TEXT NOT NULL
|
|
69
|
+
);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS hook_events (
|
|
71
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
+
imported_at TEXT NOT NULL,
|
|
73
|
+
source TEXT NOT NULL,
|
|
74
|
+
raw_line INTEGER NOT NULL,
|
|
75
|
+
event TEXT,
|
|
76
|
+
session_id TEXT,
|
|
77
|
+
cwd TEXT,
|
|
78
|
+
repo TEXT,
|
|
79
|
+
branch TEXT,
|
|
80
|
+
labels_json TEXT NOT NULL,
|
|
81
|
+
payload_json TEXT NOT NULL
|
|
82
|
+
);
|
|
83
|
+
CREATE TABLE IF NOT EXISTS label_evidence (
|
|
84
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
85
|
+
imported_at TEXT NOT NULL,
|
|
86
|
+
label TEXT NOT NULL,
|
|
87
|
+
source_type TEXT NOT NULL,
|
|
88
|
+
source_field TEXT NOT NULL,
|
|
89
|
+
source_value TEXT,
|
|
90
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
91
|
+
usage_record_id INTEGER,
|
|
92
|
+
hook_event_id INTEGER,
|
|
93
|
+
session_id TEXT,
|
|
94
|
+
repo TEXT,
|
|
95
|
+
branch TEXT,
|
|
96
|
+
cwd TEXT,
|
|
97
|
+
timestamp TEXT
|
|
98
|
+
);
|
|
99
|
+
CREATE TABLE IF NOT EXISTS import_warnings (
|
|
100
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
101
|
+
imported_at TEXT NOT NULL,
|
|
102
|
+
source TEXT NOT NULL,
|
|
103
|
+
line INTEGER,
|
|
104
|
+
code TEXT NOT NULL,
|
|
105
|
+
message TEXT NOT NULL
|
|
106
|
+
);
|
|
107
|
+
`);
|
|
108
|
+
persistDatabase(dbPath, db);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runPrepared(db, sql, rows) {
|
|
112
|
+
const statement = db.prepare(sql);
|
|
113
|
+
try {
|
|
114
|
+
for (const row of rows) statement.run(row);
|
|
115
|
+
} finally {
|
|
116
|
+
statement.free();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function lastInsertId(db) {
|
|
121
|
+
const result = db.exec('SELECT last_insert_rowid() AS id');
|
|
122
|
+
return result[0].values[0][0];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function insertLabelEvidence(db, importedAt, evidenceRows) {
|
|
126
|
+
runPrepared(
|
|
127
|
+
db,
|
|
128
|
+
`INSERT INTO label_evidence (
|
|
129
|
+
imported_at, label, source_type, source_field, source_value, confidence,
|
|
130
|
+
usage_record_id, hook_event_id, session_id, repo, branch, cwd, timestamp
|
|
131
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
132
|
+
evidenceRows.map((evidence) => [
|
|
133
|
+
importedAt,
|
|
134
|
+
evidence.label,
|
|
135
|
+
evidence.source_type,
|
|
136
|
+
evidence.source_field,
|
|
137
|
+
evidence.source_value || null,
|
|
138
|
+
evidence.confidence || 0,
|
|
139
|
+
evidence.usage_record_id || null,
|
|
140
|
+
evidence.hook_event_id || null,
|
|
141
|
+
evidence.session_id || null,
|
|
142
|
+
evidence.repo || null,
|
|
143
|
+
evidence.branch || null,
|
|
144
|
+
evidence.cwd || null,
|
|
145
|
+
evidence.timestamp || null,
|
|
146
|
+
]),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function insertImport(dbPath, source, rawRecords, usageRecords, hookEvents, warnings) {
|
|
151
|
+
await initStore(dbPath);
|
|
152
|
+
const db = await openDatabase(dbPath);
|
|
153
|
+
const importedAt = new Date().toISOString();
|
|
154
|
+
|
|
155
|
+
db.run('BEGIN');
|
|
156
|
+
try {
|
|
157
|
+
runPrepared(
|
|
158
|
+
db,
|
|
159
|
+
'INSERT INTO raw_records (imported_at, source, line, payload_json) VALUES (?, ?, ?, ?)',
|
|
160
|
+
rawRecords.map((record) => [importedAt, source, record.line, JSON.stringify(record.value)]),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const labelEvidence = [];
|
|
164
|
+
const usageStatement = db.prepare(`INSERT INTO usage_records (
|
|
165
|
+
imported_at, source, raw_line, span_id, trace_id, parent_span_id, timestamp, surface,
|
|
166
|
+
conversation_id, session_id, requested_model, resolved_model, repo, branch, cwd, commit_sha,
|
|
167
|
+
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, reasoning_tokens,
|
|
168
|
+
estimated_usd, estimated_ai_credits, estimate_label, warnings_json
|
|
169
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
170
|
+
try {
|
|
171
|
+
for (const usage of usageRecords) {
|
|
172
|
+
usageStatement.run([
|
|
173
|
+
importedAt,
|
|
174
|
+
source,
|
|
175
|
+
usage.raw_line,
|
|
176
|
+
usage.span_id,
|
|
177
|
+
usage.trace_id,
|
|
178
|
+
usage.parent_span_id,
|
|
179
|
+
usage.timestamp,
|
|
180
|
+
usage.surface,
|
|
181
|
+
usage.conversation_id,
|
|
182
|
+
usage.session_id,
|
|
183
|
+
usage.requested_model,
|
|
184
|
+
usage.resolved_model,
|
|
185
|
+
usage.repo,
|
|
186
|
+
usage.branch,
|
|
187
|
+
usage.cwd,
|
|
188
|
+
usage.commit_sha,
|
|
189
|
+
usage.input_tokens,
|
|
190
|
+
usage.output_tokens,
|
|
191
|
+
usage.cache_read_tokens,
|
|
192
|
+
usage.cache_creation_tokens,
|
|
193
|
+
usage.reasoning_tokens,
|
|
194
|
+
usage.estimated_usd,
|
|
195
|
+
usage.estimated_ai_credits,
|
|
196
|
+
usage.estimate_label,
|
|
197
|
+
JSON.stringify(usage.warnings || []),
|
|
198
|
+
]);
|
|
199
|
+
const usageRecordId = lastInsertId(db);
|
|
200
|
+
for (const evidence of usage.label_evidence || []) {
|
|
201
|
+
labelEvidence.push({
|
|
202
|
+
...evidence,
|
|
203
|
+
usage_record_id: usageRecordId,
|
|
204
|
+
session_id: usage.session_id,
|
|
205
|
+
repo: usage.repo,
|
|
206
|
+
branch: usage.branch,
|
|
207
|
+
cwd: usage.cwd,
|
|
208
|
+
timestamp: usage.timestamp,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
usageStatement.free();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const hookStatement = db.prepare('INSERT INTO hook_events (imported_at, source, raw_line, event, session_id, cwd, repo, branch, labels_json, payload_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
217
|
+
try {
|
|
218
|
+
for (const event of hookEvents) {
|
|
219
|
+
hookStatement.run([
|
|
220
|
+
importedAt,
|
|
221
|
+
source,
|
|
222
|
+
event.raw_line,
|
|
223
|
+
event.event,
|
|
224
|
+
event.session_id,
|
|
225
|
+
event.cwd,
|
|
226
|
+
event.repo,
|
|
227
|
+
event.branch,
|
|
228
|
+
JSON.stringify(event.labels || []),
|
|
229
|
+
JSON.stringify(event.payload),
|
|
230
|
+
]);
|
|
231
|
+
const hookEventId = lastInsertId(db);
|
|
232
|
+
for (const evidence of event.label_evidence || []) {
|
|
233
|
+
labelEvidence.push({
|
|
234
|
+
...evidence,
|
|
235
|
+
hook_event_id: hookEventId,
|
|
236
|
+
session_id: event.session_id,
|
|
237
|
+
repo: event.repo,
|
|
238
|
+
branch: event.branch,
|
|
239
|
+
cwd: event.cwd,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} finally {
|
|
244
|
+
hookStatement.free();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
insertLabelEvidence(db, importedAt, labelEvidence);
|
|
248
|
+
|
|
249
|
+
runPrepared(
|
|
250
|
+
db,
|
|
251
|
+
'INSERT INTO import_warnings (imported_at, source, line, code, message) VALUES (?, ?, ?, ?, ?)',
|
|
252
|
+
warnings.map((warning) => [importedAt, source, warning.line || null, warning.code, warning.message]),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
db.run('COMMIT');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
db.run('ROLLBACK');
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
persistDatabase(dbPath, db);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function queryOne(dbPath, sql) {
|
|
265
|
+
const db = await openDatabase(dbPath);
|
|
266
|
+
const result = db.exec(sql);
|
|
267
|
+
if (!result.length) return [];
|
|
268
|
+
const [{ columns, values }] = result;
|
|
269
|
+
return values.map((row) => Object.fromEntries(row.map((value, index) => [columns[index], value])));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function queryRows(dbPath, sql, params = []) {
|
|
273
|
+
const db = await openDatabase(dbPath);
|
|
274
|
+
const statement = db.prepare(sql);
|
|
275
|
+
const rows = [];
|
|
276
|
+
try {
|
|
277
|
+
statement.bind(params);
|
|
278
|
+
while (statement.step()) rows.push(statement.getAsObject());
|
|
279
|
+
} finally {
|
|
280
|
+
statement.free();
|
|
281
|
+
}
|
|
282
|
+
return rows;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
initStore,
|
|
287
|
+
insertImport,
|
|
288
|
+
queryOne,
|
|
289
|
+
queryRows,
|
|
290
|
+
};
|