@polishd/next 0.1.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 (127) hide show
  1. package/AGENTS.md +262 -0
  2. package/DATABASE.md +147 -0
  3. package/LICENSE +21 -0
  4. package/README.md +310 -0
  5. package/bin/init.mjs +224 -0
  6. package/dist/ai/actions.d.ts +23 -0
  7. package/dist/ai/actions.d.ts.map +1 -0
  8. package/dist/ai/actions.js +58 -0
  9. package/dist/ai/actions.js.map +1 -0
  10. package/dist/ai/digest.d.ts +9 -0
  11. package/dist/ai/digest.d.ts.map +1 -0
  12. package/dist/ai/digest.js +108 -0
  13. package/dist/ai/digest.js.map +1 -0
  14. package/dist/ai/github.d.ts +51 -0
  15. package/dist/ai/github.d.ts.map +1 -0
  16. package/dist/ai/github.js +175 -0
  17. package/dist/ai/github.js.map +1 -0
  18. package/dist/ai/guard.d.ts +43 -0
  19. package/dist/ai/guard.d.ts.map +1 -0
  20. package/dist/ai/guard.js +57 -0
  21. package/dist/ai/guard.js.map +1 -0
  22. package/dist/ai/issues.d.ts +21 -0
  23. package/dist/ai/issues.d.ts.map +1 -0
  24. package/dist/ai/issues.js +291 -0
  25. package/dist/ai/issues.js.map +1 -0
  26. package/dist/ai/profile.d.ts +26 -0
  27. package/dist/ai/profile.d.ts.map +1 -0
  28. package/dist/ai/profile.js +160 -0
  29. package/dist/ai/profile.js.map +1 -0
  30. package/dist/ai/providers.d.ts +40 -0
  31. package/dist/ai/providers.d.ts.map +1 -0
  32. package/dist/ai/providers.js +142 -0
  33. package/dist/ai/providers.js.map +1 -0
  34. package/dist/ai/scan.d.ts +38 -0
  35. package/dist/ai/scan.d.ts.map +1 -0
  36. package/dist/ai/scan.js +274 -0
  37. package/dist/ai/scan.js.map +1 -0
  38. package/dist/ai/settings.d.ts +31 -0
  39. package/dist/ai/settings.d.ts.map +1 -0
  40. package/dist/ai/settings.js +182 -0
  41. package/dist/ai/settings.js.map +1 -0
  42. package/dist/ai/summary.d.ts +26 -0
  43. package/dist/ai/summary.d.ts.map +1 -0
  44. package/dist/ai/summary.js +260 -0
  45. package/dist/ai/summary.js.map +1 -0
  46. package/dist/ai/types.d.ts +188 -0
  47. package/dist/ai/types.d.ts.map +1 -0
  48. package/dist/ai/types.js +13 -0
  49. package/dist/ai/types.js.map +1 -0
  50. package/dist/client/init.d.ts +14 -0
  51. package/dist/client/init.d.ts.map +1 -0
  52. package/dist/client/init.js +274 -0
  53. package/dist/client/init.js.map +1 -0
  54. package/dist/client/monitor.d.ts +23 -0
  55. package/dist/client/monitor.d.ts.map +1 -0
  56. package/dist/client/monitor.js +176 -0
  57. package/dist/client/monitor.js.map +1 -0
  58. package/dist/client.d.ts +11 -0
  59. package/dist/client.d.ts.map +1 -0
  60. package/dist/client.js +10 -0
  61. package/dist/client.js.map +1 -0
  62. package/dist/config.d.ts +36 -0
  63. package/dist/config.d.ts.map +1 -0
  64. package/dist/config.js +26 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/dashboard/elements.d.ts +6 -0
  67. package/dist/dashboard/elements.d.ts.map +1 -0
  68. package/dist/dashboard/elements.js +35 -0
  69. package/dist/dashboard/elements.js.map +1 -0
  70. package/dist/dashboard/features.d.ts +6 -0
  71. package/dist/dashboard/features.d.ts.map +1 -0
  72. package/dist/dashboard/features.js +28 -0
  73. package/dist/dashboard/features.js.map +1 -0
  74. package/dist/dashboard/index.d.ts +54 -0
  75. package/dist/dashboard/index.d.ts.map +1 -0
  76. package/dist/dashboard/index.js +140 -0
  77. package/dist/dashboard/index.js.map +1 -0
  78. package/dist/dashboard/journeys.d.ts +5 -0
  79. package/dist/dashboard/journeys.d.ts.map +1 -0
  80. package/dist/dashboard/journeys.js +158 -0
  81. package/dist/dashboard/journeys.js.map +1 -0
  82. package/dist/dashboard/pages.d.ts +5 -0
  83. package/dist/dashboard/pages.d.ts.map +1 -0
  84. package/dist/dashboard/pages.js +104 -0
  85. package/dist/dashboard/pages.js.map +1 -0
  86. package/dist/dashboard/summary.d.ts +13 -0
  87. package/dist/dashboard/summary.d.ts.map +1 -0
  88. package/dist/dashboard/summary.js +333 -0
  89. package/dist/dashboard/summary.js.map +1 -0
  90. package/dist/index.d.ts +10 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +10 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/proxy.d.ts +40 -0
  95. package/dist/proxy.d.ts.map +1 -0
  96. package/dist/proxy.js +29 -0
  97. package/dist/proxy.js.map +1 -0
  98. package/dist/route.d.ts +24 -0
  99. package/dist/route.d.ts.map +1 -0
  100. package/dist/route.js +37 -0
  101. package/dist/route.js.map +1 -0
  102. package/dist/server/ingest.d.ts +14 -0
  103. package/dist/server/ingest.d.ts.map +1 -0
  104. package/dist/server/ingest.js +73 -0
  105. package/dist/server/ingest.js.map +1 -0
  106. package/dist/server/queries.d.ts +245 -0
  107. package/dist/server/queries.d.ts.map +1 -0
  108. package/dist/server/queries.js +547 -0
  109. package/dist/server/queries.js.map +1 -0
  110. package/dist/server/store.d.ts +29 -0
  111. package/dist/server/store.d.ts.map +1 -0
  112. package/dist/server/store.js +359 -0
  113. package/dist/server/store.js.map +1 -0
  114. package/dist/server.d.ts +15 -0
  115. package/dist/server.d.ts.map +1 -0
  116. package/dist/server.js +15 -0
  117. package/dist/server.js.map +1 -0
  118. package/dist/session.d.ts +16 -0
  119. package/dist/session.d.ts.map +1 -0
  120. package/dist/session.js +29 -0
  121. package/dist/session.js.map +1 -0
  122. package/dist/shared/types.d.ts +56 -0
  123. package/dist/shared/types.d.ts.map +1 -0
  124. package/dist/shared/types.js +27 -0
  125. package/dist/shared/types.js.map +1 -0
  126. package/docs/SETUP.md +315 -0
  127. package/package.json +92 -0
