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
56 changes: 49 additions & 7 deletions docs/content/docs/1.guides/2.first-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,69 @@ export default defineNuxtConfig({
})
```

### Privacy Modes
### Privacy Controls

First-party mode supports two privacy levels via the `privacy` option:
Each script in the registry declares its own privacy defaults based on what data it needs. Privacy is controlled by six flags:

| Mode | Description |
| Flag | What it does |
|------|-------------|
| `'anonymize'` (default) | Anonymizes IP addresses to subnet level, generalizes screen resolution and hardware info to common buckets, normalizes User-Agent to browser family + major version. Analytics IDs are preserved so tracking still works. |
| `'proxy'` | Forwards requests as-is through your server. Strips sensitive headers (cookies, authorization) but doesn't modify analytics payloads. Privacy comes from third parties seeing your server's IP instead of the user's. |
| `ip` | Anonymizes IP addresses to subnet level in headers and payload params |
| `userAgent` | Normalizes User-Agent to browser family + major version (e.g. `Mozilla/5.0 (compatible; Chrome/131.0)`) |
| `language` | Normalizes Accept-Language to primary language tag |
| `screen` | Generalizes screen resolution, viewport, hardware concurrency, and device memory to common buckets |
| `timezone` | Generalizes timezone offset and IANA timezone names |
| `hardware` | Anonymizes canvas/webgl/audio fingerprints, plugin/font lists, browser versions, and device info |

Sensitive headers (`cookie`, `authorization`) are **always** stripped regardless of privacy settings.

#### Per-Script Defaults

Scripts declare the privacy that makes sense for their use case:

| Script | ip | userAgent | language | screen | timezone | hardware | Rationale |
|--------|:--:|:---------:|:--------:|:------:|:--------:|:--------:|-----------|
| Google Analytics | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for device, time, and OS reports |
| Google Tag Manager | - | - | - | - | - | - | Container script loading β€” no user data in requests |
| Meta Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| TikTok Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| X/Twitter Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| Snapchat Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| Reddit Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| Segment | - | - | - | - | - | - | Trusted data pipeline β€” full fidelity required |
| PostHog | - | - | - | - | - | - | Trusted, open-source β€” full fidelity required |
| Microsoft Clarity | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for heatmaps and device filtering |
βœ“ = anonymized, - = passed through

| Hotjar | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for heatmaps and device filtering |

Comment on lines +75 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Legend placement breaks table structure and markdownlint checks.

Line [87] is parsed as a malformed table row (MD055/MD056). Move the legend outside the table block (or format it as a valid 8-column row).

πŸ”§ Proposed fix
 | Microsoft Clarity | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for heatmaps and device filtering |
-βœ“ = anonymized, - = passed through
-
 | Hotjar | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for heatmaps and device filtering |
+
+`βœ“` = anonymized, `-` = passed through
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Script | ip | userAgent | language | screen | timezone | hardware | Rationale |
|--------|:--:|:---------:|:--------:|:------:|:--------:|:--------:|-----------|
| Google Analytics | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for device, time, and OS reports |
| Google Tag Manager | - | - | - | - | - | - | Container script loading β€” no user data in requests |
| Meta Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| TikTok Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| X/Twitter Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| Snapchat Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| Reddit Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| Segment | - | - | - | - | - | - | Trusted data pipeline β€” full fidelity required |
| PostHog | - | - | - | - | - | - | Trusted, open-source β€” full fidelity required |
| Microsoft Clarity | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for heatmaps and device filtering |
βœ“ = anonymized, - = passed through
| Hotjar | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for heatmaps and device filtering |
| Script | ip | userAgent | language | screen | timezone | hardware | Rationale |
|--------|:--:|:---------:|:--------:|:------:|:--------:|:--------:|-----------|
| Google Analytics | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for device, time, and OS reports |
| Google Tag Manager | - | - | - | - | - | - | Container script loading β€” no user data in requests |
| Meta Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| TikTok Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| X/Twitter Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| Snapchat Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| Reddit Pixel | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | βœ“ | Untrusted ad network β€” full anonymization |
| Segment | - | - | - | - | - | - | Trusted data pipeline β€” full fidelity required |
| PostHog | - | - | - | - | - | - | Trusted, open-source β€” full fidelity required |
| Microsoft Clarity | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for heatmaps and device filtering |
| Hotjar | βœ“ | - | βœ“ | - | - | βœ“ | UA/screen/timezone needed for heatmaps and device filtering |
`βœ“` = anonymized, `-` = passed through
🧰 Tools
πŸͺ› markdownlint-cli2 (0.21.0)

[warning] 87-87: Table pipe style
Expected: leading_and_trailing; Actual: no_leading_or_trailing; Missing leading pipe

(MD055, table-pipe-style)


[warning] 87-87: Table pipe style
Expected: leading_and_trailing; Actual: no_leading_or_trailing; Missing trailing pipe

(MD055, table-pipe-style)


[warning] 87-87: Table column count
Expected: 8; Actual: 1; Too few cells, row will be missing data

(MD056, table-column-count)

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/1.guides/2.first-party.md` around lines 75 - 90, The table
has a stray legend row ("βœ“ = anonymized, - = passed through") inside the
Markdown table which breaks parsing (MD055/MD056); remove that line from the
table block and place the legend as plain text immediately before or after the
table, or alternatively convert it into a valid 8-column row matching the table
schema; update the content around the table in
docs/content/docs/1.guides/2.first-party.md so the table contains only rows with
8 cells and the legend is outside the pipe-delimited table.

