Skip to content
Open
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
3 changes: 3 additions & 0 deletions cdn/posthog-analytics-proxy/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# PostHog API key from https://posthog.com
# Get your key from: Project Settings → API Keys
NEXT_PUBLIC_POSTHOG_KEY=phc_your_project_key_here
7 changes: 7 additions & 0 deletions cdn/posthog-analytics-proxy/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-html-link-for-pages": "off"
}
}

44 changes: 44 additions & 0 deletions cdn/posthog-analytics-proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependencies
/node_modules
/.pnp
.pnp.js

# Testing
/coverage

# Next.js
/.next/
/out/
next-env.d.ts

# Production
build
dist

# Misc
.DS_Store
*.pem

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local ENV files
.env.local
.env.development.local
.env.test.local
.env.production.local

# Vercel
.vercel

# Turborepo
.turbo

# typescript
*.tsbuildinfo
.env*.local

57 changes: 57 additions & 0 deletions cdn/posthog-analytics-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: PostHog analytics reverse proxy (vercel.ts)
slug: posthog-analytics-proxy
description: Proxy PostHog analytics through your domain as first-party traffic using vercel.ts.
framework: Next.js
useCase: Analytics
css: Tailwind
deployUrl: https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/cdn/posthog-analytics-proxy&project-name=posthog-analytics-proxy&repository-name=posthog-analytics-proxy&env=NEXT_PUBLIC_POSTHOG_KEY
demoUrl: https://posthog-analytics-proxy.vercel.app
---

# PostHog analytics reverse proxy (vercel.ts) example

This example shows how to proxy PostHog analytics requests through Vercel's routing layer as first-party traffic. The demo is a developer tool landing page with analytics tracking integrated throughout.

## Demo

https://posthog-analytics-proxy.vercel.app

## How to Use

You can choose from one of the following two methods to use this repository:

### One-Click Deploy

Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/cdn/posthog-analytics-proxy&project-name=posthog-analytics-proxy&repository-name=posthog-analytics-proxy&env=NEXT_PUBLIC_POSTHOG_KEY)

### Clone and Deploy

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
pnpm create next-app --example https://github.com/vercel/examples/tree/main/cdn/posthog-analytics-proxy
```

Next, run Next.js in development mode:

```bash
pnpm dev
```

## Environment variables

- `NEXT_PUBLIC_POSTHOG_KEY` – Your PostHog project API key

## How it works

1. `vercel.ts` configures two proxy routes:
- `/ph/static/(.*)` → `https://us-assets.i.posthog.com/static/$1` (static assets)
- `/ph/(.*)` → `https://us.i.posthog.com/$1` (API requests)
2. The `host` header is rewritten so PostHog servers correctly route the proxied requests.
3. PostHog is initialized with `api_host: '/ph'` to send all requests through your proxy.
4. Analytics requests now go through your domain as first-party traffic.

