letmecode 0.1.20 → 0.1.22
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 +10 -27
- package/ink-app/dist/index.js +68 -35
- package/ink-app/dist/providers/antigravity/models.js +46 -0
- package/ink-app/dist/providers/antigravity/provider.js +288 -0
- package/ink-app/dist/providers/antigravity/quota-parser.js +49 -0
- package/ink-app/dist/providers/antigravity/rpc/client.js +54 -0
- package/ink-app/dist/providers/antigravity/rpc/discovery.js +84 -0
- package/ink-app/dist/providers/antigravity/rpc/quota.js +25 -0
- package/ink-app/dist/providers/antigravity/rpc/usage.js +80 -0
- package/ink-app/dist/providers/antigravity/types.js +1 -0
- package/ink-app/dist/providers/antigravity/usage-parse.js +23 -0
- package/ink-app/dist/providers/antigravity.js +2 -537
- package/ink-app/dist/providers/claude.js +176 -183
- package/ink-app/dist/providers/contract.js +5 -2
- package/ink-app/dist/providers/copilot/models.js +55 -0
- package/ink-app/dist/providers/copilot/otel/configure.js +134 -0
- package/ink-app/dist/providers/copilot/otel/discover.js +94 -0
- package/ink-app/dist/providers/copilot/otel/parse.js +228 -0
- package/ink-app/dist/providers/copilot/provider.js +259 -0
- package/ink-app/dist/providers/copilot/quota.js +257 -0
- package/ink-app/dist/providers/copilot/usage/aggregate.js +84 -0
- package/ink-app/dist/providers/copilot.js +4 -373
- package/ink-app/dist/providers/index.js +1 -1
- package/ink-app/dist/providers/pricing.js +5 -0
- package/ink-app/dist/reporting.js +11 -1
- package/package.json +11 -13
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
5
|
+
import { asRecord } from "../../limits.js";
|
|
6
|
+
const VSCODE_OTEL_SETTINGS = {
|
|
7
|
+
"github.copilot.chat.otel.enabled": true,
|
|
8
|
+
"github.copilot.chat.otel.exporterType": "file",
|
|
9
|
+
"github.copilot.chat.otel.captureContent": false
|
|
10
|
+
};
|
|
11
|
+
export async function configureCopilotVsCodeLogging(options = {}) {
|
|
12
|
+
const root = path.resolve(options.root ?? os.homedir());
|
|
13
|
+
const outfile = getCopilotOtelPath(root);
|
|
14
|
+
const settingsPath = options.settingsPath ?? (await getVsCodeSettingsPath(root));
|
|
15
|
+
const settingsText = await readTextFileOrEmpty(settingsPath);
|
|
16
|
+
const { text, changed } = updateJsoncSettings(settingsText, {
|
|
17
|
+
...VSCODE_OTEL_SETTINGS,
|
|
18
|
+
"github.copilot.chat.otel.outfile": toVsCodeOutfilePath(outfile)
|
|
19
|
+
});
|
|
20
|
+
await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
21
|
+
await fs.promises.mkdir(path.dirname(outfile), { recursive: true });
|
|
22
|
+
if (changed) {
|
|
23
|
+
await fs.promises.writeFile(settingsPath, text, "utf8");
|
|
24
|
+
}
|
|
25
|
+
return { settingsPath, outfile, changed };
|
|
26
|
+
}
|
|
27
|
+
export function getCopilotOtelPath(root) {
|
|
28
|
+
return path.join(root, ".copilot", "otel", "vscode.jsonl");
|
|
29
|
+
}
|
|
30
|
+
export function toVsCodeOutfilePath(filePath) {
|
|
31
|
+
return process.platform === "win32" ? filePath.replace(/\\/g, "/") : filePath;
|
|
32
|
+
}
|
|
33
|
+
export function getCopilotCliOtelEnv(outfile) {
|
|
34
|
+
return {
|
|
35
|
+
COPILOT_OTEL_ENABLED: "true",
|
|
36
|
+
COPILOT_OTEL_FILE_EXPORTER_PATH: outfile
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function getVsCodeSettingsPath(root) {
|
|
40
|
+
const userRoots = getVsCodeUserRoots(root);
|
|
41
|
+
for (const userRoot of userRoots) {
|
|
42
|
+
if (await isDirectory(userRoot)) {
|
|
43
|
+
return path.join(userRoot, "settings.json");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return path.join(userRoots[0], "settings.json");
|
|
47
|
+
}
|
|
48
|
+
export function getVsCodeUserRoots(root) {
|
|
49
|
+
if (process.platform === "darwin") {
|
|
50
|
+
const applicationSupport = path.join(root, "Library", "Application Support");
|
|
51
|
+
return [
|
|
52
|
+
path.join(applicationSupport, "Code", "User"),
|
|
53
|
+
path.join(applicationSupport, "Code - Insiders", "User")
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
const appData = process.env.APPDATA ?? path.join(root, "AppData", "Roaming");
|
|
58
|
+
return [path.join(appData, "Code", "User"), path.join(appData, "Code - Insiders", "User")];
|
|
59
|
+
}
|
|
60
|
+
const configRoot = path.join(root, ".config");
|
|
61
|
+
return [path.join(configRoot, "Code", "User"), path.join(configRoot, "Code - Insiders", "User")];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* For each VS Code user root (stable + Insiders), read settings.json and report
|
|
65
|
+
* the configured Copilot OTEL outfile when file export is enabled. Used by the
|
|
66
|
+
* provider to detect "logging configured but the file has not been created yet".
|
|
67
|
+
*/
|
|
68
|
+
export async function getConfiguredCopilotOutfiles(root) {
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const userRoot of getVsCodeUserRoots(root)) {
|
|
71
|
+
const settings = await readJsonSettings(path.join(userRoot, "settings.json"));
|
|
72
|
+
const enabled = settings["github.copilot.chat.otel.enabled"] === true;
|
|
73
|
+
const exporterType = settings["github.copilot.chat.otel.exporterType"];
|
|
74
|
+
const outfile = settings["github.copilot.chat.otel.outfile"];
|
|
75
|
+
if (enabled && exporterType === "file" && typeof outfile === "string") {
|
|
76
|
+
results.push({ path: path.resolve(outfile), enabled: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
async function isDirectory(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
const stat = await fs.promises.stat(filePath);
|
|
84
|
+
return stat.isDirectory();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function readJsonSettings(filePath) {
|
|
91
|
+
return parseJsoncSettings(await readTextFileOrEmpty(filePath));
|
|
92
|
+
}
|
|
93
|
+
async function readTextFileOrEmpty(filePath) {
|
|
94
|
+
try {
|
|
95
|
+
return await fs.promises.readFile(filePath, "utf8");
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code === "ENOENT") {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function parseJsoncSettings(raw) {
|
|
105
|
+
if (!raw.trim()) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
const parsed = parse(raw);
|
|
109
|
+
return asRecord(parsed) ?? {};
|
|
110
|
+
}
|
|
111
|
+
function updateJsoncSettings(raw, values) {
|
|
112
|
+
let text = raw.trim() ? raw : "{\n}";
|
|
113
|
+
let changed = false;
|
|
114
|
+
for (const [key, value] of Object.entries(values)) {
|
|
115
|
+
if (parseJsoncSettings(text)[key] === value) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const edits = modify(text, [key], value, {
|
|
119
|
+
formattingOptions: {
|
|
120
|
+
eol: "\n",
|
|
121
|
+
insertSpaces: true,
|
|
122
|
+
tabSize: 4
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (edits.length > 0) {
|
|
126
|
+
text = applyEdits(text, edits);
|
|
127
|
+
changed = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (changed && !text.endsWith("\n")) {
|
|
131
|
+
text += "\n";
|
|
132
|
+
}
|
|
133
|
+
return { text, changed };
|
|
134
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getConfiguredCopilotOutfiles, getCopilotOtelPath } from "./configure.js";
|
|
4
|
+
function dedupKey(resolvedPath) {
|
|
5
|
+
return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath;
|
|
6
|
+
}
|
|
7
|
+
function isPermissionError(error) {
|
|
8
|
+
const code = error instanceof Error ? error.code : undefined;
|
|
9
|
+
return code === "EACCES" || code === "EPERM";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Discover the Copilot OTEL JSONL files to read, from three sources:
|
|
13
|
+
* 1. the `COPILOT_OTEL_FILE_EXPORTER_PATH` env var (Copilot CLI),
|
|
14
|
+
* 2. the `outfile` configured in VS Code / Insiders settings, and
|
|
15
|
+
* 3. every `*.jsonl` in `<root>/.copilot/otel/`.
|
|
16
|
+
* This covers the VS Code extension, a standalone Copilot CLI, and the CLI run
|
|
17
|
+
* from VS Code, on Linux/Windows/macOS. Paths are resolved and de-duplicated
|
|
18
|
+
* (case-insensitively on Windows); the first occurrence wins.
|
|
19
|
+
*/
|
|
20
|
+
export async function discoverCopilotOtelFiles(options) {
|
|
21
|
+
const root = options?.root ?? process.cwd();
|
|
22
|
+
const env = options?.env ?? process.env;
|
|
23
|
+
const warnings = [];
|
|
24
|
+
const candidatePaths = [];
|
|
25
|
+
// 1. Environment exporter path.
|
|
26
|
+
const envPath = env.COPILOT_OTEL_FILE_EXPORTER_PATH;
|
|
27
|
+
if (typeof envPath === "string" && envPath.length > 0) {
|
|
28
|
+
candidatePaths.push(envPath);
|
|
29
|
+
}
|
|
30
|
+
// 2. VS Code / Insiders configured outfiles.
|
|
31
|
+
try {
|
|
32
|
+
for (const entry of await getConfiguredCopilotOutfiles(root)) {
|
|
33
|
+
candidatePaths.push(entry.path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (isPermissionError(error)) {
|
|
38
|
+
warnings.push("Failed to read VS Code Copilot settings: permission denied.");
|
|
39
|
+
}
|
|
40
|
+
// Missing settings or any other read issue is not an error here.
|
|
41
|
+
}
|
|
42
|
+
// 3. Directory scan of <root>/.copilot/otel/*.jsonl.
|
|
43
|
+
const otelDir = path.dirname(getCopilotOtelPath(root));
|
|
44
|
+
try {
|
|
45
|
+
const entries = await fs.readdir(otelDir, { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (entry.name.toLowerCase().endsWith(".jsonl")) {
|
|
51
|
+
candidatePaths.push(path.join(otelDir, entry.name));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (isPermissionError(error)) {
|
|
57
|
+
warnings.push(`Failed to read Copilot OTEL directory ${otelDir}: permission denied.`);
|
|
58
|
+
}
|
|
59
|
+
// ENOENT (missing directory) and similar are not errors — skip.
|
|
60
|
+
}
|
|
61
|
+
// Resolve, de-dup by path (first wins), and keep readable regular files.
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const files = [];
|
|
64
|
+
for (const candidate of candidatePaths) {
|
|
65
|
+
const resolved = path.resolve(candidate);
|
|
66
|
+
const key = dedupKey(resolved);
|
|
67
|
+
if (seen.has(key)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
seen.add(key);
|
|
71
|
+
try {
|
|
72
|
+
const stats = await fs.stat(resolved);
|
|
73
|
+
if (!stats.isFile()) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
await fs.access(resolved, fs.constants.R_OK);
|
|
77
|
+
files.push({ path: resolved, modifiedAtMs: stats.mtimeMs });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (isPermissionError(error)) {
|
|
81
|
+
warnings.push(`Failed to read Copilot OTEL file ${resolved}: permission denied.`);
|
|
82
|
+
}
|
|
83
|
+
// Missing file or other failures simply drop the candidate.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Stable sort by modifiedAtMs ASC, then path ASC.
|
|
87
|
+
files.sort((a, b) => {
|
|
88
|
+
if (a.modifiedAtMs !== b.modifiedAtMs) {
|
|
89
|
+
return a.modifiedAtMs - b.modifiedAtMs;
|
|
90
|
+
}
|
|
91
|
+
return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
|
|
92
|
+
});
|
|
93
|
+
return { files, warnings };
|
|
94
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { asRecord } from "../../limits.js";
|
|
4
|
+
const INPUT_KEY = "gen_ai.usage.input_tokens";
|
|
5
|
+
const OUTPUT_KEY = "gen_ai.usage.output_tokens";
|
|
6
|
+
const CACHE_READ_KEYS = [
|
|
7
|
+
"gen_ai.usage.cache_read.input_tokens",
|
|
8
|
+
"gen_ai.usage.cache_read_input_tokens"
|
|
9
|
+
];
|
|
10
|
+
const CACHE_WRITE_KEYS = [
|
|
11
|
+
"gen_ai.usage.cache_write.input_tokens",
|
|
12
|
+
"gen_ai.usage.cache_creation.input_tokens",
|
|
13
|
+
"gen_ai.usage.cache_write_input_tokens",
|
|
14
|
+
"gen_ai.usage.cache_creation_input_tokens"
|
|
15
|
+
];
|
|
16
|
+
const REASONING_KEYS = [
|
|
17
|
+
"gen_ai.usage.reasoning.output_tokens",
|
|
18
|
+
"gen_ai.usage.reasoning_tokens"
|
|
19
|
+
];
|
|
20
|
+
// Completion time first, then start, then the high-resolution clocks.
|
|
21
|
+
const TIMESTAMP_KEYS = ["endTime", "startTime", "hrTime", "_hrTime", "time"];
|
|
22
|
+
/**
|
|
23
|
+
* Stream the discovered JSONL files and produce de-duplicated canonical Copilot
|
|
24
|
+
* chat token events. Only records explicitly recognizable as chat spans are
|
|
25
|
+
* kept (see {@link isChatSpan}); anything else is ignored rather than guessed.
|
|
26
|
+
*/
|
|
27
|
+
export async function parseCopilotOtelFiles(files) {
|
|
28
|
+
const events = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const warnings = [];
|
|
31
|
+
let linesRead = 0;
|
|
32
|
+
let malformedLines = 0;
|
|
33
|
+
let duplicatesRemoved = 0;
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
try {
|
|
36
|
+
await readJsonlFile(file, (payload, lineNumber) => {
|
|
37
|
+
linesRead += 1;
|
|
38
|
+
if (payload === MALFORMED) {
|
|
39
|
+
malformedLines += 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const event = toUsageEvent(payload, file, lineNumber);
|
|
43
|
+
if (!event) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const key = event.traceId && event.spanId
|
|
47
|
+
? `${event.traceId}:${event.spanId}`
|
|
48
|
+
: `${event.filePath}:${event.lineNumber}`;
|
|
49
|
+
if (seen.has(key)) {
|
|
50
|
+
duplicatesRemoved += 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
seen.add(key);
|
|
54
|
+
events.push(event);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const reason = isPermissionError(error) ? "permission denied" : "read error";
|
|
59
|
+
warnings.push(`Failed to read Copilot OTEL file ${file.path}: ${reason}.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
events.sort(compareEvents);
|
|
63
|
+
return { events, filesScanned: files.length, linesRead, malformedLines, duplicatesRemoved, warnings };
|
|
64
|
+
}
|
|
65
|
+
const MALFORMED = Symbol("malformed");
|
|
66
|
+
async function readJsonlFile(file, onLine) {
|
|
67
|
+
const stream = fs.createReadStream(file.path, { encoding: "utf8" });
|
|
68
|
+
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
69
|
+
let lineNumber = 0;
|
|
70
|
+
try {
|
|
71
|
+
for await (const line of lineReader) {
|
|
72
|
+
lineNumber += 1;
|
|
73
|
+
if (!line.trim()) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
onLine(JSON.parse(line), lineNumber);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
onLine(MALFORMED, lineNumber);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
lineReader.close();
|
|
86
|
+
stream.destroy();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function toUsageEvent(payload, file, lineNumber) {
|
|
90
|
+
const record = asRecord(payload);
|
|
91
|
+
if (!record) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const attributes = flatAttributes(record.attributes);
|
|
95
|
+
if (!attributes || !isChatSpan(record, attributes)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const inputTokens = tokenValue(attributes[INPUT_KEY]);
|
|
99
|
+
const outputTokens = tokenValue(attributes[OUTPUT_KEY]);
|
|
100
|
+
const cacheReadInputTokens = firstTokenValue(attributes, CACHE_READ_KEYS);
|
|
101
|
+
const cacheWriteInputTokens = firstTokenValue(attributes, CACHE_WRITE_KEYS);
|
|
102
|
+
const reasoningOutputTokens = firstTokenValue(attributes, REASONING_KEYS);
|
|
103
|
+
if (inputTokens <= 0 &&
|
|
104
|
+
outputTokens <= 0 &&
|
|
105
|
+
cacheReadInputTokens <= 0 &&
|
|
106
|
+
cacheWriteInputTokens <= 0 &&
|
|
107
|
+
reasoningOutputTokens <= 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const hasCache = CACHE_READ_KEYS.some((k) => attributes[k] !== undefined) ||
|
|
111
|
+
CACHE_WRITE_KEYS.some((k) => attributes[k] !== undefined);
|
|
112
|
+
const cacheStatus = hasCache ? "known" : "unavailable";
|
|
113
|
+
const event = {
|
|
114
|
+
timestampMs: resolveTimestampMs(record) ?? file.modifiedAtMs,
|
|
115
|
+
modelId: resolveModelId(record, attributes),
|
|
116
|
+
inputTokens,
|
|
117
|
+
outputTokens,
|
|
118
|
+
cacheReadInputTokens,
|
|
119
|
+
cacheWriteInputTokens,
|
|
120
|
+
reasoningOutputTokens,
|
|
121
|
+
cacheReadStatus: cacheStatus,
|
|
122
|
+
cacheWriteStatus: cacheStatus,
|
|
123
|
+
filePath: file.path,
|
|
124
|
+
lineNumber
|
|
125
|
+
};
|
|
126
|
+
const traceId = stringValue(attributes["gen_ai.trace.id"]) ??
|
|
127
|
+
stringValue(attributes.trace_id) ??
|
|
128
|
+
stringValue(record.traceId) ??
|
|
129
|
+
spanContextValue(record, "traceId");
|
|
130
|
+
if (traceId) {
|
|
131
|
+
event.traceId = traceId;
|
|
132
|
+
}
|
|
133
|
+
const spanId = stringValue(record.spanId) ??
|
|
134
|
+
stringValue(attributes.span_id) ??
|
|
135
|
+
spanContextValue(record, "spanId");
|
|
136
|
+
if (spanId) {
|
|
137
|
+
event.spanId = spanId;
|
|
138
|
+
}
|
|
139
|
+
const responseId = stringValue(attributes["gen_ai.response.id"]);
|
|
140
|
+
if (responseId) {
|
|
141
|
+
event.responseId = responseId;
|
|
142
|
+
}
|
|
143
|
+
return event;
|
|
144
|
+
}
|
|
145
|
+
/** A canonical Copilot chat span: the only record kind we count. */
|
|
146
|
+
function isChatSpan(record, attributes) {
|
|
147
|
+
const operation = stringValue(attributes["gen_ai.operation.name"]);
|
|
148
|
+
const name = stringValue(record.name) ?? "";
|
|
149
|
+
return operation === "chat" || name === "chat" || name.startsWith("chat ");
|
|
150
|
+
}
|
|
151
|
+
function resolveModelId(record, attributes) {
|
|
152
|
+
return (stringValue(attributes["gen_ai.response.model"]) ??
|
|
153
|
+
stringValue(attributes["gen_ai.request.model"]) ??
|
|
154
|
+
stringValue(record.model) ??
|
|
155
|
+
"unknown");
|
|
156
|
+
}
|
|
157
|
+
function flatAttributes(value) {
|
|
158
|
+
return Array.isArray(value) ? null : asRecord(value);
|
|
159
|
+
}
|
|
160
|
+
function spanContextValue(record, key) {
|
|
161
|
+
const spanContext = asRecord(record.spanContext);
|
|
162
|
+
return spanContext ? stringValue(spanContext[key]) : undefined;
|
|
163
|
+
}
|
|
164
|
+
function resolveTimestampMs(record) {
|
|
165
|
+
for (const key of TIMESTAMP_KEYS) {
|
|
166
|
+
const ms = timestampToMs(record[key]);
|
|
167
|
+
if (ms !== undefined) {
|
|
168
|
+
return ms;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
function timestampToMs(value) {
|
|
174
|
+
// [seconds, nanoseconds] hrTime/startTime/endTime form.
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
const [seconds, nanoseconds] = value;
|
|
177
|
+
if (typeof seconds === "number" && typeof nanoseconds === "number") {
|
|
178
|
+
return seconds * 1000 + nanoseconds / 1000000;
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
183
|
+
return value < 1e11 ? value * 1000 : value; // unix seconds vs milliseconds
|
|
184
|
+
}
|
|
185
|
+
if (typeof value === "string") {
|
|
186
|
+
const trimmed = value.trim();
|
|
187
|
+
if (!trimmed) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
if (/^\d+$/.test(trimmed)) {
|
|
191
|
+
const n = Number(trimmed);
|
|
192
|
+
return n < 1e11 ? n * 1000 : n;
|
|
193
|
+
}
|
|
194
|
+
const parsed = Date.parse(trimmed);
|
|
195
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
function firstTokenValue(attributes, keys) {
|
|
200
|
+
for (const key of keys) {
|
|
201
|
+
if (attributes[key] !== undefined) {
|
|
202
|
+
return tokenValue(attributes[key]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
function tokenValue(value) {
|
|
208
|
+
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
209
|
+
return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 0;
|
|
210
|
+
}
|
|
211
|
+
function stringValue(value) {
|
|
212
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
213
|
+
}
|
|
214
|
+
function compareEvents(a, b) {
|
|
215
|
+
if (a.timestampMs !== b.timestampMs) {
|
|
216
|
+
return a.timestampMs - b.timestampMs;
|
|
217
|
+
}
|
|
218
|
+
if (a.filePath !== b.filePath) {
|
|
219
|
+
return a.filePath < b.filePath ? -1 : 1;
|
|
220
|
+
}
|
|
221
|
+
return a.lineNumber - b.lineNumber;
|
|
222
|
+
}
|
|
223
|
+
function isPermissionError(error) {
|
|
224
|
+
const code = error && typeof error === "object" && "code" in error
|
|
225
|
+
? error.code
|
|
226
|
+
: undefined;
|
|
227
|
+
return code === "EACCES" || code === "EPERM";
|
|
228
|
+
}
|