Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FIRST_PARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Four presets in `proxy-configs.ts` cover all proxy-enabled scripts:
| `PRIVACY_HEATMAP` | ip, language, hardware | GA, Clarity, Hotjar |
| `PRIVACY_IP_ONLY` | ip only | PostHog, Plausible, Umami, Rybbit, Databuddy, Ahrefs, Fathom, CF Web Analytics, Vercel, Matomo, Carbon Ads, Lemon Squeezy, Intercom, Gravatar, YouTube, Vimeo, Calendly |

Note: GTM, Segment, Crisp, Mixpanel, Bing UET, SpeedCurve, and Pulse have no proxy capability, so no privacy transforms are applied.
Note: GTM, Segment, Crisp, Mixpanel, Bing UET, SpeedCurve, Pulse, and Statable have no proxy capability, so no privacy transforms are applied.

## Script Support

Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/1.guides/2.first-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ These integrations serve their main loader from your domain, but some runtime re
| [Google Tag Manager](/scripts/google-tag-manager) | GTM's core function is loading other scripts at runtime. Those runtime scripts bypass build-time rewriting. |
| [Fathom](/scripts/fathom-analytics) | Fathom's bot detection rejects beacons from the server's IP, so Nuxt Scripts bundles the SDK but leaves beacons direct. |
| [Pulse](/scripts/pulse-analytics) | Pulse identifies visitors from the connecting IP, so proxied beacons would merge everyone into one visitor. Nuxt Scripts bundles the tracker but leaves beacons direct. |
| [Statable](/scripts/statable-analytics) | Statable identifies visitors from the connecting IP and user agent, so proxied beacons would merge everyone into one visitor. Nuxt Scripts bundles the tracker but leaves beacons direct. |
| [Segment](/scripts/segment) | SDK constructs API URLs dynamically, bypassing request interception. |
| [Crisp](/scripts/crisp) | SDK loads secondary scripts and CSS at runtime from `client.crisp.chat`. |
| [Mixpanel](/scripts/mixpanel-analytics) | No proxy integration yet. |
Expand Down
88 changes: 88 additions & 0 deletions docs/content/scripts/statable-analytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: Statable Analytics
description: Load the Statable tracker and record custom events.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/registry/statable-analytics.ts
size: xs
---

