Skip to content
33 changes: 21 additions & 12 deletions docs/content/docs/1.guides/2.first-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,26 @@ First-party mode supports the following scripts:

| Script | Endpoints Proxied |
|--------|-------------------|
| [Google Analytics](/scripts/google-analytics) | `google-analytics.com`, `analytics.google.com`, `stats.g.doubleclick.net`, `pagead2.googlesyndication.com` |
| [Google Tag Manager](/scripts/google-tag-manager) | `www.googletagmanager.com` |
| [Meta Pixel](/scripts/meta-pixel) | `connect.facebook.net`, `www.facebook.com/tr`, `pixel.facebook.com` |
| [TikTok Pixel](/scripts/tiktok-pixel) | `analytics.tiktok.com` |
| [Segment](/scripts/segment) | `api.segment.io`, `cdn.segment.com` |
| [PostHog](/scripts/posthog) | `us.i.posthog.com`, `eu.i.posthog.com`, `us-assets.i.posthog.com`, `eu-assets.i.posthog.com` |
| [Microsoft Clarity](/scripts/clarity) | `www.clarity.ms`, `scripts.clarity.ms`, `d.clarity.ms`, `e.clarity.ms` |
| [Hotjar](/scripts/hotjar) | `static.hotjar.com`, `script.hotjar.com`, `vars.hotjar.com`, `in.hotjar.com` |
| [X/Twitter Pixel](/scripts/x-pixel) | `analytics.twitter.com`, `t.co` |
| [Snapchat Pixel](/scripts/snapchat-pixel) | `tr.snapchat.com` |
| [Reddit Pixel](/scripts/reddit-pixel) | `alb.reddit.com` |
| [Google Analytics](/scripts/analytics/google-analytics) | `google-analytics.com`, `analytics.google.com`, `stats.g.doubleclick.net`, `pagead2.googlesyndication.com` |
| [Google Tag Manager](/scripts/tracking/google-tag-manager) | `www.googletagmanager.com` |
| [Meta Pixel](/scripts/tracking/meta-pixel) | `connect.facebook.net`, `www.facebook.com/tr`, `pixel.facebook.com` |
| [TikTok Pixel](/scripts/tracking/tiktok-pixel) | `analytics.tiktok.com` |
| [Segment](/scripts/tracking/segment) | `api.segment.io`, `cdn.segment.com` |
| [PostHog](/scripts/analytics/posthog) | `us.i.posthog.com`, `eu.i.posthog.com`, `us-assets.i.posthog.com`, `eu-assets.i.posthog.com` |
| [Microsoft Clarity](/scripts/marketing/clarity) | `www.clarity.ms`, `scripts.clarity.ms`, `d.clarity.ms`, `e.clarity.ms` |
| [Hotjar](/scripts/marketing/hotjar) | `static.hotjar.com`, `script.hotjar.com`, `vars.hotjar.com`, `in.hotjar.com` |
| [X/Twitter Pixel](/scripts/tracking/x-pixel) | `analytics.twitter.com`, `t.co` |
| [Snapchat Pixel](/scripts/tracking/snapchat-pixel) | `tr.snapchat.com` |
| [Reddit Pixel](/scripts/tracking/reddit-pixel) | `alb.reddit.com`, `pixel-config.reddit.com` |
| [Plausible Analytics](/scripts/analytics/plausible-analytics) | `plausible.io` |
| [Cloudflare Web Analytics](/scripts/analytics/cloudflare-web-analytics) | `static.cloudflareinsights.com`, `cloudflareinsights.com` |
| [Rybbit Analytics](/scripts/analytics/rybbit-analytics) | `app.rybbit.io` |
| [Umami Analytics](/scripts/analytics/umami-analytics) | `cloud.umami.is` |
| [Databuddy Analytics](/scripts/analytics/databuddy-analytics) | `cdn.databuddy.cc`, `basket.databuddy.cc` |
| [Fathom Analytics](/scripts/analytics/fathom-analytics) | `cdn.usefathom.com` |
| [Vercel Analytics](/scripts/analytics/vercel-analytics) | `va.vercel-scripts.com` |
| [Intercom](/scripts/support/intercom) | `widget.intercom.io`, `api-iam.intercom.io` |
| [Crisp](/scripts/support/crisp) | `client.crisp.chat` |

## Requirements

