Skip to main content
10 min read

First-Party Tracking with Cloudflare Pages

Set up a first-party proxy using Cloudflare Pages Functions. This is the best option if you're already hosting your site on Cloudflare Pages.

Difficulty: Easy - Just add one file to your project. Takes about 5 minutes.

Why This Works (Same-Origin)

This approach uses /api/_z/script.js which is on the same origin as your website. Firefox ETP and other tracking protections only block cross-origin requests.

  • Browser sees: yourdomain.com/api/_z/script.js → Same origin ✅
  • Your function proxies to Zenovay server-to-server (browser never sees this)
  • All tracking protection is bypassed because the request is first-party

The proxied script stops honouring Do Not Track

Every bundle served through /api/_z/script.js ships with ignoreDNT enabled, so a visitor whose browser sends the legacy Do Not Track signal is still tracked on a proxied site, whether or not you asked for that. Global Privacy Control is never overridden and is still honoured. If Do Not Track matters for your audience, serve the standard script from api.zenovay.com instead. See Privacy-first.


Before You Start

Make sure you have:

  • A project deployed on Cloudflare Pages
  • Your Zenovay tracking code (found in Domains, open your site, then the General tab — in the Tracking script card)
  • Access to your project's source code

Your Tracking Code Format

Your tracking code looks like: ZV_XXXXXXXXXX

  • Starts with ZV_
  • Followed by 10 characters (letters and numbers)
  • CASE-SENSITIVE - copy it exactly

Example: ZV_Q8U0GYD70WR

Tracking script card showing the script snippet with HTML, React, Next.js, and First-Party tabs
The Tracking script card under your domain's General settings — copy the code from the First-Party tab.

How It Works

When you add a functions folder to your Cloudflare Pages project, Cloudflare automatically creates serverless functions. We'll create a function that proxies requests to Zenovay's API, making them appear as first-party requests to browsers.

Browser → yourdomain.pages.dev/api/_z/script.js ✅ (First-party)
               ↓
        Cloudflare Function
               ↓
         api.zenovay.com/fp/script.js (Server-to-server)

Step 1: Create the Functions Directory

File Location is CRITICAL

The functions folder MUST be at your project root. Not inside src/, not inside public/.

Correct location:

your-project/
├── functions/          ← HERE (project root)
│   └── api/
│       └── _z/
│           └── [[path]].ts
├── src/
├── public/
└── package.json

WRONG locations:

  • src/functions/api/_z/[[path]].ts - Functions won't work
  • public/functions/api/_z/[[path]].ts - Functions won't work

Create the directory structure:

Terminal - Run from your project rootBash
# Create the functions directory structure
mkdir -p functions/api/_z

# Verify you're in the right place
ls -la
# You should see: functions/  src/  public/  package.json  etc.

After running these commands, your project structure should look like this:

your-project/
├── functions/
│   └── api/
│       └── _z/
│           └── [[path]].ts    ← We'll create this next
├── src/
├── public/
├── package.json
└── ...

Step 2: Create the Proxy Function

File Naming is IMPORTANT

The file MUST be named [[path]].ts (or [[path]].js for JavaScript):

  • Two opening brackets: [[
  • The word path
  • Two closing brackets: ]]
  • File extension: .ts or .js

This creates a "catch-all" route that handles any path after /api/_z/.

TypeScript or JavaScript?

  • Use .ts if your project uses TypeScript
  • Use .js if your project uses JavaScript
  • Both work exactly the same way

If unsure, try .ts first. If you get TypeScript errors during build, rename it to .js instead.

Create the file functions/api/_z/[[path]].ts with this content:

functions/api/_z/[[path]].ts - Copy this entire fileTypeScript
/**
* Zenovay First-Party Proxy
* This function proxies tracking requests to make them first-party
*/

interface EventContext {
request: Request
params: { path?: string[] }
}

export async function onRequest(context: EventContext): Promise<Response> {
const { request, params } = context

// Build path from catch-all parameter
// [[path]] captures: script.js, e/CODE, settings/CODE, etc.
const path = (params.path || []).join('/')
const url = new URL(request.url)
const targetUrl = `https://api.zenovay.com/fp/${path}${url.search}`

// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  })
}