#### Global Override

Override all per-script defaults at once:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
firstParty: {
privacy: true, // Full anonymize for ALL scripts
}
}
})
```

Or selectively override specific flags:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
firstParty: {
privacy: 'proxy', // or 'anonymize' (default)
privacy: { ip: true }, // Anonymize IP for all scripts, rest uses per-script defaults
}
}
})
```

::callout{type="info"}
In `anonymize` mode, fingerprinting data is **generalized** rather than stripped β€” analytics endpoints still receive valid data, just with reduced precision. For example, screen resolution `1440x900` becomes `1920x1080` (desktop bucket), and User-Agent is normalized to `Mozilla/5.0 (compatible; Chrome/131.0)`.
When a flag is active, data is either **generalized** (reduced precision) or **redacted** (emptied/zeroed) β€” analytics endpoints still receive valid data. For example, screen resolution `1440x900` becomes `1920x1080` (desktop bucket) and User-Agent is normalized to `Mozilla/5.0 (compatible; Chrome/131.0)`, while hardware fingerprints like canvas, WebGL, plugins, and fonts are zeroed or cleared.
::

### Custom Paths
Expand Down
4 changes: 2 additions & 2 deletions docs/content/scripts/analytics/google-analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of third-party servers
- Route collection requests (`/g/collect`) through your server
- Anonymize user IP addresses to subnet level
- Generalize device fingerprinting data (`sr`, `vp`, `ul`) to common buckets
- Anonymize IP addresses, language, and hardware fingerprints (canvas, webgl, browser versions)
- Preserve User-Agent, screen resolution, and timezone for accurate device, OS, and time-based reports

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down
2 changes: 1 addition & 1 deletion docs/content/scripts/analytics/posthog.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export default defineNuxtConfig({

## First-Party Proxy

When [first-party mode](/docs/guides/first-party) is enabled, PostHog requests are automatically proxied through your own server. This improves event capture reliability by avoiding ad blockers.
When [first-party mode](/docs/guides/first-party) is enabled, PostHog requests are automatically proxied through your own server. This improves event capture reliability by avoiding ad blockers. No privacy anonymization is applied β€” PostHog is a trusted, open-source tool that requires full-fidelity data for GeoIP enrichment, feature flags, and session replay.

No additional configuration is needed β€” the module automatically sets `apiHost` to route through your server's proxy endpoint:

Expand Down
4 changes: 2 additions & 2 deletions docs/content/scripts/marketing/clarity.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of `www.clarity.ms`
- Route data/event collection (`d.clarity.ms`, `e.clarity.ms`) through your server
- Anonymize user IP addresses to subnet level
- Generalize device fingerprinting data to common buckets
- Anonymize IP addresses, language, and hardware fingerprints (canvas, webgl, browser versions)
- Preserve User-Agent, screen resolution, and timezone for accurate heatmaps and device filtering

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down
4 changes: 2 additions & 2 deletions docs/content/scripts/marketing/hotjar.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of `static.hotjar.com`
- Route configuration and data requests (`vars.hotjar.com`, `in.hotjar.com`) through your server
- Anonymize user IP addresses to subnet level
- Generalize device fingerprinting data to common buckets
- Anonymize IP addresses, language, and hardware fingerprints (canvas, webgl, browser versions)
- Preserve User-Agent, screen resolution, and timezone for accurate heatmaps and device filtering

::callout{type="info"}
Hotjar uses WebSocket connections for session recording data. The proxy handles initial setup, but WebSocket connections go directly to Hotjar servers.
Expand Down
3 changes: 1 addition & 2 deletions docs/content/scripts/tracking/google-tag-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,7 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of `www.googletagmanager.com`
- Route all GTM requests through your server
- Anonymize user IP addresses to subnet level
- Generalize device fingerprinting data to common buckets
- No privacy anonymization applied (container script loading only β€” no user data in these requests)

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down
3 changes: 1 addition & 2 deletions docs/content/scripts/tracking/meta-pixel.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,7 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of `connect.facebook.net`
- Route tracking requests (`/tr`) through your server
- Anonymize user IP addresses to subnet level
- Generalize device fingerprinting data to common buckets
- Full privacy anonymization β€” IP, User-Agent, language, screen, timezone, and hardware fingerprints are all anonymized (untrusted ad network)

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down
3 changes: 1 addition & 2 deletions docs/content/scripts/tracking/reddit-pixel.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of `alb.reddit.com`
- Route tracking requests through your server
- Anonymize user IP addresses to subnet level
- Generalize device fingerprinting data to common buckets
- Full privacy anonymization β€” IP, User-Agent, language, screen, timezone, and hardware fingerprints are all anonymized (untrusted ad network)

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down
3 changes: 1 addition & 2 deletions docs/content/scripts/tracking/segment.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of `cdn.segment.com`
- Route API requests (`api.segment.io`) through your server
- Anonymize user IP addresses to subnet level
- Normalize User-Agent and generalize device fingerprinting data to common buckets
- No privacy anonymization applied β€” Segment is a trusted data pipeline that requires full fidelity for downstream destinations

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down
3 changes: 1 addition & 2 deletions docs/content/scripts/tracking/snapchat-pixel.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,7 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of `tr.snapchat.com`
- Route tracking requests through your server
- Anonymize user IP addresses to subnet level
- Generalize device fingerprinting data (`d_os`, `d_bvs`, screen dimensions) to common buckets
- Full privacy anonymization β€” IP, User-Agent, language, screen, timezone, and hardware fingerprints are all anonymized (untrusted ad network)

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down
3 changes: 1 addition & 2 deletions docs/content/scripts/tracking/tiktok-pixel.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of `analytics.tiktok.com`
- Route tracking requests through your server
- Anonymize user IP addresses to subnet level
- Generalize device fingerprinting data to common buckets
- Full privacy anonymization β€” IP, User-Agent, language, screen, timezone, and hardware fingerprints are all anonymized (untrusted ad network)

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down
3 changes: 1 addition & 2 deletions docs/content/scripts/tracking/x-pixel.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ This script supports [First-Party Mode](/docs/guides/first-party) which routes a
When enabled globally via `scripts.firstParty: true`, this script will:
- Load from your domain instead of `analytics.twitter.com`
- Route tracking requests (`t.co`) through your server
- Anonymize user IP addresses to subnet level
- Generalize device fingerprinting data (`dv` combined device info) to common buckets
- Full privacy anonymization β€” IP, User-Agent, language, screen, timezone, and hardware fingerprints are all anonymized (untrusted ad network)

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down
52 changes: 33 additions & 19 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
import { NuxtScriptsCheckScripts } from './plugins/check-scripts'
import { registerTypeTemplates, templatePlugin, templateTriggerResolver } from './templates'
import { getAllProxyConfigs, getSWInterceptRules } from './proxy-configs'
import type { ProxyPrivacyInput } from './runtime/server/utils/privacy'

declare module '@nuxt/schema' {
interface NuxtHooks {
Expand All @@ -37,17 +38,21 @@ declare module '@nuxt/schema' {
}

/**
* Privacy mode for first-party proxy requests.
* Global privacy override for all first-party proxy requests.
*
* - `'anonymize'` (default) - Prevents fingerprinting: anonymizes IP addresses to country-level,
* normalizes device info and canvas data. All other data passes through unchanged.
* By default (`undefined`), each script uses its own privacy controls declared in the registry.
* Setting this overrides all per-script defaults:
*
* - `'proxy'` - Minimal modification: forwards headers and data, but strips sensitive
* auth/session headers (cookie, authorization) to prevent leaking credentials to
* third-party endpoints. Privacy comes from routing requests through your server
* (third parties see server IP, not user IP).
* - `true` - Full anonymize: anonymizes IP, normalizes User-Agent/language,
* generalizes screen/hardware/canvas/timezone data.
*
* - `false` - Passthrough: forwards headers and data, but strips sensitive
* auth/session headers (cookie, authorization).
*
* - `{ ip: false }` - Selective: override individual flags. Unset flags inherit
* from the per-script default.
*/
export type FirstPartyPrivacy = 'proxy' | 'anonymize'
export type FirstPartyPrivacy = ProxyPrivacyInput

export interface FirstPartyOptions {
/**
Expand All @@ -68,14 +73,16 @@ export interface FirstPartyOptions {
*/
collectPrefix?: string
/**
* Privacy level for proxied requests.
* Global privacy override for all proxied scripts.
*
* Controls what user information is forwarded to third-party analytics services.
* By default, each script uses its own privacy controls from the registry.
* Set this to override all scripts at once:
*
* - `'anonymize'` - Prevents fingerprinting by anonymizing IPs and device info (default)
* - `'proxy'` - No modification, just routes through your server
* - `true` - Full anonymize for all scripts
* - `false` - Passthrough for all scripts (still strips sensitive auth headers)
* - `{ ip: false }` - Selective override (unset flags inherit per-script defaults)
*
* @default 'anonymize'
* @default undefined
*/
privacy?: FirstPartyPrivacy
}
Expand Down Expand Up @@ -309,9 +316,9 @@ export default defineNuxtModule<ModuleOptions>({
const firstPartyCollectPrefix = typeof config.firstParty === 'object'
? config.firstParty.collectPrefix || '/_proxy'
: '/_proxy'
const firstPartyPrivacy = typeof config.firstParty === 'object'
? config.firstParty.privacy ?? 'anonymize'
: 'anonymize'
const firstPartyPrivacy: ProxyPrivacyInput | undefined = typeof config.firstParty === 'object'
? config.firstParty.privacy
: undefined
const assetsPrefix = firstPartyPrefix || config.assets?.prefix || '/_scripts'

// Process partytown shorthand - add partytown: true to specified registry scripts
Expand Down Expand Up @@ -551,6 +558,7 @@ export default defineNuxtPlugin({

// Collect routes for all configured registry scripts that support proxying
const neededRoutes: Record<string, { proxy: string }> = {}
const routePrivacyOverrides: Record<string, ProxyPrivacyInput> = {}
const unsupportedScripts: string[] = []
for (const key of registryKeys) {
// Find the registry script definition
Expand All @@ -561,6 +569,10 @@ export default defineNuxtPlugin({
const proxyConfig = proxyConfigs[proxyKey]
if (proxyConfig?.routes) {
Object.assign(neededRoutes, proxyConfig.routes)
// Record per-script privacy for each route
for (const routePath of Object.keys(proxyConfig.routes)) {
routePrivacyOverrides[routePath] = proxyConfig.privacy
}
}
else {
// Track scripts without proxy support
Expand Down Expand Up @@ -611,17 +623,19 @@ export default defineNuxtPlugin({
// Server-side config for proxy privacy handling
nuxt.options.runtimeConfig['nuxt-scripts-proxy'] = {
routes: flatRoutes,
privacy: firstPartyPrivacy,
privacy: firstPartyPrivacy, // undefined = use per-script defaults, set = global override
routePrivacy: routePrivacyOverrides, // per-script privacy from registry
rewrites: allRewrites,
}
} as any

// Proxy handler is registered before modules:done for both privacy modes
if (Object.keys(neededRoutes).length) {
// Log active proxy routes in dev
if (nuxt.options.dev) {
const routeCount = Object.keys(neededRoutes).length
const scriptsCount = registryKeys.length
logger.success(`First-party mode enabled for ${scriptsCount} script(s), ${routeCount} proxy route(s) configured (privacy: ${firstPartyPrivacy})`)
const privacyLabel = firstPartyPrivacy === undefined ? 'per-script' : typeof firstPartyPrivacy === 'boolean' ? (firstPartyPrivacy ? 'anonymize' : 'passthrough') : 'custom'
logger.success(`First-party mode enabled for ${scriptsCount} script(s), ${routeCount} proxy route(s) configured (privacy: ${privacyLabel})`)
if (logger.level >= 4) {
for (const [path, config] of Object.entries(neededRoutes)) {
logger.debug(` ${path} β†’ ${config.proxy}`)
Expand Down
Loading
Loading