@polishd/next 0.2.2 → 0.2.3-dev.1
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 +2 -1
- package/CHANGELOG.md +238 -0
- package/README.md +63 -0
- package/bin/doctor.mjs +8 -1
- package/dist/ai/digest.d.ts.map +1 -1
- package/dist/ai/digest.js +3 -2
- package/dist/ai/digest.js.map +1 -1
- package/dist/ai/github.d.ts +22 -1
- package/dist/ai/github.d.ts.map +1 -1
- package/dist/ai/github.js +99 -3
- package/dist/ai/github.js.map +1 -1
- package/dist/ai/issue-pages.d.ts +52 -0
- package/dist/ai/issue-pages.d.ts.map +1 -0
- package/dist/ai/issue-pages.js +50 -0
- package/dist/ai/issue-pages.js.map +1 -0
- package/dist/ai/issues.d.ts +13 -1
- package/dist/ai/issues.d.ts.map +1 -1
- package/dist/ai/issues.js +60 -4
- package/dist/ai/issues.js.map +1 -1
- package/dist/ai/summary.d.ts.map +1 -1
- package/dist/ai/summary.js +7 -0
- package/dist/ai/summary.js.map +1 -1
- package/dist/ai/types.d.ts +43 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/client/dom.d.ts +23 -0
- package/dist/client/dom.d.ts.map +1 -1
- package/dist/client/dom.js +45 -2
- package/dist/client/dom.js.map +1 -1
- package/dist/client/init.d.ts.map +1 -1
- package/dist/client/init.js +54 -14
- package/dist/client/init.js.map +1 -1
- package/dist/client/monitor.d.ts.map +1 -1
- package/dist/client/monitor.js +5 -1
- package/dist/client/monitor.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/chrome.d.ts +4 -2
- package/dist/dashboard/chrome.d.ts.map +1 -1
- package/dist/dashboard/chrome.js +15 -5
- package/dist/dashboard/chrome.js.map +1 -1
- package/dist/dashboard/design-review-card.d.ts.map +1 -1
- package/dist/dashboard/design-review-card.js +3 -3
- package/dist/dashboard/design-review-card.js.map +1 -1
- package/dist/dashboard/design.d.ts +5 -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/drawer.d.ts +34 -0
- package/dist/dashboard/drawer.d.ts.map +1 -0
- package/dist/dashboard/drawer.js +44 -0
- package/dist/dashboard/drawer.js.map +1 -0
- package/dist/dashboard/index.d.ts +3 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +48 -37
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/installs.d.ts.map +1 -1
- package/dist/dashboard/installs.js +2 -1
- package/dist/dashboard/installs.js.map +1 -1
- package/dist/dashboard/issues.d.ts +9 -0
- package/dist/dashboard/issues.d.ts.map +1 -0
- package/dist/dashboard/issues.js +81 -0
- package/dist/dashboard/issues.js.map +1 -0
- package/dist/dashboard/journeys.d.ts.map +1 -1
- package/dist/dashboard/journeys.js +4 -2
- package/dist/dashboard/journeys.js.map +1 -1
- package/dist/dashboard/metric-trends.d.ts +6 -0
- package/dist/dashboard/metric-trends.d.ts.map +1 -0
- package/dist/dashboard/metric-trends.js +236 -0
- package/dist/dashboard/metric-trends.js.map +1 -0
- package/dist/dashboard/pages.d.ts.map +1 -1
- package/dist/dashboard/pages.js +8 -46
- package/dist/dashboard/pages.js.map +1 -1
- package/dist/dashboard/settings.d.ts.map +1 -1
- package/dist/dashboard/settings.js +6 -2
- package/dist/dashboard/settings.js.map +1 -1
- package/dist/dashboard/show-more.d.ts.map +1 -1
- package/dist/dashboard/show-more.js +1 -1
- package/dist/dashboard/show-more.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 +13 -0
- package/dist/dashboard/snapshots.d.ts.map +1 -0
- package/dist/dashboard/snapshots.js +88 -0
- package/dist/dashboard/snapshots.js.map +1 -0
- package/dist/dashboard/summary.d.ts.map +1 -1
- package/dist/dashboard/summary.js +11 -6
- package/dist/dashboard/summary.js.map +1 -1
- package/dist/dashboard/telemetry.d.ts +3 -1
- package/dist/dashboard/telemetry.d.ts.map +1 -1
- package/dist/dashboard/telemetry.js +55 -12
- package/dist/dashboard/telemetry.js.map +1 -1
- package/dist/dashboard/ui.d.ts +33 -6
- package/dist/dashboard/ui.d.ts.map +1 -1
- package/dist/dashboard/ui.js +44 -6
- package/dist/dashboard/ui.js.map +1 -1
- package/dist/dashboard.css +193 -2
- package/dist/server/ingest.d.ts +2 -5
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +47 -10
- package/dist/server/ingest.js.map +1 -1
- package/dist/server/queries.d.ts +64 -11
- package/dist/server/queries.d.ts.map +1 -1
- package/dist/server/queries.js +207 -37
- package/dist/server/queries.js.map +1 -1
- package/dist/server/reclassify.d.ts +3 -0
- package/dist/server/reclassify.d.ts.map +1 -0
- package/dist/server/reclassify.js +65 -0
- package/dist/server/reclassify.js.map +1 -0
- package/dist/server/snapshots.d.ts +81 -0
- package/dist/server/snapshots.d.ts.map +1 -0
- package/dist/server/snapshots.js +371 -0
- package/dist/server/snapshots.js.map +1 -0
- package/dist/server/store.d.ts +5 -0
- package/dist/server/store.d.ts.map +1 -1
- package/dist/server/store.js +17 -0
- package/dist/server/store.js.map +1 -1
- package/dist/server/telemetry.d.ts +13 -0
- package/dist/server/telemetry.d.ts.map +1 -1
- package/dist/server/telemetry.js +28 -7
- package/dist/server/telemetry.js.map +1 -1
- 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/dist/shared/metric-buckets.d.ts +31 -0
- package/dist/shared/metric-buckets.d.ts.map +1 -0
- package/dist/shared/metric-buckets.js +49 -0
- package/dist/shared/metric-buckets.js.map +1 -0
- package/dist/shared/text-signals.d.ts +56 -0
- package/dist/shared/text-signals.d.ts.map +1 -0
- package/dist/shared/text-signals.js +80 -0
- package/dist/shared/text-signals.js.map +1 -0
- package/dist/shared/types.d.ts +1 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +4 -0
- package/dist/shared/types.js.map +1 -1
- package/docs/SETUP.md +25 -0
- package/package.json +9 -2
package/dist/server/telemetry.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* the host site's analytics, never its visitors' events.
|
|
20
20
|
*/
|
|
21
21
|
import { getMeta, setMeta, storeReady } from "./store";
|
|
22
|
+
import { resolveSettings } from "../ai/settings";
|
|
22
23
|
/** Where telemetry lands: the polishd site's own installation. */
|
|
23
24
|
export const DEFAULT_TELEMETRY_ENDPOINT = "https://polishd.muskanvaswan.xyz/api/polishd-telemetry";
|
|
24
25
|
const CONSENT_KEY = "telemetry_consent";
|
|
@@ -47,28 +48,48 @@ export function resolveTelemetryEndpoint() {
|
|
|
47
48
|
export async function saveTelemetryConsent(granted) {
|
|
48
49
|
await setMeta(CONSENT_KEY, JSON.stringify({ status: granted ? "granted" : "denied", at: Date.now() }));
|
|
49
50
|
}
|
|
51
|
+
/** The install-state snapshot the emitter reports. Never includes secrets. */
|
|
52
|
+
async function loadInstallState() {
|
|
53
|
+
try {
|
|
54
|
+
const { settings } = await resolveSettings();
|
|
55
|
+
return {
|
|
56
|
+
hasModel: settings.apiKey.length > 0,
|
|
57
|
+
provider: settings.provider,
|
|
58
|
+
model: settings.model,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { hasModel: false, provider: "", model: "" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
50
65
|
/** What the dashboard should do about telemetry on this render. */
|
|
51
66
|
export async function loadPolishdTelemetryState() {
|
|
52
67
|
const endpoint = resolveTelemetryEndpoint();
|
|
68
|
+
const off = (status) => ({
|
|
69
|
+
status,
|
|
70
|
+
endpoint,
|
|
71
|
+
installState: null,
|
|
72
|
+
});
|
|
53
73
|
if (telemetryDisabledByEnv())
|
|
54
|
-
return
|
|
74
|
+
return off("disabled");
|
|
55
75
|
// Without a writable store there is nowhere to keep the answer, so asking
|
|
56
76
|
// would mean asking on every single render. Stay quiet instead.
|
|
57
77
|
if (!(await storeReady()))
|
|
58
|
-
return
|
|
78
|
+
return off("disabled");
|
|
59
79
|
const raw = await getMeta(CONSENT_KEY);
|
|
60
80
|
if (!raw)
|
|
61
|
-
return
|
|
81
|
+
return off("unset");
|
|
62
82
|
try {
|
|
63
83
|
const parsed = JSON.parse(raw);
|
|
64
|
-
if (parsed.status === "granted")
|
|
65
|
-
return { status: "granted", endpoint };
|
|
84
|
+
if (parsed.status === "granted") {
|
|
85
|
+
return { status: "granted", endpoint, installState: await loadInstallState() };
|
|
86
|
+
}
|
|
66
87
|
if (parsed.status === "denied")
|
|
67
|
-
return
|
|
88
|
+
return off("denied");
|
|
68
89
|
}
|
|
69
90
|
catch {
|
|
70
91
|
// Unreadable record — treat as never asked rather than silently sending.
|
|
71
92
|
}
|
|
72
|
-
return
|
|
93
|
+
return off("unset");
|
|
73
94
|
}
|
|
74
95
|
//# sourceMappingURL=telemetry.js.map
|
|
@@ -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;
|
|
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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polishd — which time buckets a trend chart should draw.
|
|
3
|
+
*
|
|
4
|
+
* Pure: no database, no React, no clock of its own. The counting happens in
|
|
5
|
+
* SQL (see `getMetricTrends`); this decides the *shape* of the series that
|
|
6
|
+
* comes out of it, which is where the edge cases live:
|
|
7
|
+
*
|
|
8
|
+
* • **Gap filling.** An hour with no events is a measured zero, not a
|
|
9
|
+
* missing point. Without this a quiet night would render as one wide bar
|
|
10
|
+
* next to another, silently compressing the axis.
|
|
11
|
+
* • **A cap.** A site running for two years has 17,000 hourly buckets. Only
|
|
12
|
+
* the most recent `maxBuckets` are drawn.
|
|
13
|
+
* • **Anchoring on data, not on now.** The window ends at the newest bucket
|
|
14
|
+
* that has something in it, so an install last browsed three days ago
|
|
15
|
+
* still gets a chart instead of an empty one.
|
|
16
|
+
* • **Clock skew.** `ts` is the visitor's clock. One machine set to next
|
|
17
|
+
* year would otherwise anchor the window there and blank the chart for
|
|
18
|
+
* everyone, so buckets past the present are discarded.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The contiguous run of bucket indexes to render, oldest → newest.
|
|
22
|
+
*
|
|
23
|
+
* @param buckets Bucket indexes that hold data, in any order.
|
|
24
|
+
* @param maxBuckets How many to draw at most; the newest win.
|
|
25
|
+
* @param nowBucket The bucket the present falls in. Anything later is
|
|
26
|
+
* discarded as clock skew.
|
|
27
|
+
* @returns Every index from the window's start to its end inclusive — callers
|
|
28
|
+
* fill the ones with no data with zeros. Empty when nothing qualifies.
|
|
29
|
+
*/
|
|
30
|
+
export declare function trendWindow(buckets: Iterable<number>, maxBuckets: number, nowBucket: number): number[];
|
|
31
|
+
//# sourceMappingURL=metric-buckets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-buckets.d.ts","sourceRoot":"","sources":["../../src/shared/metric-buckets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,EACzB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,MAAM,EAAE,CAcV"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polishd — which time buckets a trend chart should draw.
|
|
3
|
+
*
|
|
4
|
+
* Pure: no database, no React, no clock of its own. The counting happens in
|
|
5
|
+
* SQL (see `getMetricTrends`); this decides the *shape* of the series that
|
|
6
|
+
* comes out of it, which is where the edge cases live:
|
|
7
|
+
*
|
|
8
|
+
* • **Gap filling.** An hour with no events is a measured zero, not a
|
|
9
|
+
* missing point. Without this a quiet night would render as one wide bar
|
|
10
|
+
* next to another, silently compressing the axis.
|
|
11
|
+
* • **A cap.** A site running for two years has 17,000 hourly buckets. Only
|
|
12
|
+
* the most recent `maxBuckets` are drawn.
|
|
13
|
+
* • **Anchoring on data, not on now.** The window ends at the newest bucket
|
|
14
|
+
* that has something in it, so an install last browsed three days ago
|
|
15
|
+
* still gets a chart instead of an empty one.
|
|
16
|
+
* • **Clock skew.** `ts` is the visitor's clock. One machine set to next
|
|
17
|
+
* year would otherwise anchor the window there and blank the chart for
|
|
18
|
+
* everyone, so buckets past the present are discarded.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The contiguous run of bucket indexes to render, oldest → newest.
|
|
22
|
+
*
|
|
23
|
+
* @param buckets Bucket indexes that hold data, in any order.
|
|
24
|
+
* @param maxBuckets How many to draw at most; the newest win.
|
|
25
|
+
* @param nowBucket The bucket the present falls in. Anything later is
|
|
26
|
+
* discarded as clock skew.
|
|
27
|
+
* @returns Every index from the window's start to its end inclusive — callers
|
|
28
|
+
* fill the ones with no data with zeros. Empty when nothing qualifies.
|
|
29
|
+
*/
|
|
30
|
+
export function trendWindow(buckets, maxBuckets, nowBucket) {
|
|
31
|
+
let first = null;
|
|
32
|
+
let last = null;
|
|
33
|
+
for (const b of buckets) {
|
|
34
|
+
if (b > nowBucket)
|
|
35
|
+
continue;
|
|
36
|
+
if (first === null || b < first)
|
|
37
|
+
first = b;
|
|
38
|
+
if (last === null || b > last)
|
|
39
|
+
last = b;
|
|
40
|
+
}
|
|
41
|
+
if (first === null || last === null)
|
|
42
|
+
return [];
|
|
43
|
+
const start = Math.max(first, last - (Math.max(1, maxBuckets) - 1));
|
|
44
|
+
const out = [];
|
|
45
|
+
for (let b = start; b <= last; b++)
|
|
46
|
+
out.push(b);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=metric-buckets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-buckets.js","sourceRoot":"","sources":["../../src/shared/metric-buckets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CACzB,OAAyB,EACzB,UAAkB,EAClB,SAAiB;IAEjB,IAAI,KAAK,GAAkB,IAAI,CAAC;IAChC,IAAI,IAAI,GAAkB,IAAI,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,GAAG,SAAS;YAAE,SAAS;QAC5B,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,GAAG,KAAK;YAAE,KAAK,GAAG,CAAC,CAAC;QAC3C,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,GAAG,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAE/C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACpE,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polishd — shared "was this click about text?" heuristics.
|
|
3
|
+
*
|
|
4
|
+
* One source of truth used from two sides that must never drift:
|
|
5
|
+
*
|
|
6
|
+
* • Live, in the browser (`src/client/dom.ts`) — deciding whether a click
|
|
7
|
+
* records as dead/rage or is just reading.
|
|
8
|
+
* • Retroactively, on the server (`src/server/reclassify.ts`) — applying the
|
|
9
|
+
* same decision to rows stored by older clients, using the stored
|
|
10
|
+
* `selector` (which keeps the same 4-level tag path the live ancestor walk
|
|
11
|
+
* sees) and `text` label in place of the DOM.
|
|
12
|
+
*
|
|
13
|
+
* Dependency-free and runtime-agnostic, like the rest of `shared/`.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Block-level prose containers. A click anywhere inside one — including on a
|
|
17
|
+
* bold word or code span nested in it — is reading, never a dead control.
|
|
18
|
+
*/
|
|
19
|
+
export declare const PROSE_TAGS: ReadonlySet<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Inline / cell-level text elements. These carry text but are also the exact
|
|
22
|
+
* shape of a mis-wired fake control (`<span class="link">Click here</span>`),
|
|
23
|
+
* so on their own they only read as prose past the length threshold below.
|
|
24
|
+
*/
|
|
25
|
+
export declare const INLINE_TEXT_TAGS: ReadonlySet<string>;
|
|
26
|
+
/** Tags the capture layer treats as interactive (see `isInteractive`). */
|
|
27
|
+
export declare const INTERACTIVE_TAGS: ReadonlySet<string>;
|
|
28
|
+
/**
|
|
29
|
+
* The length heuristic: standalone text this long reads as content; anything
|
|
30
|
+
* shorter could be a label on something the user believed was clickable.
|
|
31
|
+
* Stored `text` labels are capped at 80 chars, so the threshold must stay
|
|
32
|
+
* under that cap for the retro classifier to see it.
|
|
33
|
+
*/
|
|
34
|
+
export declare const PROSE_MIN_CHARS = 40;
|
|
35
|
+
/** Tag names of a stored selector path: "div.card>p.intro>span" → ["div","p","span"]. */
|
|
36
|
+
export declare const selectorTags: (selector: string) => string[];
|
|
37
|
+
/**
|
|
38
|
+
* The retro mirror of the client's `isTextTarget`, over stored columns:
|
|
39
|
+
* a prose tag anywhere in the selector path stands in for the ancestor walk,
|
|
40
|
+
* and the stored label's length stands in for the target's own text. The label
|
|
41
|
+
* is `innerText`, which includes children — so a layout container wrapping
|
|
42
|
+
* prose can classify as textual here where the live check would not. For
|
|
43
|
+
* history that's the right bias: these rows were recorded under rules known to
|
|
44
|
+
* overcount, and reclassifying reading as reading is the point.
|
|
45
|
+
*/
|
|
46
|
+
export declare const isTextualClickRow: (selector: string | undefined, text: string | undefined) => boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The single server-side retype decision, shared by the one-time history sweep
|
|
49
|
+
* and the ingest backstop: a dead/rage click that landed on text becomes a
|
|
50
|
+
* `text_click` (kept, counted by nothing); everything else keeps its type. A
|
|
51
|
+
* rage row may have hammered a real control — the selector keeps its tag path,
|
|
52
|
+
* so anything interactive in it stays rage. (Dead clicks were only ever
|
|
53
|
+
* recorded on non-interactive targets — no guard needed.)
|
|
54
|
+
*/
|
|
55
|
+
export declare const reclassifiedClickType: (type: string, selector: string | undefined, text: string | undefined) => string;
|
|
56
|
+
//# sourceMappingURL=text-signals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-signals.d.ts","sourceRoot":"","sources":["../../src/shared/text-signals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,WAAW,CAAC,MAAM,CAGzC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAG/C,CAAC;AAEH,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAE/C,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,yFAAyF;AACzF,eAAO,MAAM,YAAY,GAAI,UAAU,MAAM,KAAG,MAAM,EAIlC,CAAC;AAErB;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAC5B,UAAU,MAAM,GAAG,SAAS,EAC5B,MAAM,MAAM,GAAG,SAAS,KACvB,OAGF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,UAAU,MAAM,GAAG,SAAS,EAC5B,MAAM,MAAM,GAAG,SAAS,KACvB,MAUF,CAAC"}
|