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