Expand Down Expand Up @@ -508,7 +517,7 @@ First-party mode is for client-side scripts. For server-side tracking (Measureme

### Which scripts can I add first-party support to?

Currently, first-party mode supports the 11 scripts listed in the Supported Scripts section. For other scripts, you can:
Currently, first-party mode supports all scripts listed in the Supported Scripts section. For other scripts, you can:

1. Request support by opening an issue
2. Use the `bundle` option for self-hosting without proxy (deprecated)
Expand Down
120 changes: 120 additions & 0 deletions docs/content/scripts/vercel-analytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---

title: Vercel Analytics
description: Use Vercel Analytics in your Nuxt app.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/scripts/blob/main/src/runtime/registry/vercel-analytics.ts
size: xs

---

[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.

::script-stats
::

::script-docs
::

### Non-Vercel Deployment

When deploying outside of Vercel, provide your DSN explicitly:

```ts
useScriptVercelAnalytics({
dsn: 'YOUR_DSN',
})
```

### First-Party Mode

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.

```ts
export default defineNuxtConfig({
scripts: {
firstParty: true,
registry: {
vercelAnalytics: true,
}
}
})
```

## Defaults

- **Trigger: Client** Script will load when Nuxt is hydrating to keep web vital metrics accurate.

::script-types
::

You can access the `track` and `pageview` methods as a proxy directly or await the `$script` promise to access the object. It's recommended to use the proxy for any void functions.

::code-group

```ts [Proxy]
const { proxy } = useScriptVercelAnalytics()
proxy.track('signup', { plan: 'pro' })
```

```ts [onLoaded]
const { onLoaded } = useScriptVercelAnalytics()
onLoaded(({ track }) => {
track('signup', { plan: 'pro' })
})
```

::

## Example

Loading Vercel Analytics through `app.vue` when Nuxt is ready.

```vue [app.vue]
<script setup lang="ts">
const { proxy } = useScriptVercelAnalytics({
scriptOptions: {
trigger: 'onNuxtReady',
},
})

// Track a custom event
proxy.track('signup', { plan: 'pro' })
</script>
```

### Manual Tracking

If you want full control over what gets tracked, disable automatic tracking and call `track` / `pageview` manually.

```vue [app.vue]
<script setup lang="ts">
const { proxy } = useScriptVercelAnalytics({
disableAutoTrack: true,
})

// Track custom event
proxy.track('purchase', { product: 'widget', price: 9.99 })

// Manual pageview
proxy.pageview({ path: '/custom-page' })
</script>
```

### beforeSend

Use `beforeSend` to filter or modify events before they reach Vercel. Return `null` to cancel an event.

```vue [app.vue]
<script setup lang="ts">
const { proxy } = useScriptVercelAnalytics({
beforeSend(event) {
// Ignore admin pages
if (event.url.includes('/admin')) return null
return event
},
})
</script>
```
83 changes: 83 additions & 0 deletions playground/pages/third-parties/vercel-analytics/nuxt-scripts.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script lang="ts" setup>
import { ref, useHead } from '#imports'

useHead({
title: 'Vercel Analytics',
})

const beforeSendLog = ref<string[]>([])

const { proxy, status } = useScriptVercelAnalytics({
endpoint: '/custom/collect',
beforeSend(event) {
beforeSendLog.value.push(`${event.type}: ${event.url}`)
return event
},
scriptOptions: {
trigger: 'onNuxtReady',
},
})

const eventTracked = ref(false)
const pageviewSent = ref(false)

function trackEvent() {
proxy.track('button_click', {
button: 'demo',
page: '/third-parties/vercel-analytics',
})
eventTracked.value = true
}

function trackNested() {
proxy.track('nested_test', {
valid: 'yes',
nested: { should: 'be stripped' } as any,
})
}

function sendPageview() {
proxy.pageview({ path: '/test-page', route: '/[slug]' })
pageviewSent.value = true
}

function dumpQueue() {
// eslint-disable-next-line no-console
console.log('window.vaq:', JSON.stringify(window.vaq, null, 2))
// eslint-disable-next-line no-console
console.log('window.vam:', window.vam)
}
</script>

<template>
<div class="flex flex-col gap-4 p-4">
<ClientOnly>
<div>status: {{ status }}</div>
<div>mode (window.vam): {{ $window?.vam ?? 'n/a' }}</div>
<div v-if="eventTracked">
Event tracked!
</div>
<div v-if="pageviewSent">
Pageview sent!
</div>
<div v-if="beforeSendLog.length">
beforeSend calls: {{ beforeSendLog }}
</div>
</ClientOnly>

<div class="flex gap-2">
<button @click="trackEvent">
Track Event
</button>
<button @click="trackNested">
Track Nested (stripped in prod)
</button>
<button @click="sendPageview">
Send Pageview
</button>
<button @click="dumpQueue">
Dump Queue (console)
</button>
</div>
</div>
</template>
11 changes: 11 additions & 0 deletions src/proxy-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,17 @@ function buildProxyConfig(collectPrefix: string) {
[`${collectPrefix}/crisp/**`]: { proxy: 'https://client.crisp.chat/**' },
},
},

vercelAnalytics: {
// Vercel Analytics: trusted first-party analytics β€” minimal privacy needed
privacy: { ip: false, userAgent: false, language: false, screen: false, timezone: false, hardware: false },
rewrite: [
{ from: 'va.vercel-scripts.com', to: `${collectPrefix}/vercel` },
],
routes: {
[`${collectPrefix}/vercel/**`]: { proxy: 'https://va.vercel-scripts.com/**' },
},
},
} satisfies Record<string, ProxyConfig>
}

