@polishd/next 0.2.2 → 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 (86) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +102 -0
  3. package/README.md +32 -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-review-card.d.ts.map +1 -1
  17. package/dist/dashboard/design-review-card.js +3 -3
  18. package/dist/dashboard/design-review-card.js.map +1 -1
  19. package/dist/dashboard/design.d.ts +3 -1
  20. package/dist/dashboard/design.d.ts.map +1 -1
  21. package/dist/dashboard/design.js +3 -2
  22. package/dist/dashboard/design.js.map +1 -1
  23. package/dist/dashboard/index.d.ts.map +1 -1
  24. package/dist/dashboard/index.js +24 -19
  25. package/dist/dashboard/index.js.map +1 -1
  26. package/dist/dashboard/installs.d.ts.map +1 -1
  27. package/dist/dashboard/installs.js +2 -1
  28. package/dist/dashboard/installs.js.map +1 -1
  29. package/dist/dashboard/journeys.d.ts.map +1 -1
  30. package/dist/dashboard/journeys.js +3 -2
  31. package/dist/dashboard/journeys.js.map +1 -1
  32. package/dist/dashboard/pages.d.ts.map +1 -1
  33. package/dist/dashboard/pages.js +1 -1
  34. package/dist/dashboard/pages.js.map +1 -1
  35. package/dist/dashboard/settings.d.ts.map +1 -1
  36. package/dist/dashboard/settings.js +6 -2
  37. package/dist/dashboard/settings.js.map +1 -1
  38. package/dist/dashboard/show-more.d.ts.map +1 -1
  39. package/dist/dashboard/show-more.js +1 -1
  40. package/dist/dashboard/show-more.js.map +1 -1
  41. package/dist/dashboard/snapshot-actions.d.ts +16 -0
  42. package/dist/dashboard/snapshot-actions.d.ts.map +1 -0
  43. package/dist/dashboard/snapshot-actions.js +79 -0
  44. package/dist/dashboard/snapshot-actions.js.map +1 -0
  45. package/dist/dashboard/snapshots.d.ts +6 -0
  46. package/dist/dashboard/snapshots.d.ts.map +1 -0
  47. package/dist/dashboard/snapshots.js +86 -0
  48. package/dist/dashboard/snapshots.js.map +1 -0
  49. package/dist/dashboard/summary.d.ts.map +1 -1
  50. package/dist/dashboard/summary.js +11 -6
  51. package/dist/dashboard/summary.js.map +1 -1
  52. package/dist/dashboard/telemetry.d.ts +3 -1
  53. package/dist/dashboard/telemetry.d.ts.map +1 -1
  54. package/dist/dashboard/telemetry.js +50 -8
  55. package/dist/dashboard/telemetry.js.map +1 -1
  56. package/dist/dashboard.css +79 -2
  57. package/dist/server/ingest.d.ts +2 -5
  58. package/dist/server/ingest.d.ts.map +1 -1
  59. package/dist/server/ingest.js +40 -10
  60. package/dist/server/ingest.js.map +1 -1
  61. package/dist/server/queries.d.ts +23 -11
  62. package/dist/server/queries.d.ts.map +1 -1
  63. package/dist/server/queries.js +98 -34
  64. package/dist/server/queries.js.map +1 -1
  65. package/dist/server/snapshots.d.ts +65 -0
  66. package/dist/server/snapshots.d.ts.map +1 -0
  67. package/dist/server/snapshots.js +194 -0
  68. package/dist/server/snapshots.js.map +1 -0
  69. package/dist/server/telemetry.d.ts +13 -0
  70. package/dist/server/telemetry.d.ts.map +1 -1
  71. package/dist/server/telemetry.js +28 -7
  72. package/dist/server/telemetry.js.map +1 -1
  73. package/dist/shared/capture-status.d.ts +99 -0
  74. package/dist/shared/capture-status.d.ts.map +1 -0
  75. package/dist/shared/capture-status.js +133 -0
  76. package/dist/shared/capture-status.js.map +1 -0
  77. package/dist/shared/error-noise.d.ts +39 -0
  78. package/dist/shared/error-noise.d.ts.map +1 -0
  79. package/dist/shared/error-noise.js +101 -0
  80. package/dist/shared/error-noise.js.map +1 -0
  81. package/dist/shared/types.d.ts +1 -1
  82. package/dist/shared/types.d.ts.map +1 -1
  83. package/dist/shared/types.js +4 -0
  84. package/dist/shared/types.js.map +1 -1
  85. package/docs/SETUP.md +25 -0
  86. package/package.json +6 -1
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/server/telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,kEAAkE;AAClE,MAAM,CAAC,MAAM,0BAA0B,GACrC,wDAAwD,CAAC;AAE3D,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAiBxC,MAAM,UAAU,sBAAsB;IACpC,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IACxC,OAAO,CAAC,KAAK,SAAS,IAAI,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AACjE,CAAC;AAMD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,OAAO,CAAC,GAAG,CAAC,wBAAwB,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,wBAAwB;IACtC,OAAO,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,0BAA0B,CAAC;AAC9E,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,OAAgB;IACzD,MAAM,OAAO,CACX,WAAW,EACX,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAC3E,CAAC;AACJ,CAAC;AAED,mEAAmE;AACnE,MAAM,CAAC,KAAK,UAAU,yBAAyB;IAC7C,MAAM,QAAQ,GAAG,wBAAwB,EAAE,CAAC;IAC5C,IAAI,sBAAsB,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;IACtE,0EAA0E;IAC1E,gEAAgE;IAChE,IAAI,CAAC,CAAC,MAAM,UAAU,EAAE,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;IAEnE,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;IACvC,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAyB,CAAC;QACvD,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;YAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;QACxE,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;IAC3E,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/server/telemetry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Polishd — capture health (shared: no server, no React).
3
+ *
4
+ * Two things live here, and they are the same subject seen from both ends:
5
+ * the on-disk shape ingest writes when it drops a cookie-less batch, and the
6
+ * rule the dashboard applies to decide whether that means "your proxy isn't
7
+ * running" or "some bots posted to a public endpoint".
8
+ *
9
+ * They are together, and pure, because the rule is the part that has been
10
+ * wrong: it compared an all-time drop total against an all-time event total
11
+ * gated on a one-hour recency check, so a single fresh drop re-armed every
12
+ * drop the site had ever recorded. Anything that can be tested without a
13
+ * database or a renderer belongs in one file that a test can import.
14
+ */
15
+ /** Granularity drops are bucketed at. One hour. */
16
+ export declare const NO_SESSION_BUCKET_MS = 3600000;
17
+ /** How far back *both* sides of the comparison look. */
18
+ export declare const CAPTURE_WINDOW_MS: number;
19
+ /** One hour's worth of drops. */
20
+ export interface NoSessionBucket {
21
+ /** Hour index — `Math.floor(ms / NO_SESSION_BUCKET_MS)`. */
22
+ h: number;
23
+ /** Drops recorded in that hour. */
24
+ n: number;
25
+ }
26
+ /** What ingest stores under its `polishd_meta` key. */
27
+ export interface NoSessionRecord {
28
+ /** All-time drops. Never decays — a lifetime total, not an alert input. */
29
+ count: number;
30
+ /** Most recent drop (ms epoch). */
31
+ lastAt: number;
32
+ /** Per-hour drops, oldest first, trimmed to {@link MAX_BUCKETS}. */
33
+ buckets: NoSessionBucket[];
34
+ }
35
+ /**
36
+ * Read a stored record, tolerating both junk and the pre-0.2.3 `{count,
37
+ * lastAt}` shape. A legacy record parses with no buckets, which is the honest
38
+ * answer: those drops happened, but nothing recorded *when*, so none of them
39
+ * can count toward a window. That is the whole point — history stops voting.
40
+ */
41
+ export declare function parseNoSessionRecord(raw: string | null | undefined): NoSessionRecord | null;
42
+ /** Add `batch` drops observed at `now` to `prev` (null for a first drop). */
43
+ export declare function foldNoSessionDrop(prev: NoSessionRecord | null, now: number, batch: number): NoSessionRecord;
44
+ /**
45
+ * Drops recorded at or after `since`.
46
+ *
47
+ * Hour-granular, so the bucket straddling `since` contributes in full and the
48
+ * count leans high by up to an hour of drops. That bias is the safe direction
49
+ * for a diagnostic and it costs nothing to store.
50
+ */
51
+ export declare function noSessionCountSince(rec: NoSessionRecord | null, since: number): number;
52
+ /** Whether capture itself is wired up, as opposed to what it captured. */
53
+ export interface CaptureHealth {
54
+ /** All-time beacons dropped for want of a session cookie. */
55
+ noSessionCount: number;
56
+ /** Of those, the ones inside {@link windowMs}. */
57
+ recentNoSessionCount: number;
58
+ /** When the most recent drop happened (ms epoch), or null for none. */
59
+ lastNoSessionAt: number | null;
60
+ /** When an event was last *stored*, by the server's clock, or null. */
61
+ lastStoredAt: number | null;
62
+ /** Events stored inside the window. */
63
+ recentEvents: number;
64
+ /** Batches stored inside the window (distinct arrival stamps — approximate). */
65
+ recentBatches: number;
66
+ /** The window both recent counts were measured over. */
67
+ windowMs: number;
68
+ }
69
+ export type CaptureStatusLevel = "ok" | "notice" | "broken";
70
+ export interface CaptureStatus {
71
+ /**
72
+ * `broken` — nothing is being stored, say so in red.
73
+ * `notice` — real capture plus some cookie-less batches; neutral and true.
74
+ * `ok` — nothing worth a word.
75
+ */
76
+ level: CaptureStatusLevel;
77
+ /** Drops this status is talking about: the window's, or all-time when empty. */
78
+ droppedBatches: number;
79
+ /** Batches stored alongside them, over the same window. */
80
+ storedBatches: number;
81
+ /** Dropped share of all batches seen in the window, or null if unknowable. */
82
+ sharePct: number | null;
83
+ /** A store landed *after* the last drop — proof the proxy is minting now. */
84
+ storedSinceDrop: boolean;
85
+ /** Nothing has ever been stored. Gates the "nothing is being stored" copy. */
86
+ nothingStored: boolean;
87
+ }
88
+ /**
89
+ * Decide what, if anything, the dashboard should say about cookie-less
90
+ * batches.
91
+ *
92
+ * The load-bearing signal is `storedSinceDrop`. Every event is attributed to
93
+ * the cookie the proxy mints, so a proxy that isn't running drops *every*
94
+ * batch — one successful store after the most recent drop is proof it is
95
+ * running, and no threshold can argue with it. Everything else here only
96
+ * decides how loudly to describe traffic that is already known to be flowing.
97
+ */
98
+ export declare function captureStatus(health: CaptureHealth, totalEvents: number, now?: number): CaptureStatus;
99
+ //# sourceMappingURL=capture-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-status.d.ts","sourceRoot":"","sources":["../../src/shared/capture-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,mDAAmD;AACnD,eAAO,MAAM,oBAAoB,UAAY,CAAC;AAE9C,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,QAA4B,CAAC;AAoB3D,iCAAiC;AACjC,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,CAAC,EAAE,MAAM,CAAC;IACV,mCAAmC;IACnC,CAAC,EAAE,MAAM,CAAC;CACX;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,GAAG,IAAI,CAgB3F;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,eAAe,GAAG,IAAI,EAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GACZ,eAAe,CAYjB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAItF;AAED,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,cAAc,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uEAAuE;IACvE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,uEAAuE;IACvE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,uCAAuC;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,aAAa,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,KAAK,EAAE,kBAAkB,CAAC;IAC1B,gFAAgF;IAChF,cAAc,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAC;IACzB,8EAA8E;IAC9E,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,MAAmB,GACvB,aAAa,CAqCf"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Polishd — capture health (shared: no server, no React).
3
+ *
4
+ * Two things live here, and they are the same subject seen from both ends:
5
+ * the on-disk shape ingest writes when it drops a cookie-less batch, and the
6
+ * rule the dashboard applies to decide whether that means "your proxy isn't
7
+ * running" or "some bots posted to a public endpoint".
8
+ *
9
+ * They are together, and pure, because the rule is the part that has been
10
+ * wrong: it compared an all-time drop total against an all-time event total
11
+ * gated on a one-hour recency check, so a single fresh drop re-armed every
12
+ * drop the site had ever recorded. Anything that can be tested without a
13
+ * database or a renderer belongs in one file that a test can import.
14
+ */
15
+ /** Granularity drops are bucketed at. One hour. */
16
+ export const NO_SESSION_BUCKET_MS = 3_600_000;
17
+ /** How far back *both* sides of the comparison look. */
18
+ export const CAPTURE_WINDOW_MS = 24 * NO_SESSION_BUCKET_MS;
19
+ /** Buckets retained: the window plus slack, so the oldest ages out cleanly. */
20
+ const MAX_BUCKETS = 26;
21
+ /**
22
+ * Drops in the window before a *neutral* note is worth showing. Below this,
23
+ * cookie-less batches are ordinary background on a public endpoint and saying
24
+ * so is clutter.
25
+ */
26
+ const MIN_NOTICE_DROPS = 3;
27
+ /**
28
+ * Drops in the window before "nothing stored in the window" is allowed to
29
+ * read as a broken proxy. A real proxy failure drops *every* batch, so on any
30
+ * site with traffic the count climbs fast; the floor is what keeps a dormant
31
+ * site poked twice by a crawler from accusing a working install.
32
+ */
33
+ const MIN_BROKEN_DROPS = 5;
34
+ /**
35
+ * Read a stored record, tolerating both junk and the pre-0.2.3 `{count,
36
+ * lastAt}` shape. A legacy record parses with no buckets, which is the honest
37
+ * answer: those drops happened, but nothing recorded *when*, so none of them
38
+ * can count toward a window. That is the whole point — history stops voting.
39
+ */
40
+ export function parseNoSessionRecord(raw) {
41
+ if (!raw)
42
+ return null;
43
+ try {
44
+ const rec = JSON.parse(raw);
45
+ if (!rec || typeof rec !== "object")
46
+ return null;
47
+ const count = numOr(rec.count, 0);
48
+ const lastAt = numOr(rec.lastAt, 0);
49
+ const buckets = Array.isArray(rec.buckets)
50
+ ? rec.buckets
51
+ .filter((b) => !!b && Number.isFinite(b.h) && Number.isFinite(b.n))
52
+ .map((b) => ({ h: Math.floor(b.h), n: Math.max(0, Math.floor(b.n)) }))
53
+ : [];
54
+ return { count, lastAt, buckets };
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ /** Add `batch` drops observed at `now` to `prev` (null for a first drop). */
61
+ export function foldNoSessionDrop(prev, now, batch) {
62
+ const h = Math.floor(now / NO_SESSION_BUCKET_MS);
63
+ const buckets = (prev?.buckets ?? []).filter((b) => b.h > h - MAX_BUCKETS);
64
+ const current = buckets.find((b) => b.h === h);
65
+ if (current)
66
+ current.n += batch;
67
+ else
68
+ buckets.push({ h, n: batch });
69
+ buckets.sort((a, b) => a.h - b.h);
70
+ return {
71
+ count: (prev?.count ?? 0) + batch,
72
+ lastAt: Math.max(now, prev?.lastAt ?? 0),
73
+ buckets: buckets.slice(-MAX_BUCKETS),
74
+ };
75
+ }
76
+ /**
77
+ * Drops recorded at or after `since`.
78
+ *
79
+ * Hour-granular, so the bucket straddling `since` contributes in full and the
80
+ * count leans high by up to an hour of drops. That bias is the safe direction
81
+ * for a diagnostic and it costs nothing to store.
82
+ */
83
+ export function noSessionCountSince(rec, since) {
84
+ if (!rec)
85
+ return 0;
86
+ const first = Math.floor(since / NO_SESSION_BUCKET_MS);
87
+ return rec.buckets.reduce((sum, b) => (b.h >= first ? sum + b.n : sum), 0);
88
+ }
89
+ /**
90
+ * Decide what, if anything, the dashboard should say about cookie-less
91
+ * batches.
92
+ *
93
+ * The load-bearing signal is `storedSinceDrop`. Every event is attributed to
94
+ * the cookie the proxy mints, so a proxy that isn't running drops *every*
95
+ * batch — one successful store after the most recent drop is proof it is
96
+ * running, and no threshold can argue with it. Everything else here only
97
+ * decides how loudly to describe traffic that is already known to be flowing.
98
+ */
99
+ export function captureStatus(health, totalEvents, now = Date.now()) {
100
+ const storedSinceDrop = health.lastStoredAt !== null &&
101
+ health.lastNoSessionAt !== null &&
102
+ health.lastStoredAt > health.lastNoSessionAt;
103
+ const nothingStored = totalEvents === 0;
104
+ const dropsInWindow = health.lastNoSessionAt !== null &&
105
+ health.lastNoSessionAt >= now - health.windowMs &&
106
+ health.recentNoSessionCount > 0;
107
+ const droppedBatches = dropsInWindow ? health.recentNoSessionCount : health.noSessionCount;
108
+ const storedBatches = health.recentBatches;
109
+ const totalBatches = droppedBatches + storedBatches;
110
+ const sharePct = totalBatches > 0 ? (droppedBatches * 100) / totalBatches : null;
111
+ const base = { droppedBatches, storedBatches, sharePct, storedSinceDrop, nothingStored };
112
+ // Never stored anything, and beacons are arriving: the drop *is* the whole
113
+ // story, whenever it happened. This is the case the banner was written for.
114
+ if (health.lastNoSessionAt !== null && nothingStored)
115
+ return { ...base, level: "broken" };
116
+ // Storing has happened at some point, so judge only the window: drops in it,
117
+ // nothing stored in it, and no store since the last drop. Enough drops to
118
+ // rule out a dormant site being poked by a crawler.
119
+ if (dropsInWindow &&
120
+ !storedSinceDrop &&
121
+ health.recentEvents === 0 &&
122
+ health.recentNoSessionCount >= MIN_BROKEN_DROPS) {
123
+ return { ...base, level: "broken" };
124
+ }
125
+ if (dropsInWindow && health.recentNoSessionCount >= MIN_NOTICE_DROPS) {
126
+ return { ...base, level: "notice" };
127
+ }
128
+ return { ...base, level: "ok" };
129
+ }
130
+ function numOr(v, fallback) {
131
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
132
+ }
133
+ //# sourceMappingURL=capture-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-status.js","sourceRoot":"","sources":["../../src/shared/capture-status.ts"],"names":[],"mappings":"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"}
@@ -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"}
@@ -10,7 +10,7 @@
10
10
  * the host notes app.
11
11
  */
12
12
  /** Every kind of signal we capture. Ordered roughly by synthesis value. */
13
- export type PolishdEventType = "page_view" | "click" | "rage_click" | "dead_click" | "scroll_depth" | "viewport" | "js_error" | "web_vital" | "hover" | "component_view" | "mount" | "session_end" | "design_scan";
13
+ export type PolishdEventType = "page_view" | "click" | "rage_click" | "dead_click" | "scroll_depth" | "viewport" | "js_error" | "web_vital" | "hover" | "component_view" | "mount" | "session_end" | "design_scan" | "install_state";
14
14
  /**
15
15
  * A single captured event. The client sends these without a session id; the
16
16
  * ingest layer attaches `session_id` server-side from the signed cookie, so a
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GACxB,WAAW,GACX,OAAO,GACP,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,UAAU,GACV,UAAU,GACV,WAAW,GACX,OAAO,GACP,gBAAgB,GAChB,OAAO,GACP,aAAa,GACb,aAAa,CAAC;AAElB;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAC;IACX,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;CACzD;AAED,iDAAiD;AACjD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8DAA8D;AAC9D,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,gBAAgB,CAc3D,CAAC;AASH,2DAA2D;AAC3D,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC/E,mBAAmB;AACnB,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAChD,2DAA2D;AAC3D,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;AACtE,+DAA+D;AAC/D,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACjD,yBAAyB;AACzB,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE9C,MAAM,WAAW,wBAAwB;IACvC,8BAA8B;IAC9B,CAAC,EAAE,CAAC,CAAC;IACL,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,sDAAsD;IACtD,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,kEAAkE;IAClE,EAAE,EAAE,gBAAgB,EAAE,CAAC;IACvB,mDAAmD;IACnD,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,uDAAuD;IACvD,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,gEAAgE;IAChE,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,kEAAkE;IAClE,GAAG,EAAE,cAAc,EAAE,CAAC;CACvB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GACxB,WAAW,GACX,OAAO,GACP,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,UAAU,GACV,UAAU,GACV,WAAW,GACX,OAAO,GACP,gBAAgB,GAChB,OAAO,GACP,aAAa,GACb,aAAa,GACb,eAAe,CAAC;AAEpB;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAC;IACX,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;CACzD;AAED,iDAAiD;AACjD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8DAA8D;AAC9D,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,gBAAgB,CAkB3D,CAAC;AASH,2DAA2D;AAC3D,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC/E,mBAAmB;AACnB,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAChD,2DAA2D;AAC3D,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;AACtE,+DAA+D;AAC/D,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACjD,yBAAyB;AACzB,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE9C,MAAM,WAAW,wBAAwB;IACvC,8BAA8B;IAC9B,CAAC,EAAE,CAAC,CAAC;IACL,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,sDAAsD;IACtD,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,kEAAkE;IAClE,EAAE,EAAE,gBAAgB,EAAE,CAAC;IACvB,mDAAmD;IACnD,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,uDAAuD;IACvD,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,gEAAgE;IAChE,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,kEAAkE;IAClE,GAAG,EAAE,cAAc,EAAE,CAAC;CACvB"}
@@ -24,5 +24,9 @@ export const CLIENT_EVENT_TYPES = new Set([
24
24
  "mount",
25
25
  "session_end",
26
26
  "design_scan",
27
+ // Sent only by the dashboard telemetry emitter (once per session): whether
28
+ // the reporting install has a model connected, and which provider/model.
29
+ // Never carries a key or any secret — see dashboard/telemetry.tsx.
30
+ "install_state",
27
31
  ]);
28
32
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgFH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAkC,IAAI,GAAG,CAAmB;IACzF,WAAW;IACX,OAAO;IACP,YAAY;IACZ,YAAY;IACZ,cAAc;IACd,UAAU;IACV,UAAU;IACV,WAAW;IACX,OAAO;IACP,gBAAgB;IAChB,OAAO;IACP,aAAa;IACb,aAAa;CACd,CAAC,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiFH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAkC,IAAI,GAAG,CAAmB;IACzF,WAAW;IACX,OAAO;IACP,YAAY;IACZ,YAAY;IACZ,cAAc;IACd,UAAU;IACV,UAAU;IACV,WAAW;IACX,OAAO;IACP,gBAAgB;IAChB,OAAO;IACP,aAAa;IACb,aAAa;IACb,2EAA2E;IAC3E,yEAAyE;IACzE,mEAAmE;IACnE,eAAe;CAChB,CAAC,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",
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
  },