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.
Files changed (2) hide show
  1. package/dist/index.js +240 -3
  2. 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.26",
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 (const token of rest) {
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
- flags[token.slice(2)] = true;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapfail",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "type": "module",
5
5
  "description": "CLI for snapfail — project setup, incident inspection and AI diagnostics",
6
6
  "license": "MIT",