Skip to content

Commit b9f56fb

Browse files
committed
feat: add Vercel Analytics support
- New useScriptVercelAnalytics composable with track/pageview API - Queue init matching @vercel/analytics (window.va/window.vaq) - beforeSend callback for privacy filtering - FirstParty proxy support (rewrites va.vercel-scripts.com) - DSN option for non-Vercel deployments - Documentation, playground page, and E2E test
1 parent 0485d59 commit b9f56fb

File tree

8 files changed

+468
-1
lines changed

8 files changed

+468
-1
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
---
2+
title: Vercel Analytics
3+
description: Use Vercel Analytics in your Nuxt app.
4+
links:
5+
- label: Source
6+
icon: i-simple-icons-github
7+
to: https://github.com/nuxt/scripts/blob/main/src/runtime/registry/vercel-analytics.ts
8+
size: xs
9+
- label: Vercel Analytics
10+
icon: i-simple-icons-vercel
11+
to: https://vercel.com/docs/analytics
12+
size: xs
13+
---
14+
15+
[Vercel Analytics](https://vercel.com/docs/analytics) provides lightweight, privacy-friendly web analytics for your Nuxt app. It tracks page views and custom events with zero configuration when deployed on Vercel.
16+
17+
The simplest way to load Vercel Analytics globally in your Nuxt App is to use Nuxt config. Alternatively you can directly
18+
use the [useScriptVercelAnalytics](#usescriptvercelanalytics) composable.
19+
20+
## Loading Globally
21+
22+
If you'd like to avoid loading the analytics in development, you can use the [Environment overrides](https://nuxt.com/docs/getting-started/configuration#environment-overrides) in your Nuxt config.
23+
24+
::code-group
25+
26+
```ts [Always enabled]
27+
export default defineNuxtConfig({
28+
scripts: {
29+
registry: {
30+
vercelAnalytics: true,
31+
}
32+
}
33+
})
34+
```
35+
36+
```ts [Production only]
37+
export default defineNuxtConfig({
38+
$production: {
39+
scripts: {
40+
registry: {
41+
vercelAnalytics: true,
42+
}
43+
}
44+
}
45+
})
46+
```
47+
48+
```ts [Non-Vercel deployment]
49+
export default defineNuxtConfig({
50+
scripts: {
51+
registry: {
52+
vercelAnalytics: {
53+
dsn: 'YOUR_DSN',
54+
}
55+
}
56+
}
57+
})
58+
```
59+
60+
::
61+
62+
### First-Party Mode
63+
64+
When `scripts.firstParty` is enabled, the analytics script is bundled locally and data collection requests are proxied through your server. This prevents ad blockers from blocking analytics and removes sensitive data from third-party requests.
65+
66+
```ts
67+
export default defineNuxtConfig({
68+
scripts: {
69+
firstParty: true,
70+
registry: {
71+
vercelAnalytics: true,
72+
}
73+
}
74+
})
75+
```
76+
77+
## useScriptVercelAnalytics
78+
79+
The `useScriptVercelAnalytics` composable lets you have fine-grain control over when and how Vercel Analytics is loaded on your site.
80+
81+
```ts
82+
function useScriptVercelAnalytics<T extends VercelAnalyticsApi>(_options?: VercelAnalyticsInput & { beforeSend?: BeforeSend }) {}
83+
```
84+
85+
Please follow the [Registry Scripts](/docs/guides/registry-scripts) guide to learn more about advanced usage.
86+
87+
The composable comes with the following defaults:
88+
- **Trigger: Client** Script will load when the Nuxt is hydrating to keep web vital metrics accurate.
89+
90+
### VercelAnalyticsInput
91+
92+
```ts
93+
export const VercelAnalyticsOptions = object({
94+
/**
95+
* The DSN of the project to send events to.
96+
* Only required when self-hosting or deploying outside of Vercel.
97+
*/
98+
dsn: optional(string()),
99+
/**
100+
* Whether to disable automatic page view tracking on route changes.
101+
* Set to true if you want to manually call pageview().
102+
*/
103+
disableAutoTrack: optional(boolean()),
104+
/**
105+
* The mode to use for the analytics script.
106+
* - `auto` - Automatically detect the environment (default)
107+
* - `production` - Always use production script
108+
* - `development` - Always use development script (logs to console)
109+
*/
110+
mode: optional(union([literal('auto'), literal('development'), literal('production')])),
111+
/**
112+
* Whether to enable debug logging in development.
113+
* @default true
114+
*/
115+
debug: optional(boolean()),
116+
})
117+
```
118+
119+
### VercelAnalyticsApi
120+
121+
```ts
122+
export interface VercelAnalyticsApi {
123+
va: (event: string, properties?: unknown) => void
124+
track: (name: string, properties?: Record<string, AllowedPropertyValues>) => void
125+
pageview: (options?: { route?: string | null, path?: string }) => void
126+
}
127+
```
128+
129+
### BeforeSend
130+
131+
You can pass a `beforeSend` callback to modify or filter events before they're sent. This is useful for stripping sensitive data from URLs.
132+
133+
```ts
134+
const { proxy } = useScriptVercelAnalytics({
135+
beforeSend(event) {
136+
// Strip query params from URLs
137+
const url = new URL(event.url)
138+
url.search = ''
139+
return { ...event, url: url.toString() }
140+
},
141+
})
142+
```
143+
144+
Returning `null` from `beforeSend` will prevent the event from being sent.
145+
146+
## Example
147+
148+
Loading Vercel Analytics through `app.vue` when Nuxt is ready.
149+
150+
```vue [app.vue]
151+
<script setup lang="ts">
152+
const { proxy } = useScriptVercelAnalytics({
153+
scriptOptions: {
154+
trigger: 'onNuxtReady',
155+
},
156+
})
157+
158+
// Track a custom event
159+
proxy.track('signup', { plan: 'pro' })
160+
</script>
161+
```
162+
163+
### Manual Tracking
164+
165+
If you want full control over what gets tracked, disable automatic tracking and call `track` / `pageview` manually.
166+
167+
```vue [app.vue]
168+
<script setup lang="ts">
169+
const { proxy } = useScriptVercelAnalytics({
170+
disableAutoTrack: true,
171+
})
172+
173+
// Track custom event
174+
proxy.track('purchase', { product: 'widget', price: 9.99 })
175+
176+
// Manual pageview
177+
proxy.pageview({ path: '/custom-page' })
178+
</script>
179+
```
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script lang="ts" setup>
2+
import { ref, useHead } from '#imports'
3+
4+
useHead({
5+
title: 'Vercel Analytics',
6+
})
7+
8+
const { proxy, status } = useScriptVercelAnalytics({
9+
scriptOptions: {
10+
trigger: 'onNuxtReady',
11+
},
12+
})
13+
14+
const eventTracked = ref(false)
15+
16+
function trackEvent() {
17+
proxy.track('button_click', {
18+
button: 'demo',
19+
page: '/third-parties/vercel-analytics',
20+
})
21+
eventTracked.value = true
22+
}
23+
</script>
24+
25+
<template>
26+
<div>
27+
<ClientOnly>
28+
<div>
29+
status: {{ status }}
30+
</div>
31+
<div v-if="eventTracked">
32+
Event tracked!
33+
</div>
34+
</ClientOnly>
35+
<button @click="trackEvent">
36+
Track Event
37+
</button>
38+
</div>
39+
</template>

src/proxy-configs.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,28 @@ function buildProxyConfig(collectPrefix: string) {
172172
[`${collectPrefix}/hotjar-insights/**`]: { proxy: 'https://insights.hotjar.com/**' },
173173
},
174174
},
175+
176+
gravatar: {
177+
rewrite: [
178+
// Hovercards JS and related scripts
179+
{ from: 'secure.gravatar.com', to: `${collectPrefix}/gravatar` },
180+
// Avatar images (used by hovercards internally)
181+
{ from: 'gravatar.com/avatar', to: `${collectPrefix}/gravatar-avatar` },
182+
],
183+
routes: {
184+
[`${collectPrefix}/gravatar/**`]: { proxy: 'https://secure.gravatar.com/**' },
185+
[`${collectPrefix}/gravatar-avatar/**`]: { proxy: 'https://gravatar.com/avatar/**' },
186+
},
187+
},
188+
189+
vercelAnalytics: {
190+
rewrite: [
191+
{ from: 'va.vercel-scripts.com', to: `${collectPrefix}/vercel` },
192+
],
193+
routes: {
194+
[`${collectPrefix}/vercel/**`]: { proxy: 'https://va.vercel-scripts.com/**' },
195+
},
196+
},
175197
} satisfies Record<string, ProxyConfig>
176198
}
177199