Expand Down
12 changes: 12 additions & 0 deletions src/registry-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,18 @@
"code": "const ScriptYouTubePlayerDefaults = {\n \"cookies\": \"false\",\n \"trigger\": \"'mousedown'\",\n \"thumbnailSize\": \"'hq720'\",\n \"webp\": \"true\",\n \"playerVars\": \"{ autoplay: 0, playsinline: 1 }\",\n \"width\": \"640\",\n \"height\": \"360\",\n \"ratio\": \"'16/9'\",\n \"placeholderObjectFit\": \"'cover'\"\n}"
}
],
"vercel-analytics": [
{
"name": "VercelAnalyticsOptions",
"kind": "const",
"code": "export const VercelAnalyticsOptions = object({\n /**\n * The DSN of the project to send events to.\n * Only required when self-hosting or deploying outside of Vercel.\n */\n dsn: optional(string()),\n /**\n * Whether to disable automatic page view tracking on route changes.\n * Set to true if you want to manually call pageview().\n */\n disableAutoTrack: optional(boolean()),\n /**\n * The mode to use for the analytics script.\n * - `auto` - Automatically detect the environment (default)\n * - `production` - Always use production script\n * - `development` - Always use development script (logs to console)\n */\n mode: optional(union([literal('auto'), literal('development'), literal('production')])),\n /**\n * Whether to enable debug logging.\n * Automatically enabled in development/test environments.\n */\n debug: optional(boolean()),\n /**\n * Custom endpoint for data collection.\n * Useful for self-hosted or proxied setups.\n */\n endpoint: optional(string()),\n})"
},
{
"name": "VercelAnalyticsApi",
"kind": "interface",
"code": "export interface VercelAnalyticsApi {\n va: (event: string, properties?: unknown) => void\n track: (name: string, properties?: Record<string, AllowedPropertyValues>) => void\n pageview: (options?: { route?: string | null, path?: string }) => void\n}"
}
],
"carbon-ads": [
{
"name": "ScriptCarbonAdsProps",
Expand Down
11 changes: 11 additions & 0 deletions src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export async function registry(resolve?: (path: string, opts?: ResolvePathOption
from: await resolve('./runtime/registry/cloudflare-web-analytics'),
},
},
{
label: 'Vercel Analytics',
src: 'https://va.vercel-scripts.com/v1/script.js',
proxy: 'vercelAnalytics',
category: 'analytics',
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>`,
import: {
name: 'useScriptVercelAnalytics',
from: await resolve('./runtime/registry/vercel-analytics'),
},
},
{
label: 'PostHog',
src: false,
Expand Down
30 changes: 30 additions & 0 deletions src/runtime/registry/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,36 @@ export const XEmbedOptions = object({
imageProxyEndpoint: optional(string()),
})

export const VercelAnalyticsOptions = object({
/**
* The DSN of the project to send events to.
* Only required when self-hosting or deploying outside of Vercel.
*/
dsn: optional(string()),
/**
* Whether to disable automatic page view tracking on route changes.
* Set to true if you want to manually call pageview().
*/
disableAutoTrack: optional(boolean()),
/**
* The mode to use for the analytics script.
* - `auto` - Automatically detect the environment (default)
* - `production` - Always use production script
* - `development` - Always use development script (logs to console)
*/
mode: optional(union([literal('auto'), literal('development'), literal('production')])),
/**
* Whether to enable debug logging.
* Automatically enabled in development/test environments.
*/
debug: optional(boolean()),
/**
* Custom endpoint for data collection.
* Useful for self-hosted or proxied setups.
*/
endpoint: optional(string()),
})

export const XPixelOptions = object({
/**
* Your X (Twitter) Pixel ID.
Expand Down
Loading
Loading