@polishd/next 0.2.2-dev.4 → 0.2.3-dev.0

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 (56) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +102 -12
  3. package/README.md +29 -0
  4. package/bin/doctor.mjs +8 -1
  5. package/dist/client/init.d.ts.map +1 -1
  6. package/dist/client/init.js +39 -8
  7. package/dist/client/init.js.map +1 -1
  8. package/dist/client/script-provenance.d.ts +21 -0
  9. package/dist/client/script-provenance.d.ts.map +1 -0
  10. package/dist/client/script-provenance.js +127 -0
  11. package/dist/client/script-provenance.js.map +1 -0
  12. package/dist/config.d.ts +10 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +1 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/dashboard/design.d.ts +3 -1
  17. package/dist/dashboard/design.d.ts.map +1 -1
  18. package/dist/dashboard/design.js +3 -2
  19. package/dist/dashboard/design.js.map +1 -1
  20. package/dist/dashboard/index.d.ts.map +1 -1
  21. package/dist/dashboard/index.js +23 -18
  22. package/dist/dashboard/index.js.map +1 -1
  23. package/dist/dashboard/snapshot-actions.d.ts +16 -0
  24. package/dist/dashboard/snapshot-actions.d.ts.map +1 -0
  25. package/dist/dashboard/snapshot-actions.js +79 -0
  26. package/dist/dashboard/snapshot-actions.js.map +1 -0
  27. package/dist/dashboard/snapshots.d.ts +6 -0
  28. package/dist/dashboard/snapshots.d.ts.map +1 -0
  29. package/dist/dashboard/snapshots.js +86 -0
  30. package/dist/dashboard/snapshots.js.map +1 -0
  31. package/dist/dashboard/telemetry.d.ts.map +1 -1
  32. package/dist/dashboard/telemetry.js +20 -3
  33. package/dist/dashboard/telemetry.js.map +1 -1
  34. package/dist/dashboard.css +77 -0
  35. package/dist/server/ingest.d.ts +2 -5
  36. package/dist/server/ingest.d.ts.map +1 -1
  37. package/dist/server/ingest.js +40 -10
  38. package/dist/server/ingest.js.map +1 -1
  39. package/dist/server/queries.d.ts +18 -11
  40. package/dist/server/queries.d.ts.map +1 -1
  41. package/dist/server/queries.js +64 -24
  42. package/dist/server/queries.js.map +1 -1
  43. package/dist/server/snapshots.d.ts +65 -0
  44. package/dist/server/snapshots.d.ts.map +1 -0
  45. package/dist/server/snapshots.js +194 -0
  46. package/dist/server/snapshots.js.map +1 -0
  47. package/dist/shared/capture-status.d.ts +99 -0
  48. package/dist/shared/capture-status.d.ts.map +1 -0
  49. package/dist/shared/capture-status.js +133 -0
  50. package/dist/shared/capture-status.js.map +1 -0
  51. package/dist/shared/error-noise.d.ts +39 -0
  52. package/dist/shared/error-noise.d.ts.map +1 -0
  53. package/dist/shared/error-noise.js +101 -0
  54. package/dist/shared/error-noise.js.map +1 -0
  55. package/docs/SETUP.md +25 -0
  56. package/package.json +6 -1
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Polishd — browser-extension error noise.
3
+ *
4
+ * A page's `error` and `unhandledrejection` handlers hear everything that
5
+ * throws in the tab, not just the site's own code. Extensions inject scripts
6
+ * into the page context, and when they fail — a wallet that can't reach its
7
+ * background worker, a content script whose port closed on navigation — the
8
+ * throw surfaces on `window` exactly like a real bug would. "Failed to connect
9
+ * to MetaMask" is the canonical one: nothing on the site can cause it, nothing
10
+ * on the site can fix it, and left unfiltered it can be the loudest error the
11
+ * dashboard shows.
12
+ *
13
+ * These are pure predicates, shared rather than client-local, so the browser
14
+ * capture layer and the ingest endpoint apply the same rule — a cached bundle
15
+ * can lag a config change by a full session, so the server keeps its own copy
16
+ * of the decision (the same reasoning as the dashboard-path exclusion).
17
+ */
18
+ /** True when `url` was served by an extension or the browser, not the site. */
19
+ export declare function isExtensionUrl(url: string): boolean;
20
+ /** The shape of an error, reduced to what both the client and ingest can see. */
21
+ export interface ErrorSignature {
22
+ /** The thrown message, already stringified. */
23
+ message: string;
24
+ /** The failing script's URL, if the browser gave one. */
25
+ source?: string;
26
+ /** The stack, when available. Only the client has this. */
27
+ stack?: string;
28
+ /** Extra ignore patterns from the host app's config. */
29
+ ignore?: readonly (string | RegExp)[];
30
+ }
31
+ /**
32
+ * True when this error is noise the site's owner cannot act on.
33
+ *
34
+ * Checked in order of confidence: where the script came from, then what the
35
+ * stack blames, then the small set of messages extension messaging emits, then
36
+ * the opaque cross-origin case, then whatever the host app chose to ignore.
37
+ */
38
+ export declare function isIgnorableError({ message, source, stack, ignore }: ErrorSignature): boolean;
39
+ //# sourceMappingURL=error-noise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-noise.d.ts","sourceRoot":"","sources":["../../src/shared/error-noise.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AA8CH,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,MAAM,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,OAAO,EAAE,MAAW,EAAE,KAAU,EAAE,MAAM,EAAE,EAAE,cAAc,GAAG,OAAO,CAiBtG"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Polishd — browser-extension error noise.
3
+ *
4
+ * A page's `error` and `unhandledrejection` handlers hear everything that
5
+ * throws in the tab, not just the site's own code. Extensions inject scripts
6
+ * into the page context, and when they fail — a wallet that can't reach its
7
+ * background worker, a content script whose port closed on navigation — the
8
+ * throw surfaces on `window` exactly like a real bug would. "Failed to connect
9
+ * to MetaMask" is the canonical one: nothing on the site can cause it, nothing
10
+ * on the site can fix it, and left unfiltered it can be the loudest error the
11
+ * dashboard shows.
12
+ *
13
+ * These are pure predicates, shared rather than client-local, so the browser
14
+ * capture layer and the ingest endpoint apply the same rule — a cached bundle
15
+ * can lag a config change by a full session, so the server keeps its own copy
16
+ * of the decision (the same reasoning as the dashboard-path exclusion).
17
+ */
18
+ /**
19
+ * URL schemes that only ever belong to an extension or the browser itself.
20
+ * If the failing script came from one of these, the site did not load it.
21
+ */
22
+ const EXTENSION_SCHEMES = [
23
+ "chrome-extension://",
24
+ "moz-extension://",
25
+ "safari-extension://",
26
+ "safari-web-extension://",
27
+ "ms-browser-extension://",
28
+ "extension://", // unknown vendors, and the bare form some builds report
29
+ "webkit-masked-url://", // Safari's opaque stand-in for injected scripts
30
+ "resource://", // Firefox internals and legacy add-ons
31
+ "chrome://", // browser-internal pages and resources
32
+ ];
33
+ /**
34
+ * Messages produced by extension plumbing running *in page context*, where the
35
+ * filename is the page's own URL and the scheme check above can't help.
36
+ *
37
+ * Deliberately narrow: each of these is a string the extension messaging layer
38
+ * itself emits, not something a site's code would produce. Broad matching here
39
+ * (say, anything mentioning MetaMask) would swallow a dapp's genuine wallet
40
+ * errors, which are real bugs worth seeing.
41
+ */
42
+ const EXTENSION_MESSAGE_PATTERNS = [
43
+ /failed to connect to metamask/i,
44
+ /could not establish connection\.\s*receiving end does not exist/i,
45
+ /the message port closed before a response was received/i,
46
+ /extension context invalidated/i,
47
+ /^chrome\.runtime\b/i,
48
+ ];
49
+ /**
50
+ * The browser's placeholder for an error thrown by a cross-origin script it
51
+ * won't describe: no message, no file, no line. Extensions and third-party
52
+ * embeds are the usual sources, and either way there is nothing here to act
53
+ * on — the event is a report that *something* failed, with every detail
54
+ * stripped.
55
+ */
56
+ function isOpaqueError(message, source) {
57
+ return /^script error\.?$/i.test(message.trim()) && !source;
58
+ }
59
+ /** True when `url` was served by an extension or the browser, not the site. */
60
+ export function isExtensionUrl(url) {
61
+ // `blob:` and `filesystem:` wrap the origin that minted them, so an
62
+ // extension's blob reads as `blob:chrome-extension://…` — unwrap before
63
+ // testing the scheme, or code an extension generated at runtime slips past.
64
+ const u = url.trim().toLowerCase().replace(/^(?:blob|filesystem):/, "");
65
+ if (!u)
66
+ return false;
67
+ return EXTENSION_SCHEMES.some((scheme) => u.startsWith(scheme));
68
+ }
69
+ /**
70
+ * True when this error is noise the site's owner cannot act on.
71
+ *
72
+ * Checked in order of confidence: where the script came from, then what the
73
+ * stack blames, then the small set of messages extension messaging emits, then
74
+ * the opaque cross-origin case, then whatever the host app chose to ignore.
75
+ */
76
+ export function isIgnorableError({ message, source = "", stack = "", ignore }) {
77
+ if (isExtensionUrl(source))
78
+ return true;
79
+ // A stack is a list of frames, not a single URL, so this looks anywhere in
80
+ // it: injected code often has the page as its top frame and the extension
81
+ // further down.
82
+ const lowerStack = stack.toLowerCase();
83
+ if (lowerStack && EXTENSION_SCHEMES.some((scheme) => lowerStack.includes(scheme)))
84
+ return true;
85
+ if (EXTENSION_MESSAGE_PATTERNS.some((re) => re.test(message)))
86
+ return true;
87
+ if (isOpaqueError(message, source))
88
+ return true;
89
+ if (ignore?.length) {
90
+ for (const pattern of ignore) {
91
+ if (typeof pattern === "string") {
92
+ if (pattern && message.toLowerCase().includes(pattern.toLowerCase()))
93
+ return true;
94
+ }
95
+ else if (pattern.test(message))
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+ //# sourceMappingURL=error-noise.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-noise.js","sourceRoot":"","sources":["../../src/shared/error-noise.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;GAGG;AACH,MAAM,iBAAiB,GAAG;IACxB,qBAAqB;IACrB,kBAAkB;IAClB,qBAAqB;IACrB,yBAAyB;IACzB,yBAAyB;IACzB,cAAc,EAAE,wDAAwD;IACxE,sBAAsB,EAAE,gDAAgD;IACxE,aAAa,EAAE,uCAAuC;IACtD,WAAW,EAAE,uCAAuC;CACrD,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,0BAA0B,GAAa;IAC3C,gCAAgC;IAChC,kEAAkE;IAClE,yDAAyD;IACzD,gCAAgC;IAChC,qBAAqB;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,OAAe,EAAE,MAAc;IACpD,OAAO,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC;AAC9D,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,oEAAoE;IACpE,wEAAwE;IACxE,4EAA4E;IAC5E,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC;IACxE,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACrB,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAClE,CAAC;AAcD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,EAAE,EAAE,KAAK,GAAG,EAAE,EAAE,MAAM,EAAkB;IAC3F,IAAI,cAAc,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,2EAA2E;IAC3E,0EAA0E;IAC1E,gBAAgB;IAChB,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IACvC,IAAI,UAAU,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/F,IAAI,0BAA0B,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3E,IAAI,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,IAAI,MAAM,EAAE,MAAM,EAAE,CAAC;QACnB,KAAK,MAAM,OAAO,IAAI,MAAM,EAAE,CAAC;YAC7B,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAChC,IAAI,OAAO,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;oBAAE,OAAO,IAAI,CAAC;YACpF,CAAC;iBAAM,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;gBAAE,OAAO,IAAI,CAAC;QAChD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
package/docs/SETUP.md CHANGED
@@ -476,9 +476,34 @@ export default definePolishdConfig({
476
476
  rageClick: { count: 3, windowMs: 500 }, // N clicks on one element
477
477
  flushIntervalMs: 10_000,
478
478
  dashboardRoute: "/polishd", // never captured; see below
479
+ ignoreErrors: [], // extra error messages to drop
479
480
  });
480
481
  ```
481
482
 
483
+ Errors thrown by **browser extensions are dropped automatically**. Your page's
484
+ `error` handler hears every throw in the tab, not just your code's, so a wallet
485
+ extension failing to reach its background worker ("Failed to connect to
486
+ MetaMask") would otherwise land in your dashboard as one of your site's loudest
487
+ bugs.
488
+
489
+ This isn't a list of known extensions — it's worked out per error. The browser
490
+ records every script your document actually fetched (Resource Timing), and
491
+ extension code is never in it: the browser injects content scripts, your page
492
+ never asked for them. So an error is classified by where its stack points:
493
+
494
+ | Verdict | Meaning | What happens |
495
+ | --- | --- | --- |
496
+ | `site` | every frame is code you shipped — including third-party scripts your page loads, like Stripe | recorded normally |
497
+ | `extension` | a frame names an extension URL | dropped |
498
+ | `foreign` | a frame names a script your document never loaded | recorded, marked *not your code* |
499
+ | `unknown` | nothing attributable — an opaque cross-origin `Script error.`, an empty stack | recorded, marked *unattributable* |
500
+
501
+ Only `extension` is dropped, because only it is certain. The other two are
502
+ shown dimmed in Recent errors rather than hidden — a filter that quietly
503
+ discards what it can't identify is how a real bug goes missing. `ignoreErrors`
504
+ is the escape hatch if you want them gone anyway: strings match
505
+ case-insensitively anywhere in the message, regexes are tested as-is.
506
+
482
507
  The dashboard's own traffic is **never captured**. Reading your analytics is
483
508
  browsing too, and left alone it would rank "Refresh summary" as your site's
484
509
  most-used feature. Nothing on `dashboardRoute` (or below it) is collected, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polishd/next",
3
- "version": "0.2.2-dev.4",
3
+ "version": "0.2.3-dev.0",
4
4
  "description": "Drop-in product analytics for Next.js — capture real user behavioral signals (rage/dead clicks, scroll depth, errors, web vitals) and explore them on a built-in dashboard. What gets measured gets improved.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -75,6 +75,7 @@
75
75
  "scripts": {
76
76
  "build": "tsc -p tsconfig.build.json && npm run build:css",
77
77
  "typecheck": "tsc -p tsconfig.json",
78
+ "test": "node --experimental-strip-types --test \"src/**/*.test.ts\"",
78
79
  "clean": "rm -rf dist",
79
80
  "prepublishOnly": "npm run clean && npm run build",
80
81
  "build:css": "node scripts/build-css.mjs"
@@ -84,6 +85,10 @@
84
85
  "react": ">=18",
85
86
  "react-dom": ">=18"
86
87
  },
88
+ "dependencies": {
89
+ "allpages": "^0.3.3",
90
+ "playwright-core": "^1.62.1"
91
+ },
87
92
  "optionalDependencies": {
88
93
  "pg": "^8.0.0"
89
94
  },