メインコンテンツへスキップ
10分で読めます

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:

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

このページは役に立ちましたか?