Skip to content
Merged
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
33 changes: 21 additions & 12 deletions docs/content/docs/1.guides/2.first-party.md
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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