snapfail 0.0.20 → 0.0.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/dist/index.js +118 -84
- package/package.json +1 -1
- package/src/index.ts +4 -2
package/dist/index.js
CHANGED
|
@@ -95,7 +95,9 @@ import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
|
95
95
|
import { resolve } from "path";
|
|
96
96
|
var DEFAULT_ENDPOINT = "https://app.snapfail.com";
|
|
97
97
|
var ENV_FILE = ".env";
|
|
98
|
-
function loadConfig(cwd = process.cwd()) {
|
|
98
|
+
function loadConfig(cwd = process.cwd(), pk) {
|
|
99
|
+
if (pk)
|
|
100
|
+
return { projectKey: pk, endpoint: DEFAULT_ENDPOINT };
|
|
99
101
|
const fromEnv = process.env["SNAPFAIL_PROJECT_KEY"];
|
|
100
102
|
if (fromEnv)
|
|
101
103
|
return { projectKey: fromEnv, endpoint: DEFAULT_ENDPOINT };
|
|
@@ -127,6 +129,37 @@ ${line}
|
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
// src/session.ts
|
|
133
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2, rmSync } from "fs";
|
|
134
|
+
import { join } from "path";
|
|
135
|
+
import { homedir } from "os";
|
|
136
|
+
var SESSION_DIR = join(homedir(), ".snapfail");
|
|
137
|
+
var SESSION_FILE = join(SESSION_DIR, "auth.json");
|
|
138
|
+
function readSession() {
|
|
139
|
+
try {
|
|
140
|
+
if (!existsSync2(SESSION_FILE))
|
|
141
|
+
return null;
|
|
142
|
+
const raw = JSON.parse(readFileSync2(SESSION_FILE, "utf-8"));
|
|
143
|
+
if (!raw.token || !raw.endpoint)
|
|
144
|
+
return null;
|
|
145
|
+
return raw;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function writeSession(session) {
|
|
151
|
+
mkdirSync(SESSION_DIR, { recursive: true });
|
|
152
|
+
writeFileSync2(SESSION_FILE, JSON.stringify(session, null, 2) + `
|
|
153
|
+
`, "utf-8");
|
|
154
|
+
}
|
|
155
|
+
function requireSession() {
|
|
156
|
+
const session = readSession();
|
|
157
|
+
if (!session) {
|
|
158
|
+
throw new Error('No active session. Run "snapfail init" to authenticate.');
|
|
159
|
+
}
|
|
160
|
+
return session;
|
|
161
|
+
}
|
|
162
|
+
|
|
130
163
|
// src/api.ts
|
|
131
164
|
async function apiFetch(url, options) {
|
|
132
165
|
let res;
|
|
@@ -234,6 +267,68 @@ function formatIncidentList(data, total, status) {
|
|
|
234
267
|
${bold(String(total))} ${status ?? "unresolved"} incident${total !== 1 ? "s" : ""}`;
|
|
235
268
|
return table + summary;
|
|
236
269
|
}
|
|
270
|
+
function formatLLMContext(group, samples) {
|
|
271
|
+
const lines = [];
|
|
272
|
+
lines.push(`# Incident: ${group.title}`);
|
|
273
|
+
lines.push(`Group ID: ${group.id}`);
|
|
274
|
+
lines.push(`Fingerprint: ${group.fingerprint}`);
|
|
275
|
+
lines.push(`Occurrences: ${group.count} | First: ${new Date(group.firstSeen).toISOString()} | Last: ${new Date(group.lastSeen).toISOString()}`);
|
|
276
|
+
lines.push(`Environments: ${group.environments.join(", ")}`);
|
|
277
|
+
lines.push(`Status: ${group.status}`);
|
|
278
|
+
lines.push("");
|
|
279
|
+
lines.push("## Error");
|
|
280
|
+
lines.push(`Type: ${group.errorType}`);
|
|
281
|
+
lines.push(`Message: ${samples[0]?.errorMessage ?? group.title}`);
|
|
282
|
+
lines.push(`Normalized: ${samples[0]?.normalizedMessage ?? group.title}`);
|
|
283
|
+
if (samples[0] && samples[0].stackFrames.length > 0) {
|
|
284
|
+
lines.push("");
|
|
285
|
+
lines.push("## Stack");
|
|
286
|
+
for (const f of samples[0].stackFrames) {
|
|
287
|
+
lines.push(` ${f.fn ?? "(anonymous)"} ${f.file}:${f.line}${f.col != null ? `:${f.col}` : ""}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (let i = 0;i < samples.length; i++) {
|
|
291
|
+
const s = samples[i];
|
|
292
|
+
lines.push("");
|
|
293
|
+
lines.push(`## Sample ${i + 1} / ${group.sampleIds.length}`);
|
|
294
|
+
lines.push(`Device: ${s.device.userAgent}`);
|
|
295
|
+
if (s.device.viewport) {
|
|
296
|
+
lines.push(`Viewport: ${s.device.viewport.width}x${s.device.viewport.height} Language: ${s.device.language ?? "unknown"}`);
|
|
297
|
+
}
|
|
298
|
+
lines.push(`URL: ${s.url}`);
|
|
299
|
+
if (s.route)
|
|
300
|
+
lines.push(`Route: ${s.route}`);
|
|
301
|
+
lines.push(`Environment: ${s.environmentMode}`);
|
|
302
|
+
if (s.consoleEntries.length > 0) {
|
|
303
|
+
lines.push("");
|
|
304
|
+
lines.push("### Console");
|
|
305
|
+
for (const e of s.consoleEntries) {
|
|
306
|
+
const args = e.args.map((a) => String(a)).join(" ");
|
|
307
|
+
lines.push(` [${e.level}] ${args}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (s.networkEntries.length > 0) {
|
|
311
|
+
lines.push("");
|
|
312
|
+
lines.push("### Network");
|
|
313
|
+
for (const n of s.networkEntries) {
|
|
314
|
+
const status = n.status ? ` \u2192 ${n.status}` : n.error ? ` \u2192 ERROR` : "";
|
|
315
|
+
lines.push(` ${n.method} ${n.url}${status} (${n.durationMs}ms)`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (s.timeline.length > 0) {
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push("### Timeline");
|
|
321
|
+
for (const e of s.timeline) {
|
|
322
|
+
lines.push(` +${e.t}ms ${e.kind}: ${e.summary}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (i < samples.length - 1)
|
|
326
|
+
lines.push(`
|
|
327
|
+
---`);
|
|
328
|
+
}
|
|
329
|
+
return lines.join(`
|
|
330
|
+
`);
|
|
331
|
+
}
|
|
237
332
|
function formatIncidentDetail(group, sample) {
|
|
238
333
|
const lines = [];
|
|
239
334
|
lines.push(bold(`${group.errorType}: ${truncate(group.title, 80)}`));
|
|
@@ -285,7 +380,8 @@ function formatIncidentDetail(group, sample) {
|
|
|
285
380
|
|
|
286
381
|
// src/commands/incidents.ts
|
|
287
382
|
async function runIncidents(opts) {
|
|
288
|
-
|
|
383
|
+
requireSession();
|
|
384
|
+
const config = loadConfig(process.cwd(), opts.pk);
|
|
289
385
|
const result = await fetchIncidents(config, {
|
|
290
386
|
status: opts.status ?? "unresolved",
|
|
291
387
|
limit: opts.limit,
|
|
@@ -301,7 +397,8 @@ async function runIncidents(opts) {
|
|
|
301
397
|
|
|
302
398
|
// src/commands/incident.ts
|
|
303
399
|
async function runIncident(opts) {
|
|
304
|
-
|
|
400
|
+
requireSession();
|
|
401
|
+
const config = loadConfig(process.cwd(), opts.pk);
|
|
305
402
|
if (opts.delete) {
|
|
306
403
|
const ok = await deleteGroup(config, opts.id);
|
|
307
404
|
if (!ok) {
|
|
@@ -1902,30 +1999,6 @@ Config file: \`.snapfail.json\` (gitignored)
|
|
|
1902
1999
|
`;
|
|
1903
2000
|
}
|
|
1904
2001
|
|
|
1905
|
-
// src/session.ts
|
|
1906
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2, rmSync } from "fs";
|
|
1907
|
-
import { join } from "path";
|
|
1908
|
-
import { homedir } from "os";
|
|
1909
|
-
var SESSION_DIR = join(homedir(), ".snapfail");
|
|
1910
|
-
var SESSION_FILE = join(SESSION_DIR, "auth.json");
|
|
1911
|
-
function readSession() {
|
|
1912
|
-
try {
|
|
1913
|
-
if (!existsSync2(SESSION_FILE))
|
|
1914
|
-
return null;
|
|
1915
|
-
const raw = JSON.parse(readFileSync2(SESSION_FILE, "utf-8"));
|
|
1916
|
-
if (!raw.token || !raw.endpoint)
|
|
1917
|
-
return null;
|
|
1918
|
-
return raw;
|
|
1919
|
-
} catch {
|
|
1920
|
-
return null;
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
function writeSession(session) {
|
|
1924
|
-
mkdirSync(SESSION_DIR, { recursive: true });
|
|
1925
|
-
writeFileSync2(SESSION_FILE, JSON.stringify(session, null, 2) + `
|
|
1926
|
-
`, "utf-8");
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
2002
|
// src/auth.ts
|
|
1930
2003
|
import { execSync } from "child_process";
|
|
1931
2004
|
function openBrowser(url) {
|
|
@@ -2295,69 +2368,28 @@ async function runInit(cwd = process.cwd()) {
|
|
|
2295
2368
|
}
|
|
2296
2369
|
|
|
2297
2370
|
// src/commands/explain.ts
|
|
2298
|
-
async function fetchDiagnosis(config, id, force) {
|
|
2299
|
-
const url = new URL(`${config.endpoint}/api/diagnose/${id}`);
|
|
2300
|
-
if (force)
|
|
2301
|
-
url.searchParams.set("force", "1");
|
|
2302
|
-
let res;
|
|
2303
|
-
try {
|
|
2304
|
-
res = await fetch(url.toString(), {
|
|
2305
|
-
method: "POST",
|
|
2306
|
-
headers: { "X-Project-Key": config.projectKey, "Content-Type": "application/json" },
|
|
2307
|
-
body: "{}"
|
|
2308
|
-
});
|
|
2309
|
-
} catch (err) {
|
|
2310
|
-
throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2311
|
-
}
|
|
2312
|
-
if (res.status === 401)
|
|
2313
|
-
throw new Error("Unauthorized: check your project key.");
|
|
2314
|
-
if (res.status === 404)
|
|
2315
|
-
return null;
|
|
2316
|
-
if (res.status === 422)
|
|
2317
|
-
throw new Error("No samples available to diagnose this incident.");
|
|
2318
|
-
if (!res.ok) {
|
|
2319
|
-
const body = await res.text().catch(() => "");
|
|
2320
|
-
throw new Error(`API error ${res.status}: ${body}`);
|
|
2321
|
-
}
|
|
2322
|
-
return res.json();
|
|
2323
|
-
}
|
|
2324
|
-
function confidenceColor(c3) {
|
|
2325
|
-
if (c3 === "high")
|
|
2326
|
-
return green(c3);
|
|
2327
|
-
if (c3 === "medium")
|
|
2328
|
-
return yellow(c3);
|
|
2329
|
-
return red(c3);
|
|
2330
|
-
}
|
|
2331
|
-
function formatDiagnosis(d) {
|
|
2332
|
-
const lines = [];
|
|
2333
|
-
lines.push(bold("Root cause"));
|
|
2334
|
-
lines.push(` ${d.rootCause}`);
|
|
2335
|
-
lines.push("");
|
|
2336
|
-
lines.push(bold("In plain language"));
|
|
2337
|
-
lines.push(` ${d.plainSummary}`);
|
|
2338
|
-
if (d.fixPrompt) {
|
|
2339
|
-
lines.push("");
|
|
2340
|
-
lines.push(bold("Fix prompt") + dim(" (paste into your AI assistant)"));
|
|
2341
|
-
lines.push(` ${d.fixPrompt}`);
|
|
2342
|
-
}
|
|
2343
|
-
lines.push("");
|
|
2344
|
-
lines.push(dim("Confidence:") + " " + confidenceColor(d.confidence) + dim(" \xB7 analyzed ") + d.analyzedSampleIds.length + dim(" samples \xB7 model: ") + dim(d.model));
|
|
2345
|
-
return lines.join(`
|
|
2346
|
-
`);
|
|
2347
|
-
}
|
|
2348
2371
|
async function runExplain(opts) {
|
|
2349
|
-
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2372
|
+
requireSession();
|
|
2373
|
+
const config = loadConfig(process.cwd(), opts.pk);
|
|
2374
|
+
const result = await fetchIncident(config, opts.id);
|
|
2375
|
+
if (!result) {
|
|
2352
2376
|
console.error(`Incident ${opts.id} not found.`);
|
|
2353
2377
|
process.exit(1);
|
|
2354
2378
|
}
|
|
2379
|
+
const { group, sample } = result;
|
|
2380
|
+
const samples = [sample];
|
|
2381
|
+
for (let i2 = 1;i2 < Math.min(group.sampleIds.length, 3); i2++) {
|
|
2382
|
+
const s = await fetchSample(config, opts.id, i2);
|
|
2383
|
+
if (s)
|
|
2384
|
+
samples.push(s);
|
|
2385
|
+
}
|
|
2355
2386
|
if (opts.json) {
|
|
2356
|
-
process.stdout.write(JSON.stringify(
|
|
2387
|
+
process.stdout.write(JSON.stringify({ group, samples }, null, 2) + `
|
|
2357
2388
|
`);
|
|
2358
2389
|
return;
|
|
2359
2390
|
}
|
|
2360
|
-
|
|
2391
|
+
process.stdout.write(formatLLMContext(group, samples) + `
|
|
2392
|
+
`);
|
|
2361
2393
|
}
|
|
2362
2394
|
|
|
2363
2395
|
// src/index.ts
|
|
@@ -2384,6 +2416,7 @@ var VERSION = "0.0.18";
|
|
|
2384
2416
|
async function main() {
|
|
2385
2417
|
const { command, args, flags } = parseArgs(process.argv);
|
|
2386
2418
|
const json = flags["json"] === true;
|
|
2419
|
+
const pk = typeof flags["pk"] === "string" ? flags["pk"] : undefined;
|
|
2387
2420
|
if (command === "--version" || command === "-v" || flags["version"] === true) {
|
|
2388
2421
|
console.log(VERSION);
|
|
2389
2422
|
return;
|
|
@@ -2396,6 +2429,7 @@ async function main() {
|
|
|
2396
2429
|
if (command === "incidents") {
|
|
2397
2430
|
await runIncidents({
|
|
2398
2431
|
json,
|
|
2432
|
+
pk,
|
|
2399
2433
|
status: typeof flags["status"] === "string" ? flags["status"] : undefined,
|
|
2400
2434
|
limit: typeof flags["limit"] === "string" ? parseInt(flags["limit"]) : undefined,
|
|
2401
2435
|
offset: typeof flags["offset"] === "string" ? parseInt(flags["offset"]) : undefined
|
|
@@ -2410,7 +2444,7 @@ async function main() {
|
|
|
2410
2444
|
}
|
|
2411
2445
|
const sampleFlag = flags["sample"];
|
|
2412
2446
|
const sample = typeof sampleFlag === "string" ? parseInt(sampleFlag) : undefined;
|
|
2413
|
-
await runIncident({ id, sample, json, delete: flags["delete"] === true });
|
|
2447
|
+
await runIncident({ id, sample, json, pk, delete: flags["delete"] === true });
|
|
2414
2448
|
return;
|
|
2415
2449
|
}
|
|
2416
2450
|
if (command === "explain") {
|
|
@@ -2419,7 +2453,7 @@ async function main() {
|
|
|
2419
2453
|
console.error("Usage: snapfail explain <id> [--force] [--json]");
|
|
2420
2454
|
process.exit(1);
|
|
2421
2455
|
}
|
|
2422
|
-
await runExplain({ id,
|
|
2456
|
+
await runExplain({ id, json, pk });
|
|
2423
2457
|
return;
|
|
2424
2458
|
}
|
|
2425
2459
|
console.error(`Unknown command: ${command}`);
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ const VERSION = "0.0.18";
|
|
|
35
35
|
async function main(): Promise<void> {
|
|
36
36
|
const { command, args, flags } = parseArgs(process.argv);
|
|
37
37
|
const json = flags["json"] === true;
|
|
38
|
+
const pk = typeof flags["pk"] === "string" ? flags["pk"] : undefined;
|
|
38
39
|
|
|
39
40
|
if (command === "--version" || command === "-v" || flags["version"] === true) {
|
|
40
41
|
console.log(VERSION);
|
|
@@ -50,6 +51,7 @@ async function main(): Promise<void> {
|
|
|
50
51
|
if (command === "incidents") {
|
|
51
52
|
await runIncidents({
|
|
52
53
|
json,
|
|
54
|
+
pk,
|
|
53
55
|
status: typeof flags["status"] === "string" ? flags["status"] : undefined,
|
|
54
56
|
limit: typeof flags["limit"] === "string" ? parseInt(flags["limit"]) : undefined,
|
|
55
57
|
offset: typeof flags["offset"] === "string" ? parseInt(flags["offset"]) : undefined,
|
|
@@ -66,7 +68,7 @@ async function main(): Promise<void> {
|
|
|
66
68
|
const sampleFlag = flags["sample"];
|
|
67
69
|
const sample =
|
|
68
70
|
typeof sampleFlag === "string" ? parseInt(sampleFlag) : undefined;
|
|
69
|
-
await runIncident({ id, sample, json, delete: flags["delete"] === true });
|
|
71
|
+
await runIncident({ id, sample, json, pk, delete: flags["delete"] === true });
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -76,7 +78,7 @@ async function main(): Promise<void> {
|
|
|
76
78
|
console.error("Usage: snapfail explain <id> [--force] [--json]");
|
|
77
79
|
process.exit(1);
|
|
78
80
|
}
|
|
79
|
-
await runExplain({ id,
|
|
81
|
+
await runExplain({ id, json, pk });
|
|
80
82
|
return;
|
|
81
83
|
}
|
|
82
84
|
|