@polishd/next 0.1.0 → 0.2.1-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 +161 -46
- package/CHANGELOG.md +163 -0
- package/README.md +275 -49
- package/bin/doctor.mjs +374 -0
- package/bin/init.mjs +215 -188
- package/bin/polishd.mjs +85 -0
- package/bin/shared.mjs +114 -0
- package/dist/ai/actions.d.ts +13 -2
- package/dist/ai/actions.d.ts.map +1 -1
- package/dist/ai/actions.js +19 -1
- package/dist/ai/actions.js.map +1 -1
- package/dist/ai/design.d.ts +28 -0
- package/dist/ai/design.d.ts.map +1 -0
- package/dist/ai/design.js +245 -0
- package/dist/ai/design.js.map +1 -0
- package/dist/ai/guard.d.ts +44 -3
- package/dist/ai/guard.d.ts.map +1 -1
- package/dist/ai/guard.js +13 -2
- package/dist/ai/guard.js.map +1 -1
- package/dist/ai/providers.d.ts +2 -0
- package/dist/ai/providers.d.ts.map +1 -1
- package/dist/ai/providers.js +70 -0
- package/dist/ai/providers.js.map +1 -1
- package/dist/ai/settings.d.ts +15 -1
- package/dist/ai/settings.d.ts.map +1 -1
- package/dist/ai/settings.js +38 -1
- package/dist/ai/settings.js.map +1 -1
- package/dist/ai/types.d.ts +57 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/client/design-scan.d.ts +37 -0
- package/dist/client/design-scan.d.ts.map +1 -0
- package/dist/client/design-scan.js +319 -0
- package/dist/client/design-scan.js.map +1 -0
- package/dist/client/init.d.ts.map +1 -1
- package/dist/client/init.js +67 -2
- package/dist/client/init.js.map +1 -1
- package/dist/config.d.ts +57 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +62 -3
- package/dist/config.js.map +1 -1
- package/dist/dashboard/design-review-card.d.ts +15 -0
- package/dist/dashboard/design-review-card.d.ts.map +1 -0
- package/dist/dashboard/design-review-card.js +82 -0
- package/dist/dashboard/design-review-card.js.map +1 -0
- package/dist/dashboard/design.d.ts +10 -0
- package/dist/dashboard/design.d.ts.map +1 -0
- package/dist/dashboard/design.js +83 -0
- package/dist/dashboard/design.js.map +1 -0
- package/dist/dashboard/index.d.ts +35 -5
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +176 -18
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard/journeys.d.ts.map +1 -1
- package/dist/dashboard/journeys.js +1 -0
- package/dist/dashboard/journeys.js.map +1 -1
- package/dist/dashboard/summary.d.ts.map +1 -1
- package/dist/dashboard/summary.js +59 -5
- package/dist/dashboard/summary.js.map +1 -1
- package/dist/dashboard/token-auth.d.ts +56 -0
- package/dist/dashboard/token-auth.d.ts.map +1 -0
- package/dist/dashboard/token-auth.js +158 -0
- package/dist/dashboard/token-auth.js.map +1 -0
- package/dist/dashboard/unlock.d.ts +2 -0
- package/dist/dashboard/unlock.d.ts.map +1 -0
- package/dist/dashboard/unlock.js +54 -0
- package/dist/dashboard/unlock.js.map +1 -0
- package/dist/dashboard.css +2740 -0
- package/dist/proxy.d.ts +53 -5
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +73 -3
- package/dist/proxy.js.map +1 -1
- package/dist/route.d.ts.map +1 -1
- package/dist/route.js +11 -3
- package/dist/route.js.map +1 -1
- package/dist/server/design.d.ts +93 -0
- package/dist/server/design.d.ts.map +1 -0
- package/dist/server/design.js +430 -0
- package/dist/server/design.js.map +1 -0
- package/dist/server/ingest.d.ts +33 -2
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +70 -8
- package/dist/server/ingest.js.map +1 -1
- package/dist/server/queries.d.ts +16 -0
- package/dist/server/queries.d.ts.map +1 -1
- package/dist/server/queries.js +80 -16
- package/dist/server/queries.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +4 -2
- package/dist/session.js.map +1 -1
- package/dist/shared/types.d.ts +31 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +1 -0
- package/dist/shared/types.js.map +1 -1
- package/docs/SETUP.md +308 -51
- package/package.json +14 -7
package/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
Drop-in **product analytics** for Next.js (App Router). Capture real user
|
|
6
6
|
behavioral signals — rage clicks, dead clicks, scroll depth, JS errors, web
|
|
7
7
|
vitals, component engagement — and explore them on a built-in, Vercel-style
|
|
8
|
-
dashboard at `/polishd`.
|
|
8
|
+
dashboard at `/polishd`. A second **Design** tab reverse-engineers your site's
|
|
9
|
+
design system from the rendered pages — typography, palette, radii, spacing —
|
|
10
|
+
and flags what breaks it.
|
|
9
11
|
|
|
10
12
|
No third-party service. Events go to your own database: **SQLite in dev**
|
|
11
13
|
(zero-config, built into Node), **Postgres in production**. Anonymous by design —
|
|
@@ -13,6 +15,32 @@ one httpOnly UUID cookie per session, no fingerprinting, no PII.
|
|
|
13
15
|
|
|
14
16
|
---
|
|
15
17
|
|
|
18
|
+
## Installing with a coding agent
|
|
19
|
+
|
|
20
|
+
Polishd ships a machine-readable install procedure. If you use Claude Code,
|
|
21
|
+
Cursor, Copilot, or any other coding agent, **paste this prompt** and let it do
|
|
22
|
+
the whole install:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
Install @polishd/next into this Next.js app.
|
|
26
|
+
|
|
27
|
+
Follow the procedure in AGENTS.md from the package exactly — after installing,
|
|
28
|
+
read node_modules/@polishd/next/AGENTS.md. If the package isn't installed yet,
|
|
29
|
+
read it at https://github.com/muskanvaswan/polishd/blob/main/AGENTS.md
|
|
30
|
+
|
|
31
|
+
Work through its phases in order and do not skip Phase 7 (verification).
|
|
32
|
+
Several of its constraints fail silently at runtime rather than at build time,
|
|
33
|
+
so a passing build does not mean the install worked.
|
|
34
|
+
|
|
35
|
+
Finish by running `npx @polishd/next doctor` and fixing everything it reports.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`npx @polishd/next doctor` is the ground truth: it checks the wiring that fails
|
|
39
|
+
quietly — the proxy named for the wrong Next major, route segment config
|
|
40
|
+
re-exported instead of declared, a matcher that isn't an inline literal, the
|
|
41
|
+
dashboard stylesheet not imported. Add `--url http://localhost:3000` to also
|
|
42
|
+
verify live that the session cookie is minted and events actually store.
|
|
43
|
+
|
|
16
44
|
## Documentation
|
|
17
45
|
|
|
18
46
|
| | |
|
|
@@ -20,27 +48,36 @@ one httpOnly UUID cookie per session, no fingerprinting, no PII.
|
|
|
20
48
|
| **[docs/SETUP.md](./docs/SETUP.md)** | Full walkthrough — every variant, verification, troubleshooting |
|
|
21
49
|
| **[AGENTS.md](./AGENTS.md)** | Install procedure written for AI coding agents |
|
|
22
50
|
| **[DATABASE.md](./DATABASE.md)** | Production database setup, schema, retention |
|
|
51
|
+
| **[CHANGELOG.md](./CHANGELOG.md)** | Release notes — **start here if you're upgrading from 0.1.x** |
|
|
23
52
|
|
|
24
53
|
## Requirements
|
|
25
54
|
|
|
26
55
|
- Next.js **15+**, App Router (no Pages Router support)
|
|
27
56
|
- Node **22.5+** (the dev store uses the built-in `node:sqlite`)
|
|
28
|
-
- Tailwind CSS v3 or v4, if you want the dashboard styled — see [Styling](#styling-required)
|
|
29
57
|
- Postgres in production; nothing extra for local development
|
|
30
58
|
|
|
31
59
|
## Quickstart
|
|
32
60
|
|
|
33
61
|
```bash
|
|
34
62
|
npm install @polishd/next
|
|
35
|
-
npx polishd init
|
|
36
|
-
npm run dev
|
|
63
|
+
npx @polishd/next init
|
|
64
|
+
npm run dev # then visit /polishd
|
|
65
|
+
npx @polishd/next doctor # confirm the wiring
|
|
37
66
|
```
|
|
38
67
|
|
|
68
|
+
(`npx polishd init` is the shorter form once the package is installed;
|
|
69
|
+
`npx @polishd/next init` also works before it is.)
|
|
70
|
+
|
|
39
71
|
That's it locally — events write to `.polishd/analytics.db` with no
|
|
40
|
-
configuration. `init` detects your layout (`src/` or root),
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
72
|
+
configuration. `init` detects your layout (`src/` or root), detects your
|
|
73
|
+
installed Next major to name the proxy file correctly, writes the four files
|
|
74
|
+
below, and adds `.polishd/` to `.gitignore`.
|
|
75
|
+
|
|
76
|
+
Files it already wrote are skipped unless you pass `--force`, which backs them
|
|
77
|
+
up to `.bak` first. **Files it did not write are never overwritten**, with or
|
|
78
|
+
without `--force` — if you already have a proxy, it prints how to merge instead
|
|
79
|
+
of clobbering your code. Other flags: `--dry-run` to preview, `--js` for
|
|
80
|
+
JavaScript, `--config` to also emit a `polishd.config.ts`.
|
|
44
81
|
|
|
45
82
|
`pg` installs automatically as an optional dependency and is loaded only when
|
|
46
83
|
you point Polishd at a Postgres URL.
|
|
@@ -70,11 +107,12 @@ export const runtime = "nodejs"; // must be declared here, not re-exporte
|
|
|
70
107
|
export const dynamic = "force-dynamic";
|
|
71
108
|
export const POST = createPolishdRoute();
|
|
72
109
|
|
|
73
|
-
// src/app/polishd/page.tsx — the dashboard
|
|
110
|
+
// src/app/polishd/page.tsx — the dashboard
|
|
111
|
+
import "@polishd/next/dashboard.css"; // self-contained; no Tailwind needed
|
|
74
112
|
import { createPolishdPage } from "@polishd/next/dashboard";
|
|
75
113
|
export const runtime = "nodejs"; // Next requires these inline in the page
|
|
76
114
|
export const dynamic = "force-dynamic";
|
|
77
|
-
export default createPolishdPage();
|
|
115
|
+
export default createPolishdPage(); // see Protecting the dashboard
|
|
78
116
|
```
|
|
79
117
|
|
|
80
118
|
> **Route segment config can't be re-exported.** Next statically parses
|
|
@@ -88,35 +126,127 @@ export default createPolishdPage();
|
|
|
88
126
|
> under Next 16 + Turbopack, a proxy matcher that touches any `/api/*` route
|
|
89
127
|
> breaks resolution for the entire `/api` segment.
|
|
90
128
|
|
|
91
|
-
##
|
|
129
|
+
## Composing with an existing proxy
|
|
92
130
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
131
|
+
Next allows exactly **one** proxy module per app (`proxy.ts` on Next 16+,
|
|
132
|
+
`middleware.ts` on Next 15), so if you already have one, Polishd has to go
|
|
133
|
+
inside it. `init` detects this and prints instructions rather than overwriting.
|
|
96
134
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
The subtle part is the matcher. One file means one `config.matcher`, so adding
|
|
136
|
+
Polishd means widening it to the union — and Polishd's matcher is near-total.
|
|
137
|
+
Your handler, which you deliberately scoped to a couple of routes, would
|
|
138
|
+
silently start running on every request in the app. Nothing errors; your
|
|
139
|
+
redirects and rewrites just begin firing where they never did before.
|
|
140
|
+
|
|
141
|
+
Declare your original scope and `composePolishd` re-applies it as an internal
|
|
142
|
+
guard:
|
|
102
143
|
|
|
103
144
|
```ts
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
// proxy.ts (middleware.ts on Next 15, with the function named `middleware`)
|
|
146
|
+
import { composePolishd } from "@polishd/next/proxy";
|
|
147
|
+
import type { NextRequest } from "next/server";
|
|
148
|
+
|
|
149
|
+
function myProxy(request: NextRequest) {
|
|
150
|
+
if (!hasSession(request)) return NextResponse.redirect(new URL("/login", request.url));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Your handler still only runs on /admin/*, even though the matcher below
|
|
154
|
+
// now feeds Polishd every page request.
|
|
155
|
+
export const proxy = composePolishd(myProxy, { matcher: "/admin/:path*" });
|
|
156
|
+
|
|
157
|
+
// Written by hand: Next statically parses this and it must be an inline
|
|
158
|
+
// literal, so no helper can generate it. The union of your paths and Polishd's.
|
|
159
|
+
export const config = {
|
|
160
|
+
matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
|
|
161
|
+
};
|
|
109
162
|
```
|
|
110
163
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
164
|
+
Your handler runs first and its response is threaded through, so a redirect or
|
|
165
|
+
rewrite you return **carries the analytics cookie** rather than losing it.
|
|
166
|
+
Returning nothing is fine. `matcher` accepts Next's matcher strings
|
|
167
|
+
(`"/admin/:path*"`, `"/((?!api).*)"`) or a `RegExp` when you want to be exact;
|
|
168
|
+
omit it only if your handler genuinely did run on everything.
|
|
169
|
+
|
|
170
|
+
Widening the matcher is safe from Polishd's side: `withPolishdSession` returns
|
|
171
|
+
early once the cookie is present, so the extra paths cost a cookie lookup and
|
|
172
|
+
nothing else.
|
|
173
|
+
|
|
174
|
+
## Styling
|
|
175
|
+
|
|
176
|
+
The dashboard ships its own stylesheet. One import, no configuration:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// app/polishd/page.tsx — `init` writes this for you
|
|
180
|
+
import "@polishd/next/dashboard.css";
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Tailwind is not required in your app.** The stylesheet is compiled at publish
|
|
184
|
+
time, so it doesn't depend on your build setup, your Tailwind version, or
|
|
185
|
+
whether you use Tailwind at all.
|
|
186
|
+
|
|
187
|
+
It is built not to interfere with the app it lands in:
|
|
188
|
+
|
|
189
|
+
- **No global preflight.** Tailwind's reset is what would restyle every element
|
|
190
|
+
on your page; it's left out. A scoped equivalent applies to the dashboard's
|
|
191
|
+
own subtree instead, so your headings, lists and buttons are untouched.
|
|
192
|
+
- **Every rule is scoped to `.polishd-root`**, verified at build time — nothing
|
|
193
|
+
in the file can match your markup.
|
|
194
|
+
- **The scope is doubled (`.polishd-root.polishd-root`)** so the dashboard wins
|
|
195
|
+
specificity ties against a host that happens to have its own `.flex`,
|
|
196
|
+
`.hidden` or `.container`, rather than the winner depending on stylesheet
|
|
197
|
+
order.
|
|
198
|
+
- **No cascade layers**, because unlayered host CSS beats layered CSS outright —
|
|
199
|
+
a plain `h1 { color: … }` in your app would otherwise override the dashboard's
|
|
200
|
+
own typography.
|
|
201
|
+
|
|
202
|
+
The one thing it can't win is a host `!important` rule aimed at the same
|
|
203
|
+
property. Nothing short of `!important` on our side would, and a package
|
|
204
|
+
shouting over its host is worse than a rare collision.
|
|
205
|
+
|
|
206
|
+
> **Upgrading from 0.1.x?** Nothing to do — your existing `@source
|
|
207
|
+
> "…/@polishd/next/dist"` keeps compiling the dashboard's utilities exactly as
|
|
208
|
+
> before, so the dashboard stays styled whether or not you add the import.
|
|
209
|
+
>
|
|
210
|
+
> To move over at your own pace, **add the import first, then** delete the
|
|
211
|
+
> `@source` line — in that order, so the dashboard is never unstyled in between.
|
|
212
|
+
> `polishd doctor` reports which setup it finds and won't call a working 0.1.x
|
|
213
|
+
> install broken.
|
|
114
214
|
|
|
115
215
|
## Protecting the dashboard
|
|
116
216
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
217
|
+
The dashboard is not a read-only report. Its settings change the configured
|
|
218
|
+
model and **API base URL**, so anyone who can reach it can point your requests
|
|
219
|
+
at their own endpoint and spend your key. Treat it as a credential-adjacent
|
|
220
|
+
surface.
|
|
221
|
+
|
|
222
|
+
In **development** it is open and says so once in the console. In
|
|
223
|
+
**production** Polishd refuses to serve it until you have made a decision.
|
|
224
|
+
There are three ways to make one.
|
|
225
|
+
|
|
226
|
+
### 1. A token — no code, works with no auth of your own
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
POLISHD_DASHBOARD_TOKEN=$(openssl rand -hex 32)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
That is the whole integration. The locked screen shows a password field; the
|
|
233
|
+
token is exchanged for an httpOnly, path-scoped cookie by a server action, so
|
|
234
|
+
it never appears in a URL, in browser history, in server logs, or in a
|
|
235
|
+
`Referer` header. The cookie holds an HMAC of the token rather than the token
|
|
236
|
+
itself, comparison is constant-time, expiry is enforced server-side, and the
|
|
237
|
+
unlock form is rate-limited per IP.
|
|
238
|
+
|
|
239
|
+
Tune it if you moved the dashboard off `/polishd`:
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
import { createPolishdPage, polishdTokenAuth } from "@polishd/next/dashboard";
|
|
243
|
+
|
|
244
|
+
export default createPolishdPage({
|
|
245
|
+
authenticate: polishdTokenAuth({ basePath: "/admin/polishd" }),
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 2. Your app's own auth
|
|
120
250
|
|
|
121
251
|
```tsx
|
|
122
252
|
import { createPolishdPage } from "@polishd/next/dashboard";
|
|
@@ -127,21 +257,37 @@ export const runtime = "nodejs";
|
|
|
127
257
|
export const dynamic = "force-dynamic";
|
|
128
258
|
|
|
129
259
|
export default createPolishdPage({
|
|
130
|
-
authenticate: isAuthenticated,
|
|
131
|
-
unauthorized: <MyLogin />,
|
|
260
|
+
authenticate: isAuthenticated, // (ctx) => boolean | Promise<boolean>
|
|
261
|
+
unauthorized: <MyLogin />, // optional; a minimal screen otherwise
|
|
132
262
|
});
|
|
133
263
|
```
|
|
134
264
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
an addressable POST endpoint whose id ships in the public client bundle, so
|
|
138
|
-
gating only the page would leave those actions callable by anyone. They re-run
|
|
139
|
-
`authenticate` against the caller's cookies on every call, and deny when no
|
|
140
|
-
dashboard page has registered a policy.
|
|
265
|
+
`authenticate` receives the request's `cookies` and `headers`, so it is an
|
|
266
|
+
ordinary function of its inputs and can be unit tested without a live request:
|
|
141
267
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
268
|
+
```ts
|
|
269
|
+
const authenticate = (ctx) => ctx.cookies.get("session")?.value === expected;
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
A zero-argument callback still works unchanged.
|
|
273
|
+
|
|
274
|
+
### 3. Public on purpose
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
POLISHD_DASHBOARD_PUBLIC=true
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
An explicit, grep-able opt-in. You will still get one console warning per boot,
|
|
281
|
+
in production, because that is the only place it is worth acting on.
|
|
282
|
+
|
|
283
|
+
> **The gate covers the server actions, not just the page.** The AI actions
|
|
284
|
+
> (generate summary, save settings, scan codebase, file GitHub issues) are
|
|
285
|
+
> independently addressable POST endpoints whose ids ship in the public client
|
|
286
|
+
> bundle, so gating only the page would leave them callable by anyone. They
|
|
287
|
+
> re-run your check against the caller's cookies on every call, and deny when
|
|
288
|
+
> no dashboard page has registered a policy — including when you delete
|
|
289
|
+
> `app/polishd/page.tsx` but keep the package, in which case the actions stay
|
|
290
|
+
> routable and always deny.
|
|
145
291
|
|
|
146
292
|
## AI summary
|
|
147
293
|
|
|
@@ -216,6 +362,40 @@ authoritative context, so:
|
|
|
216
362
|
> provide. Your source is read only during an explicit scan (or a targeted
|
|
217
363
|
> gap-fill), and always under strict size budgets.
|
|
218
364
|
|
|
365
|
+
## Design review
|
|
366
|
+
|
|
367
|
+
The dashboard's **Design** tab (in the sidebar) is your site's brand guideline
|
|
368
|
+
reverse-engineered from what actually shipped. Every page measures its own
|
|
369
|
+
rendered design once per visitor session — the computed typography, colors,
|
|
370
|
+
corner radii and padding, reduced to a compact tally of a few KB — and the tab
|
|
371
|
+
assembles the site-wide picture:
|
|
372
|
+
|
|
373
|
+
- **Typography** — every font family / size / weight combination in use,
|
|
374
|
+
rendered as specimens with real text from your pages.
|
|
375
|
+
- **Color palette** — swatches weighted by how much of the UI each color
|
|
376
|
+
covers, split by role (text / background / border).
|
|
377
|
+
- **Contrast** — each text-on-background pairing checked against WCAG AA.
|
|
378
|
+
- **Corner radii & spacing** — the rounding and padding scales in use.
|
|
379
|
+
|
|
380
|
+
The first layer of review is **deterministic** — rule-based checks, no model:
|
|
381
|
+
too many font sizes to be a type scale, extra font families, near-duplicate
|
|
382
|
+
colors (`#fefefe` living next to `#ffffff`), contrast failures with the exact
|
|
383
|
+
ratio, one-off values that appear on a single page, padding off the 4px grid.
|
|
384
|
+
|
|
385
|
+
The second layer is the **Aesthetic review** card: the same model you
|
|
386
|
+
configured for the analytics summary reads the measured tokens and findings
|
|
387
|
+
and answers the real question — does this read as one deliberate design, or an
|
|
388
|
+
accumulation of one-off decisions? Issues it raises must cite a token that
|
|
389
|
+
actually appears in the metrics (a hex value, a px size, a page path), and each
|
|
390
|
+
comes with a concrete fix. It follows the summary's token-thrift rules: the
|
|
391
|
+
model sees a few hundred tokens of aggregated metrics, the result is cached and
|
|
392
|
+
fingerprinted, and regenerating over unchanged metrics is free. Refresh buttons
|
|
393
|
+
for both the metrics and the analysis are on the tab.
|
|
394
|
+
|
|
395
|
+
Nothing extra to install or wire: the scan ships with the existing client,
|
|
396
|
+
flows through the existing ingest route, and the dashboard itself is never
|
|
397
|
+
scanned.
|
|
398
|
+
|
|
219
399
|
## Component-level tracking
|
|
220
400
|
|
|
221
401
|
Wrap any element to track it explicitly. Interactive controls get hover + click
|
|
@@ -236,8 +416,7 @@ import { PolishdMonitor } from "@polishd/next/client";
|
|
|
236
416
|
|
|
237
417
|
## Configuration
|
|
238
418
|
|
|
239
|
-
Defaults suit a low-traffic site. Override via `polishd.config.ts
|
|
240
|
-
`initPolishd`:
|
|
419
|
+
Defaults suit a low-traffic site. Override via `polishd.config.ts`:
|
|
241
420
|
|
|
242
421
|
```ts
|
|
243
422
|
// polishd.config.ts
|
|
@@ -248,11 +427,32 @@ export default definePolishdConfig({
|
|
|
248
427
|
});
|
|
249
428
|
```
|
|
250
429
|
|
|
430
|
+
The dashboard never measures itself: nothing on `/polishd` (or below it) is
|
|
431
|
+
captured, so reading your analytics can't show up as your site's most-used
|
|
432
|
+
feature. Moved the dashboard elsewhere? Set `dashboardRoute`.
|
|
433
|
+
|
|
434
|
+
> **Nothing imports this file for you.** It cannot be auto-loaded: the three
|
|
435
|
+
> places that read config run in three different runtimes — the browser, the
|
|
436
|
+
> Edge proxy, and the Node route handler — and the Edge one has no filesystem.
|
|
437
|
+
> Pass it to each explicitly:
|
|
438
|
+
|
|
251
439
|
```ts
|
|
252
|
-
// src/instrumentation-client.ts
|
|
253
|
-
import { initPolishd } from "@polishd/next/client";
|
|
254
440
|
import config from "../polishd.config";
|
|
255
|
-
|
|
441
|
+
|
|
442
|
+
initPolishd(config); // instrumentation-client.ts
|
|
443
|
+
export const POST = createPolishdRoute(config); // app/api/polishd/route.ts
|
|
444
|
+
const { proxy } = createPolishdProxy(config); // proxy.ts
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Thread it into one but not another and the layers disagree silently — a proxy
|
|
448
|
+
minting cookie `a` while ingest looks for `b` produces exactly the empty
|
|
449
|
+
dashboard described below.
|
|
450
|
+
|
|
451
|
+
For **`sessionCookie` specifically, prefer the environment variable**. It is the
|
|
452
|
+
one channel every runtime can read, so it cannot go half-applied:
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
POLISHD_SESSION_COOKIE=my_session
|
|
256
456
|
```
|
|
257
457
|
|
|
258
458
|
## Environment variables
|
|
@@ -261,6 +461,9 @@ initPolishd(config);
|
|
|
261
461
|
|---|---|---|
|
|
262
462
|
| `POLISHD_DATABASE_URL` | production | Pooled Postgres connection string — enables capture |
|
|
263
463
|
| `POLISHD_DB_PATH` | dev (optional) | Custom SQLite path (default `.polishd/analytics.db`) |
|
|
464
|
+
| `POLISHD_DASHBOARD_TOKEN` | production | Protects `/polishd` with the built-in gate. Generate with `openssl rand -hex 32` |
|
|
465
|
+
| `POLISHD_DASHBOARD_PUBLIC` | production (optional) | `true` — serve the dashboard unguarded on purpose |
|
|
466
|
+
| `POLISHD_SESSION_COOKIE` | optional | Rename the anonymous session cookie. Read by both the proxy and ingest, so they cannot disagree |
|
|
264
467
|
| `POLISHD_AI_PROVIDER` | AI (optional) | `anthropic` \| `openai` \| `openai-compatible` \| `google` |
|
|
265
468
|
| `POLISHD_AI_MODEL` | AI (optional) | Model id (defaults per provider, e.g. `claude-opus-4-8`) |
|
|
266
469
|
| `POLISHD_AI_API_KEY` | AI (optional) | Model API key — preset instead of using the dashboard |
|
|
@@ -280,16 +483,39 @@ Local dev needs nothing. If no writable store is available the package degrades
|
|
|
280
483
|
to a safe no-op (a console warning, dashboard notice) — your app never breaks.
|
|
281
484
|
See [DATABASE.md](./DATABASE.md) for production setup.
|
|
282
485
|
|
|
486
|
+
## Troubleshooting
|
|
487
|
+
|
|
488
|
+
Run `npx @polishd/next doctor` first — it catches most of these by name, and
|
|
489
|
+
`--url http://localhost:3000` checks the live path as well.
|
|
490
|
+
|
|
491
|
+
| Symptom | Cause | Fix |
|
|
492
|
+
|---|---|---|
|
|
493
|
+
| Dashboard empty, beacons all return 200 | The proxy isn't running, so no session cookie is minted and ingest drops every batch | Check the file is named for your Next major — `proxy.ts` on 16+, `middleware.ts` on 15 — and exports a function of the same name. The dashboard shows a banner when it detects this |
|
|
494
|
+
| Every `/api/*` route 404s | The proxy matcher touches `/api` | Restore the `(?!api…)` exclusion. Under Next 16 + Turbopack this breaks the whole `/api` segment |
|
|
495
|
+
| Build fails: *"can't recognize the exported `runtime` field"* | Route segment config re-exported | Declare `runtime`/`dynamic` inline in the module |
|
|
496
|
+
| Dashboard renders unstyled | `@polishd/next/dashboard.css` isn't imported | See [Styling](#styling) |
|
|
497
|
+
| Dashboard content cut off below the fold | A host `body` that doesn't scroll | Fixed by default; if an ancestor sets `transform`/`filter`/`contain`, pass `shell: false` and see [docs/SETUP.md](./docs/SETUP.md) |
|
|
498
|
+
| `ExperimentalWarning: SQLite is an experimental feature` | Node's built-in `node:sqlite` | **Expected and harmless.** Node prints this for its own SQLite module on 22.x. Not a sign the package is unstable, and it disappears in production where Postgres is used |
|
|
499
|
+
| `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite` | Node < 22.5 | Upgrade Node, or configure Postgres |
|
|
500
|
+
| Server actions fail with an opaque digest | No dashboard page registered a policy | Expected if you deleted `app/polishd/page.tsx` — the actions fail closed by design |
|
|
501
|
+
|
|
283
502
|
## Entry points
|
|
284
503
|
|
|
285
504
|
| Import | Contents |
|
|
286
505
|
|---|---|
|
|
287
|
-
| `@polishd/next` | config, `definePolishdConfig`, event types (isomorphic) |
|
|
506
|
+
| `@polishd/next` | config, `definePolishdConfig`, `resolveSessionCookie`, event types (isomorphic) |
|
|
288
507
|
| `@polishd/next/client` | `initPolishd`, `PolishdMonitor` |
|
|
289
508
|
| `@polishd/next/server` | store, ingest, queries, `withPolishdSession` (Node only) |
|
|
290
509
|
| `@polishd/next/route` | `POST`, `createPolishdRoute` |
|
|
291
|
-
| `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher` |
|
|
292
|
-
| `@polishd/next/dashboard` | `createPolishdPage`, `PolishdDashboard`, `loadPolishdDashboardData` |
|
|
510
|
+
| `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher`, `composePolishd` |
|
|
511
|
+
| `@polishd/next/dashboard` | `createPolishdPage`, `polishdTokenAuth`, `PolishdDashboard`, `loadPolishdDashboardData` |
|
|
512
|
+
|
|
513
|
+
## CLI
|
|
514
|
+
|
|
515
|
+
| Command | Purpose |
|
|
516
|
+
|---|---|
|
|
517
|
+
| `npx @polishd/next init` | Scaffold the glue files. `--dry-run`, `--force`, `--js`, `--config` |
|
|
518
|
+
| `npx @polishd/next doctor` | Check an install. `--url <origin>` adds live checks |
|
|
293
519
|
|
|
294
520
|
## Development
|
|
295
521
|
|