try {
  // Get the real client IP for accurate geolocation
  const clientIP = request.headers.get('CF-Connecting-IP') || ''

  // Forward request with real IP
  const proxyHeaders = new Headers(request.headers)
  proxyHeaders.set('X-Zenovay-Real-IP', clientIP)
  proxyHeaders.delete('Host')

  // Build the request
  const requestInit: RequestInit = {
    method: request.method,
    headers: proxyHeaders,
  }

  // Include body for POST/PUT/PATCH requests
  if (['POST', 'PUT', 'PATCH'].includes(request.method)) {
    requestInit.body = await request.arrayBuffer()
  }

  // Forward to Zenovay
  const response = await fetch(targetUrl, requestInit)

  // Add CORS headers to response
  const responseHeaders = new Headers(response.headers)
  responseHeaders.set('Access-Control-Allow-Origin', '*')

  return new Response(response.body, {
    status: response.status,
    headers: responseHeaders,
  })
} catch (error) {
  console.error('Proxy error:', error)
  return new Response(JSON.stringify({ error: 'Proxy error' }), {
    status: 502,
    headers: { 'Content-Type': 'application/json' },
  })
}
}

JavaScript Alternative

If you prefer JavaScript (no TypeScript), create functions/api/_z/[[path]].js:

functions/api/_z/[[path]].js - JavaScript versionJavaScript
/**
* Zenovay First-Party Proxy
* This function proxies tracking requests to make them first-party
*/

export async function onRequest(context) {
const { request, params } = context

// Build path from catch-all parameter
const path = (params.path || []).join('/')
const url = new URL(request.url)
const targetUrl = `https://api.zenovay.com/fp/${path}${url.search}`

// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  })
}

try {
  // Get the real client IP for accurate geolocation
  const clientIP = request.headers.get('CF-Connecting-IP') || ''

  // Forward request with real IP
  const headers = new Headers(request.headers)
  headers.set('X-Zenovay-Real-IP', clientIP)
  headers.delete('Host')

  // Forward to Zenovay
  const response = await fetch(targetUrl, {
    method: request.method,
    headers: headers,
    body: ['POST', 'PUT', 'PATCH'].includes(request.method)
      ? await request.arrayBuffer()
      : undefined,
  })

  // Add CORS headers to response
  const responseHeaders = new Headers(response.headers)
  responseHeaders.set('Access-Control-Allow-Origin', '*')

  return new Response(response.body, {
    status: response.status,
    headers: responseHeaders,
  })
} catch (error) {
  return new Response(JSON.stringify({ error: 'Proxy error' }), {
    status: 502,
    headers: { 'Content-Type': 'application/json' },
  })
}
}

Step 3: Add the Tracking Script

Add this script to your website's HTML. The location depends on your framework:

Tracking script - Add to your HTML <head> sectionHTML
<!-- Zenovay Analytics - First-Party Tracking -->
<script defer
data-tracking-code="YOUR_TRACKING_CODE"
src="/api/_z/script.js">
</script>

Replace YOUR_TRACKING_CODE with your actual tracking code from the Zenovay dashboard (e.g., ZV_Q8U0GYD70WR).

Framework-Specific Examples

React / Next.js

app/layout.tsx or _app.tsxTSX
export default function RootLayout({ children }) {
return (
  <html lang="en">
    <head>
      <script
        defer
        data-tracking-code="YOUR_TRACKING_CODE"
        src="/api/_z/script.js"
      />
    </head>
    <body>{children}</body>
  </html>
)
}

Vue / Nuxt

app.vue or nuxt.config.tsVUE
<!-- In app.vue -->
<script setup>
useHead({
script: [
  {
    src: '/api/_z/script.js',
    defer: true,
    'data-tracking-code': 'YOUR_TRACKING_CODE'
  }
]
})
</script>

Astro

src/layouts/Layout.astroASTRO
---
const { title } = Astro.props;
---
<html lang="en">
<head>
  <title>{title}</title>
  <script defer data-tracking-code="YOUR_TRACKING_CODE" src="/api/_z/script.js"></script>
</head>
<body>
  <slot />
</body>
</html>

Plain HTML

index.htmlHTML
<!DOCTYPE html>
<html>
<head>
  <title>My Website</title>
  <script defer data-tracking-code="YOUR_TRACKING_CODE" src="/api/_z/script.js"></script>
</head>
<body>
  <!-- Your content -->
</body>
</html>

