@paikko/widget 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.
- package/README.md +131 -0
- package/package.json +41 -0
- package/src/PaikkoNav.tsx +92 -0
- package/src/PaikkoProvider.tsx +79 -0
- package/src/ReportButton.tsx +591 -0
- package/src/build/provenancePlugin.cjs +200 -0
- package/src/capture.ts +991 -0
- package/src/index.ts +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# @paikko/widget
|
|
2
|
+
|
|
3
|
+
The consumer-installable paikko report widget. Mount it once in your app and
|
|
4
|
+
users get a floating report button that captures the DOM, storage, client state,
|
|
5
|
+
and provenance of what they were looking at, then POSTs a contract-valid
|
|
6
|
+
`ReportBundle` to your paikko backend.
|
|
7
|
+
|
|
8
|
+
The widget is backend-agnostic and store-agnostic: you configure where reports
|
|
9
|
+
go (`endpoint`), which tenant they belong to (`projectKey`), where the review
|
|
10
|
+
queue lives (`ticketsUrl`), and how to read your app's state (`getClientState`).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @paikko/widget @paikko/contract react
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`react` is a peer dependency (>= 18.3.1). `@paikko/contract` carries the shared
|
|
19
|
+
wire types/zod schemas and is required.
|
|
20
|
+
|
|
21
|
+
> The widget ships raw TS/TSX source (its `exports` point at `./src`). With
|
|
22
|
+
> Next.js, add it to `transpilePackages` so the host build transpiles it:
|
|
23
|
+
>
|
|
24
|
+
> ```js
|
|
25
|
+
> // next.config.js
|
|
26
|
+
> module.exports = { transpilePackages: ["@paikko/widget", "@paikko/contract"] };
|
|
27
|
+
> ```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Mount `<PaikkoProvider>` once, typically inside a `"use client"` island near the
|
|
32
|
+
root of your app (the function-valued `getClientState` prop cannot cross the
|
|
33
|
+
server/client boundary, so it must live in a client module):
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
"use client";
|
|
37
|
+
import { PaikkoProvider } from "@paikko/widget";
|
|
38
|
+
import { getState } from "@/lib/store";
|
|
39
|
+
|
|
40
|
+
export function PaikkoMount() {
|
|
41
|
+
return (
|
|
42
|
+
<PaikkoProvider
|
|
43
|
+
endpoint="http://localhost:8788/api/reports"
|
|
44
|
+
projectKey="my-app"
|
|
45
|
+
ticketsUrl="http://localhost:8788/tickets"
|
|
46
|
+
getClientState={getState}
|
|
47
|
+
reporter="anonymous"
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `<PaikkoProvider>` props
|
|
54
|
+
|
|
55
|
+
| Prop | Type | Required | Description |
|
|
56
|
+
|------------------|------------------------------------|----------|-------------|
|
|
57
|
+
| `endpoint` | `string` | yes | Backend reports intake URL the bundle is POSTed to. May be cross-origin. |
|
|
58
|
+
| `projectKey` | `string \| null` | no | Project/tenant key stamped onto every report (SaaS seam). Defaults to `null`. |
|
|
59
|
+
| `apiKey` | `string` | no | Project **publishable** key (`pk_...`), sent as `x-paikko-key`. Required whenever the backend enforces auth (the default; off only with `PAIKKO_AUTH=disabled`). Public - never pass a secret key (`sk_...`). See the repo's `AUTH.md`. |
|
|
60
|
+
| `ticketsUrl` | `string` | no | Absolute URL of the backend review queue. Renders a nav pill linking to it; omit to hide the pill. |
|
|
61
|
+
| `getClientState` | `() => Record<string, unknown>` | no | Reader for your app's client state, snapshotted at report time. Pass your store's `getState` (or equivalent). Omit and client state is captured as `{}`. |
|
|
62
|
+
| `reporter` | `string` | no | Who is filing the report. Defaults to `"anonymous"`. |
|
|
63
|
+
| `enabled` | `boolean` | no | Whether to mount at all. **Defaults to dev-only** (`NODE_ENV !== "production"`), so the pills never ship to real users. Pass `true`/`false` to force it. |
|
|
64
|
+
|
|
65
|
+
`PaikkoProvider` is the default export and also a named export. `ReportButton`,
|
|
66
|
+
`PaikkoNav`, and the capture primitives are exported individually for hosts that
|
|
67
|
+
want to compose the pieces themselves.
|
|
68
|
+
|
|
69
|
+
## Environment & production (read this before deploying)
|
|
70
|
+
|
|
71
|
+
paikko is a development/internal tool. Both the widget and the provenance plugin
|
|
72
|
+
default to **dev-only** so nothing leaks into a production deploy, but there are
|
|
73
|
+
two footguns worth knowing:
|
|
74
|
+
|
|
75
|
+
- **Widget rendering.** `<PaikkoProvider>` renders only when `NODE_ENV !==
|
|
76
|
+
"production"` unless you pass `enabled`. To gate it from an env file, drive the
|
|
77
|
+
prop explicitly and put the flag in `.env.development` (loaded only by `next
|
|
78
|
+
dev`), **not** `.env.local` - Next loads `.env.local` in production builds too,
|
|
79
|
+
so a "dev" flag there silently enables the widget in prod:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<PaikkoProvider
|
|
83
|
+
enabled={process.env.NEXT_PUBLIC_PAIKKO_ENABLED === "true"}
|
|
84
|
+
endpoint="https://paikko.example.com/api/reports"
|
|
85
|
+
/* ... */
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **Provenance attributes.** The babel plugin no-ops when `NODE_ENV ===
|
|
90
|
+
"production"` by default, so `data-src` source paths never ship in prod HTML.
|
|
91
|
+
A JSON `.babelrc` cannot read env vars; if you need to force it on/off, use a
|
|
92
|
+
`.babelrc.js` and pass `{ enabled: process.env.NEXT_PUBLIC_PAIKKO_ENABLED ===
|
|
93
|
+
"true" }`. Clear `.next` / `node_modules/.cache` after changing this - a stale
|
|
94
|
+
babel cache keeps the old behavior.
|
|
95
|
+
|
|
96
|
+
- **Cross-origin requests are safe.** The capture layer injects its
|
|
97
|
+
`x-paikko-trace` / `x-paikko-session` headers only on **same-origin** fetch/XHR
|
|
98
|
+
calls. Cross-origin requests (third-party CDNs, wasm, media) are still recorded
|
|
99
|
+
but their headers are left untouched, so paikko never trips a CORS preflight on
|
|
100
|
+
someone else's endpoint.
|
|
101
|
+
|
|
102
|
+
## Provenance babel plugin (required)
|
|
103
|
+
|
|
104
|
+
The widget's capture relies on a babel plugin that stamps source provenance onto
|
|
105
|
+
your components at build time. Wire it into the consumer's babel config:
|
|
106
|
+
|
|
107
|
+
- Plugin: `@paikko/widget/build/provenancePlugin`
|
|
108
|
+
(`packages/widget/src/build/provenancePlugin.cjs`)
|
|
109
|
+
- Reference config: `examples/calculator/.babelrc`
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
// .babelrc
|
|
113
|
+
{
|
|
114
|
+
"presets": ["next/babel"],
|
|
115
|
+
"plugins": [
|
|
116
|
+
["@paikko/widget/build/provenancePlugin", { "rootDir": "." }]
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The `examples/calculator` app references the plugin by relative path
|
|
122
|
+
(`../../packages/widget/src/build/provenancePlugin.cjs`) because it runs from
|
|
123
|
+
inside this monorepo; a published consumer would use the package subpath shown
|
|
124
|
+
above. The subpath resolves via the package `exports` map to
|
|
125
|
+
`./src/build/provenancePlugin.cjs` (there is no literal `build/` dir); if your
|
|
126
|
+
toolchain doesn't honor the exports map, fall back to the explicit path
|
|
127
|
+
`./node_modules/@paikko/widget/src/build/provenancePlugin.cjs`.
|
|
128
|
+
|
|
129
|
+
The plugin no-ops in production by default (see **Environment & production**), so
|
|
130
|
+
the plain JSON `.babelrc` above is safe to commit - it emits provenance in dev
|
|
131
|
+
and strips it from prod builds automatically.
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@paikko/widget",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "paikko consumer report widget (PaikkoProvider, ReportButton, PaikkoNav, capture) + provenance plugin. Backend-agnostic: configurable endpoint, projectKey, ticketsUrl.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/willyt3hwhale/paikko.git",
|
|
9
|
+
"directory": "packages/widget"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.ts",
|
|
14
|
+
"./build/provenancePlugin": "./src/build/provenancePlugin.cjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src/**/*.ts",
|
|
18
|
+
"src/**/*.tsx",
|
|
19
|
+
"src/build/provenancePlugin.cjs",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@paikko/contract": "^0.1.0",
|
|
30
|
+
"html2canvas": "^1.4.1",
|
|
31
|
+
"zod": "^3.23.8"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^18.3.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^18.3.11",
|
|
38
|
+
"react": "^18.3.1",
|
|
39
|
+
"typescript": "^5.6.3"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* paikko `<PaikkoNav>` - a small global navigation pill that sits beside the
|
|
5
|
+
* `<ReportButton>` FAB and links to the review queue.
|
|
6
|
+
*
|
|
7
|
+
* It is mounted app-wide (alongside `<ReportButton>` in `<PaikkoProvider>`) so
|
|
8
|
+
* the user can reach the queue from anywhere. It is deliberately visually
|
|
9
|
+
* secondary to the Report FAB - an outlined dark pill rather than the solid
|
|
10
|
+
* primary fill - so Report stays the one-tap prominent action.
|
|
11
|
+
*
|
|
12
|
+
* Layout: the FAB lives at `right:20 bottom:20` (~78px wide). This pill sits
|
|
13
|
+
* just to its LEFT at `right:108 bottom:20`, clear of both the FAB and the
|
|
14
|
+
* report confirmation panel (which anchors at `right:20 bottom:76`).
|
|
15
|
+
*
|
|
16
|
+
* It is tagged `data-paikko-ui` so the capture controller ignores clicks on it
|
|
17
|
+
* (point-mode skips anything inside `[data-paikko-ui]`).
|
|
18
|
+
*
|
|
19
|
+
* The review UI now lives on the BACKEND origin (a separate deployment from the
|
|
20
|
+
* consumer app the widget is installed in), so the link target is a configurable
|
|
21
|
+
* `ticketsUrl` - typically an absolute URL like
|
|
22
|
+
* `https://api.example.com/tickets`. We render a plain `<a>` (not `next/link`)
|
|
23
|
+
* because the destination is cross-origin and because the widget must not depend
|
|
24
|
+
* on the host being a Next app. When `ticketsUrl` is omitted the pill is not
|
|
25
|
+
* rendered at all (an unconfigured nav is worse than no nav).
|
|
26
|
+
*/
|
|
27
|
+
import React from "react";
|
|
28
|
+
|
|
29
|
+
export interface PaikkoNavProps {
|
|
30
|
+
/**
|
|
31
|
+
* Absolute URL of the backend review queue (e.g.
|
|
32
|
+
* `https://api.example.com/tickets`). Omit to hide the nav pill.
|
|
33
|
+
*/
|
|
34
|
+
ticketsUrl?: string;
|
|
35
|
+
/** Pill label. Defaults to "Tickets". */
|
|
36
|
+
label?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function PaikkoNav({
|
|
40
|
+
ticketsUrl,
|
|
41
|
+
label = "Tickets",
|
|
42
|
+
}: PaikkoNavProps): React.JSX.Element | null {
|
|
43
|
+
if (!ticketsUrl) return null;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div data-paikko-ui="nav" style={styles.root}>
|
|
47
|
+
<a
|
|
48
|
+
href={ticketsUrl}
|
|
49
|
+
data-paikko-ui="nav-link"
|
|
50
|
+
style={styles.pill}
|
|
51
|
+
aria-label="Review tickets"
|
|
52
|
+
>
|
|
53
|
+
{label}
|
|
54
|
+
</a>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const Z = 2147483000; // match the ReportButton stacking context
|
|
60
|
+
|
|
61
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
62
|
+
root: {
|
|
63
|
+
position: "fixed",
|
|
64
|
+
inset: 0,
|
|
65
|
+
pointerEvents: "none",
|
|
66
|
+
zIndex: Z,
|
|
67
|
+
fontFamily:
|
|
68
|
+
"system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
|
|
69
|
+
},
|
|
70
|
+
pill: {
|
|
71
|
+
position: "fixed",
|
|
72
|
+
right: 108, // just left of the FAB (right:20, ~78px wide) - never overlaps
|
|
73
|
+
bottom: 20,
|
|
74
|
+
pointerEvents: "auto",
|
|
75
|
+
display: "inline-flex",
|
|
76
|
+
alignItems: "center",
|
|
77
|
+
padding: "10px 16px",
|
|
78
|
+
borderRadius: 999,
|
|
79
|
+
// Outlined / lighter so it reads as secondary to the solid Report FAB.
|
|
80
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
81
|
+
background: "rgba(17,24,39,0.85)",
|
|
82
|
+
color: "#e5e7eb",
|
|
83
|
+
fontSize: 14,
|
|
84
|
+
fontWeight: 600,
|
|
85
|
+
textDecoration: "none",
|
|
86
|
+
cursor: "pointer",
|
|
87
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.25)",
|
|
88
|
+
whiteSpace: "nowrap",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default PaikkoNav;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Client boundary that mounts the paikko `<ReportButton>` (and optional
|
|
5
|
+
* `<PaikkoNav>` pill) once for the whole host app.
|
|
6
|
+
*
|
|
7
|
+
* The host app's root layout is typically a server component, so the capture
|
|
8
|
+
* entry point has to be mounted inside a `"use client"` island. This is that
|
|
9
|
+
* island. The widget is backend-agnostic and store-agnostic:
|
|
10
|
+
*
|
|
11
|
+
* - `endpoint` / `projectKey` configure where reports go and which tenant they
|
|
12
|
+
* belong to (forwarded to `<ReportButton>`); the bundle is POSTed there
|
|
13
|
+
* cross-origin.
|
|
14
|
+
* - `ticketsUrl` configures the review-queue link (forwarded to `<PaikkoNav>`);
|
|
15
|
+
* omit it to hide the nav pill.
|
|
16
|
+
* - `getClientState` is supplied BY THE HOST APP - the provider does not import
|
|
17
|
+
* any store. The host passes its own store's `getState` (or equivalent) so
|
|
18
|
+
* the `clientState` artifact is captured from the host's app state at report
|
|
19
|
+
* time. Omit it and client state is captured as `{}`.
|
|
20
|
+
*/
|
|
21
|
+
import React from "react";
|
|
22
|
+
import { ReportButton } from "./ReportButton";
|
|
23
|
+
import { PaikkoNav } from "./PaikkoNav";
|
|
24
|
+
|
|
25
|
+
export interface PaikkoProviderProps {
|
|
26
|
+
/** Backend reports intake URL the bundle is POSTed to (may be cross-origin). */
|
|
27
|
+
endpoint: string;
|
|
28
|
+
/** Project/tenant key stamped onto every report (SaaS seam). */
|
|
29
|
+
projectKey?: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* The project's PUBLISHABLE api key (pk_...), forwarded to `<ReportButton>` and
|
|
32
|
+
* sent as `x-paikko-key`. Required only when the backend enforces auth
|
|
33
|
+
* (enforced by default; off via PAIKKO_AUTH=disabled). Public key - never pass a secret (sk_...).
|
|
34
|
+
*/
|
|
35
|
+
apiKey?: string;
|
|
36
|
+
/** Absolute URL of the backend review queue; omit to hide the nav pill. */
|
|
37
|
+
ticketsUrl?: string;
|
|
38
|
+
/** Reader for the host app's client state, snapshotted at report time. */
|
|
39
|
+
getClientState?: () => Record<string, unknown>;
|
|
40
|
+
/** Who is filing. Defaults to "anonymous" inside `<ReportButton>`. */
|
|
41
|
+
reporter?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Whether to mount the widget at all. paikko is a development/internal tool, so
|
|
44
|
+
* by DEFAULT it renders only when `NODE_ENV !== "production"` - the pills never
|
|
45
|
+
* ship to real end-users unless you opt in. Pass `true`/`false` to force it
|
|
46
|
+
* (e.g. `enabled={process.env.NEXT_PUBLIC_PAIKKO_ENABLED === "true"}` to drive
|
|
47
|
+
* it from a dev-only env file like `.env.development`).
|
|
48
|
+
*/
|
|
49
|
+
enabled?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function PaikkoProvider({
|
|
53
|
+
endpoint,
|
|
54
|
+
projectKey = null,
|
|
55
|
+
apiKey,
|
|
56
|
+
ticketsUrl,
|
|
57
|
+
getClientState,
|
|
58
|
+
reporter,
|
|
59
|
+
enabled,
|
|
60
|
+
}: PaikkoProviderProps): React.JSX.Element | null {
|
|
61
|
+
const active =
|
|
62
|
+
enabled ?? process.env.NODE_ENV !== "production";
|
|
63
|
+
if (!active) return null;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<ReportButton
|
|
68
|
+
endpoint={endpoint}
|
|
69
|
+
projectKey={projectKey}
|
|
70
|
+
apiKey={apiKey}
|
|
71
|
+
getClientState={getClientState}
|
|
72
|
+
reporter={reporter}
|
|
73
|
+
/>
|
|
74
|
+
<PaikkoNav ticketsUrl={ticketsUrl} />
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default PaikkoProvider;
|