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