@getpinax/widget 0.0.0-stage → 1.0.0-beta.2
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/LICENSE +21 -0
- package/README.md +513 -2
- package/dist/a11y/scan.d.ts +25 -0
- package/dist/api.d.ts +83 -0
- package/dist/capture/breadcrumbs.d.ts +49 -0
- package/dist/capture/console.d.ts +17 -0
- package/dist/capture/env.d.ts +25 -0
- package/dist/capture/network.d.ts +23 -0
- package/dist/capture/ring.d.ts +14 -0
- package/dist/capture/signature.d.ts +7 -0
- package/dist/capture/text.d.ts +23 -0
- package/dist/capture/ua.d.ts +13 -0
- package/dist/chunks/editor-0Kr31Uly.mjs +1368 -0
- package/dist/chunks/privacy-B_b1_Y1n.mjs +39 -0
- package/dist/chunks/recorder-tu4yugLw.mjs +2874 -0
- package/dist/chunks/scan-CFQwrtAQ.mjs +46137 -0
- package/dist/chunks/text-D9SaDBT0.mjs +36 -0
- package/dist/chunks/widget-BpTBdX2R.mjs +1272 -0
- package/dist/editor/annotator.d.ts +20 -0
- package/dist/editor/capture.d.ts +28 -0
- package/dist/editor/encode.d.ts +7 -0
- package/dist/editor/index.d.ts +5 -0
- package/dist/editor/mask.d.ts +29 -0
- package/dist/editor/pii.d.ts +22 -0
- package/dist/editor/shapes.d.ts +31 -0
- package/dist/editor-loader.d.ts +9 -0
- package/dist/editor-loader.embed.d.ts +6 -0
- package/dist/fixcheck.d.ts +42 -0
- package/dist/index.d.ts +85 -0
- package/dist/known.d.ts +27 -0
- package/dist/mount.d.ts +8 -0
- package/dist/navigation.d.ts +1 -0
- package/dist/paths.d.ts +4 -0
- package/dist/pinax-axe.js +46167 -0
- package/dist/pinax-editor.js +1417 -0
- package/dist/pinax-recorder.js +2915 -0
- package/dist/pinax.js +135 -0
- package/dist/pinax.mjs +1270 -0
- package/dist/preview.d.ts +48 -0
- package/dist/preview.mjs +67 -0
- package/dist/privacy.d.ts +24 -0
- package/dist/queue.d.ts +10 -0
- package/dist/replay/buffer.d.ts +40 -0
- package/dist/replay/recorder.d.ts +25 -0
- package/dist/replay/sanitize.d.ts +19 -0
- package/dist/report.d.ts +128 -0
- package/dist/segment.d.ts +31 -0
- package/dist/settings.d.ts +40 -0
- package/dist/types.d.ts +247 -0
- package/dist/ui/color.d.ts +7 -0
- package/dist/ui/dom.d.ts +14 -0
- package/dist/ui/styles.d.ts +3 -0
- package/dist/ui/widget.d.ts +241 -0
- package/package.json +74 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pinax
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,514 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Pinax widget
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The script customers embed (requirements sections 1 and 2): a feedback button,
|
|
4
|
+
a panel with three kinds of feedback, screenshots with markup, and automatic
|
|
5
|
+
page context.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### npm (S-10)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @getpinax/widget
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { init, open, identify, consent, destroy } from '@getpinax/widget';
|
|
17
|
+
|
|
18
|
+
// Once, as early as you like (e.g. in your app's entry file). Browser only.
|
|
19
|
+
init({ key: 'pk_...' });
|
|
20
|
+
|
|
21
|
+
// From your cookie banner, on sites that wait for consent (see Visitor consent).
|
|
22
|
+
consent({ diagnostics: true, replay: true });
|
|
23
|
+
|
|
24
|
+
// When someone signs in, so their reports carry who they are (C-09).
|
|
25
|
+
identify({ id: user.id, name: user.name, email: user.email });
|
|
26
|
+
identify(null); // on sign-out
|
|
27
|
+
|
|
28
|
+
// From your own "Give feedback" link or button (S-05).
|
|
29
|
+
document.querySelector('#feedback')?.addEventListener('click', () => open());
|
|
30
|
+
|
|
31
|
+
// On teardown (tests, micro-frontends). Restores console, fetch and XHR.
|
|
32
|
+
destroy();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
React example:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { useEffect } from 'react';
|
|
39
|
+
import { init, identify } from '@getpinax/widget';
|
|
40
|
+
|
|
41
|
+
export function Feedback({ user }: { user?: { id: string; email: string } }) {
|
|
42
|
+
useEffect(() => init({ key: 'pk_...' }), []);
|
|
43
|
+
useEffect(() => identify(user ?? null), [user]);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The package is an ES module with TypeScript types. It does nothing on import;
|
|
49
|
+
`init()` starts it. The screenshot editor and the session recorder are
|
|
50
|
+
dynamic `import()` chunks, so your bundler splits them out and they load only
|
|
51
|
+
when needed. On server-rendered frameworks, call `init()` in the browser only
|
|
52
|
+
(`useEffect`, `onMount`, a client component).
|
|
53
|
+
|
|
54
|
+
Options: `key` (the website key from Install), `apiUrl` (default
|
|
55
|
+
`https://api.getpinax.com`), `cdnUrl` (default `https://cdn.getpinax.com`,
|
|
56
|
+
where the site's settings are read from, P-15) and `consent` (`'wait'` or
|
|
57
|
+
`'none'`, overriding the site's "Wait for visitor consent" setting; see
|
|
58
|
+
Visitor consent). With only `apiUrl` set, settings come from that API, which
|
|
59
|
+
is what you want for a local backend.
|
|
60
|
+
|
|
61
|
+
### Script tag (CDN)
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<script src="https://cdn.getpinax.com/pinax.js" data-key="pk_..." async></script>
|
|
65
|
+
<!-- data-api="https://api.example" overrides the API origin, data-cdn="https://cdn.example" the settings origin,
|
|
66
|
+
data-consent="wait" or "none" the site's "Wait for visitor consent" setting -->
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Publishing
|
|
70
|
+
|
|
71
|
+
Releases go to npm and cdn.getpinax.com together, from a clean `main`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
yarn release # next beta: 1.0.0-beta.1 → 1.0.0-beta.2 (npm tag "beta")
|
|
75
|
+
yarn release 1.0.0 # a stable version (npm tag "latest"); also patch|minor|major
|
|
76
|
+
yarn release --dry-run # check, test and pack; changes nothing
|
|
77
|
+
yarn release --cdn-only # npm worked but the CDN upload failed: upload again
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`scripts/release.sh` checks you're on `main`, clean and in step with
|
|
81
|
+
`origin`, runs lint and tests, bumps `version`, commits and tags `v<version>`,
|
|
82
|
+
then publishes to npm. `prepublishOnly` builds with
|
|
83
|
+
`VITE_PINAX_API=https://api.getpinax.com` and `VITE_PINAX_CDN=https://cdn.getpinax.com`
|
|
84
|
+
and tests again. Next it uploads the build with `cdn/deploy.sh` (the Worker is
|
|
85
|
+
redeployed only when `cdn/` changed, or with `--worker`), pushes the commit and
|
|
86
|
+
tag, and checks npm and the CDN serve the new version. If npm refuses (no 2FA
|
|
87
|
+
code, version taken), the local commit and tag are undone.
|
|
88
|
+
|
|
89
|
+
It needs `npm login` (with 2FA; pass `--otp <code>` or confirm in the browser)
|
|
90
|
+
and the Cloudflare deploy credentials `CLOUDFLARE_API_TOKEN`,
|
|
91
|
+
`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_ZONE_ID`, exported or in
|
|
92
|
+
`~/.config/pinax/cdn.env` (outside the repo). `version` is also the `?v=` the
|
|
93
|
+
script tag uses to load matching editor and recorder files, and
|
|
94
|
+
`/v/<version>/` files are cached for a year, so a version is never reused.
|
|
95
|
+
|
|
96
|
+
## API
|
|
97
|
+
|
|
98
|
+
| Call | What it does |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `init({ key, apiUrl?, cdnUrl?, segment?, consent? })` | Starts capture and fetches settings (from the CDN, P-15). Later calls are ignored. The script tag calls it after the page's `load` event. |
|
|
101
|
+
| `open()` | Opens the panel from the site's own link (S-05). Before settings arrive, it opens once they do. Works with `hideButton` and on hidden paths. Closing returns focus to whatever had it before. |
|
|
102
|
+
| `identify({ id?, name?, email? } \| null)` | Attaches the signed-in visitor's id and name to reports (C-09). Works before `init`. The email only fills in the form's email box (unless the site's `identifyPrefill` is off) and is sent only if the visitor leaves it there; it's never sent on its own. |
|
|
103
|
+
| `setBasket({ value, currency } \| null)` | What's in the shopper's basket now, in major units (`42.5`, not `4250`) and an ISO code (`'GBP'`); `null` once it's empty. Works before `init`. Sent only with a sampled sighting of a known error (AI-21), never on its own. |
|
|
104
|
+
| `consent(true \| false \| { diagnostics?, replay? })` | The visitor's answer to the site's cookie banner (see Visitor consent). `true`/`false` set both parts; the object form changes only the parts given. Works before `init`, and as often as the banner reports a change; withdrawing stops what it covers at once. Only matters where the page waits for consent. |
|
|
105
|
+
| `destroy()` | Removes the widget and restores `console`, `fetch` and `XMLHttpRequest`. |
|
|
106
|
+
|
|
107
|
+
With the script tag these are on `window.Pinax`.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// A shop: keep the basket up to date.
|
|
111
|
+
setBasket({ value: cart.total, currency: 'GBP' });
|
|
112
|
+
setBasket(null); // emptied
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Settings
|
|
116
|
+
|
|
117
|
+
The widget follows the settings from `GET /v1/widget/{key}/config`
|
|
118
|
+
(`backend/src/modules/widget/widget-settings.ts`). Anything missing or
|
|
119
|
+
malformed falls back to the defaults (`src/settings.ts`).
|
|
120
|
+
|
|
121
|
+
| Setting | Effect |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `color`, `text`, `position` | Button colour, label and placement (S-01). |
|
|
124
|
+
| `types`, `rating` | Which feedback types are offered; stars on General (S-02). |
|
|
125
|
+
| `hidePaths` | No button on matching paths (S-04). Matched against `location.pathname`; `*` matches anything, so `/checkout/*` covers `/checkout`, `/checkout/` and `/checkout/pay/2`. `/pricing` covers `/pricing` and `/pricing/` only. Case-sensitive. Re-checked on `popstate`, `history.pushState` and `history.replaceState`, so single-page sites work. An open panel stays open when the visitor navigates to a hidden path. |
|
|
126
|
+
| `smallOnMobile` | A smaller button on screens up to 520 px wide (S-04). |
|
|
127
|
+
| `segment` | Off by default. When on and the page runs Segment, the widget sends its events to `window.analytics` (see Segment below). |
|
|
128
|
+
| `hideButton` | No floating button at all; the site calls `Pinax.open()` (S-05). The panel then sits 24 px from the bottom instead of above the button. |
|
|
129
|
+
| `knownIssues` | `{ enabled, notify }`, both on by default. `enabled` shows the "We know about this" card (see Known issues below); `notify` offers "Email me when it's fixed" after Me too. |
|
|
130
|
+
| `consent` | `{ required }`, off by default. On: the site shows a cookie banner, so replay, technical logs, error counts and the page-view count wait for `Pinax.consent()` (see Visitor consent). `data-consent` / the `consent` option overrides it. |
|
|
131
|
+
| `identifyPrefill` | On by default. The email given to `identify()` fills in the form's email box; the visitor can change or clear it, and a box they cleared isn't filled in again on this page. Off: the box starts empty. Either way only what's in the box is sent (`email`); `user` carries the id and name only. |
|
|
132
|
+
| `privacyUrl` | The site's privacy policy (an `https:` address on one of its domains; the dashboard checks that). When set, the form's notice links it ("Privacy policy", new tab). Anything that isn't `https:` is ignored. |
|
|
133
|
+
| `wording` | S-06. An empty string keeps the default. `chooserTitle` is the "What would you like to do?" heading. `problemTitle`, `ideaTitle` and `generalTitle` rename the type in the chooser and head its form (the General default heading is "How was your visit?" with stars, "General feedback" without). `messagePlaceholder` replaces the message placeholder for all three types. `confirmation` replaces "Thanks! We've got it." (not the offline "Saved for later"). |
|
|
134
|
+
|
|
135
|
+
History wrapping follows the same rules as the `fetch` wrapping: the original
|
|
136
|
+
always runs with the caller's arguments and its return value is passed back,
|
|
137
|
+
our callback can't throw into the page, it's only installed when `hidePaths`
|
|
138
|
+
is non-empty and the button isn't hidden anyway, and `destroy()` restores the
|
|
139
|
+
originals unless something else wrapped them on top of us.
|
|
140
|
+
|
|
141
|
+
## Known issues
|
|
142
|
+
|
|
143
|
+
A team can mark an open report "Show to visitors" with a public title they
|
|
144
|
+
write. The config then lists it in `known` (`{ id, title, paths, signature }`,
|
|
145
|
+
up to 20, newest first; missing on older APIs, which means none). A known
|
|
146
|
+
issue matches this page view when one of its `paths` is the current path
|
|
147
|
+
(`/book` and `/book/` are the same page, as in fix checks; `*` is not
|
|
148
|
+
special) or when its error `signature` showed up in this page view's
|
|
149
|
+
console errors or failed requests (the fix-check signatures, every page view,
|
|
150
|
+
no sampling; `src/known.ts`). Nothing is sent while watching.
|
|
151
|
+
|
|
152
|
+
When the panel opens and something matches, it starts on a card above the
|
|
153
|
+
type chooser: "We know about this", up to 3 public titles, and a **Me too**
|
|
154
|
+
button for each (`POST /v1/widget/:key/known/:id/me-too {}`). After it:
|
|
155
|
+
"Thanks, we've added you." (announced through the panel's live region) and,
|
|
156
|
+
with `knownIssues.notify`, an "Email me when it's fixed" field and **Send**
|
|
157
|
+
(the same request with `{ email }`). "Report something else" leaves the card
|
|
158
|
+
for the normal flow (straight to the form when the site has one type). It
|
|
159
|
+
works with `hideButton` through `open()`. A report already under way (a note
|
|
160
|
+
typed) isn't interrupted by the card.
|
|
161
|
+
|
|
162
|
+
## Visitor consent
|
|
163
|
+
|
|
164
|
+
On sites that ask visitors for cookie consent (EU and UK cookie rules, and
|
|
165
|
+
CNIL's guidance on session replay), the widget waits for the visitor to
|
|
166
|
+
agree before anything beyond the button and its settings leaves the
|
|
167
|
+
browser, or before replay starts at all. A page waits when the site's
|
|
168
|
+
`settings.consent.required` is on ("Wait for visitor consent" in the
|
|
169
|
+
dashboard), or when the script tag says `data-consent="wait"` (`init({
|
|
170
|
+
consent: 'wait' })`). `data-consent="none"` makes a page not wait whatever
|
|
171
|
+
the setting. Sites that don't wait behave exactly as before.
|
|
172
|
+
|
|
173
|
+
Two parts, given with `Pinax.consent()`:
|
|
174
|
+
|
|
175
|
+
- `diagnostics`: technical logs (console, failed requests and breadcrumbs)
|
|
176
|
+
attached to reports, fix checks (AI-17) and error counts (AI-20, with the
|
|
177
|
+
basket, AI-21), the Segment user id read when a report is sent, and the
|
|
178
|
+
page-view count (B-16).
|
|
179
|
+
- `replay`: the session recorder (AI-15).
|
|
180
|
+
|
|
181
|
+
| While the page waits | Without consent | With consent |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| Console, network and breadcrumb capture | Keeps running, in memory only: nothing leaves the browser | The same |
|
|
184
|
+
| Logs on a report | Only if the visitor ticks "Include technical details" in the form | Attached (as far as the site's capture settings allow, C-10) |
|
|
185
|
+
| Fix checks, error counts | Not started | Started once; `POST /fix-check` and `/sighting` carry `consent: true` (see The flags) |
|
|
186
|
+
| Segment user id on a report | Not read | Read, as before |
|
|
187
|
+
| Page-view count | Not sent | One `POST {cdn}/count/{key}` per page load (`POST {api}/v1/widget/{key}/count` without a CDN): no body, no cookies, `keepalive` |
|
|
188
|
+
| Session replay | Not started; stopped, and its 30 seconds dropped, when consent is withdrawn | Started if this page view was in the sample |
|
|
189
|
+
|
|
190
|
+
- **Withdrawing** (`consent(false)`, or `{ replay: false }`) stops the fix
|
|
191
|
+
checks and error counts and stops the recorder, dropping what it held.
|
|
192
|
+
Given again, they start again; errors from before are never sent twice.
|
|
193
|
+
- **The replay sample** is drawn once per page view, when the settings
|
|
194
|
+
arrive, so consent given later still follows `replaySampleRate`.
|
|
195
|
+
- **Reports** still work without consent: the note, the screenshot, the page
|
|
196
|
+
address and the browser details. The body carries `consent: { diagnostics,
|
|
197
|
+
replay }` and `includeLogs: true` when the box was ticked; the API drops
|
|
198
|
+
logs and the replay that weren't agreed to, as a second line of defence.
|
|
199
|
+
- **The flags** (`consent` on reports, `consent: true` on fix checks and
|
|
200
|
+
error counts) go wherever the page waits *or* the site's setting does,
|
|
201
|
+
with what is actually allowed. The API goes by the site's setting alone,
|
|
202
|
+
so on a site that waits, `data-consent="none"` sends everything as agreed
|
|
203
|
+
(both `true`) rather than having it dropped. Sites that don't wait, with
|
|
204
|
+
no override, send no flags.
|
|
205
|
+
- **The count**: on these sites the CDN doesn't count the settings request
|
|
206
|
+
(`cdn/README.md`); the widget counts the page load once the visitor
|
|
207
|
+
agrees. It follows the site's setting, like the CDN, so with a
|
|
208
|
+
`data-consent` override a page load is never counted twice or not at all.
|
|
209
|
+
- **Before `pinax.js` loads**, calls wait in the stub queue (below) and are
|
|
210
|
+
replayed in order. `consent()` before `init()` is kept, like `identify()`;
|
|
211
|
+
`destroy()` forgets it.
|
|
212
|
+
|
|
213
|
+
### The form's notice
|
|
214
|
+
|
|
215
|
+
Above Send, every form says what goes with the note, listing only what will
|
|
216
|
+
actually be attached:
|
|
217
|
+
|
|
218
|
+
> Sent to harbourcafe.co.uk with your note: the page address, your browser
|
|
219
|
+
> details, the screenshot, technical logs, a recording of the last 30
|
|
220
|
+
> seconds. [Privacy policy]
|
|
221
|
+
|
|
222
|
+
The site is this page's host name (without `www.`); the screenshot,
|
|
223
|
+
technical logs and recording appear only when they'll go. With `privacyUrl`
|
|
224
|
+
set, "Privacy policy" links it (new tab, `rel="noopener noreferrer"`). Send
|
|
225
|
+
is described by the notice (`aria-describedby`), so screen readers read it
|
|
226
|
+
with the button.
|
|
227
|
+
|
|
228
|
+
The screenshot "Report a problem" takes on its own is only in the visitor's
|
|
229
|
+
browser until Send: **Remove** under it (named "Remove screenshot" for screen
|
|
230
|
+
readers) drops it, and the notice stops listing it. The optional email field
|
|
231
|
+
says what the address is for and nothing more: "Only used to tell you about
|
|
232
|
+
this report." on problems, "Only used to reply to you about this." on the
|
|
233
|
+
other types, and "Only used to tell you when it's fixed." under a known
|
|
234
|
+
issue's "Email me when it's fixed".
|
|
235
|
+
|
|
236
|
+
While the page waits and the visitor hasn't given `diagnostics`, and console
|
|
237
|
+
or network capture is on, an **unticked** checkbox sits above the notice:
|
|
238
|
+
"Include technical details (console messages and failed requests) to help
|
|
239
|
+
fix this". Ticking it adds "technical logs" to the notice and sends the logs
|
|
240
|
+
with this report (`includeLogs: true`). It's a native, labelled checkbox
|
|
241
|
+
(Space toggles it). If consent changes while the form is open, the notice
|
|
242
|
+
and the box update in place without disturbing what the visitor is typing.
|
|
243
|
+
Nothing is attached that the notice didn't list.
|
|
244
|
+
|
|
245
|
+
### Banner recipes
|
|
246
|
+
|
|
247
|
+
Put the stub first, so `Pinax.consent()` calls from a banner that answers
|
|
248
|
+
before `pinax.js` has loaded are queued:
|
|
249
|
+
|
|
250
|
+
```html
|
|
251
|
+
<script>
|
|
252
|
+
window.Pinax = window.Pinax || { q: [] }; ['init','open','identify','consent'].forEach(function (m) { window.Pinax[m] = window.Pinax[m] || function () { window.Pinax.q.push([m, [].slice.call(arguments)]); }; });
|
|
253
|
+
</script>
|
|
254
|
+
<script src="https://cdn.getpinax.com/pinax.js" data-key="pk_..." async></script>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Then one of these, after the stub:
|
|
258
|
+
|
|
259
|
+
**Cookiebot**
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
window.addEventListener('CookiebotOnConsentReady', () => Pinax.consent({ diagnostics: Cookiebot.consent.statistics, replay: Cookiebot.consent.statistics }));
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**OneTrust** (`C0002` is OneTrust's default Performance group; the id can
|
|
266
|
+
differ in your OneTrust account)
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
window.addEventListener('OneTrustGroupsUpdated', (e) => { const ok = e.detail.includes('C0002'); Pinax.consent({ diagnostics: ok, replay: ok }); });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Usercentrics** (add "Pinax" as a service in Usercentrics first)
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
window.addEventListener('UC_CONSENT', (e) => { const ok = !!e.detail.services['Pinax']?.consent?.given; Pinax.consent({ diagnostics: ok, replay: ok }); });
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Google Tag Manager**: a Custom HTML tag
|
|
279
|
+
|
|
280
|
+
```html
|
|
281
|
+
<script>Pinax.consent(true);</script>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
with Consent Settings → "Require additional consent for tag to fire:
|
|
285
|
+
`analytics_storage`", fired on the consent update event. A tag can't take
|
|
286
|
+
consent back: withdrawal needs the banner's own event (`Pinax.consent(false)`)
|
|
287
|
+
or a page reload.
|
|
288
|
+
|
|
289
|
+
**Shopify**
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
window.Shopify.loadFeatures([{ name: 'consent-tracking-api', version: '0.2' }], (err) => { if (err) return; const apply = () => { const ok = window.Shopify.customerPrivacy.analyticsProcessingAllowed(); Pinax.consent({ diagnostics: ok, replay: ok }); }; apply(); document.addEventListener('visitorConsentCollected', apply); });
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**Anything else**
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
Pinax.consent({ diagnostics: true, replay: true }); // the visitor agreed
|
|
299
|
+
Pinax.consent(false); // refused or withdrew
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
With replay on, CNIL recommends naming it on the banner's first layer, for
|
|
303
|
+
example: "We record your actions on this page to fix technical problems
|
|
304
|
+
(session replay)."
|
|
305
|
+
|
|
306
|
+
## Segment
|
|
307
|
+
|
|
308
|
+
Sites that use Segment (analytics.js) can install Pinax alongside it and get
|
|
309
|
+
the widget's events in Segment. Turn on "Send events to Segment" on the
|
|
310
|
+
dashboard's Widget screen (setting `segment`), or force it per page with
|
|
311
|
+
`data-segment="true"` on the script tag or `init({ key, segment: true })`.
|
|
312
|
+
|
|
313
|
+
```html
|
|
314
|
+
<!-- 1. Segment's own snippet, as Segment gives it to you -->
|
|
315
|
+
<script>!function(){var analytics=window.analytics=window.analytics||[]; /* … */ analytics.load("YOUR_WRITE_KEY");analytics.page();}();</script>
|
|
316
|
+
<!-- 2. Pinax, after it -->
|
|
317
|
+
<script src="https://cdn.getpinax.com/pinax.js" data-key="pk_..." data-segment="true" async></script>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Loading Pinax through Segment instead (for example from a Segment-managed tag
|
|
321
|
+
or after `analytics.ready`) works the same way: the widget only looks for
|
|
322
|
+
`window.analytics` when something happens, so the order doesn't matter.
|
|
323
|
+
|
|
324
|
+
| Event | Properties |
|
|
325
|
+
|---|---|
|
|
326
|
+
| `Pinax Widget Opened` | `source: "pinax"` |
|
|
327
|
+
| `Feedback Submitted` | `source`, `type` (`problem`, `idea`, `general`), `rating` (when given), `reportId` (when sent), `queued: true` (saved offline) |
|
|
328
|
+
|
|
329
|
+
Never sent: the message, the email, screenshots or anything else the visitor
|
|
330
|
+
typed. If the site hasn't called `Pinax.identify()` with an `id`, reports use
|
|
331
|
+
Segment's signed-in user id (`analytics.user().id()`) as the visitor's user
|
|
332
|
+
ID (on pages that wait for consent, only once `diagnostics` is given);
|
|
333
|
+
anonymous Segment visitors stay anonymous. Errors from Segment never reach
|
|
334
|
+
the page.
|
|
335
|
+
|
|
336
|
+
## Live preview (dashboard, S-03)
|
|
337
|
+
|
|
338
|
+
`@getpinax/widget/preview` (`dist/preview.mjs`, ~10 KB gzipped with its shared
|
|
339
|
+
chunk) renders the real button and panel into a container's shadow root from
|
|
340
|
+
a settings object. It makes no API calls and installs nothing: no console,
|
|
341
|
+
network or history wrapping, no screenshot library, no reports. "Send" just
|
|
342
|
+
shows the confirmation; the problem form shows a placeholder screenshot.
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import { preview } from '@getpinax/widget/preview';
|
|
346
|
+
|
|
347
|
+
// The container needs a size; the widget is positioned inside it as it
|
|
348
|
+
// would be in the visitor's window. Call only in the browser (useEffect).
|
|
349
|
+
const p = preview(element, settings, { view: 'choose', phone: false, poweredBy: true });
|
|
350
|
+
p.update(nextSettings); // on every form change; keeps the screen on show
|
|
351
|
+
p.update(nextSettings, { phone: true });
|
|
352
|
+
p.show('idea'); // 'button' | 'choose' | 'problem' | 'idea' | 'general' | 'done'
|
|
353
|
+
p.view(); // the screen on show (visitors can click around)
|
|
354
|
+
p.destroy();
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`settings` is the same shape as the backend's widget settings; any field may
|
|
358
|
+
be left out. `known: [{ id, title, paths, signature }]` in the options shows
|
|
359
|
+
the known issues card on the chooser as if they matched (the first 3); without
|
|
360
|
+
it the card never shows in the preview. `site: 'harbourcafe.co.uk'` names the
|
|
361
|
+
site in the form's notice ("Sent to … with your note", default "this
|
|
362
|
+
website"); with `consent: { required: true }` in the settings the forms show
|
|
363
|
+
the "Include technical details" box. `phone: true` shows the phone layout (bottom sheet,
|
|
364
|
+
`smallOnMobile`) whatever the page width. `hidePaths` has no effect in the
|
|
365
|
+
preview. The preview never moves focus, so it can update while the owner
|
|
366
|
+
types. `playground/preview.html` is a working example.
|
|
367
|
+
|
|
368
|
+
## Pinax AI in the browser (Phase 4)
|
|
369
|
+
|
|
370
|
+
`GET /config` also returns `ai` (`widgetAiConfig` in
|
|
371
|
+
`backend/src/modules/widget/widget.service.ts`); `resolveAi` (`src/settings.ts`)
|
|
372
|
+
turns everything off when it's missing, and caps both sample rates at
|
|
373
|
+
`samplingRate` (B-17).
|
|
374
|
+
|
|
375
|
+
| Feature | What the widget does |
|
|
376
|
+
|---|---|
|
|
377
|
+
| AI-11 breadcrumbs | A ring buffer of the last 20 clicks, page views (load and SPA navigation) and "typed in field X" events, sent with every report (`src/capture/breadcrumbs.ts`). A target is its role plus its accessible name or text, cut to 60 characters: `button "Book a table"`, `link "Menu"`, `textbox "Email"`. Typed values are never read. Nothing is read from inside inputs, `contenteditable`, `[data-pinax-mask]` or `.pinax-mask`; password and card fields are just `password field` / `card field`. Paths keep the query's keys but not its values (`/search?q=…`) or the fragment; emails and phone numbers in a target are starred out when the report is sent. |
|
|
378
|
+
| AI-10 follow-up | With `ai.followUp`, a problem or idea with a note of 140 characters or fewer calls `POST /follow-up` first (4 s timeout). A question shows as one more panel step ("One quick question", the note, the question as the answer field's label, **Send** and **Skip and send**). The answer goes in the report as `followUp`. On no question, an error or the timeout, the report is sent straight away. Asked once per note. |
|
|
379
|
+
| AI-12 auto-blur | With `ai.autoBlur`, the editor chunk finds visible emails, phone numbers (UK and international) and likely names (after labels such as "Name", "Customer", "Guest name", "Booked by", "Signed in as", in a "Name" table column or `<dt>`, or in `autocomplete=name` / schema.org Person elements) and paints the same striped mask as form fields over them (`src/editor/pii.ts`, `src/privacy.ts`). The live page is never changed. The panel says "We blurred N private details for you." and the editor outlines them with an "Auto-blurred" tag; they're part of the image, so only the visitor's own Blur marks can be undone. |
|
|
380
|
+
| AI-15 replay (Pro, Agency) | With `ai.replay` and a passing draw of `replaySampleRate` (once per page view), `pinax-recorder.js` (rrweb's `record`, MIT) is loaded after the page's `load` event (and, where the page waits for consent, once `replay` is given; see Visitor consent). It keeps a rolling 30 s in memory, in segments that each start with a full snapshot (checkout every 10 s), so at most 40 s is held. Always: inputs are masked (`maskAllInputs`), hidden inputs, `.pinax-mask` / `[data-pinax-mask]` and the widget are blocked, and attributes are cleaned on the way out (URL secrets, emails, phone numbers). `ai.replayMask` decides the rest: **`'all'`** (the default, also when the config doesn't say; CNIL's "mask everything") stars out every text and text attributes (`alt`, `title`, `aria-label`, `placeholder`, `value`…) and leaves out `img`, `picture`, `video`, `canvas` and `iframe` (the replay shows an empty box of the same size); **`'inputs'`** keeps the page's text, starring out editable text and emails and phone numbers in it. **Nothing is sent by the recorder.** When a report is sent, the buffer is gzipped (`CompressionStream`; no replay where it's missing), base64-encoded and added as `replay` if it fits 4.5 M characters and the whole request stays under 11 MB (fewer seconds are tried before giving up). |
|
|
381
|
+
| AI-17 fix check (P-14) | For each `ai.fixChecks` entry whose `path` equals `location.pathname` (re-checked on SPA navigation), one draw of `fixCheckSampleRate` per page view. If it passes, this page view's console errors and failed requests (from the existing capture) are signed with the backend's own `errorSignature` (`src/capture/signature.ts`, ported exactly), and a match sends `POST /fix-check {checkId, signature}` once per check per page view. Nothing else is sent, and nothing on success. |
|
|
382
|
+
|
|
383
|
+
## Privacy
|
|
384
|
+
|
|
385
|
+
- Every form field (inputs, textareas, selects, contenteditable) is hidden in
|
|
386
|
+
screenshots, as is anything marked `data-pinax-mask` or `class="pinax-mask"`.
|
|
387
|
+
The live page is never changed: masking happens on the screenshot library's
|
|
388
|
+
private copy, and striped blocks are painted over the field positions.
|
|
389
|
+
- The network log keeps only failed (status 0, 4xx, 5xx) and slow (over 2 s)
|
|
390
|
+
requests: method, URL, status, duration and time. Headers, cookies and
|
|
391
|
+
bodies are never read. URL fragments and credentials are dropped, and
|
|
392
|
+
every query value is replaced with `…` (`/api/orders?email=…&page=…`):
|
|
393
|
+
the keys stay, so the team can still see what was asked.
|
|
394
|
+
- The page address on a report keeps only the values of `utm_*`, `page`,
|
|
395
|
+
`lang`, `ref`, `variant`, `tab` and `sort`; every other value is `…`
|
|
396
|
+
(`/search?q=…&page=2`), and there's no fragment. The `?pinax_verify` ping
|
|
397
|
+
sends the address the same way.
|
|
398
|
+
- Console messages and stacks have emails and phone numbers starred out
|
|
399
|
+
(`redactPersonal`, `src/privacy.ts`) when a report is built. The error
|
|
400
|
+
signature that fix checks and error counts compare is made from the
|
|
401
|
+
starred-out text too, so it never carries them (it's published in the
|
|
402
|
+
site's settings).
|
|
403
|
+
- The API applies all of these again (`widget.sanitise.ts`), for older
|
|
404
|
+
cached widgets.
|
|
405
|
+
- No keystrokes are recorded. Pinax's own requests never appear in the log.
|
|
406
|
+
- A report that can't be sent (offline, or a server error; W-16) waits in
|
|
407
|
+
`localStorage` (`pinax:queue:<key>`) for an hour at most, without the
|
|
408
|
+
email address, `identify()` data, console log, screenshot or replay. The
|
|
409
|
+
email and `identify()` data stay in the page's memory and go with a retry
|
|
410
|
+
from the same page only; after a reload the report goes without them, and
|
|
411
|
+
the "Saved for later" screen says so when an email was given. A report is
|
|
412
|
+
removed once it's sent or refused for good, and anything older than an
|
|
413
|
+
hour is deleted unsent the next time the widget loads (`src/queue.ts`).
|
|
414
|
+
- Breadcrumbs and the replay follow the same masking rules; see the table above.
|
|
415
|
+
- On sites that wait for visitor consent, logs, error counts, the page-view
|
|
416
|
+
count and the replay wait for it (see Visitor consent).
|
|
417
|
+
|
|
418
|
+
## How it loads
|
|
419
|
+
|
|
420
|
+
| File | Gzipped | When |
|
|
421
|
+
|---|---|---|
|
|
422
|
+
| `dist/pinax.js` | ~23 KB | The script tag. Button, panel, capture. Limit 50 KB (W-13). |
|
|
423
|
+
| `dist/pinax-editor.js` | ~17 KB | ES module loaded with `import()` from the same folder as `pinax.js`, only when a visitor takes a screenshot. Screenshot capture (`modern-screenshot`, MIT), auto-blur and the annotation editor. |
|
|
424
|
+
| `dist/pinax-recorder.js` | ~27 KB | ES module loaded the same way, after page load, only on sampled Pro/Agency visits with replay on (after consent where the page waits for it). Session recorder (`@rrweb/record`, MIT). |
|
|
425
|
+
| `dist/pinax.mjs` + `dist/chunks/` | | npm build. The editor is a normal dynamic-import chunk. |
|
|
426
|
+
| `dist/preview.mjs` + `dist/chunks/` | ~10 KB | `@getpinax/widget/preview`, the dashboard's live preview. Shares the UI chunk with `pinax.mjs`. |
|
|
427
|
+
|
|
428
|
+
`pinax-editor.js` and `pinax-recorder.js` must be served next to `pinax.js`
|
|
429
|
+
with CORS allowed (they are ES module imports). On page load the widget makes
|
|
430
|
+
one request, for the site's settings from the CDN
|
|
431
|
+
(`https://cdn.getpinax.com/config/<key>.json`, P-15), plus `POST /ping` only
|
|
432
|
+
when the URL has `?pinax_verify`, a retry only when this visitor has a
|
|
433
|
+
report waiting from an earlier offline send (W-16), and, on sites that wait
|
|
434
|
+
for visitor consent, one `POST /count/<key>` once the visitor agrees. It asks the API
|
|
435
|
+
(`GET /v1/widget/<key>/config`) only when the CDN can't answer, or when there's
|
|
436
|
+
no CDN (local builds, or `apiUrl`/`data-api` set without `cdnUrl`/`data-cdn`).
|
|
437
|
+
|
|
438
|
+
### The CDN (P-15, B-16)
|
|
439
|
+
|
|
440
|
+
`cdn/` is the Cloudflare Worker behind `cdn.getpinax.com`; see
|
|
441
|
+
[`cdn/README.md`](cdn/README.md). In short: `pinax.js` is cached for an hour
|
|
442
|
+
(the URL customers paste never changes), `?v=<version>` files for a year as
|
|
443
|
+
immutable, and each site's settings are static JSON the API republishes on
|
|
444
|
+
every change, revalidated by browsers on each page load so the edge can count
|
|
445
|
+
it as a page view (B-16) without the API ever being called. Sites that wait
|
|
446
|
+
for visitor consent are counted by `POST /count/<key>` instead, after consent.
|
|
447
|
+
|
|
448
|
+
## Accessibility and touch (W-12, W-15)
|
|
449
|
+
|
|
450
|
+
`src/__tests__/a11y.test.ts` runs axe-core (WCAG 2.0 to 2.2 A and AA, plus
|
|
451
|
+
best practice) on every panel screen, the follow-up question and the
|
|
452
|
+
screenshot editor, and checks text contrast (4.5:1), non-text contrast (3:1)
|
|
453
|
+
and target sizes (24 px, 2.5.8) from the CSS. `annotator.test.ts` drives the
|
|
454
|
+
editor with touch, pen and mouse pointer events, the keyboard fallbacks
|
|
455
|
+
(Enter on the image places a mark or selects the middle area; Ctrl/Cmd+Z;
|
|
456
|
+
Escape; focus stays inside) and area selection (W-04).
|
|
457
|
+
|
|
458
|
+
jsdom has no layout or real touch, so these still need a person with a device
|
|
459
|
+
before each release:
|
|
460
|
+
|
|
461
|
+
- iPhone (Safari) and Android (Chrome): open the panel, take a screenshot,
|
|
462
|
+
draw a box, an arrow and a blur with a finger, select an area, send. The
|
|
463
|
+
page mustn't scroll or zoom while drawing, and focusing a field mustn't
|
|
464
|
+
zoom the page.
|
|
465
|
+
- iPad (Safari) in both orientations, plus Apple Pencil.
|
|
466
|
+
- iOS screenshots: pages with cross-origin images or fonts without CORS
|
|
467
|
+
headers show those images as grey blocks and fall back to system fonts (a
|
|
468
|
+
browser rule; nothing the widget can fix). Captures are capped at 2560 px
|
|
469
|
+
on the long side and 5 MP in total (`captureScale`), well under iOS
|
|
470
|
+
Safari's 16.7 MP canvas limit, and the editor frees its scratch canvas.
|
|
471
|
+
- VoiceOver (iOS and macOS) and NVDA (Windows): open with the keyboard, hear
|
|
472
|
+
the dialog name, choose a type, hear errors and the confirmation, use the
|
|
473
|
+
editor without a pointer.
|
|
474
|
+
- 200% and 400% zoom, and Windows high contrast (forced colours).
|
|
475
|
+
|
|
476
|
+
## Development
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
yarn dev # http://localhost:5175 (index.html, ES module from src/)
|
|
480
|
+
yarn playground # build, then http://localhost:5177/playground/embed.html (the built script tag)
|
|
481
|
+
# and /playground/preview.html (the dashboard preview). PORT=5176 to change the port.
|
|
482
|
+
yarn build # types, four bundles, declarations, size check
|
|
483
|
+
yarn test # vitest (jsdom)
|
|
484
|
+
yarn run check # biome. Plain `yarn check` is Yarn's own command.
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Accessibility checks (dev).** `yarn dev` adds an "Accessibility (dev)" panel
|
|
488
|
+
(`playground/a11y-check.js`, never shipped). "Check the widget" steps through
|
|
489
|
+
every screen (button, chooser, each form, thanks) and runs axe-core inside the
|
|
490
|
+
shadow root in a real browser, so colour contrast and target size are measured
|
|
491
|
+
rather than read from the CSS as in `src/__tests__/a11y.test.ts` (W-15). It
|
|
492
|
+
uses `globalThis.__pinaxDevUI`, set in `src/index.ts` only when
|
|
493
|
+
`import.meta.env.DEV` (Vite drops it from builds). "Scan this page" runs the
|
|
494
|
+
scan an accessibility report attaches (W-20, `src/a11y/scan.ts`). Both
|
|
495
|
+
playground pages have a "Broken on purpose" card with six failing controls
|
|
496
|
+
(label, select name, image alt, link and button names, contrast) for trying
|
|
497
|
+
accessibility reports; the site needs `types.accessibility` on.
|
|
498
|
+
|
|
499
|
+
Both playground pages take `?key=pk_...` (and `?api=...`). They also have
|
|
500
|
+
"Pinax AI switches" (`playground/fake-ai.js`): because a local backend has no
|
|
501
|
+
model key, they patch the config in the page to turn on the follow-up
|
|
502
|
+
question (with a canned question), force auto-blur, turn replay off, or add
|
|
503
|
+
a known issue on the current page (tied to the "console.error" button's
|
|
504
|
+
error, its Me too answered in the page). The
|
|
505
|
+
widget code is unchanged; the switches wrap the page's `fetch` before the
|
|
506
|
+
widget loads. `index.html` also
|
|
507
|
+
reads `VITE_PINAX_KEY` from `.env.local`. To get a key, sign up through the
|
|
508
|
+
API and create a website for `http://localhost:5175`; its allowed domain is
|
|
509
|
+
`localhost`, which covers every port.
|
|
510
|
+
|
|
511
|
+
The report body is built in `src/report.ts` to the limits in
|
|
512
|
+
`backend/src/modules/widget/widget.validator.ts`. When `backend/` is checked
|
|
513
|
+
out next to `widget/`, `report.test.ts` also checks the body against that
|
|
514
|
+
real schema.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import axe from 'axe-core';
|
|
2
|
+
import type { A11yScan } from '../types';
|
|
3
|
+
export declare const SCAN_LIMITS: {
|
|
4
|
+
readonly violations: 50;
|
|
5
|
+
readonly nodes: 5;
|
|
6
|
+
readonly target: 300;
|
|
7
|
+
readonly html: 300;
|
|
8
|
+
readonly summary: 500;
|
|
9
|
+
readonly help: 300;
|
|
10
|
+
};
|
|
11
|
+
/** "wcag143" → "1.4.3", "wcag2aa" → null. */
|
|
12
|
+
export declare function wcagCriterion(tag: string): string | null;
|
|
13
|
+
/**
|
|
14
|
+
* The element's opening tag only: no text inside it, no typed values and
|
|
15
|
+
* no data attributes, with emails and phone numbers starred out.
|
|
16
|
+
*/
|
|
17
|
+
export declare function describeElement(el: Element | undefined | null): string;
|
|
18
|
+
/**
|
|
19
|
+
* Runs axe-core on the page, leaving out the widget itself (`exclude`, the
|
|
20
|
+
* shadow host's selector). Resolves to null if axe fails.
|
|
21
|
+
*/
|
|
22
|
+
export declare function scanPage(options?: {
|
|
23
|
+
exclude?: string;
|
|
24
|
+
}): Promise<A11yScan | null>;
|
|
25
|
+
export declare function summarise(result: axe.AxeResults, durationMs: number): A11yScan;
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ReportBody } from './report';
|
|
2
|
+
import type { PinaxBasket, WidgetConfig } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* The API origin. Production builds set VITE_PINAX_API (e.g. the real API
|
|
5
|
+
* domain); without it the widget talks to a local backend. `data-api` on the
|
|
6
|
+
* script tag, or `apiUrl` in init(), still overrides this per site.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_API: string;
|
|
9
|
+
export declare function apiBase(apiUrl?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* P-15: the CDN origin that serves each site's settings as static JSON
|
|
12
|
+
* (`/config/<key>.json`). Production builds set VITE_PINAX_CDN; without it
|
|
13
|
+
* (local builds) settings come straight from the API.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_CDN: string;
|
|
16
|
+
/**
|
|
17
|
+
* Where settings come from: `cdnUrl` / `data-cdn` if given; otherwise the
|
|
18
|
+
* build's CDN, unless the API was overridden (a local or self-hosted API has
|
|
19
|
+
* no CDN in front of it). Null means "ask the API".
|
|
20
|
+
*/
|
|
21
|
+
export declare function cdnBase(cdnUrl?: string, apiUrl?: string): string | null;
|
|
22
|
+
export declare function cdnConfigUrl(cdn: string, key: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The fetch the widget itself uses: the page's fetch as it was before Pinax
|
|
25
|
+
* wrapped it, so Pinax's own calls never show up in the network log.
|
|
26
|
+
*/
|
|
27
|
+
export type Fetch = typeof fetch;
|
|
28
|
+
/**
|
|
29
|
+
* P-15: settings from the CDN, so a page load never reaches our servers.
|
|
30
|
+
* The API is asked only when there's no CDN, or the CDN can't answer
|
|
31
|
+
* (down, blocked, or not set up yet).
|
|
32
|
+
*/
|
|
33
|
+
export declare function fetchConfig(doFetch: Fetch, api: string, key: string, cdn?: string | null): Promise<WidgetConfig | null>;
|
|
34
|
+
/** S-12: only when the owner opens their site with ?pinax_verify. */
|
|
35
|
+
export declare function pingIfVerifying(doFetch: Fetch, api: string, key: string, href: string): void;
|
|
36
|
+
export type SendResult = {
|
|
37
|
+
ok: true;
|
|
38
|
+
id: string;
|
|
39
|
+
} | {
|
|
40
|
+
ok: false;
|
|
41
|
+
retry: boolean;
|
|
42
|
+
status: number;
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
45
|
+
export declare function sendReport(doFetch: Fetch, api: string, key: string, body: ReportBody): Promise<SendResult>;
|
|
46
|
+
/** AI-10: the follow-up question gets this long before the report goes without it. */
|
|
47
|
+
export declare const FOLLOW_UP_TIMEOUT_MS = 4000;
|
|
48
|
+
/**
|
|
49
|
+
* AI-10: asks the backend whether a short note needs one follow-up question.
|
|
50
|
+
* Resolves to null on any error or after the timeout, so sending never
|
|
51
|
+
* waits longer than that.
|
|
52
|
+
*/
|
|
53
|
+
export declare function fetchFollowUp(doFetch: Fetch, api: string, key: string, input: {
|
|
54
|
+
type: string;
|
|
55
|
+
message: string;
|
|
56
|
+
pagePath: string;
|
|
57
|
+
}, timeoutMs?: number): Promise<string | null>;
|
|
58
|
+
/**
|
|
59
|
+
* AI-17: the watched error came back on a page marked Fixed; AI-20
|
|
60
|
+
* (`'sighting'`): an open report's error showed up. Fire and forget; only
|
|
61
|
+
* the report id and the signature are sent (P-14, P-17). `consent: true`
|
|
62
|
+
* only on sites that wait for consent, once the visitor agreed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function sendFixCheck(doFetch: Fetch, api: string, key: string, hit: {
|
|
65
|
+
checkId: string;
|
|
66
|
+
signature: string;
|
|
67
|
+
basket?: PinaxBasket;
|
|
68
|
+
consent?: boolean;
|
|
69
|
+
}, kind?: 'fix-check' | 'sighting'): void;
|
|
70
|
+
/** Where the page-view count goes: the CDN, or the API when there's none. */
|
|
71
|
+
export declare function countUrl(api: string, key: string, cdn: string | null): string;
|
|
72
|
+
/**
|
|
73
|
+
* B-16 on sites that wait for consent: the settings request isn't counted
|
|
74
|
+
* there, so once the visitor agrees, this page load is counted with one
|
|
75
|
+
* empty POST (no body, no cookies). Fire and forget.
|
|
76
|
+
*/
|
|
77
|
+
export declare function sendCount(doFetch: Fetch, api: string, key: string, cdn: string | null): void;
|
|
78
|
+
/**
|
|
79
|
+
* Known issues: "Me too" on a public report, with an email to hear when
|
|
80
|
+
* it's fixed. Sending the email after a Me too repeats the call; the API
|
|
81
|
+
* counts each visitor once and keeps the email.
|
|
82
|
+
*/
|
|
83
|
+
export declare function sendMeToo(doFetch: Fetch, api: string, key: string, id: string, email?: string): Promise<boolean>;
|