[Statable](https://statable.com/) is privacy-first web analytics by Key Arg B.V., a Dutch company. The tracker sets no cookies and stores nothing on the visitor's device; the one thing it reads from `localStorage` is an opt-out flag. A visitor is counted on the server from a keyed one-way hash of the site, the address, the user agent and the date, and the raw inputs are not stored. Do Not Track and Global Privacy Control are honoured without configuration. The [script reference](https://statable.com/docs/developers/tracking-script/) lists every attribute this composable maps.

Check warning on line 11 in docs/content/scripts/statable-analytics.md

View workflow job for this annotation

GitHub Actions / lint

"It's not X — it's Y" is a common AI writing pattern. Rewrite to state the point directly

::script-stats
::

::script-docs
::

## Proxying is not supported

Statable derives visitor identity on its server from the connecting IP address and user agent. Beacons routed through your Nuxt server would all arrive from one address, so every visitor on the same browser would collapse into a single identity.

Nuxt Scripts therefore bundles the tracker and serves it from your origin, while its beacons go straight to the Statable API. The composable pins the site id and the API endpoint on the script tag, because the tracker would otherwise read both from the URL it was served from. There is no identifier in the browser for a first-party proxy to shield, so nothing is given up by leaving the beacons direct.

## Serving through your own domain

If you already proxy Statable through your own domain, `host` moves both the script and its beacons there. `trackingApi` overrides the endpoint on its own.

```ts
useScriptStatableAnalytics({
siteId: 'YOUR_SITE_ID',
host: 'https://stats.example.com',
})
```

## Custom events

The tracker exposes one function, `t(name, props)`. Use the composable's `proxy` object for it: a call made before the script has loaded is held and replayed once the tracker is in. Property values can be strings, numbers or booleans.

Check warning on line 38 in docs/content/scripts/statable-analytics.md

View workflow job for this annotation

GitHub Actions / lint

Inline code `t(name, props)` should have `{lang="ts"}` tag

::code-group

```ts [Proxy]
const { proxy } = useScriptStatableAnalytics()
function trackSignup() {
proxy.t('Sign Up', { plan: 'pro' })
}
```

```ts [onLoaded]
const { onLoaded } = useScriptStatableAnalytics()
onLoaded(({ t }) => {
t('Purchase', { plan: 'annual', amount: 99 })
})
```

::

Outbound link clicks, file downloads and elements with a `data-statable-event` attribute fire on click or submit without any code. Each needs its site module enabled under Site settings → Tracking Code. Pageviews are always on; the rest can be switched off, and the Nano preset ships pageviews and SPA navigation only. See the [JavaScript API](https://statable.com/docs/developers/javascript-api/).

## Sticky properties

`props` attaches custom properties to every event from the page load, which is handy for a cohort, an environment or an experiment tag. They render as `data-statable-*` attributes on the script tag.

```ts
useScriptStatableAnalytics({
siteId: 'YOUR_SITE_ID',
props: { env: 'production', cohort: 'beta' },
})
```

## Page views in a single-page app

The tracker hooks `pushState` and `replaceState` itself, so client-side navigation in Nuxt is counted as a page view without extra configuration. It also sends engagement time and scroll depth when the visitor leaves the page, if the site enables the engagement module.

::script-types
::

## Example

The default trigger waits until Nuxt is ready:

```vue [app.vue]
<script setup lang="ts">
useScriptStatableAnalytics({
siteId: 'YOUR_SITE_ID',
})
</script>
```
1 change: 1 addition & 0 deletions packages/script/src/registry-logos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const LOGOS = {
posthog: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 128 128"><path fill="#1d4aff" d="M0 .52v32.15l31.79 31.78V32.3L0 .52zm32.3 32.15v32.15l31.78 31.78V64.45L32.3 32.67zM0 64.97v32.15l31.79 31.78V96.75L0 64.97zm64.6-32.3v32.15l31.78 31.78V64.45L64.6 32.67zm31.78 31.78v32.15l31.78 31.78V96.23l-31.78-31.78zm-64.08.52v32.15l31.78 31.78V96.75L32.3 64.97zM64.6 .52v32.15l31.78 31.78V32.3L64.6 .52zm0 64.45v32.15l31.78 31.78V96.75L64.6 64.97z"/></svg>`,
fathomAnalytics: `<svg width="32" height="32" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><circle cx="512" cy="512" r="512" style="fill:#9187ff"/><path d="M558.62 256c-36.31.16-78.78 10-129.45 28.4-170.71 62.15-206.9 139.74-144.79 310.45s139.73 206.89 310.45 144.76S801.74 599.88 739.6 429.16c-43.69-120-95-173.55-181-173.17zm59.1 140.16h26.73a5.33 5.33 0 0 1 5.16 6.72l-59.26 220.48a5.34 5.34 0 0 1-5.15 4h-26.75a5.33 5.33 0 0 1-5.16-6.72l3.6-13.4 2.63-9.75 53-197.38a5.33 5.33 0 0 1 5.14-3.94zM421.79 413.4h10.75a5.33 5.33 0 0 1 5.33 5.33v18a5.33 5.33 0 0 1-5.33 5.33h-9.13a36.76 36.76 0 0 0-5.51.24 4.7 4.7 0 0 0-2.56 1 4.19 4.19 0 0 0-1 1.66 18.91 18.91 0 0 0-.92 6.72v13.67h19.16a5.33 5.33 0 0 1 5.33 5.33v18a5.34 5.34 0 0 1-5.33 5.33h-19.21v108.71a5.34 5.34 0 0 1-5.34 5.34H387a5.33 5.33 0 0 1-5.33-5.34V448.48a36.74 36.74 0 0 1 3.6-16.64 29.76 29.76 0 0 1 9.73-11.16c7.9-5.48 17.62-7.27 26.82-7.31zm82.14 50c16.37 0 30.27 4.65 40.17 13.27s15.47 21.21 15.42 35.59v35.91l-16.11 59.92h-10.24a5.33 5.33 0 0 1-5.33-5.34v-4a39.13 39.13 0 0 1-4.76 3.56c-7.14 4.55-16.85 7.51-29.65 7.51a62.65 62.65 0 0 1-28.52-6.18 40.49 40.49 0 0 1-18.84-19.35 46.81 46.81 0 0 1-4-19.54 40.72 40.72 0 0 1 5.23-21.12 36.78 36.78 0 0 1 13.78-13.18c11.09-6.25 24.75-8.45 38.14-10.24 7.3-1 13.14-1.61 17.64-2.2a42 42 0 0 0 9.2-1.88 3.16 3.16 0 0 0 1.39-.86l.24-.48a6.77 6.77 0 0 0 .16-1.84v-.73a17.24 17.24 0 0 0-5.85-13.6c-3.8-3.31-9.77-5.55-18.07-5.57s-14.64 2.26-19 5.59a17.51 17.51 0 0 0-7.21 12.54 5.33 5.33 0 0 1-5.31 4.86h-22.25a5.33 5.33 0 0 1-5.33-5.57 45.64 45.64 0 0 1 17.6-34c10.47-8.34 24.85-13.12 41.49-13.12zm23.92 80.71c-1.92.48-4 1-6.31 1.45-6.47 1.28-14.29 2.41-21.87 3.48a61 61 0 0 0-14.76 3.65c-4.18 1.75-7.1 4-8.68 6.57a12.12 12.12 0 0 0-1.71 6.54v.2a12.93 12.93 0 0 0 1.32 5.87 11.81 11.81 0 0 0 3.76 4.22c3.41 2.45 9.13 4.14 16.85 4.14 11.95 0 19.52-3.5 24.32-8.32s7-11.56 7.08-19.11v-8.65zm0 0" style="fill:#fff"/></svg>`,
pulseAnalytics: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="1.5 -0.7 193.2 193.2"><path fill="#FD740F" d="M33 0a4593 4593 0 0 1 63.5-.6h5.7C117.6-1 132.2 1 144 12l2.6 2.4L149 17l1.5 1.5a54 54 0 0 1 13 40 52 52 0 0 1-13.7 31.2A40 40 0 0 1 137 100l-2.3 1.4c-9 5-18.3 6.7-28.4 6.6H100l.5 72.2v2.2c0 3.4-.3 5.3-2.2 8-2.7 1.9-4.1 2.1-7.4 2.1H47.4c-3.4-.7-4.4-1.7-6.4-4.5-.4-2.8-.5-5.2-.5-8v-69.4c0-3.3 0-5.1 2.4-7.6 2.1-1 2.1-1 4.1-1l1-3c3.4-3 3.4-3 6-3v-2l3.6-2.9q6.4-4.8 12.3-10Q83.2 69.7 97 59h-3.7l-19.7-.3H62.4c-9-.3-16.6-3.9-23.4-9.7-5.7-6-6.3-12-6.2-19.9V18.6l.1-5.4z"/><path fill="#837E80" d="m100 58 .5 117.6v9.9c-.7 3.3-2 4.4-4.5 6.5-2.4.5-2.4.5-5 .5H47.3c-3.4-.7-4.4-1.7-6.4-4.5-.4-2.8-.5-5.2-.5-8v-69.4c0-3.3 0-5.1 2.4-7.6 2.1-1 2.1-1 4.1-1l1-3c3.4-3 3.4-3 6-3v-2l3.6-2.9q6.4-4.8 12.3-10L88 66l2.8-2.3 2.8-2.3 2.4-2c2-1.4 2-1.4 4-1.4"/><path fill="#414141" d="m70 82 .5 32v2.3q.7 37.8.5 75.7l-12.7.3H51l-3.3.1c-3.6-.5-4.6-1.4-6.8-4.4-.4-2.8-.5-5.2-.5-8v-69.4c0-3.3 0-5.1 2.4-7.6 2.1-1 2.1-1 4.1-1l1-3c3.4-3 3.4-3 6-3v-2l4.1-3.3q4.4-3.2 8.4-6.6C69 82 69 82 70 82"/><path fill="#D9BD9C" d="M100 58v49l-14.4.6q-2.2 0-4.6.2h-3.6l-3.6.2c-2.8 0-2.8 0-3.8-1-2.8-20.3-2.8-20.3 1-28.1 2.8-3.4 6.3-5.5 10-7.7q5.4-3.5 10.2-7.9C97.7 58 97.7 58 100 58"/><path fill="#8D6653" d="M70 82v25l-29 1v-3c3.3-3 3.3-3 6-3l1-3c3.4-3 3.4-3 6-3v-2l4.1-3.3q4.4-3.2 8.4-6.6C69 82 69 82 70 82"/></svg>`,
statableAnalytics: `<svg width="32" height="32" viewBox="0 0 37 37" xmlns="http://www.w3.org/2000/svg"><rect width="37" height="37" rx="7" fill="#0061FE"/><path fill="#fff" d="M12.2927 25.5017C15.736 28.719 19.7446 31.3835 22.7253 31.9867C17.8945 35.1035 11.3676 34.7013 6.84512 30.8304L12.2927 25.5017Z"/><path fill="#fff" d="M18.8709 19.1172C24.1642 24.4962 25.963 29.2218 24.8837 30.3277C23.7531 31.3834 18.9222 29.6741 13.4233 24.4459L18.8709 19.1172Z"/><path fill="#fff" d="M25.3977 12.7328C29.3548 17.1064 29.766 23.4908 26.5797 28.2666C25.963 25.3508 23.2391 21.3794 19.9501 18.0615L25.3977 12.7328Z"/><path fill="#fff" d="M11.162 7.85642C11.7787 10.7722 14.5025 14.7436 17.7916 18.0615L12.344 23.3902C8.38689 19.0166 7.97571 12.6322 11.162 7.85642Z"/><path fill="#fff" d="M12.858 5.79526C13.9886 4.73957 18.8195 6.44878 24.3184 11.677L18.8709 17.0058C13.5262 11.6268 11.7787 6.90124 12.858 5.79526Z"/><path fill="#fff" d="M14.9651 4.13641C19.8473 1.0196 26.374 1.4217 30.8451 5.29255L25.3977 10.6213C21.9544 7.40399 17.9458 4.73967 14.9651 4.13641Z"/></svg>`,
matomoAnalytics: `<svg xmlns="http://www.w3.org/2000/svg" width="56.5" height="32" viewBox="0 0 256 145"><defs><path id="logosMatomoIcon0" d="m105.426 70.887l.035-.021l-.663-1.01c-.1-.153-.2-.313-.303-.46L58.935 0L0 43.91l43.078 66.305c.185.281.36.566.55.847l.229.35l.025-.016c6.676 9.471 17.678 15.673 30.144 15.673c20.373 0 36.889-16.513 36.889-36.89c0-7.083-2.029-13.675-5.489-19.292"/><path id="logosMatomoIcon1" fill="#000" d="M64.549 19.33c0-20.374-16.517-36.89-36.89-36.89S-9.23-1.044-9.23 19.33a36.686 36.686 0 0 0 6.08 20.263c-.003 0-.003 0-.003-.003l-.019.003L-31.179 0h-.04c-6.499-10.524-18.101-17.56-31.376-17.56c-13.275 0-24.877 7.036-31.376 17.56h-.037l-44.61 69.525c6.633-9.8 17.848-16.235 30.57-16.235c13.39 0 25.077 7.158 31.54 17.832h.047l29.15 40.921h.047c6.718 9.1 17.486 15.026 29.663 15.026c12.181 0 22.95-5.927 29.666-15.026h.05l.297-.46a36.949 36.949 0 0 0 2.116-3.312l43.675-68.256v.003A36.747 36.747 0 0 0 64.55 19.33M2.372 46.141c.213.204.435.397.654.594c-.22-.197-.438-.39-.654-.594m3.28 2.745c.243.181.48.369.728.544c-.247-.175-.485-.363-.729-.544m8.096 4.598c.306.128.628.228.94.347c-.312-.12-.634-.22-.94-.347m8.28 2.263c.428.065.853.143 1.287.197c-.434-.054-.856-.132-1.287-.197m9.93.203c.438-.05.869-.135 1.303-.197c-.434.062-.862.147-1.303.197m8.368-2.01c.393-.144.797-.275 1.184-.434c-.387.159-.788.29-1.185.434m8.368-4.326c.313-.216.61-.456.916-.684c-.307.228-.603.465-.916.684m6.258-5.526c.259-.285.528-.563.778-.857c-.25.294-.519.572-.778.857"/><path id="logosMatomoIcon2" fill="#95C748" d="m250.511 88.448l.035-.022l-.663-1.01c-.1-.153-.2-.312-.303-.46L204.02 17.56l-58.935 43.91l43.078 66.305c.185.281.36.566.55.847l.229.35l.025-.016c6.676 9.471 17.678 15.673 30.144 15.673c20.373 0 36.889-16.513 36.889-36.89c0-7.083-2.029-13.675-5.489-19.291"/><filter id="logosMatomoIcon3" width="106.9%" height="109.7%" x="-3.4%" y="-3.5%" filterUnits="objectBoundingBox"><feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="2"/><feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/></filter></defs><use href="#logosMatomoIcon2"/><path fill="#35BFC0" d="M73.779 107.74c0-20.374-16.516-36.89-36.89-36.89C16.516 70.85 0 87.366 0 107.74c0 20.376 16.516 36.892 36.89 36.892c20.373 0 36.889-16.52 36.889-36.893"/><path fill="#3253A0" d="M172.744 0c20.373 0 36.89 16.516 36.89 36.89a36.747 36.747 0 0 1-6.346 20.688v-.003l-43.675 68.256a36.949 36.949 0 0 1-2.116 3.313l-.297.46h-.05c-6.717 9.098-17.485 15.025-29.666 15.025c-12.177 0-22.945-5.927-29.663-15.026h-.046l-29.15-40.921h-.047C62.114 78.008 50.427 70.85 37.036 70.85c-12.721 0-23.936 6.436-30.569 16.235l44.61-69.525h.037C57.613 7.036 69.215 0 82.49 0c13.275 0 24.877 7.036 31.376 17.56h.04l28.006 39.593l.02-.003c0 .003 0 .003.002.003a36.684 36.684 0 0 1-6.08-20.264C135.855 16.516 152.372 0 172.745 0"/><use href="#logosMatomoIcon2"/><g transform="translate(145.085 17.56)"><mask id="logosMatomoIcon4" fill="#fff"><use href="#logosMatomoIcon0"/></mask><g mask="url(#logosMatomoIcon4)"><use filter="url(#logosMatomoIcon3)" href="#logosMatomoIcon1"/></g></g><path fill="#F38334" d="M209.487 36.89c0-20.374-16.516-36.89-36.89-36.89c-20.373 0-36.89 16.516-36.89 36.89c0 20.373 16.517 36.889 36.89 36.889c20.374 0 36.89-16.516 36.89-36.89"/><path fill="#3152A0" d="M172.597 73.782c-12.887 0-24.214-6.617-30.81-16.629h-.021L113.759 17.56h-.04C107.22 7.04 95.618.003 82.343.003C69.068.003 57.466 7.04 50.967 17.56h-.037L6.323 87.085c6.63-9.796 17.848-16.232 30.566-16.232c13.39 0 25.08 7.155 31.545 17.829h.047l29.15 40.921h.044c6.72 9.096 17.488 15.029 29.665 15.029c12.178 0 22.946-5.93 29.663-15.029h.05l.297-.462a37.588 37.588 0 0 0 2.12-3.307l43.672-68.256c-6.636 9.774-17.839 16.204-30.545 16.204"/></svg>`,
rybbitAnalytics: {
light: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 263.33 173.53" width="37.24" height="32"><g><polygon fill="#0a3a3a" points="181.28 171.2 227.21 123.96 261.15 171.2 181.28 171.2"/><path fill="#0a3a3a" d="M261.15,89.05L206.64,2.33l-33.22,17.75-34.61-7.4c2.88,5.56,4.56,12.11,4.56,19.15,0,20.03-13.46,36.26-30.06,36.26-13.66,0-25.17-11-28.83-26.06l-39.92,71.46L2.18,94.19l22.66,77.01h55.81l22.28-54.01v54.01h64.66l-49.95-82.15h143.51Z"/></g><ellipse fill="#0a3a3a" cx="105.94" cy="28.62" rx="12.9" ry="18.88"/></svg>`,
Expand Down
39 changes: 39 additions & 0 deletions packages/script/src/registry-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,18 @@
"code": "type _AllForwardedKeysListed = AssertNever<Exclude<ForwardedKey, typeof LUX_USER_CONFIG_KEYS[number]>>"
}
],
"statable-analytics": [
{
"name": "StatableAnalyticsOptions",
"kind": "const",
"code": "export const StatableAnalyticsOptions = object({\n /**\n * The numeric Site ID from Site settings, Tracking Code, in Statable.\n * @see https://statable.com/docs/developers/tracking-script/\n */\n // minLength(1) rather than a bare string(): envDefaults resolves an unset siteId to '',\n // which would validate happily and render data-id=\"\", silently tracking nothing.\n // Failing loudly in dev is the point.\n siteId: pipe(string(), minLength(1)),\n /**\n * Origin the tracker script is served from. Only needed when you proxy\n * Statable through your own domain.\n * @default 'https://statable.com'\n */\n host: optional(string()),\n /**\n * Endpoint the tracker posts events to. Defaults to `/api/event` on `host`.\n * @see https://statable.com/docs/developers/tracking-script/#data-tracking-api-optional\n */\n trackingApi: optional(string()),\n /**\n * Custom properties attached to every event from the page load, rendered\n * as `data-statable-*` attributes on the script tag.\n * @see https://statable.com/docs/developers/tracking-script/#data-statable-optional\n */\n props: optional(record(string(), string())),\n})"
},
{
"name": "StatableAnalyticsApi",
"kind": "interface",
"code": "export interface StatableAnalyticsApi {\n /**\n * Records a custom event.\n * @param name Event name. Title Case by convention.\n * @param props Optional properties, merged into the event as `p.*`.\n * Strings, numbers and booleans index best in the dashboard.\n * @see https://statable.com/docs/developers/javascript-api/\n */\n t: (name: string, props?: Record<string, any>) => void\n}"
}
],
"stripe": [
{
"name": "StripeOptions",
Expand Down Expand Up @@ -3085,6 +3097,33 @@
"defaultValue": "'g'"
}
],
"StatableAnalyticsOptions": [
{
"name": "siteId",
"type": "string",
"required": true,
"description": "The numeric Site ID from Site settings, Tracking Code, in Statable."
},
{
"name": "host",
"type": "string",
"required": false,
"description": "Origin the tracker script is served from. Only needed when you proxy Statable through your own domain.",
"defaultValue": "'https://statable.com'"
},
{
"name": "trackingApi",
"type": "string",
"required": false,
"description": "Endpoint the tracker posts events to. Defaults to `/api/event` on `host`."
},
{
"name": "props",
"type": "Record<string, string>",
"required": false,
"description": "Custom properties attached to every event from the page load, rendered as `data-statable-*` attributes on the script tag."
}
],
"ScriptGoogleMapsProps": [
{
"name": "trigger",
Expand Down
Loading
Loading