You can extend this pattern to any analytics provider by configuring the appropriate rewrite rules.
153 changes: 153 additions & 0 deletions cdn/posthog-analytics-proxy/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
export default function About() {
return (
<main className="min-h-screen py-16 px-4">
<div className="max-w-3xl mx-auto">
<h1 className="text-4xl font-bold text-black dark:text-white mb-6">
How it works
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-12">
This site demonstrates how to proxy analytics through Vercel using <code className="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded text-sm font-mono">vercel.ts</code> for first-party data collection.
</p>

<div className="space-y-12">
<section>
<h2 className="text-2xl font-semibold text-black dark:text-white mb-4">
First-party analytics proxy
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
By proxying PostHog requests through your own domain, analytics data is collected as first-party traffic. This improves data accuracy and ensures consistent tracking across all users.
</p>
<div className="bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-6">
<div className="font-mono text-sm">
<div className="text-gray-500 dark:text-gray-500 mb-2">// vercel.ts</div>
<pre className="text-black dark:text-white overflow-x-auto">{`import { routes } from '@vercel/config/v1'

export const config = {
routes: [
routes.rewrite('/ph/static/(.*)',
'https://us-assets.i.posthog.com/static/$1',
{ requestHeaders: { 'host': 'us-assets.i.posthog.com' } }
),
routes.rewrite('/ph/(.*)',
'https://us.i.posthog.com/$1',
{ requestHeaders: { 'host': 'us.i.posthog.com' } }
),
],
}`}</pre>
</div>
</div>
</section>

<section>
<h2 className="text-2xl font-semibold text-black dark:text-white mb-4">
How the proxy works
</h2>
<div className="space-y-4">
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-black dark:bg-white text-white dark:text-black rounded-full flex items-center justify-center text-sm font-medium">
1
</div>
<div>
<h3 className="font-medium text-black dark:text-white">Route configuration</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">
The <code className="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-xs font-mono">vercel.ts</code> file defines rewrite rules that map <code className="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-xs font-mono">/ph/*</code> paths to PostHog's servers.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-black dark:bg-white text-white dark:text-black rounded-full flex items-center justify-center text-sm font-medium">
2
</div>
<div>
<h3 className="font-medium text-black dark:text-white">Header rewriting</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">
The <code className="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-xs font-mono">host</code> header is rewritten so PostHog's servers correctly route the proxied requests.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-black dark:bg-white text-white dark:text-black rounded-full flex items-center justify-center text-sm font-medium">
3
</div>
<div>
<h3 className="font-medium text-black dark:text-white">Client initialization</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm">
PostHog is initialized with <code className="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-xs font-mono">api_host: '/ph'</code> to send all requests through your proxy endpoint.
</p>
</div>
</div>
</div>
</section>

<section>
<h2 className="text-2xl font-semibold text-black dark:text-white mb-4">
Benefits
</h2>
<ul className="space-y-3">
<li className="flex items-start gap-3">
<svg className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-600 dark:text-gray-400">
<strong className="text-black dark:text-white">Better data accuracy</strong> — First-party requests aren't subject to the same restrictions as third-party tracking
</span>
</li>
<li className="flex items-start gap-3">
<svg className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-600 dark:text-gray-400">
<strong className="text-black dark:text-white">No edge function costs</strong> — Uses Vercel's built-in reverse proxy layer
</span>
</li>
<li className="flex items-start gap-3">
<svg className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-600 dark:text-gray-400">
<strong className="text-black dark:text-white">Framework agnostic</strong> — Works with Next.js, Vue, Astro, or any framework
</span>
</li>
<li className="flex items-start gap-3">
<svg className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-600 dark:text-gray-400">
<strong className="text-black dark:text-white">Simple setup</strong> — Just configure routes and set your API key
</span>
</li>
</ul>
</section>

<section>
<h2 className="text-2xl font-semibold text-black dark:text-white mb-4">
Try it yourself
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Open your browser's Network tab and look for requests to <code className="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-xs font-mono">/ph/</code>. You'll see analytics calls going through your domain instead of directly to PostHog.
</p>
<div className="flex gap-4">
<a
href="https://github.com/vercel/examples/tree/main/cdn/posthog-analytics-proxy"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center bg-black dark:bg-white text-white dark:text-black px-6 py-2 rounded-lg font-medium hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors text-sm"
>
View on GitHub
</a>
<a
href="https://posthog.com/docs/advanced/proxy"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-gray-100 px-6 py-2 rounded-lg font-medium hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors text-sm"
>
PostHog Docs
</a>
</div>
</section>
</div>
</div>
</main>
)
}

127 changes: 127 additions & 0 deletions cdn/posthog-analytics-proxy/app/docs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
'use client'

import posthog from 'posthog-js'

export default function Docs() {
return (
<main className="min-h-screen py-16 px-4">
<div className="max-w-3xl mx-auto">
<h1 className="text-4xl font-bold text-black dark:text-white mb-6">
Documentation
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-12">
Get started with Forge CLI in minutes.
</p>

<div className="space-y-12">
<section>
<h2 className="text-2xl font-semibold text-black dark:text-white mb-4">
Installation
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Install Forge CLI globally using your preferred package manager:
</p>
<div className="bg-black dark:bg-gray-900 rounded-lg p-4 font-mono text-sm">
<div className="flex items-center gap-2 text-gray-400">
<span className="text-green-400">$</span>
<span className="text-white">npm install -g @forge/cli</span>
</div>
</div>
</section>

<section>
<h2 className="text-2xl font-semibold text-black dark:text-white mb-4">
Quick Start
</h2>
<div className="space-y-6">
<div>
<h3 className="font-medium text-black dark:text-white mb-2">1. Create a new project</h3>
<div className="bg-black dark:bg-gray-900 rounded-lg p-4 font-mono text-sm">
<span className="text-green-400">$</span>
<span className="text-white ml-2">forge init my-app</span>
</div>
</div>
<div>
<h3 className="font-medium text-black dark:text-white mb-2">2. Navigate to your project</h3>
<div className="bg-black dark:bg-gray-900 rounded-lg p-4 font-mono text-sm">
<span className="text-green-400">$</span>
<span className="text-white ml-2">cd my-app</span>
</div>
</div>
<div>
<h3 className="font-medium text-black dark:text-white mb-2">3. Start development server</h3>
<div className="bg-black dark:bg-gray-900 rounded-lg p-4 font-mono text-sm">
<span className="text-green-400">$</span>
<span className="text-white ml-2">forge dev</span>
</div>
</div>
</div>
</section>

<section>
<h2 className="text-2xl font-semibold text-black dark:text-white mb-4">
Commands
</h2>
<div className="border border-gray-200 dark:border-gray-800 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50 dark:bg-gray-900">
<tr>
<th className="text-left px-4 py-3 font-medium text-black dark:text-white">Command</th>
<th className="text-left px-4 py-3 font-medium text-black dark:text-white">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
<tr
onClick={() => posthog.capture('docs_command_viewed', { command: 'init' })}
className="hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer"
>
<td className="px-4 py-3 font-mono text-gray-600 dark:text-gray-400">forge init</td>
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">Create a new project</td>
</tr>
<tr
onClick={() => posthog.capture('docs_command_viewed', { command: 'dev' })}
className="hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer"
>
<td className="px-4 py-3 font-mono text-gray-600 dark:text-gray-400">forge dev</td>
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">Start development server</td>
</tr>
<tr
onClick={() => posthog.capture('docs_command_viewed', { command: 'build' })}
className="hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer"
>
<td className="px-4 py-3 font-mono text-gray-600 dark:text-gray-400">forge build</td>
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">Build for production</td>
</tr>
<tr
onClick={() => posthog.capture('docs_command_viewed', { command: 'deploy' })}
className="hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer"
>
<td className="px-4 py-3 font-mono text-gray-600 dark:text-gray-400">forge deploy</td>
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">Deploy to production</td>
</tr>
<tr
onClick={() => posthog.capture('docs_command_viewed', { command: 'add' })}
className="hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer"
>
<td className="px-4 py-3 font-mono text-gray-600 dark:text-gray-400">forge add</td>
<td className="px-4 py-3 text-gray-600 dark:text-gray-400">Add a plugin or component</td>
</tr>
</tbody>
</table>
</div>
</section>

<section className="border-t border-gray-200 dark:border-gray-800 pt-12">
<p className="text-sm text-gray-500 dark:text-gray-500">
This is a demo site showcasing PostHog analytics integration via Vercel's reverse proxy.{' '}
<a href="/about" className="text-black dark:text-white hover:underline">
Learn how it works →
</a>
</p>
</section>
</div>
</div>
</main>
)
}

Loading