snapfail 0.0.26 → 0.0.28
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 +243 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1692,6 +1692,36 @@ function requireSession() {
|
|
|
1692
1692
|
}
|
|
1693
1693
|
|
|
1694
1694
|
// src/api.ts
|
|
1695
|
+
async function listSuppressionRules(token, projectKey) {
|
|
1696
|
+
const url = `${DEFAULT_ENDPOINT}/api/cli/suppress?projectKey=${encodeURIComponent(projectKey)}`;
|
|
1697
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
1698
|
+
if (!res.ok)
|
|
1699
|
+
throw new Error(`API error ${res.status}`);
|
|
1700
|
+
const { rules } = await res.json();
|
|
1701
|
+
return rules;
|
|
1702
|
+
}
|
|
1703
|
+
async function addSuppressionRule(token, projectKey, field, pattern) {
|
|
1704
|
+
const res = await fetch(`${DEFAULT_ENDPOINT}/api/cli/suppress`, {
|
|
1705
|
+
method: "POST",
|
|
1706
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
1707
|
+
body: JSON.stringify({ projectKey, field, pattern })
|
|
1708
|
+
});
|
|
1709
|
+
if (!res.ok) {
|
|
1710
|
+
const body = await res.text().catch(() => "");
|
|
1711
|
+
throw new Error(`API error ${res.status}: ${body}`);
|
|
1712
|
+
}
|
|
1713
|
+
const { rule } = await res.json();
|
|
1714
|
+
return rule;
|
|
1715
|
+
}
|
|
1716
|
+
async function removeSuppressionRule(token, projectKey, id) {
|
|
1717
|
+
const url = `${DEFAULT_ENDPOINT}/api/cli/suppress/${id}?projectKey=${encodeURIComponent(projectKey)}`;
|
|
1718
|
+
const res = await fetch(url, {
|
|
1719
|
+
method: "DELETE",
|
|
1720
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1721
|
+
});
|
|
1722
|
+
if (!res.ok)
|
|
1723
|
+
throw new Error(`API error ${res.status}`);
|
|
1724
|
+
}
|
|
1695
1725
|
async function apiFetch(url, options) {
|
|
1696
1726
|
let res;
|
|
1697
1727
|
try {
|
|
@@ -1799,10 +1829,11 @@ function formatIncidentList(data, total, status) {
|
|
|
1799
1829
|
cyan(truncate(g2.id, 12)),
|
|
1800
1830
|
truncate(g2.title, 38),
|
|
1801
1831
|
String(g2.count),
|
|
1832
|
+
g2.affectedUsers > 0 ? String(g2.affectedUsers) : dim("\u2014"),
|
|
1802
1833
|
g2.environments.join(","),
|
|
1803
1834
|
relativeTime(g2.lastSeen)
|
|
1804
1835
|
]);
|
|
1805
|
-
const table = formatTable(rows, ["ID", "TITLE", "COUNT", "ENV", "LAST SEEN"]);
|
|
1836
|
+
const table = formatTable(rows, ["ID", "TITLE", "COUNT", "USERS", "ENV", "LAST SEEN"]);
|
|
1806
1837
|
const summary = `
|
|
1807
1838
|
${bold(String(total))} ${status ?? "unresolved"} incident${total !== 1 ? "s" : ""}`;
|
|
1808
1839
|
return table + summary;
|
|
@@ -1872,7 +1903,7 @@ function formatLLMContext(group, samples) {
|
|
|
1872
1903
|
function formatIncidentDetail(group, sample) {
|
|
1873
1904
|
const lines = [];
|
|
1874
1905
|
lines.push(bold(`${group.errorType}: ${truncate(group.title, 80)}`));
|
|
1875
|
-
lines.push(`${dim("Status:")} ${group.status} ${dim("\xB7")} ${group.count} occurrences ${dim("\xB7")} ${group.environments.join(", ")}`);
|
|
1906
|
+
lines.push(`${dim("Status:")} ${group.status} ${dim("\xB7")} ${group.count} occurrences ${dim("\xB7")} ${group.affectedUsers > 0 ? `${group.affectedUsers} users ${dim("\xB7")} ` : ""}${group.environments.join(", ")}`);
|
|
1876
1907
|
lines.push(`${dim("First seen:")} ${new Date(group.firstSeen).toISOString().slice(0, 10)} ${dim("\xB7")} ${dim("Last seen:")} ${relativeTime(group.lastSeen)}`);
|
|
1877
1908
|
if (sample.stackFrames.length > 0) {
|
|
1878
1909
|
lines.push("");
|
|
@@ -2551,6 +2582,163 @@ async function runLogin() {
|
|
|
2551
2582
|
Logged in as ${session.email}`);
|
|
2552
2583
|
}
|
|
2553
2584
|
|
|
2585
|
+
// src/commands/suppress.ts
|
|
2586
|
+
var FIELD_LABELS = {
|
|
2587
|
+
message: "Error message contains",
|
|
2588
|
+
useragent: "User-agent contains",
|
|
2589
|
+
url: "URL contains",
|
|
2590
|
+
script_origin: "Script origin contains"
|
|
2591
|
+
};
|
|
2592
|
+
async function runSuppress(opts) {
|
|
2593
|
+
const session = requireSession();
|
|
2594
|
+
const projectKey = await resolveProjectKey(opts.pk, !opts.json, session.token);
|
|
2595
|
+
const config = loadConfig(process.cwd(), projectKey);
|
|
2596
|
+
const sub = opts.subcommand;
|
|
2597
|
+
if (!sub || sub === "list") {
|
|
2598
|
+
const rules = await listSuppressionRules(session.token, projectKey);
|
|
2599
|
+
if (opts.json) {
|
|
2600
|
+
process.stdout.write(JSON.stringify({ rules }, null, 2) + `
|
|
2601
|
+
`);
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
if (rules.length === 0) {
|
|
2605
|
+
console.log("No suppression rules configured.");
|
|
2606
|
+
console.log("Add one with: snapfail suppress add or snapfail suppress <incident-id>");
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
console.log(`
|
|
2610
|
+
Suppression rules for ${projectKey}
|
|
2611
|
+
`);
|
|
2612
|
+
for (const rule2 of rules) {
|
|
2613
|
+
const label = FIELD_LABELS[rule2.field] ?? rule2.field;
|
|
2614
|
+
console.log(` [${rule2.id.slice(0, 8)}] ${label}: "${rule2.pattern}"`);
|
|
2615
|
+
}
|
|
2616
|
+
console.log(`
|
|
2617
|
+
Remove a rule: snapfail suppress delete <id-prefix>`);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (sub === "delete") {
|
|
2621
|
+
const id = opts.args[0];
|
|
2622
|
+
if (!id) {
|
|
2623
|
+
console.error("Usage: snapfail suppress delete <rule-id>");
|
|
2624
|
+
process.exit(1);
|
|
2625
|
+
}
|
|
2626
|
+
const rules = await listSuppressionRules(session.token, projectKey);
|
|
2627
|
+
const match = rules.find((r2) => r2.id.startsWith(id));
|
|
2628
|
+
if (!match) {
|
|
2629
|
+
console.error(`No rule found with id starting with "${id}"`);
|
|
2630
|
+
process.exit(1);
|
|
2631
|
+
}
|
|
2632
|
+
await removeSuppressionRule(session.token, projectKey, match.id);
|
|
2633
|
+
if (opts.json) {
|
|
2634
|
+
process.stdout.write(JSON.stringify({ ok: true, deleted: match.id }) + `
|
|
2635
|
+
`);
|
|
2636
|
+
} else {
|
|
2637
|
+
const label = FIELD_LABELS[match.field] ?? match.field;
|
|
2638
|
+
console.log(`Deleted rule: ${label} "${match.pattern}"`);
|
|
2639
|
+
}
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
if (sub === "add") {
|
|
2643
|
+
const field = await select({
|
|
2644
|
+
message: "What field should the rule match on?",
|
|
2645
|
+
options: Object.entries(FIELD_LABELS).map(([value, label]) => ({
|
|
2646
|
+
value,
|
|
2647
|
+
label
|
|
2648
|
+
}))
|
|
2649
|
+
});
|
|
2650
|
+
if (isCancel(field)) {
|
|
2651
|
+
cancel("Cancelled.");
|
|
2652
|
+
process.exit(0);
|
|
2653
|
+
}
|
|
2654
|
+
const pattern = await text({
|
|
2655
|
+
message: "Pattern (case-insensitive substring)",
|
|
2656
|
+
placeholder: "e.g. Instagram",
|
|
2657
|
+
validate: (v) => (v ?? "").trim() ? undefined : "Pattern is required."
|
|
2658
|
+
});
|
|
2659
|
+
if (isCancel(pattern)) {
|
|
2660
|
+
cancel("Cancelled.");
|
|
2661
|
+
process.exit(0);
|
|
2662
|
+
}
|
|
2663
|
+
const rule2 = await addSuppressionRule(session.token, projectKey, field, pattern.trim());
|
|
2664
|
+
console.log(`
|
|
2665
|
+
Rule added: ${FIELD_LABELS[rule2.field]} "${rule2.pattern}"`);
|
|
2666
|
+
console.log("Future incidents matching this pattern will be silently dropped at ingest.");
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
const incidentId = sub;
|
|
2670
|
+
const result = await fetchIncident(config, incidentId);
|
|
2671
|
+
if (!result) {
|
|
2672
|
+
console.error(`Incident ${incidentId} not found.`);
|
|
2673
|
+
process.exit(1);
|
|
2674
|
+
}
|
|
2675
|
+
const { group, sample } = result;
|
|
2676
|
+
const suggestions = [];
|
|
2677
|
+
const msgPattern = group.title.replace(/\[.*?\]/g, "").trim().slice(0, 60);
|
|
2678
|
+
if (msgPattern.length > 5) {
|
|
2679
|
+
suggestions.push({
|
|
2680
|
+
field: "message",
|
|
2681
|
+
pattern: msgPattern,
|
|
2682
|
+
reason: `matches "${group.title}"`
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
const ua = sample.device?.userAgent ?? "";
|
|
2686
|
+
const uaMatch = ua.match(/\b(Instagram|FBAN|FBAV|Twitter|TikTok|LinkedInApp|Snapchat|Pinterest)\b/i);
|
|
2687
|
+
if (uaMatch) {
|
|
2688
|
+
suggestions.push({
|
|
2689
|
+
field: "useragent",
|
|
2690
|
+
pattern: uaMatch[1],
|
|
2691
|
+
reason: `this error only appears from the ${uaMatch[1]} in-app browser`
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
const firstFrame = sample.stackFrames?.[0];
|
|
2695
|
+
const ownDomain = new URL(sample.url ?? "https://example.com").hostname;
|
|
2696
|
+
if (firstFrame?.source) {
|
|
2697
|
+
try {
|
|
2698
|
+
const frameDomain = new URL(firstFrame.source).hostname;
|
|
2699
|
+
if (frameDomain && frameDomain !== ownDomain) {
|
|
2700
|
+
suggestions.push({
|
|
2701
|
+
field: "script_origin",
|
|
2702
|
+
pattern: frameDomain,
|
|
2703
|
+
reason: `error originates from third-party script at ${frameDomain}`
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
} catch {}
|
|
2707
|
+
}
|
|
2708
|
+
if (suggestions.length === 0) {
|
|
2709
|
+
console.log(`No automatic suppression suggestions for incident ${incidentId}.`);
|
|
2710
|
+
console.log("Use `snapfail suppress add` to create a rule manually.");
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
console.log(`
|
|
2714
|
+
Suppression suggestions for: ${group.title}
|
|
2715
|
+
`);
|
|
2716
|
+
for (const s of suggestions) {
|
|
2717
|
+
console.log(` ${FIELD_LABELS[s.field]}: "${s.pattern}"`);
|
|
2718
|
+
console.log(` Reason: ${s.reason}
|
|
2719
|
+
`);
|
|
2720
|
+
}
|
|
2721
|
+
const chosen = await select({
|
|
2722
|
+
message: "Create a suppression rule?",
|
|
2723
|
+
options: [
|
|
2724
|
+
...suggestions.map((s, i2) => ({
|
|
2725
|
+
value: String(i2),
|
|
2726
|
+
label: `${FIELD_LABELS[s.field]}: "${s.pattern}"`
|
|
2727
|
+
})),
|
|
2728
|
+
{ value: "none", label: "Don't create a rule" }
|
|
2729
|
+
]
|
|
2730
|
+
});
|
|
2731
|
+
if (isCancel(chosen) || chosen === "none") {
|
|
2732
|
+
console.log("No rule created.");
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
const selected = suggestions[Number(chosen)];
|
|
2736
|
+
const rule = await addSuppressionRule(session.token, projectKey, selected.field, selected.pattern);
|
|
2737
|
+
console.log(`
|
|
2738
|
+
Rule added: ${FIELD_LABELS[rule.field]} "${rule.pattern}"`);
|
|
2739
|
+
console.log("Future incidents matching this pattern will be silently dropped at ingest.");
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2554
2742
|
// src/help.ts
|
|
2555
2743
|
var GLOBAL_HELP = `
|
|
2556
2744
|
snapfail \u2014 browser error capture for AI-assisted debugging
|
|
@@ -2565,6 +2753,7 @@ COMMANDS
|
|
|
2565
2753
|
incidents List incident groups for your project
|
|
2566
2754
|
incident View or update a single incident group
|
|
2567
2755
|
explain Output raw evidence context for LLM analysis
|
|
2756
|
+
suppress Manage suppression rules to filter out noise
|
|
2568
2757
|
help Show this help, or help for a specific command
|
|
2569
2758
|
|
|
2570
2759
|
OPTIONS
|
|
@@ -2683,6 +2872,42 @@ EXAMPLES
|
|
|
2683
2872
|
snapfail incident abc123 --resolve
|
|
2684
2873
|
snapfail incident abc123 --json --pk proj_abc123
|
|
2685
2874
|
|
|
2875
|
+
EXIT CODES
|
|
2876
|
+
0 Success
|
|
2877
|
+
1 Not found or API error
|
|
2878
|
+
`.trimStart(),
|
|
2879
|
+
suppress: `
|
|
2880
|
+
snapfail suppress \u2014 Manage suppression rules to filter out noise
|
|
2881
|
+
|
|
2882
|
+
USAGE
|
|
2883
|
+
snapfail suppress [list] List active rules
|
|
2884
|
+
snapfail suppress add Create a rule interactively
|
|
2885
|
+
snapfail suppress delete <id-prefix> Remove a rule by ID prefix
|
|
2886
|
+
snapfail suppress <incident-id> Suggest rules based on an incident
|
|
2887
|
+
|
|
2888
|
+
OPTIONS
|
|
2889
|
+
--pk <key> Project key (overrides .env / SNAPFAIL_PROJECT_KEY)
|
|
2890
|
+
--json Output raw JSON (for list/delete)
|
|
2891
|
+
|
|
2892
|
+
DESCRIPTION
|
|
2893
|
+
Suppression rules filter out errors at ingest time \u2014 matching incidents are
|
|
2894
|
+
silently dropped before being grouped or stored.
|
|
2895
|
+
|
|
2896
|
+
Rules match by case-insensitive substring on one of four fields:
|
|
2897
|
+
message The error message text
|
|
2898
|
+
useragent The browser/app user-agent string
|
|
2899
|
+
url The page URL where the error occurred
|
|
2900
|
+
script_origin The URL of the script file that threw the error
|
|
2901
|
+
|
|
2902
|
+
Pass an incident ID instead of a subcommand to get automatic suggestions
|
|
2903
|
+
based on that incident's data (user-agent, error message, script origin).
|
|
2904
|
+
|
|
2905
|
+
EXAMPLES
|
|
2906
|
+
snapfail suppress
|
|
2907
|
+
snapfail suppress ba7b555c (auto-suggest from incident)
|
|
2908
|
+
snapfail suppress add
|
|
2909
|
+
snapfail suppress delete a1b2c3
|
|
2910
|
+
|
|
2686
2911
|
EXIT CODES
|
|
2687
2912
|
0 Success
|
|
2688
2913
|
1 Not found or API error
|
|
@@ -2735,7 +2960,7 @@ function printHelp(command) {
|
|
|
2735
2960
|
// package.json
|
|
2736
2961
|
var package_default = {
|
|
2737
2962
|
name: "snapfail",
|
|
2738
|
-
version: "0.0.
|
|
2963
|
+
version: "0.0.28",
|
|
2739
2964
|
type: "module",
|
|
2740
2965
|
description: "CLI for snapfail \u2014 project setup, incident inspection and AI diagnostics",
|
|
2741
2966
|
license: "MIT",
|
|
@@ -2760,13 +2985,21 @@ function parseArgs(argv) {
|
|
|
2760
2985
|
const command = rawCommand;
|
|
2761
2986
|
const args = [];
|
|
2762
2987
|
const flags = {};
|
|
2763
|
-
for (
|
|
2988
|
+
for (let i2 = 0;i2 < rest.length; i2++) {
|
|
2989
|
+
const token = rest[i2];
|
|
2764
2990
|
if (token.startsWith("--")) {
|
|
2765
2991
|
const eq = token.indexOf("=");
|
|
2766
2992
|
if (eq !== -1) {
|
|
2767
2993
|
flags[token.slice(2, eq)] = token.slice(eq + 1);
|
|
2768
2994
|
} else {
|
|
2769
|
-
|
|
2995
|
+
const key = token.slice(2);
|
|
2996
|
+
const next = rest[i2 + 1];
|
|
2997
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
2998
|
+
flags[key] = next;
|
|
2999
|
+
i2++;
|
|
3000
|
+
} else {
|
|
3001
|
+
flags[key] = true;
|
|
3002
|
+
}
|
|
2770
3003
|
}
|
|
2771
3004
|
} else {
|
|
2772
3005
|
args.push(token);
|
|
@@ -2805,6 +3038,11 @@ async function main() {
|
|
|
2805
3038
|
await runSkill();
|
|
2806
3039
|
return;
|
|
2807
3040
|
}
|
|
3041
|
+
if (command === "suppress") {
|
|
3042
|
+
const [subcommand, ...rest] = args;
|
|
3043
|
+
await runSuppress({ subcommand, args: rest, json, pk });
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
2808
3046
|
if (command === "incidents") {
|
|
2809
3047
|
await runIncidents({
|
|
2810
3048
|
json,
|