@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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +102 -12
- package/README.md +29 -0
- package/bin/doctor.mjs +8 -1
- package/dist/client/init.d.ts.map +1 -1
- package/dist/client/init.js +39 -8
- package/dist/client/init.js.map +1 -1
- package/dist/client/script-provenance.d.ts +21 -0
- package/dist/client/script-provenance.d.ts.map +1 -0
- package/dist/client/script-provenance.js +127 -0
- package/dist/client/script-provenance.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard/design.d.ts +3 -1
- package/dist/dashboard/design.d.ts.map +1 -1
- package/dist/dashboard/design.js +3 -2
- package/dist/dashboard/design.js.map +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +23 -18
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/snapshot-actions.d.ts +16 -0
- package/dist/dashboard/snapshot-actions.d.ts.map +1 -0
- package/dist/dashboard/snapshot-actions.js +79 -0
- package/dist/dashboard/snapshot-actions.js.map +1 -0
- package/dist/dashboard/snapshots.d.ts +6 -0
- package/dist/dashboard/snapshots.d.ts.map +1 -0
- package/dist/dashboard/snapshots.js +86 -0
- package/dist/dashboard/snapshots.js.map +1 -0
- package/dist/dashboard/telemetry.d.ts.map +1 -1
- package/dist/dashboard/telemetry.js +20 -3
- package/dist/dashboard/telemetry.js.map +1 -1
- package/dist/dashboard.css +77 -0
- package/dist/server/ingest.d.ts +2 -5
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +40 -10
- package/dist/server/ingest.js.map +1 -1
- package/dist/server/queries.d.ts +18 -11
- package/dist/server/queries.d.ts.map +1 -1
- package/dist/server/queries.js +64 -24
- package/dist/server/queries.js.map +1 -1
- package/dist/server/snapshots.d.ts +65 -0
- package/dist/server/snapshots.d.ts.map +1 -0
- package/dist/server/snapshots.js +194 -0
- package/dist/server/snapshots.js.map +1 -0
- package/dist/shared/capture-status.d.ts +99 -0
- package/dist/shared/capture-status.d.ts.map +1 -0
- package/dist/shared/capture-status.js +133 -0
- package/dist/shared/capture-status.js.map +1 -0
- package/dist/shared/error-noise.d.ts +39 -0
- package/dist/shared/error-noise.d.ts.map +1 -0
- package/dist/shared/error-noise.js +101 -0
- package/dist/shared/error-noise.js.map +1 -0
- package/docs/SETUP.md +25 -0
- 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.
|
|
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
|
},
|