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
Before You Start
Make sure you have:
- A project deployed on Cloudflare Pages
- Your Zenovay tracking code (found in Dashboard → Your Site → Settings)
- 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
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:
# 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:
.tsor.js
This creates a "catch-all" route that handles any path after /api/_z/.
TypeScript or JavaScript?
- Use
.tsif your project uses TypeScript - Use
.jsif 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:
/**
* 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:
/**
* 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:
<!-- 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
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
<!-- In app.vue -->
<script setup>
useHead({
script: [
{
src: '/api/_z/script.js',
defer: true,
'data-tracking-code': 'YOUR_TRACKING_CODE'
}
]
})
</script>Astro
---
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
<!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:
Option A: Using Git (Recommended)
If you have automatic deployments from Git:
git add .
git commit -m "Add Zenovay first-party tracking"
git pushCloudflare Pages will automatically build and deploy.
Option B: Using Wrangler CLI
# Build your project first
npm run build
# Deploy to Cloudflare Pages
npx wrangler pages deploy ./dist --project-name=your-project-nameReplace ./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
- Go to Cloudflare Dashboard → Pages
- Click on your project
- Click the Functions tab
- You should see
api/_z/[[path]]listed
Don't see the function?
- Make sure
functions/folder is at project root (not insidesrc/) - Make sure the file is named
[[path]].tsor[[path]].js - Check that the deployment completed successfully
Check 2: Network Tab in Browser
- Open your deployed site
- Press F12 (or Cmd+Option+I on Mac) to open DevTools
- Click the Network tab
- Refresh the page (Cmd+R or Ctrl+R)
- In the filter box, type
script.js - 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.
- Open Firefox browser
- Click the menu (☰) → Settings
- Click Privacy & Security in the left sidebar
- Under "Enhanced Tracking Protection", select Strict
- Visit your website
- Open DevTools (F12) → Network tab
- Refresh and verify
/api/_z/script.jsloads with status 200
Check 4: Zenovay Dashboard
- Go to app.zenovay.com and log in
- Click on your website
- Visit your deployed site in another tab
- 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]].tsfile exists at project root - Function appears in Cloudflare Pages Functions tab
- Script loads at
/api/_z/script.jswith status 200 -
data-tracking-codeattribute 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:
- Verify the file is at exactly
functions/api/_z/[[path]].tsfunctionsfolder must be at project root- Must have
api/_z/subdirectories - Must be named
[[path]].tswith double brackets
- Make sure the
functionsfolder is included in your deployment - Check the Functions tab in Cloudflare dashboard
Function Returns 500
Cause: There's a syntax error or runtime error in the function code.
Solution:
- Check the Functions logs in Cloudflare for error details:
- Go to your Pages project → Functions → Logs
- Verify all the code was copied correctly
- Make sure you're exporting
onRequest(notexport default)
TypeScript Errors During Build
Cause: Missing type definitions.
Solution: Option 1: Install Cloudflare Workers types:
npm install --save-dev @cloudflare/workers-typesOption 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:
- The OPTIONS handler for preflight requests
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:
- Open DevTools Console (F12 → Console tab) and look for errors
- Verify your
data-tracking-codematches exactly what's in your Zenovay dashboard (case-sensitive!) - Make sure the domain is registered in Zenovay
Next Steps
- Custom Events - Track user interactions
- Visitor Identification - Link analytics to users
- Troubleshooting - More help with issues