package/docs/SETUP.md ADDED
@@ -0,0 +1,315 @@
1
+ # Setting up Polishd
2
+
3
+ A complete walkthrough for adding Polishd to a Next.js app — what each piece
4
+ does, every variant you might hit, and how to confirm it's working.
5
+
6
+ If you just want the short version, the [README](../README.md) quickstart is
7
+ three commands. If you're an AI coding agent, read [AGENTS.md](../AGENTS.md)
8
+ instead.
9
+
10
+ ---
11
+
12
+ ## Requirements
13
+
14
+ - **Next.js 15 or later**, App Router. There is no Pages Router support.
15
+ - **Node 22.5 or later.** The development store uses Node's built-in
16
+ `node:sqlite`. On older Node, Polishd degrades to a no-op rather than
17
+ crashing, but you won't capture anything locally.
18
+ - **Tailwind CSS** (v3 or v4) if you want the dashboard styled. See
19
+ [Styling](#4-styling) — this step is easy to miss.
20
+ - **Postgres** for production. Not needed for local development.
21
+
22
+ ## 1. Install
23
+
24
+ ```bash
25
+ npm install @polishd/next
26
+ ```
27
+
28
+ `pg` comes along as an optional dependency and is only loaded when you point
29
+ Polishd at a Postgres URL. You don't need to install it yourself.
30
+
31
+ ## 2. Scaffold
32
+
33
+ ```bash
34
+ npx polishd init
35
+ ```
36
+
37
+ This detects whether your app uses `src/` or the repo root, writes four files,
38
+ adds `.polishd/` to `.gitignore`, and wires Tailwind. Useful flags:
39
+
40
+ | Flag | Effect |
41
+ |---|---|
42
+ | `--dry-run` | Print what would happen; write nothing |
43
+ | `--force` | Overwrite files that already exist |
44
+ | `--js` | Emit JavaScript instead of TypeScript |
45
+ | `--config` | Also write a starter `polishd.config.ts` |
46
+
47
+ If a file already exists, `init` skips it and tells you. For an existing
48
+ `middleware.ts` it prints a merge snippet rather than clobbering your code.
49
+
50
+ ## 3. What the four files do
51
+
52
+ Paths below assume a `src/` layout; drop the `src/` prefix if your `app/`
53
+ directory sits at the repo root.
54
+
55
+ ### `src/proxy.ts` — anonymous sessions
56
+
57
+ Every visitor gets one httpOnly UUID cookie. No fingerprinting, no PII, so it's
58
+ GDPR-safe by construction. The ingest endpoint trusts this cookie as the only
59
+ source of session identity — the browser never sends a session id itself.
60
+
61
+ ```ts
62
+ import type { NextRequest } from "next/server";
63
+ import { withPolishdSession } from "@polishd/next/proxy";
64
+
65
+ export function proxy(request: NextRequest) {
66
+ return withPolishdSession(request);
67
+ }
68
+
69
+ export const config = {
70
+ matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
71
+ };
72
+ ```
73
+
74
+ **On Next 15**, name the file `middleware.ts` and the exported function
75
+ `middleware`. Everything else is identical.
76
+
77
+ > **The matcher is load-bearing.** Next parses `config.matcher` statically, so
78
+ > it has to be an inline literal in this file — it cannot be imported from the
79
+ > package. And it must exclude all of `/api`: under Next 16 with Turbopack, a
80
+ > proxy matcher that touches any `/api/*` route breaks route resolution for the
81
+ > entire `/api` segment. The proxy's only job is setting a cookie, which page
82
+ > navigations already trigger.
83
+
84
+ **Already have a proxy or middleware?** Don't replace it. Thread your response
85
+ through instead, and merge the matcher — keeping the `api` exclusion:
86
+
87
+ ```ts
88
+ export function proxy(request: NextRequest) {
89
+ const response = myExistingLogic(request);
90
+ return withPolishdSession(request, response);
91
+ }
92
+ ```
93
+
94
+ ### `src/instrumentation-client.ts` — capture
95
+
96
+ Next runs this before hydration, so capture starts on the first paint.
97
+
98
+ ```ts
99
+ import { initPolishd } from "@polishd/next/client";
100
+
101
+ initPolishd();
102
+ ```
103
+
104
+ This is vanilla DOM code with no React dependency. It batches events and flushes
105
+ every 10 seconds, plus on `pagehide` via `sendBeacon` so the last batch of a
106
+ visit isn't lost.
107
+
108
+ ### `src/app/api/polishd/route.ts` — ingest
109
+
110
+ ```ts
111
+ import { createPolishdRoute } from "@polishd/next/route";
112
+
113
+ export const runtime = "nodejs";
114
+ export const dynamic = "force-dynamic";
115
+
116
+ export const POST = createPolishdRoute();
117
+ ```
118
+
119
+ > **`runtime` and `dynamic` must be declared here, inline.** Next parses route
120
+ > segment config statically, so re-exporting them from the package
121
+ > (`export { POST, runtime, dynamic } from "@polishd/next/route"`) fails the
122
+ > build. `nodejs` is required because the store uses `node:sqlite`/`pg`, neither
123
+ > of which runs on the Edge runtime.
124
+
125
+ ### `src/app/polishd/page.tsx` — the dashboard
126
+
127
+ ```tsx
128
+ import { createPolishdPage } from "@polishd/next/dashboard";
129
+
130
+ export const runtime = "nodejs";
131
+ export const dynamic = "force-dynamic";
132
+
133
+ export default createPolishdPage();
134
+ ```
135
+
136
+ Same inline-config rule as the route. **This is unguarded** — see
137
+ [Protecting the dashboard](#5-protecting-the-dashboard).
138
+
139
+ ## 4. Styling
140
+
141
+ The dashboard is built entirely from Tailwind utility classes and ships no
142
+ stylesheet of its own. Tailwind has to be told to scan the package — it will not
143
+ find it by default. Miss this and `/polishd` loads fine but renders as unstyled
144
+ HTML.
145
+
146
+ **Tailwind v4** (no config file; your CSS starts with `@import "tailwindcss"`):
147
+
148
+ ```css
149
+ @import "tailwindcss";
150
+ @source "../../node_modules/@polishd/next/dist";
151
+ ```
152
+
153
+ The path is relative to the stylesheet. From `src/app/globals.css` that's
154
+ `../../`. v4's automatic content detection deliberately skips `node_modules`, so
155
+ this line is not optional.
156
+
157
+ **Tailwind v3** — add the glob to `content` in `tailwind.config.ts`:
158
+
159
+ ```ts
160
+ content: [
161
+ "./src/**/*.{ts,tsx}",
162
+ "./node_modules/@polishd/next/dist/**/*.js",
163
+ ],
164
+ ```
165
+
166
+ **Not using Tailwind?** Everything works, but the dashboard will be unstyled.
167
+ The colors are self-contained (no dependency on your theme), so adding Tailwind
168
+ purely for the dashboard is a reasonable option if you want it to look right.
169
+
170
+ ## 5. Protecting the dashboard
171
+
172
+ By default `/polishd` is **public**, with a development-only console warning.
173
+ Before deploying, gate it:
174
+
175
+ ```tsx
176
+ import { createPolishdPage } from "@polishd/next/dashboard";
177
+ import { isAuthenticated } from "@/lib/auth";
178
+ import { MyLogin } from "./login";
179
+
180
+ export const runtime = "nodejs";
181
+ export const dynamic = "force-dynamic";
182
+
183
+ export default createPolishdPage({
184
+ authenticate: isAuthenticated,
185
+ unauthorized: <MyLogin />,
186
+ });
187
+ ```
188
+
189
+ `authenticate` returns a boolean or a promise of one. When it returns false the
190
+ `unauthorized` node renders, or a minimal built-in screen if you didn't pass
191
+ one. Polishd ships no auth code, so it never pulls an auth library into your
192
+ dependency tree.
193
+
194
+ **This also guards the AI server actions.** That matters more than it might
195
+ sound: a Next server action is an independently addressable POST endpoint whose
196
+ id ships inside your public client bundle. Gating only the page render would
197
+ leave the actions callable by anyone — able to change the configured model and
198
+ API base URL, and spend your model tokens. The callback re-runs against the
199
+ caller's cookies on every action call.
200
+
201
+ To stay open locally but protected in production:
202
+
203
+ ```tsx
204
+ authenticate: async () =>
205
+ process.env.NODE_ENV !== "production" || (await isAuthenticated()),
206
+ ```
207
+
208
+ ## 6. Verify it works
209
+
210
+ ```bash
211
+ npm run build && npm start
212
+ ```
213
+
214
+ Then, in order:
215
+
216
+ 1. **Cookie.** `curl -sI http://localhost:3000/ | grep -i set-cookie` should
217
+ show `polishd_session=<uuid>` with `HttpOnly`.
218
+ 2. **Dashboard.** Visit `/polishd` — expect HTTP 200, styled, with a "no events
219
+ yet" state.
220
+ 3. **Capture.** Click around your app, then reload the dashboard. Events flush
221
+ every 10 seconds and on page hide, so give it a moment.
222
+ 4. **Store.** In development, `.polishd/analytics.db` should exist. That file
223
+ appearing is proof the whole write path works.
224
+
225
+ If the dashboard shows a notice about the store being unavailable, capture is
226
+ disabled — check Node ≥ 22.5 locally, or your database URL in production.
227
+
228
+ ## 7. Deploying
229
+
230
+ Serverless hosts run on a read-only, ephemeral filesystem, so the development
231
+ SQLite file cannot persist there. Production capture needs Postgres.
232
+
233
+ 1. Provision a database (Neon, Vercel Postgres, Supabase, RDS).
234
+ 2. Set `POLISHD_DATABASE_URL` to the **pooled** connection string.
235
+ 3. Set your host's Node version to 22.x or later.
236
+ 4. Deploy. Tables are created automatically on first use.
237
+
238
+ See [DATABASE.md](../DATABASE.md) for schema, pooling notes, and retention.
239
+
240
+ Until a database URL is set, Polishd runs as a safe no-op in production: a
241
+ console warning, a notice on the dashboard, and zero impact on your app. It is
242
+ designed never to be the reason your site goes down.
243
+
244
+ ## 8. Optional — component-level tracking
245
+
246
+ Wrap anything you want attributed by name rather than by CSS selector:
247
+
248
+ ```tsx
249
+ import { PolishdMonitor } from "@polishd/next/client";
250
+
251
+ <PolishdMonitor name="checkout-button">
252
+ <CheckoutButton />
253
+ </PolishdMonitor>
254
+
255
+ <PolishdMonitor name={slug} content className="block">
256
+ <Article />
257
+ </PolishdMonitor>
258
+ ```
259
+
260
+ Interactive controls get hover and click attribution. Adding `content` also
261
+ measures viewport time, scroll depth, and rendered size — meant for articles and
262
+ regions rather than buttons.
263
+
264
+ ## 9. Optional — configuration
265
+
266
+ Defaults suit a low-traffic site. To change them:
267
+
268
+ ```ts
269
+ // polishd.config.ts
270
+ import { definePolishdConfig } from "@polishd/next";
271
+
272
+ export default definePolishdConfig({
273
+ sampleRate: 1, // fraction of sessions captured
274
+ rageClick: { count: 3, windowMs: 500 }, // N clicks on one element
275
+ flushIntervalMs: 10_000,
276
+ });
277
+ ```
278
+
279
+ ```ts
280
+ // src/instrumentation-client.ts
281
+ import { initPolishd } from "@polishd/next/client";
282
+ import config from "../polishd.config";
283
+
284
+ initPolishd(config);
285
+ ```
286
+
287
+ Pass the same object to `createPolishdRoute(config)` and `createPolishdProxy(config)`
288
+ if you change `apiRoute` or `sessionCookie`, so all three agree.
289
+
290
+ ## Troubleshooting
291
+
292
+ **Build fails: "can't recognize the exported `runtime` field in route"**
293
+ Route segment config was re-exported. Declare `runtime` and `dynamic` inline in
294
+ the route and page modules.
295
+
296
+ **Every `/api/*` route suddenly 404s**
297
+ Your proxy matcher is matching `/api`. Restore the `(?!api|...)` exclusion.
298
+
299
+ **The dashboard is unstyled**
300
+ Tailwind isn't scanning the package. See [Styling](#4-styling).
301
+
302
+ **`ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite`**
303
+ Node is older than 22.5. Upgrade, or configure Postgres.
304
+
305
+ **No `polishd_session` cookie**
306
+ The proxy file is missing or misnamed — `proxy.ts` on Next 16, `middleware.ts`
307
+ on Next 15 — or its matcher excludes the path you're testing.
308
+
309
+ **Dashboard is empty but the app works**
310
+ Expected before any events flush. If it persists, check the browser console for
311
+ failed requests to `/api/polishd`, and confirm the route file exists.
312
+
313
+ **Events stop after a deploy**
314
+ Almost always a missing or unreachable `POLISHD_DATABASE_URL`. The store latches
315
+ to a no-op and logs a warning on the server rather than failing requests.
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@polishd/next",
3
+ "version": "0.1.0",
4
+ "description": "Drop-in product analytics for Next.js \u2014 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
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "sideEffects": false,
11
+ "keywords": [
12
+ "nextjs",
13
+ "analytics",
14
+ "product-analytics",
15
+ "ux",
16
+ "rage-click",
17
+ "web-vitals"
18
+ ],
19
+ "engines": {
20
+ "node": ">=22.5.0"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/muskanvaswan/polishd.git"
25
+ },
26
+ "homepage": "https://github.com/muskanvaswan/polishd#readme",
27
+ "bugs": {
28
+ "url": "https://github.com/muskanvaswan/polishd/issues"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "docs",
33
+ "AGENTS.md",
34
+ "DATABASE.md",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "bin": {
39
+ "polishd": "./bin/init.mjs"
40
+ },
41
+ "exports": {
42
+ ".": {
43
+ "types": "./dist/index.d.ts",
44
+ "default": "./dist/index.js"
45
+ },
46
+ "./client": {
47
+ "types": "./dist/client.d.ts",
48
+ "default": "./dist/client.js"
49
+ },
50
+ "./server": {
51
+ "types": "./dist/server.d.ts",
52
+ "default": "./dist/server.js"
53
+ },
54
+ "./route": {
55
+ "types": "./dist/route.d.ts",
56
+ "default": "./dist/route.js"
57
+ },
58
+ "./proxy": {
59
+ "types": "./dist/proxy.d.ts",
60
+ "default": "./dist/proxy.js"
61
+ },
62
+ "./dashboard": {
63
+ "types": "./dist/dashboard/index.d.ts",
64
+ "default": "./dist/dashboard/index.js"
65
+ },
66
+ "./package.json": "./package.json"
67
+ },
68
+ "scripts": {
69
+ "build": "tsc -p tsconfig.build.json",
70
+ "typecheck": "tsc -p tsconfig.json",
71
+ "clean": "rm -rf dist",
72
+ "prepublishOnly": "npm run clean && npm run build"
73
+ },
74
+ "peerDependencies": {
75
+ "next": ">=15",
76
+ "react": ">=18",
77
+ "react-dom": ">=18"
78
+ },
79
+ "optionalDependencies": {
80
+ "pg": "^8.0.0"
81
+ },
82
+ "devDependencies": {
83
+ "@types/node": "^20.14.8",
84
+ "@types/pg": "^8.11.0",
85
+ "@types/react": "^19",
86
+ "@types/react-dom": "^19",
87
+ "next": "^16",
88
+ "react": "^19",
89
+ "react-dom": "^19",
90
+ "typescript": "^5.5.2"
91
+ }
92
+ }