Step 4: Deploy

Deploy your project to Cloudflare Pages:

If you have automatic deployments from Git:

TerminalBash
git add .
git commit -m "Add Zenovay first-party tracking"
git push

Cloudflare Pages will automatically build and deploy.

Option B: Using Wrangler CLI

TerminalBash
# Build your project first
npm run build

# Deploy to Cloudflare Pages
npx wrangler pages deploy ./dist --project-name=your-project-name

Replace ./dist with your build output directory (could be ./out, ./build, etc.) and your-project-name with your Cloudflare Pages project name.


Step 5: Verify It's Working

Check 1: Functions Tab in Cloudflare

  1. Go to Cloudflare Dashboard → Pages
  2. Click on your project
  3. Click the Functions tab
  4. You should see api/_z/[[path]] listed

Don't see the function?

  • Make sure functions/ folder is at project root (not inside src/)
  • Make sure the file is named [[path]].ts or [[path]].js
  • Check that the deployment completed successfully

Check 2: Network Tab in Browser

  1. Open your deployed site
  2. Press F12 (or Cmd+Option+I on Mac) to open DevTools
  3. Click the Network tab
  4. Refresh the page (Cmd+R or Ctrl+R)
  5. In the filter box, type script.js
  6. Look for /api/_z/script.js

What you should see:

  • Status: 200 ✅
  • Domain: Your Pages domain (e.g., your-project.pages.dev)
  • Response: JavaScript code

Check 3: Firefox Strict Mode (Most Important!)

Firefox has the strictest tracking protection. If it works in Firefox, it works everywhere.

  1. Open Firefox browser
  2. Click the menu (☰) → Settings
  3. Click Privacy & Security in the left sidebar
  4. Under "Enhanced Tracking Protection", select Strict
  5. Visit your website
  6. Open DevTools (F12) → Network tab
  7. Refresh and verify /api/_z/script.js loads with status 200

Check 4: Zenovay Dashboard

  1. Go to app.zenovay.com and log in
  2. Click on your website
  3. Visit your deployed site in another tab
  4. Within 1-2 minutes, you should see the visit appear in your dashboard

Final Checklist

Before you're done, verify ALL of these:

  • functions/api/_z/[[path]].ts file exists at project root
  • Function appears in Cloudflare Pages Functions tab
  • Script loads at /api/_z/script.js with status 200
  • data-tracking-code attribute contains your correct tracking code
  • Tested in Firefox with Enhanced Tracking Protection set to Strict
  • Visits appearing in Zenovay dashboard

Troubleshooting

Function Returns 404

Cause: The function file is in the wrong location or has the wrong name.

Solution:

  1. Verify the file is at exactly functions/api/_z/[[path]].ts
    • functions folder must be at project root
    • Must have api/_z/ subdirectories
    • Must be named [[path]].ts with double brackets
  2. Make sure the functions folder is included in your deployment
  3. Check the Functions tab in Cloudflare dashboard

Function Returns 500

Cause: There's a syntax error or runtime error in the function code.

Solution:

  1. Check the Functions logs in Cloudflare for error details:
    • Go to your Pages project → Functions → Logs
  2. Verify all the code was copied correctly
  3. Make sure you're exporting onRequest (not export default)

TypeScript Errors During Build

Cause: Missing type definitions.

Solution: Option 1: Install Cloudflare Workers types:

TerminalBash
npm install --save-dev @cloudflare/workers-types

Option 2: Use the JavaScript version instead (rename to [[path]].js)

CORS Errors in Console

Cause: CORS headers aren't being added correctly.

Solution: Make sure your function includes both:

  1. The OPTIONS handler for preflight requests
  2. responseHeaders.set('Access-Control-Allow-Origin', '*') in the response

Geolocation is Wrong

Cause: Client IP not being forwarded.

Solution: Verify your function includes:

const clientIP = request.headers.get('CF-Connecting-IP') || ''
proxyHeaders.set('X-Zenovay-Real-IP', clientIP)

Script Loads But No Data in Dashboard

Cause: Tracking code mismatch.

Solution:

  1. Open DevTools Console (F12 → Console tab) and look for errors
  2. Verify your data-tracking-code matches exactly what's in your Zenovay dashboard (case-sensitive!)
  3. Make sure the domain is registered in Zenovay

Next Steps

Was this page helpful?