Skip to content

Commit 6c4675f

Browse files
committed
feat(posthog): support proxy mode
Fixes #591
1 parent ff0135e commit 6c4675f

File tree

5 files changed

+70
-19
lines changed

5 files changed

+70
-19
lines changed

docs/content/scripts/analytics/posthog.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export interface PostHogApi {
104104
export const PostHogOptions = object({
105105
apiKey: string(),
106106
region: optional(union([literal('us'), literal('eu')])),
107+
apiHost: optional(string()), // Custom API host URL (e.g. '/ph' for reverse proxy)
107108
autocapture: optional(boolean()),
108109
capturePageview: optional(boolean()),
109110
capturePageleave: optional(boolean()),
@@ -158,6 +159,47 @@ export default defineNuxtConfig({
158159
})
159160
```
160161

162+
## First-Party Proxy
163+
164+
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.
165+
166+
No additional configuration is needed — the module automatically sets `apiHost` to route through your server's proxy endpoint:
167+
168+
```ts
169+
export default defineNuxtConfig({
170+
scripts: {
171+
firstParty: true, // enabled by default
172+
registry: {
173+
posthog: {
174+
apiKey: 'YOUR_API_KEY',
175+
// apiHost is auto-set to '/_proxy/ph' (or '/_proxy/ph-eu' for EU region)
176+
}
177+
}
178+
}
179+
})
180+
```
181+
182+
The proxy handles both API requests and static assets (e.g. session recording SDK), routing them to the correct PostHog endpoints.
183+
184+
## Custom API Host
185+
186+
To use a custom reverse proxy or self-hosted PostHog instance, set `apiHost` directly:
187+
188+
```ts
189+
export default defineNuxtConfig({
190+
scripts: {
191+
registry: {
192+
posthog: {
193+
apiKey: 'YOUR_API_KEY',
194+
apiHost: '/my-proxy'
195+
}
196+
}
197+
}
198+
})
199+
```
200+
201+
The `apiHost` option accepts any URL or relative path, overriding both the `region` default and the first-party proxy auto-configuration. For additional PostHog SDK options like `ui_host`, use the `config` passthrough.
202+
161203
## Feature Flags
162204

163205
Feature flag methods return values, so you need to wait for PostHog to load first:

src/module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,18 @@ export default defineNuxtPlugin({
569569
}
570570
}
571571

572+
// Auto-inject apiHost for PostHog when first-party proxy is enabled
573+
// PostHog uses NPM mode so URL rewrites don't apply - we set api_host via config instead
574+
if (config.registry?.posthog && typeof config.registry.posthog === 'object') {
575+
const phConfig = config.registry.posthog as Record<string, any>
576+
if (!phConfig.apiHost) {
577+
const region = phConfig.region || 'us'
578+
phConfig.apiHost = region === 'eu'
579+
? `${firstPartyCollectPrefix}/ph-eu`
580+
: `${firstPartyCollectPrefix}/ph`
581+
}
582+
}
583+
572584
// Warn about scripts that don't support first-party mode
573585
if (unsupportedScripts.length && nuxt.options.dev) {
574586
logger.warn(

src/proxy-configs.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ function buildProxyConfig(collectPrefix: string) {
145145
},
146146
},
147147

148+
posthog: {
149+
// No rewrites needed - PostHog uses NPM mode, SDK URLs are set via api_host config
150+
routes: {
151+
// US region
152+
[`${collectPrefix}/ph/static/**`]: { proxy: 'https://us-assets.i.posthog.com/static/**' },
153+
[`${collectPrefix}/ph/**`]: { proxy: 'https://us.i.posthog.com/**' },
154+
// EU region
155+
[`${collectPrefix}/ph-eu/static/**`]: { proxy: 'https://eu-assets.i.posthog.com/static/**' },
156+
[`${collectPrefix}/ph-eu/**`]: { proxy: 'https://eu.i.posthog.com/**' },
157+
},
158+
},
159+
148160
hotjar: {
149161
rewrite: [
150162
// Static assets

src/registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export async function registry(resolve?: (path: string, opts?: ResolvePathOption
4343
{
4444
label: 'PostHog',
4545
src: false,
46+
proxy: 'posthog',
4647
scriptBundling: false,
4748
category: 'analytics',
4849
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 128 128"><path fill="#1d4aff" d="M0 .52v32.15l31.79 31.78V32.3L0 .52zm32.3 32.15v32.15l31.78 31.78V64.45L32.3 32.67zM0 64.97v32.15l31.79 31.78V96.75L0 64.97zm64.6-32.3v32.15l31.78 31.78V64.45L64.6 32.67zm31.78 31.78v32.15l31.78 31.78V96.23l-31.78-31.78zm-64.08.52v32.15l31.78 31.78V96.75L32.3 64.97zM64.6 .52v32.15l31.78 31.78V32.3L64.6 .52zm0 64.45v32.15l31.78 31.78V96.75L64.6 64.97z"/></svg>`,

src/runtime/registry/posthog.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { logger } from '../logger'
77
export const PostHogOptions = object({
88
apiKey: string(),
99
region: optional(union([literal('us'), literal('eu')])),
10+
apiHost: optional(string()),
1011
autocapture: optional(boolean()),
1112
capturePageview: optional(boolean()),
1213
capturePageleave: optional(boolean()),
@@ -74,17 +75,12 @@ export function useScriptPostHog<T extends PostHogApi>(_options?: PostHogInput)
7475
}
7576

7677
const region = options?.region || 'us'
77-
const apiHost = region === 'eu'
78+
const apiHost = options?.apiHost || (region === 'eu'
7879
? 'https://eu.i.posthog.com'
79-
: 'https://us.i.posthog.com'
80-
81-
// eslint-disable-next-line no-console
82-
console.log('[PostHog] Starting dynamic import of posthog-js...')
80+
: 'https://us.i.posthog.com')
8381

8482
window.__posthogInitPromise = import('posthog-js')
8583
.then(({ default: posthog }) => {
86-
// eslint-disable-next-line no-console
87-
console.log('[PostHog] posthog-js imported successfully')
8884
const config: Partial<PostHogConfig> = {
8985
api_host: apiHost,
9086
...options?.config as Partial<PostHogConfig>,
@@ -98,35 +94,23 @@ export function useScriptPostHog<T extends PostHogApi>(_options?: PostHogInput)
9894
if (typeof options?.disableSessionRecording === 'boolean')
9995
config.disable_session_recording = options.disableSessionRecording
10096

101-
// eslint-disable-next-line no-console
102-
console.log('[PostHog] Calling posthog.init with apiKey:', options.apiKey, 'config:', config)
10397
const instance = posthog.init(options.apiKey, config)
10498
if (!instance) {
10599
logger.error('PostHog init returned undefined - initialization failed')
106-
// Clear queue on init failure to prevent memory leak
107100
delete window._posthogQueue
108101
return undefined
109102
}
110103

111-
// eslint-disable-next-line no-console
112-
console.log('[PostHog] posthog.init succeeded, instance:', instance)
113104
window.posthog = instance
114105
// Flush queued calls now that PostHog is ready
115106
if (window._posthogQueue && window._posthogQueue.length > 0) {
116-
// eslint-disable-next-line no-console
117-
console.log('[PostHog] Flushing', window._posthogQueue.length, 'queued calls')
118107
window._posthogQueue.forEach(q => (window.posthog as any)[q.prop]?.(...q.args))
119108
delete window._posthogQueue
120109
}
121-
// eslint-disable-next-line no-console
122-
console.log('[PostHog] Initialization complete!')
123110
return window.posthog
124111
})
125112
.catch((e) => {
126113
logger.error('Failed to load posthog-js:', e)
127-
128-
console.error('[PostHog] Import/init error:', e)
129-
// Clear queue on error to prevent memory leak
130114
delete window._posthogQueue
131115
return undefined
132116
})

0 commit comments

Comments
 (0)