src/registry.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ export async function registry(resolve?: (path: string, opts?: ResolvePathOption
4040
from: await resolve('./runtime/registry/cloudflare-web-analytics'),
4141
},
4242
},
43+
{
44+
label: 'Vercel Analytics',
45+
src: 'https://va.vercel-scripts.com/v1/script.js',
46+
proxy: 'vercelAnalytics',
47+
category: 'analytics',
48+
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><path d="M256 48L496 464H16z" fill="currentColor"/></svg>`,
49+
import: {
50+
name: 'useScriptVercelAnalytics',
51+
from: await resolve('./runtime/registry/vercel-analytics'),
52+
},
53+
},
4354
{
4455
label: 'PostHog',
4556
src: false,
@@ -405,5 +416,16 @@ export async function registry(resolve?: (path: string, opts?: ResolvePathOption
405416
from: await resolve('./runtime/registry/umami-analytics'),
406417
},
407418
},
419+
{
420+
label: 'Gravatar',
421+
proxy: 'gravatar',
422+
src: 'https://secure.gravatar.com/js/gprofiles.js',
423+
category: 'utility',
424+
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><circle cx="128" cy="128" r="128" fill="#1d4fc4"/><path d="M128 28c-55.2 0-100 44.8-100 100s44.8 100 100 100 100-44.8 100-100S183.2 28 128 28zm0 180c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z" fill="#fff"/></svg>`,
425+
import: {
426+
name: 'useScriptGravatar',
427+
from: await resolve('./runtime/registry/gravatar'),
428+
},
429+
},
408430
]
409431
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useRegistryScript } from '../utils'
2+
import { boolean, literal, object, optional, string, union } from '#nuxt-scripts-validator'
3+
import type { RegistryScriptInput } from '#nuxt-scripts/types'
4+
5+
export type AllowedPropertyValues = string | number | boolean | null | undefined
6+
7+
export interface BeforeSendEvent {
8+
type: 'pageview' | 'event'
9+
url: string
10+
}
11+
12+
export type BeforeSend = (event: BeforeSendEvent) => BeforeSendEvent | null
13+
14+
export type VercelAnalyticsMode = 'auto' | 'development' | 'production'
15+
16+
export const VercelAnalyticsOptions = object({
17+
/**
18+
* The DSN of the project to send events to.
19+
* Only required when self-hosting or deploying outside of Vercel.
20+
*/
21+
dsn: optional(string()),
22+
/**
23+
* Whether to disable automatic page view tracking on route changes.
24+
* Set to true if you want to manually call pageview().
25+
*/
26+
disableAutoTrack: optional(boolean()),
27+
/**
28+
* The mode to use for the analytics script.
29+
* - `auto` - Automatically detect the environment (default)
30+
* - `production` - Always use production script
31+
* - `development` - Always use development script (logs to console)
32+
*/
33+
mode: optional(union([literal('auto'), literal('development'), literal('production')])),
34+
/**
35+
* Whether to enable debug logging in development.
36+
* @default true
37+
*/
38+
debug: optional(boolean()),
39+
})
40+
41+
export type VercelAnalyticsInput = RegistryScriptInput<typeof VercelAnalyticsOptions, false, false, false>
42+
43+
export interface VercelAnalyticsApi {
44+
va: (event: string, properties?: unknown) => void
45+
track: (name: string, properties?: Record<string, AllowedPropertyValues>) => void
46+
pageview: (options?: { route?: string | null, path?: string }) => void
47+
}
48+
49+
declare global {
50+
interface Window {
51+
va?: (event: string, properties?: unknown) => void
52+
vaq?: [string, unknown?][]
53+
vam?: VercelAnalyticsMode
54+
}
55+
}
56+
57+
export function useScriptVercelAnalytics<T extends VercelAnalyticsApi>(_options?: VercelAnalyticsInput & { beforeSend?: BeforeSend }) {
58+
return useRegistryScript<T, typeof VercelAnalyticsOptions>('vercelAnalytics', (options) => {
59+
const scriptInput: { src: string, defer: boolean, 'data-dsn'?: string, 'data-disable-auto-track'?: string, 'data-debug'?: string } = {
60+
src: 'https://va.vercel-scripts.com/v1/script.js',
61+
defer: true,
62+
}
63+
64+
if (options?.dsn)
65+
scriptInput['data-dsn'] = options.dsn
66+
if (options?.disableAutoTrack)
67+
scriptInput['data-disable-auto-track'] = '1'
68+
if (options?.debug === false)
69+
scriptInput['data-debug'] = 'false'
70+
71+
return {
72+
scriptInput,
73+
schema: import.meta.dev ? VercelAnalyticsOptions : undefined,
74+
scriptOptions: {
75+
use: () => ({
76+
va: window.va as VercelAnalyticsApi['va'],
77+
track(name: string, properties?: Record<string, AllowedPropertyValues>) {
78+
if (!properties) {
79+
window.va?.('event', { name })
80+
return
81+
}
82+
// Strip non-primitive values (objects) in production
83+
const cleaned: Record<string, AllowedPropertyValues> = {}
84+
for (const [key, value] of Object.entries(properties)) {
85+
if (typeof value !== 'object' || value === null)
86+
cleaned[key] = value
87+
}
88+
window.va?.('event', { name, data: cleaned })
89+
},
90+
pageview(opts?: { route?: string | null, path?: string }) {
91+
window.va?.('pageview', opts)
92+
},
93+
}),
94+
},
95+
clientInit: import.meta.server
96+
? undefined
97+
: () => {
98+
if (window.va) return
99+
// Set up the queue exactly as @vercel/analytics does
100+
window.va = function (...params: [string, unknown?]) {
101+
; (window.vaq = window.vaq || []).push(params)
102+
}
103+
// Set mode
104+
if (options?.mode && options.mode !== 'auto') {
105+
window.vam = options.mode
106+
}
107+
// Register beforeSend middleware
108+
if (_options?.beforeSend) {
109+
window.va('beforeSend', _options.beforeSend)
110+
}
111+
},
112+
}
113+
}, _options)
114+
}

0 commit comments

Comments
 (0)