@revenium/claude-code-metering 0.1.0 → 0.1.2
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 -28
- package/README.md +323 -139
- package/dist/cli/commands/backfill.d.ts +106 -1
- package/dist/cli/commands/backfill.d.ts.map +1 -1
- package/dist/cli/commands/backfill.js +359 -146
- package/dist/cli/commands/backfill.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +2 -0
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +55 -49
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +23 -30
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/test.d.ts.map +1 -1
- package/dist/cli/commands/test.js +4 -3
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +44 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/core/api/client.d.ts +17 -8
- package/dist/core/api/client.d.ts.map +1 -1
- package/dist/core/api/client.js +58 -49
- package/dist/core/api/client.js.map +1 -1
- package/dist/core/config/loader.d.ts +5 -13
- package/dist/core/config/loader.d.ts.map +1 -1
- package/dist/core/config/loader.js +70 -46
- package/dist/core/config/loader.js.map +1 -1
- package/dist/core/config/validator.d.ts +5 -1
- package/dist/core/config/validator.d.ts.map +1 -1
- package/dist/core/config/validator.js +37 -22
- package/dist/core/config/validator.js.map +1 -1
- package/dist/core/config/writer.d.ts +1 -1
- package/dist/core/config/writer.d.ts.map +1 -1
- package/dist/core/config/writer.js +82 -74
- package/dist/core/config/writer.js.map +1 -1
- package/dist/core/shell/detector.d.ts +8 -1
- package/dist/core/shell/detector.d.ts.map +1 -1
- package/dist/core/shell/detector.js +38 -24
- package/dist/core/shell/detector.js.map +1 -1
- package/dist/core/shell/profile-updater.d.ts +1 -1
- package/dist/core/shell/profile-updater.d.ts.map +1 -1
- package/dist/core/shell/profile-updater.js +40 -27
- package/dist/core/shell/profile-updater.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +30 -25
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +2 -2
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +2 -2
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/hashing.d.ts +18 -0
- package/dist/utils/hashing.d.ts.map +1 -0
- package/dist/utils/hashing.js +27 -0
- package/dist/utils/hashing.js.map +1 -0
- package/package.json +6 -3
|
@@ -3,6 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sleep = sleep;
|
|
7
|
+
exports.sanitizeErrorMessage = sanitizeErrorMessage;
|
|
8
|
+
exports.isRetryableError = isRetryableError;
|
|
9
|
+
exports.sendBatchWithRetry = sendBatchWithRetry;
|
|
10
|
+
exports.parseRelativeDate = parseRelativeDate;
|
|
11
|
+
exports.parseSinceDate = parseSinceDate;
|
|
12
|
+
exports.findJsonlFiles = findJsonlFiles;
|
|
13
|
+
exports.calculateStatistics = calculateStatistics;
|
|
14
|
+
exports.parseJsonlLine = parseJsonlLine;
|
|
15
|
+
exports.streamJsonlRecords = streamJsonlRecords;
|
|
16
|
+
exports.toUnixNano = toUnixNano;
|
|
17
|
+
exports.createOtlpPayload = createOtlpPayload;
|
|
6
18
|
exports.backfillCommand = backfillCommand;
|
|
7
19
|
const node_fs_1 = require("node:fs");
|
|
8
20
|
const promises_1 = require("node:fs/promises");
|
|
@@ -14,6 +26,83 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
14
26
|
const loader_js_1 = require("../../core/config/loader.js");
|
|
15
27
|
const client_js_1 = require("../../core/api/client.js");
|
|
16
28
|
const constants_js_1 = require("../../utils/constants.js");
|
|
29
|
+
const hashing_js_1 = require("../../utils/hashing.js");
|
|
30
|
+
/**
|
|
31
|
+
* Sleep for a specified number of milliseconds.
|
|
32
|
+
*/
|
|
33
|
+
function sleep(ms) {
|
|
34
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sanitize error message to prevent API key leakage.
|
|
38
|
+
* Truncates long messages and removes potential sensitive data.
|
|
39
|
+
*/
|
|
40
|
+
function sanitizeErrorMessage(errorMsg) {
|
|
41
|
+
const maxLength = 500;
|
|
42
|
+
let sanitized = errorMsg;
|
|
43
|
+
if (sanitized.length > maxLength) {
|
|
44
|
+
sanitized = `${sanitized.substring(0, maxLength)}...`;
|
|
45
|
+
}
|
|
46
|
+
return sanitized;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if an error is retryable based on HTTP status code.
|
|
50
|
+
* 4xx errors (except 429) are not retryable as they indicate client errors.
|
|
51
|
+
*/
|
|
52
|
+
function isRetryableError(errorMsg) {
|
|
53
|
+
const statusMatch = errorMsg.match(/OTLP request failed: (\d{3})/);
|
|
54
|
+
if (!statusMatch) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const statusCode = parseInt(statusMatch[1], 10);
|
|
58
|
+
if (statusCode === 429) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Send a batch with retry logic and exponential backoff.
|
|
68
|
+
*/
|
|
69
|
+
async function sendBatchWithRetry(endpoint, apiKey, payload, maxRetries, verbose) {
|
|
70
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
await (0, client_js_1.sendOtlpLogs)(endpoint, apiKey, payload);
|
|
73
|
+
if (verbose && attempt > 0) {
|
|
74
|
+
console.log(chalk_1.default.green(` ✓ Succeeded after ${attempt + 1} attempts`));
|
|
75
|
+
}
|
|
76
|
+
return { success: true, attempts: attempt + 1 };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const rawErrorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
80
|
+
const errorMsg = sanitizeErrorMessage(rawErrorMsg);
|
|
81
|
+
const isRetryable = isRetryableError(errorMsg);
|
|
82
|
+
if (!isRetryable) {
|
|
83
|
+
if (verbose) {
|
|
84
|
+
console.log(chalk_1.default.red(` ✗ Non-retryable error (client error): ${errorMsg}`));
|
|
85
|
+
}
|
|
86
|
+
return { success: false, attempts: attempt + 1, error: errorMsg };
|
|
87
|
+
}
|
|
88
|
+
if (attempt < maxRetries - 1) {
|
|
89
|
+
const backoffDelay = 1000 * Math.pow(2, attempt);
|
|
90
|
+
if (verbose) {
|
|
91
|
+
console.log(chalk_1.default.yellow(` ✗ Attempt ${attempt + 1} failed: ${errorMsg}`));
|
|
92
|
+
console.log(chalk_1.default.blue(` ⏳ Retrying in ${backoffDelay}ms...`));
|
|
93
|
+
}
|
|
94
|
+
await sleep(backoffDelay);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
if (verbose) {
|
|
98
|
+
console.log(chalk_1.default.red(` ✗ All ${maxRetries} attempts failed`));
|
|
99
|
+
}
|
|
100
|
+
return { success: false, attempts: maxRetries, error: errorMsg };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { success: false, attempts: maxRetries };
|
|
105
|
+
}
|
|
17
106
|
/**
|
|
18
107
|
* Parses a relative date string like "7d" or "1m" into a Date.
|
|
19
108
|
*/
|
|
@@ -25,19 +114,19 @@ function parseRelativeDate(input) {
|
|
|
25
114
|
const unit = match[2];
|
|
26
115
|
const now = new Date();
|
|
27
116
|
switch (unit) {
|
|
28
|
-
case
|
|
117
|
+
case "d":
|
|
29
118
|
now.setDate(now.getDate() - amount);
|
|
30
119
|
break;
|
|
31
|
-
case
|
|
120
|
+
case "w":
|
|
32
121
|
now.setDate(now.getDate() - amount * 7);
|
|
33
122
|
break;
|
|
34
|
-
case
|
|
123
|
+
case "m":
|
|
35
124
|
now.setMonth(now.getMonth() - amount);
|
|
36
125
|
break;
|
|
37
|
-
case
|
|
126
|
+
case "M":
|
|
38
127
|
now.setMonth(now.getMonth() - amount);
|
|
39
128
|
break;
|
|
40
|
-
case
|
|
129
|
+
case "y":
|
|
41
130
|
now.setFullYear(now.getFullYear() - amount);
|
|
42
131
|
break;
|
|
43
132
|
default:
|
|
@@ -73,7 +162,7 @@ async function findJsonlFiles(dir, errors = []) {
|
|
|
73
162
|
const result = await findJsonlFiles(fullPath, errors);
|
|
74
163
|
files.push(...result.files);
|
|
75
164
|
}
|
|
76
|
-
else if (entry.isFile() && entry.name.endsWith(
|
|
165
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
77
166
|
files.push(fullPath);
|
|
78
167
|
}
|
|
79
168
|
}
|
|
@@ -84,6 +173,76 @@ async function findJsonlFiles(dir, errors = []) {
|
|
|
84
173
|
}
|
|
85
174
|
return { files, errors };
|
|
86
175
|
}
|
|
176
|
+
function calculateStatistics(records) {
|
|
177
|
+
if (records.length === 0) {
|
|
178
|
+
return {
|
|
179
|
+
totalRecords: 0,
|
|
180
|
+
oldestTimestamp: "",
|
|
181
|
+
newestTimestamp: "",
|
|
182
|
+
totalInputTokens: 0,
|
|
183
|
+
totalOutputTokens: 0,
|
|
184
|
+
totalCacheReadTokens: 0,
|
|
185
|
+
totalCacheCreationTokens: 0,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const sortedRecords = [...records].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
189
|
+
return {
|
|
190
|
+
totalRecords: records.length,
|
|
191
|
+
oldestTimestamp: sortedRecords[0].timestamp,
|
|
192
|
+
newestTimestamp: sortedRecords[sortedRecords.length - 1].timestamp,
|
|
193
|
+
totalInputTokens: records.reduce((sum, r) => sum + r.inputTokens, 0),
|
|
194
|
+
totalOutputTokens: records.reduce((sum, r) => sum + r.outputTokens, 0),
|
|
195
|
+
totalCacheReadTokens: records.reduce((sum, r) => sum + r.cacheReadTokens, 0),
|
|
196
|
+
totalCacheCreationTokens: records.reduce((sum, r) => sum + r.cacheCreationTokens, 0),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function parseJsonlLine(line, sinceDate) {
|
|
200
|
+
if (!line.trim()) {
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
let entry;
|
|
204
|
+
try {
|
|
205
|
+
entry = JSON.parse(line);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return { parseError: true };
|
|
209
|
+
}
|
|
210
|
+
if (entry.type !== "assistant" || !entry.message?.usage) {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
const usage = entry.message.usage;
|
|
214
|
+
const timestamp = entry.timestamp;
|
|
215
|
+
const sessionId = entry.sessionId;
|
|
216
|
+
const model = entry.message.model;
|
|
217
|
+
if (!timestamp || !sessionId || !model) {
|
|
218
|
+
return { missingFields: true };
|
|
219
|
+
}
|
|
220
|
+
const entryDate = new Date(timestamp);
|
|
221
|
+
if (!Number.isFinite(entryDate.getTime())) {
|
|
222
|
+
return {};
|
|
223
|
+
}
|
|
224
|
+
if (sinceDate && entryDate < sinceDate) {
|
|
225
|
+
return {};
|
|
226
|
+
}
|
|
227
|
+
const totalTokens = (usage.input_tokens || 0) +
|
|
228
|
+
(usage.output_tokens || 0) +
|
|
229
|
+
(usage.cache_read_input_tokens || 0) +
|
|
230
|
+
(usage.cache_creation_input_tokens || 0);
|
|
231
|
+
if (totalTokens === 0) {
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
record: {
|
|
236
|
+
sessionId,
|
|
237
|
+
timestamp,
|
|
238
|
+
model,
|
|
239
|
+
inputTokens: usage.input_tokens || 0,
|
|
240
|
+
outputTokens: usage.output_tokens || 0,
|
|
241
|
+
cacheReadTokens: usage.cache_read_input_tokens || 0,
|
|
242
|
+
cacheCreationTokens: usage.cache_creation_input_tokens || 0,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
87
246
|
/**
|
|
88
247
|
* Streams a JSONL file and extracts records with usage data.
|
|
89
248
|
* Yields objects indicating either a valid record or a parse error.
|
|
@@ -96,56 +255,13 @@ async function* streamJsonlRecords(filePath, sinceDate) {
|
|
|
96
255
|
});
|
|
97
256
|
try {
|
|
98
257
|
for await (const line of rl) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const entry = JSON.parse(line);
|
|
103
|
-
// Only process assistant messages with usage data
|
|
104
|
-
if (entry.type !== 'assistant' || !entry.message?.usage)
|
|
105
|
-
continue;
|
|
106
|
-
const usage = entry.message.usage;
|
|
107
|
-
const timestamp = entry.timestamp;
|
|
108
|
-
const sessionId = entry.sessionId;
|
|
109
|
-
const model = entry.message.model;
|
|
110
|
-
// Skip if missing required fields
|
|
111
|
-
if (!timestamp || !sessionId || !model)
|
|
112
|
-
continue;
|
|
113
|
-
// Validate timestamp is a valid date
|
|
114
|
-
const entryDate = new Date(timestamp);
|
|
115
|
-
if (!Number.isFinite(entryDate.getTime()))
|
|
116
|
-
continue;
|
|
117
|
-
// Check date filter
|
|
118
|
-
if (sinceDate) {
|
|
119
|
-
if (entryDate < sinceDate)
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
// Skip entries with no actual token usage
|
|
123
|
-
const totalTokens = (usage.input_tokens || 0) +
|
|
124
|
-
(usage.output_tokens || 0) +
|
|
125
|
-
(usage.cache_read_input_tokens || 0) +
|
|
126
|
-
(usage.cache_creation_input_tokens || 0);
|
|
127
|
-
if (totalTokens === 0)
|
|
128
|
-
continue;
|
|
129
|
-
yield {
|
|
130
|
-
record: {
|
|
131
|
-
sessionId,
|
|
132
|
-
timestamp,
|
|
133
|
-
model,
|
|
134
|
-
inputTokens: usage.input_tokens || 0,
|
|
135
|
-
outputTokens: usage.output_tokens || 0,
|
|
136
|
-
cacheReadTokens: usage.cache_read_input_tokens || 0,
|
|
137
|
-
cacheCreationTokens: usage.cache_creation_input_tokens || 0,
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// Invalid JSON line, signal parse error
|
|
143
|
-
yield { parseError: true };
|
|
258
|
+
const result = parseJsonlLine(line, sinceDate);
|
|
259
|
+
if (result.record || result.parseError || result.missingFields) {
|
|
260
|
+
yield result;
|
|
144
261
|
}
|
|
145
262
|
}
|
|
146
263
|
}
|
|
147
264
|
finally {
|
|
148
|
-
// Ensure file stream is properly closed even on early exit
|
|
149
265
|
fileStream.destroy();
|
|
150
266
|
rl.close();
|
|
151
267
|
}
|
|
@@ -162,55 +278,95 @@ function toUnixNano(timestamp) {
|
|
|
162
278
|
}
|
|
163
279
|
return (BigInt(ms) * BigInt(1_000_000)).toString();
|
|
164
280
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
const
|
|
172
|
-
|
|
281
|
+
function createOtlpPayload(records, options) {
|
|
282
|
+
const { costMultiplier, email, organizationName, organizationId, productName, productId, } = options;
|
|
283
|
+
// Support both new and old field names with fallback
|
|
284
|
+
const organizationValue = organizationName || organizationId;
|
|
285
|
+
const productValue = productName || productId;
|
|
286
|
+
// Filter and map records, skipping any with invalid timestamps
|
|
287
|
+
const logRecords = records
|
|
288
|
+
.map((record) => {
|
|
173
289
|
const timeUnixNano = toUnixNano(record.timestamp);
|
|
174
|
-
if (timeUnixNano === null)
|
|
175
|
-
|
|
176
|
-
|
|
290
|
+
if (timeUnixNano === null) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
// Build attributes array with required fields
|
|
177
294
|
const attributes = [
|
|
178
|
-
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
295
|
+
{
|
|
296
|
+
key: "transaction_id",
|
|
297
|
+
value: { stringValue: (0, hashing_js_1.generateTransactionId)(record) },
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
key: "session.id",
|
|
301
|
+
value: { stringValue: record.sessionId },
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
key: "model",
|
|
305
|
+
value: { stringValue: record.model },
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
key: "input_tokens",
|
|
309
|
+
value: { intValue: record.inputTokens },
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
key: "output_tokens",
|
|
313
|
+
value: { intValue: record.outputTokens },
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
key: "cache_read_tokens",
|
|
317
|
+
value: { intValue: record.cacheReadTokens },
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
key: "cache_creation_tokens",
|
|
321
|
+
value: { intValue: record.cacheCreationTokens },
|
|
322
|
+
},
|
|
189
323
|
];
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
},
|
|
324
|
+
// Add optional subscriber/attribution attributes at log record level
|
|
325
|
+
// (backend ClaudeCodeMapper reads these from log record attrs, not resource attrs)
|
|
326
|
+
if (email) {
|
|
327
|
+
attributes.push({ key: "user.email", value: { stringValue: email } });
|
|
328
|
+
}
|
|
329
|
+
if (organizationValue) {
|
|
330
|
+
attributes.push({
|
|
331
|
+
key: "organization.name",
|
|
332
|
+
value: { stringValue: organizationValue },
|
|
200
333
|
});
|
|
201
334
|
}
|
|
202
|
-
|
|
335
|
+
if (productValue) {
|
|
336
|
+
attributes.push({
|
|
337
|
+
key: "product.name",
|
|
338
|
+
value: { stringValue: productValue },
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
timeUnixNano,
|
|
343
|
+
body: { stringValue: "claude_code.api_request" },
|
|
344
|
+
attributes,
|
|
345
|
+
};
|
|
346
|
+
})
|
|
347
|
+
.filter((record) => record !== null);
|
|
203
348
|
return {
|
|
204
|
-
|
|
349
|
+
resourceLogs: [
|
|
205
350
|
{
|
|
206
351
|
resource: {
|
|
207
352
|
attributes: [
|
|
208
|
-
{
|
|
353
|
+
{
|
|
354
|
+
key: "service.name",
|
|
355
|
+
value: { stringValue: "claude-code" },
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
key: "cost_multiplier",
|
|
359
|
+
value: { doubleValue: costMultiplier },
|
|
360
|
+
},
|
|
209
361
|
],
|
|
210
362
|
},
|
|
211
|
-
|
|
363
|
+
scopeLogs: [
|
|
212
364
|
{
|
|
213
|
-
|
|
365
|
+
scope: {
|
|
366
|
+
name: "claude-code",
|
|
367
|
+
version: "1.0.0",
|
|
368
|
+
},
|
|
369
|
+
logRecords,
|
|
214
370
|
},
|
|
215
371
|
],
|
|
216
372
|
},
|
|
@@ -220,17 +376,18 @@ function createOtlpPayload(records, costMultiplier) {
|
|
|
220
376
|
/**
|
|
221
377
|
* Backfill command - imports historical Claude Code usage data.
|
|
222
378
|
*/
|
|
223
|
-
async function backfillCommand(options = {}) {
|
|
224
|
-
const { since, dryRun = false, batchSize = 100, verbose = false } = options;
|
|
225
|
-
|
|
379
|
+
async function backfillCommand(options = {}, deps = {}) {
|
|
380
|
+
const { since, dryRun = false, batchSize = 100, delay = 100, verbose = false, } = options;
|
|
381
|
+
const { loadConfig: getConfig = loader_js_1.loadConfig, findJsonlFiles: findFiles = findJsonlFiles, streamJsonlRecords: streamRecords = streamJsonlRecords, sendBatchWithRetry: sendBatch = sendBatchWithRetry, homedir: getHomedir = node_os_1.homedir, } = deps;
|
|
382
|
+
console.log(chalk_1.default.bold("\nRevenium Claude Code Backfill\n"));
|
|
226
383
|
if (dryRun) {
|
|
227
|
-
console.log(chalk_1.default.yellow(
|
|
384
|
+
console.log(chalk_1.default.yellow("Running in dry-run mode - no data will be sent\n"));
|
|
228
385
|
}
|
|
229
386
|
// Load configuration
|
|
230
|
-
const config = await (
|
|
387
|
+
const config = await getConfig();
|
|
231
388
|
if (!config) {
|
|
232
|
-
console.log(chalk_1.default.red(
|
|
233
|
-
console.log(chalk_1.default.yellow(
|
|
389
|
+
console.log(chalk_1.default.red("Configuration not found"));
|
|
390
|
+
console.log(chalk_1.default.yellow("\nRun `revenium-metering setup` to configure Claude Code metering."));
|
|
234
391
|
process.exit(1);
|
|
235
392
|
}
|
|
236
393
|
// Parse since date
|
|
@@ -239,21 +396,23 @@ async function backfillCommand(options = {}) {
|
|
|
239
396
|
sinceDate = parseSinceDate(since);
|
|
240
397
|
if (!sinceDate) {
|
|
241
398
|
console.log(chalk_1.default.red(`Invalid --since value: ${since}`));
|
|
242
|
-
console.log(chalk_1.default.dim(
|
|
399
|
+
console.log(chalk_1.default.dim("Use ISO format (2024-01-15) or relative format (7d, 1m, 1y)"));
|
|
243
400
|
process.exit(1);
|
|
244
401
|
}
|
|
245
402
|
console.log(chalk_1.default.dim(`Filtering records since: ${sinceDate.toISOString()}\n`));
|
|
246
403
|
}
|
|
247
404
|
// Get cost multiplier (use ?? to allow explicit 0 override for free tier/testing)
|
|
248
405
|
const costMultiplier = config.costMultiplierOverride ??
|
|
249
|
-
(config.subscriptionTier
|
|
406
|
+
(config.subscriptionTier
|
|
407
|
+
? (0, constants_js_1.getCostMultiplier)(config.subscriptionTier)
|
|
408
|
+
: 0.08);
|
|
250
409
|
// Discover JSONL files
|
|
251
|
-
const projectsDir = (0, node_path_1.join)((
|
|
252
|
-
const discoverSpinner = (0, ora_1.default)(
|
|
253
|
-
const { files: jsonlFiles, errors: discoveryErrors } = await
|
|
410
|
+
const projectsDir = (0, node_path_1.join)(getHomedir(), ".claude", "projects");
|
|
411
|
+
const discoverSpinner = (0, ora_1.default)("Discovering JSONL files...").start();
|
|
412
|
+
const { files: jsonlFiles, errors: discoveryErrors } = await findFiles(projectsDir);
|
|
254
413
|
if (discoveryErrors.length > 0 && verbose) {
|
|
255
414
|
discoverSpinner.warn(`Found ${jsonlFiles.length} JSONL file(s) with ${discoveryErrors.length} directory error(s)`);
|
|
256
|
-
console.log(chalk_1.default.yellow(
|
|
415
|
+
console.log(chalk_1.default.yellow("\nDirectory access errors:"));
|
|
257
416
|
for (const error of discoveryErrors.slice(0, 5)) {
|
|
258
417
|
console.log(chalk_1.default.yellow(` ${error}`));
|
|
259
418
|
}
|
|
@@ -262,10 +421,10 @@ async function backfillCommand(options = {}) {
|
|
|
262
421
|
}
|
|
263
422
|
}
|
|
264
423
|
else if (jsonlFiles.length === 0) {
|
|
265
|
-
discoverSpinner.fail(
|
|
424
|
+
discoverSpinner.fail("No JSONL files found");
|
|
266
425
|
console.log(chalk_1.default.dim(`Searched in: ${projectsDir}`));
|
|
267
426
|
if (discoveryErrors.length > 0) {
|
|
268
|
-
console.log(chalk_1.default.yellow(
|
|
427
|
+
console.log(chalk_1.default.yellow("\nDirectory access errors:"));
|
|
269
428
|
for (const error of discoveryErrors) {
|
|
270
429
|
console.log(chalk_1.default.yellow(` ${error}`));
|
|
271
430
|
}
|
|
@@ -276,27 +435,31 @@ async function backfillCommand(options = {}) {
|
|
|
276
435
|
discoverSpinner.succeed(`Found ${jsonlFiles.length} JSONL file(s)`);
|
|
277
436
|
}
|
|
278
437
|
if (verbose) {
|
|
279
|
-
console.log(chalk_1.default.dim(
|
|
438
|
+
console.log(chalk_1.default.dim("\nFiles:"));
|
|
280
439
|
for (const file of jsonlFiles.slice(0, 10)) {
|
|
281
440
|
console.log(chalk_1.default.dim(` ${file}`));
|
|
282
441
|
}
|
|
283
442
|
if (jsonlFiles.length > 10) {
|
|
284
443
|
console.log(chalk_1.default.dim(` ... and ${jsonlFiles.length - 10} more`));
|
|
285
444
|
}
|
|
286
|
-
console.log(
|
|
445
|
+
console.log("");
|
|
287
446
|
}
|
|
288
447
|
// Process files and collect records
|
|
289
|
-
const processSpinner = (0, ora_1.default)(
|
|
448
|
+
const processSpinner = (0, ora_1.default)("Processing files...").start();
|
|
290
449
|
const allRecords = [];
|
|
291
450
|
let processedFiles = 0;
|
|
292
451
|
let skippedLines = 0;
|
|
293
452
|
let skippedFiles = 0;
|
|
453
|
+
let skippedMissingFields = 0;
|
|
294
454
|
for (const file of jsonlFiles) {
|
|
295
455
|
try {
|
|
296
|
-
for await (const result of
|
|
456
|
+
for await (const result of streamRecords(file, sinceDate)) {
|
|
297
457
|
if (result.parseError) {
|
|
298
458
|
skippedLines++;
|
|
299
459
|
}
|
|
460
|
+
else if (result.missingFields) {
|
|
461
|
+
skippedMissingFields++;
|
|
462
|
+
}
|
|
300
463
|
else if (result.record) {
|
|
301
464
|
allRecords.push(result.record);
|
|
302
465
|
}
|
|
@@ -315,76 +478,126 @@ async function backfillCommand(options = {}) {
|
|
|
315
478
|
// Build status message with skipped line info
|
|
316
479
|
let statusMessage = `Processed ${processedFiles} files, found ${allRecords.length} usage records`;
|
|
317
480
|
if (skippedLines > 0) {
|
|
318
|
-
statusMessage += chalk_1.default.yellow(` (${skippedLines} malformed line${skippedLines > 1 ?
|
|
481
|
+
statusMessage += chalk_1.default.yellow(` (${skippedLines} malformed line${skippedLines > 1 ? "s" : ""} skipped)`);
|
|
482
|
+
}
|
|
483
|
+
if (skippedMissingFields > 0) {
|
|
484
|
+
statusMessage += chalk_1.default.yellow(` (${skippedMissingFields} record${skippedMissingFields > 1 ? "s" : ""} missing required fields)`);
|
|
319
485
|
}
|
|
320
486
|
if (skippedFiles > 0) {
|
|
321
|
-
statusMessage += chalk_1.default.yellow(` (${skippedFiles} file${skippedFiles > 1 ?
|
|
487
|
+
statusMessage += chalk_1.default.yellow(` (${skippedFiles} file${skippedFiles > 1 ? "s" : ""} failed)`);
|
|
322
488
|
}
|
|
323
489
|
processSpinner.succeed(statusMessage);
|
|
324
490
|
if (allRecords.length === 0) {
|
|
325
|
-
console.log(chalk_1.default.yellow(
|
|
491
|
+
console.log(chalk_1.default.yellow("\nNo usage records found to backfill."));
|
|
492
|
+
if (skippedMissingFields > 0) {
|
|
493
|
+
console.log(chalk_1.default.dim(`${skippedMissingFields} record${skippedMissingFields > 1 ? "s were" : " was"} skipped due to missing required fields (timestamp, sessionId, or model).`));
|
|
494
|
+
}
|
|
326
495
|
if (since) {
|
|
327
496
|
console.log(chalk_1.default.dim(`Try a broader date range or remove the --since filter.`));
|
|
328
497
|
}
|
|
329
498
|
return;
|
|
330
499
|
}
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
console.log('\n' + chalk_1.default.bold('Summary:'));
|
|
341
|
-
console.log(` Records: ${allRecords.length.toLocaleString()}`);
|
|
342
|
-
console.log(` Date range: ${oldestRecord.timestamp.split('T')[0]} to ${newestRecord.timestamp.split('T')[0]}`);
|
|
343
|
-
console.log(` Input tokens: ${totalInputTokens.toLocaleString()}`);
|
|
344
|
-
console.log(` Output tokens: ${totalOutputTokens.toLocaleString()}`);
|
|
345
|
-
console.log(` Cache read tokens: ${totalCacheReadTokens.toLocaleString()}`);
|
|
346
|
-
console.log(` Cache creation: ${totalCacheCreationTokens.toLocaleString()}`);
|
|
500
|
+
// Calculate statistics
|
|
501
|
+
const stats = calculateStatistics(allRecords);
|
|
502
|
+
console.log("\n" + chalk_1.default.bold("Summary:"));
|
|
503
|
+
console.log(` Records: ${stats.totalRecords.toLocaleString()}`);
|
|
504
|
+
console.log(` Date range: ${stats.oldestTimestamp.split("T")[0]} to ${stats.newestTimestamp.split("T")[0]}`);
|
|
505
|
+
console.log(` Input tokens: ${stats.totalInputTokens.toLocaleString()}`);
|
|
506
|
+
console.log(` Output tokens: ${stats.totalOutputTokens.toLocaleString()}`);
|
|
507
|
+
console.log(` Cache read tokens: ${stats.totalCacheReadTokens.toLocaleString()}`);
|
|
508
|
+
console.log(` Cache creation: ${stats.totalCacheCreationTokens.toLocaleString()}`);
|
|
347
509
|
console.log(` Cost multiplier: ${costMultiplier}`);
|
|
510
|
+
if (verbose &&
|
|
511
|
+
(skippedLines > 0 || skippedMissingFields > 0 || skippedFiles > 0)) {
|
|
512
|
+
console.log("\n" + chalk_1.default.dim("Skipped records:"));
|
|
513
|
+
if (skippedLines > 0) {
|
|
514
|
+
console.log(chalk_1.default.dim(` Malformed JSON: ${skippedLines.toLocaleString()}`));
|
|
515
|
+
}
|
|
516
|
+
if (skippedMissingFields > 0) {
|
|
517
|
+
console.log(chalk_1.default.dim(` Missing fields: ${skippedMissingFields.toLocaleString()} (timestamp, sessionId, or model)`));
|
|
518
|
+
}
|
|
519
|
+
if (skippedFiles > 0) {
|
|
520
|
+
console.log(chalk_1.default.dim(` Failed files: ${skippedFiles.toLocaleString()}`));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
348
523
|
if (dryRun) {
|
|
349
|
-
console.log(
|
|
524
|
+
console.log("\n" +
|
|
525
|
+
chalk_1.default.yellow("Dry run complete. Use without --dry-run to send data."));
|
|
350
526
|
if (verbose) {
|
|
351
|
-
console.log(
|
|
527
|
+
console.log("\n" + chalk_1.default.dim("Sample OTLP payload (first batch):"));
|
|
352
528
|
const sampleRecords = allRecords.slice(0, Math.min(batchSize, 3));
|
|
353
|
-
const samplePayload = createOtlpPayload(sampleRecords,
|
|
529
|
+
const samplePayload = createOtlpPayload(sampleRecords, {
|
|
530
|
+
costMultiplier,
|
|
531
|
+
email: config.email,
|
|
532
|
+
organizationName: config.organizationName || config.organizationId,
|
|
533
|
+
productName: config.productName || config.productId,
|
|
534
|
+
});
|
|
354
535
|
console.log(chalk_1.default.dim(JSON.stringify(samplePayload, null, 2)));
|
|
355
536
|
}
|
|
356
537
|
return;
|
|
357
538
|
}
|
|
358
539
|
// Send data in batches
|
|
359
540
|
const totalBatches = Math.ceil(allRecords.length / batchSize);
|
|
360
|
-
const sendSpinner = (0, ora_1.default)(`Sending data... (0/${totalBatches} batches)`).start();
|
|
541
|
+
const sendSpinner = (0, ora_1.default)(`Sending data... (0/${totalBatches} batches, ~${delay}ms delay)`).start();
|
|
361
542
|
let sentBatches = 0;
|
|
362
543
|
let sentRecords = 0;
|
|
363
|
-
let
|
|
544
|
+
let permanentlyFailedBatches = 0;
|
|
545
|
+
let totalRetryAttempts = 0;
|
|
546
|
+
const failedBatchDetails = [];
|
|
547
|
+
const maxRetries = 3;
|
|
364
548
|
for (let i = 0; i < allRecords.length; i += batchSize) {
|
|
549
|
+
const batchNumber = Math.floor(i / batchSize) + 1;
|
|
365
550
|
const batch = allRecords.slice(i, i + batchSize);
|
|
366
|
-
const payload = createOtlpPayload(batch,
|
|
367
|
-
|
|
368
|
-
|
|
551
|
+
const payload = createOtlpPayload(batch, {
|
|
552
|
+
costMultiplier,
|
|
553
|
+
email: config.email,
|
|
554
|
+
organizationName: config.organizationName || config.organizationId,
|
|
555
|
+
productName: config.productName || config.productId,
|
|
556
|
+
});
|
|
557
|
+
sendSpinner.text = `Sending batch ${batchNumber}/${totalBatches}...`;
|
|
558
|
+
const result = await sendBatch(config.endpoint, config.apiKey, payload, maxRetries, verbose);
|
|
559
|
+
totalRetryAttempts += result.attempts;
|
|
560
|
+
if (result.success) {
|
|
369
561
|
sentBatches++;
|
|
370
562
|
sentRecords += batch.length;
|
|
371
|
-
sendSpinner.text = `Sending data... (${sentBatches}/${totalBatches} batches)`;
|
|
563
|
+
sendSpinner.text = `Sending data... (${sentBatches}/${totalBatches} batches, ~${delay}ms delay)`;
|
|
372
564
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
565
|
+
else {
|
|
566
|
+
permanentlyFailedBatches++;
|
|
567
|
+
failedBatchDetails.push({
|
|
568
|
+
batchNumber,
|
|
569
|
+
error: result.error || "Unknown error",
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
// Apply rate limiting delay between batches (except after the last batch)
|
|
573
|
+
if (i + batchSize < allRecords.length) {
|
|
574
|
+
sendSpinner.text = `Waiting ${delay}ms before next batch...`;
|
|
575
|
+
await sleep(delay);
|
|
379
576
|
}
|
|
380
577
|
}
|
|
381
|
-
if (
|
|
578
|
+
if (permanentlyFailedBatches === 0) {
|
|
382
579
|
sendSpinner.succeed(`Sent ${sentRecords.toLocaleString()} records in ${sentBatches} batches`);
|
|
383
580
|
}
|
|
384
581
|
else {
|
|
385
|
-
sendSpinner.warn(`Sent ${sentRecords.toLocaleString()} records in ${sentBatches} batches (${
|
|
582
|
+
sendSpinner.warn(`Sent ${sentRecords.toLocaleString()} records in ${sentBatches} batches (${permanentlyFailedBatches} permanently failed)`);
|
|
583
|
+
}
|
|
584
|
+
// Show retry statistics if there were retries
|
|
585
|
+
const retriedBatches = totalRetryAttempts - totalBatches;
|
|
586
|
+
if (retriedBatches > 0 && verbose) {
|
|
587
|
+
console.log("\n" + chalk_1.default.bold("Retry Statistics:"));
|
|
588
|
+
console.log(` Total retry attempts: ${retriedBatches}`);
|
|
589
|
+
console.log(` Average attempts/batch: ${(totalRetryAttempts / totalBatches).toFixed(2)}`);
|
|
590
|
+
}
|
|
591
|
+
// Show permanently failed batches details
|
|
592
|
+
if (permanentlyFailedBatches > 0) {
|
|
593
|
+
console.log("\n" + chalk_1.default.red.bold("Permanently Failed Batches:"));
|
|
594
|
+
for (const failed of failedBatchDetails) {
|
|
595
|
+
console.log(chalk_1.default.red(` Batch ${failed.batchNumber}: ${failed.error}`));
|
|
596
|
+
}
|
|
597
|
+
console.log("\n" +
|
|
598
|
+
chalk_1.default.yellow("Tip: You can re-run the backfill command to retry failed batches."));
|
|
386
599
|
}
|
|
387
|
-
console.log(
|
|
388
|
-
console.log(chalk_1.default.dim(
|
|
600
|
+
console.log("\n" + chalk_1.default.green.bold("Backfill complete!"));
|
|
601
|
+
console.log(chalk_1.default.dim("Check your Revenium dashboard to see the imported data."));
|
|
389
602
|
}
|
|
390
603
|
//# sourceMappingURL=backfill.js.map
|