.claude/skills/posthog-site-inspector/SKILL.md
Escalation-only browser inspection skill for public sites using Playwright CLI. Checks PostHog presence, config, replay, flags, network requests, debug console output, bundled implementations, custom proxy detection, and competing analytics.
npx skillsauth add mongo-ai/posthog-triage-agent posthog-site-inspectorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Always append ?__posthog_debug=true (or &__posthog_debug=true if query params exist).
playwright-cli open "https://customer-site.com/?__posthog_debug=true"
playwright-cli snapshot
Check for login page indicators (URL contains login/signin/auth/sso, page has password fields). If login required, tell the operator and wait.
playwright-cli eval "(() => {
if (typeof posthog === 'undefined') return { installed: false };
const ph = posthog;
return {
installed: true,
version: ph.version || ph.LIB_VERSION || 'unknown',
config: {
token: ph.config?.token || ph.get_config?.('token') || 'not accessible',
apiHost: ph.config?.api_host || ph.get_config?.('api_host') || 'not accessible',
autocapture: ph.config?.autocapture ?? ph.get_config?.('autocapture') ?? 'not accessible',
capturePageview: ph.config?.capture_pageview ?? ph.get_config?.('capture_pageview') ?? 'not accessible',
sessionRecording: ph.config?.enable_recording_console_log !== undefined || ph.sessionRecording?.started || 'check network',
persistence: ph.config?.persistence || ph.get_config?.('persistence') || 'not accessible',
debug: ph.config?.debug ?? ph.get_config?.('debug') ?? false,
person_profiles: ph.config?.person_profiles || 'not accessible',
disable_session_recording: ph.config?.disable_session_recording || false,
advanced_disable_feature_flags: ph.config?.advanced_disable_feature_flags || false
},
distinctId: ph.get_distinct_id?.() || 'not accessible',
sessionId: ph.get_session_id?.() || 'not accessible',
featureFlags: ph.getFeatureFlag ? Object.keys(ph.featureFlags?.flags || {}) : [],
activeFeatureFlags: ph.getFeatureFlag ? Object.entries(ph.featureFlags?.flags || {}).filter(([_, v]) => v).map(([k]) => k) : []
};
})()"
If posthog is not on window, the site may use a bundled NPM install:
playwright-cli eval "(() => {
const remoteConfig = window._POSTHOG_REMOTE_CONFIG;
if (!remoteConfig) return { found: false };
const tokens = Object.keys(remoteConfig);
return {
found: true,
bundled: true,
configs: tokens.map(token => {
const cfg = remoteConfig[token]?.config || {};
return {
token,
hasFeatureFlags: cfg.hasFeatureFlags || false,
autocapture: !cfg.autocapture_opt_out,
sessionRecording: cfg.sessionRecording || false,
heatmaps: cfg.heatmaps || false,
surveys: cfg.surveys || false
};
})
};
})()"
With ?__posthog_debug=true, PostHog outputs [PostHog.js] logs. Key messages:
| Message pattern | Meaning |
|----------------|---------|
| [PostHog.js] Persistence loaded | Shows persistence type |
| [PostHog.js] [Surveys] Surveys loaded successfully | Surveys module loaded |
| [PostHog.js] [Surveys] flags response received, isSurveysEnabled: X | Whether surveys are enabled |
| [PostHog.js] [SessionRecording] | Session recording status |
| [PostHog.js] [WebExperiments] | Web experiments/flags |
Important: Module loaded ≠ feature enabled. A module can load but be disabled in project settings.
playwright-cli eval "(() => {
const scripts = Array.from(document.querySelectorAll('script'));
const posthogScripts = scripts.filter(s =>
(s.src && (s.src.includes('posthog') || s.src.includes('ph.js'))) ||
(s.textContent && (s.textContent.includes('posthog.init') || s.textContent.includes('!function(t,e)')))
);
return {
found: posthogScripts.length > 0,
scripts: posthogScripts.map(s => ({
src: s.src || 'inline',
async: s.async,
defer: s.defer,
type: s.type || 'text/javascript'
}))
};
})()"
PostHog domains and endpoints to look for:
Standard domains: *.posthog.com, us.i.posthog.com, eu.i.posthog.com
Custom proxy patterns: ph.company.com, analytics.company.com, any domain with /array/phc_ in the path
Endpoints:
/e/ or /capture/ — Events/s/ — Session recording/decide/ or /flags/ — Feature flags/batch/ — Batched events/array/phc_*/config.js — Remote config/static/surveys.js — Surveys module/static/recorder.js — Session recording moduleplaywright-cli eval "performance.getEntriesByType('resource')
.filter(r => r.name.toLowerCase().includes('posthog') || /\/(e|s|decide|flags|batch|capture)\//.test(r.name) || /\/array\/phc_/.test(r.name))
.map(r => ({name: r.name, initiatorType: r.initiatorType, duration: r.duration, transferSize: r.transferSize}))
.slice(0, 30)"
playwright-cli eval "(() => {
const issues = [];
if (typeof posthog === 'undefined') {
issues.push('PostHog not found on window object');
return { issues };
}
if (window.__POSTHOG_INSTANCES__ && window.__POSTHOG_INSTANCES__.length > 1)
issues.push('Multiple PostHog instances detected — may cause duplicate events');
if (!posthog.get_distinct_id || !posthog.get_distinct_id())
issues.push('PostHog may not be fully initialized');
if (posthog.has_opted_out_capturing && posthog.has_opted_out_capturing())
issues.push('User has opted out of tracking');
const isDebug = posthog.config?.debug || posthog.get_config?.('debug');
const hostname = window.location.hostname;
if (isDebug && !hostname.includes('localhost') && !hostname.includes('127.0.0.1'))
issues.push('Debug mode is enabled in production');
const autocapture = posthog.config?.autocapture ?? posthog.get_config?.('autocapture');
if (autocapture === false)
issues.push('Autocapture is disabled — only manual events will be tracked');
return { issues: issues.length > 0 ? issues : ['No issues detected'] };
})()"
playwright-cli eval "(() => {
const scripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src);
const hostname = window.location.hostname.replace('www.', '');
const tools = [];
const patterns = {
'Google Analytics': /google-analytics\.com|gtag\/js/i,
'Google Tag Manager': /googletagmanager\.com\/gtm/i,
'Segment': /cdn\.segment\.com/i,
'Mixpanel': /cdn\.mxpnl\.com|mixpanel\.com/i,
'Amplitude': /cdn\.amplitude\.com/i,
'Hotjar': /static\.hotjar\.com/i,
'FullStory': /fullstory\.com\/s\/fs\.js/i,
'Sentry': /browser\.sentry-cdn\.com/i,
'Datadog': /datadoghq\.com/i,
'LaunchDarkly': /sdk\.launchdarkly\.com/i,
'Heap': /heap-analytics\.com|heapanalytics\.com/i,
'LogRocket': /cdn\.logrocket\.com/i,
'Intercom': /widget\.intercom\.io|intercomcdn\.com/i,
'Zendesk': /static\.zdassets\.com/i,
'Pendo': /cdn\.pendo\.io/i,
'Rudderstack': /cdn\.rudderlabs\.com/i,
'Snowplow': /cdn\.snowplow/i,
'HubSpot': /js\.hs-scripts\.com/i,
'Cookiebot': /consent\.cookiebot\.com/i,
'OneTrust': /cdn\.cookielaw\.org/i
};
for (const [name, pattern] of Object.entries(patterns)) {
for (const src of scripts) {
if (pattern.test(src)) { tools.push(name); break; }
}
}
return { analyticsTools: [...new Set(tools)].sort() };
})()"
playwright-cli close
Present findings as a structured summary:
## PostHog Implementation Summary
### Status
✅ Installed / ✅ Installed (bundled) / ❌ Not Found
### Configuration
- **Version:** [version]
- **API Host:** [host] ([US/EU Cloud] / [Custom proxy] / [Self-hosted])
- **Project Token:** [REDACTED — first 8 chars: phc_xxxx...]
- **Persistence:** [type]
### Features
| Feature | Module Loaded | Enabled |
|---------|---------------|---------|
| Autocapture | ✅/❌ | ✅/❌ |
| Session Recording | ✅/❌ | ✅/❌ |
| Feature Flags | ✅/❌ | ✅/❌ |
| Surveys | ✅/❌ | ✅/❌ |
### Identifiers
- **Distinct ID:** [REDACTED — type: anonymous/identified, first 8 chars only]
- **Session ID:** [REDACTED — present: yes/no, first 8 chars only]
- **Active Feature Flags:** [list flag keys only, no payloads]
### Network Activity
- Events endpoint: ✅/❌
- Session recording: ✅/❌
- Decide/flags endpoint: ✅/❌
### Issues Found
[List or "✅ None detected"]
### Other Analytics Tools
[List of detected tools]
window.posthog missing but _POSTHOG_REMOTE_CONFIG present → bundled NPM install, not snippetapi_host is us.i.posthog.com → US Cloud; eu.i.posthog.com → EU Cloud; custom domain → proxy or self-hostedapi_host points directly at PostHog → likely no reverse proxy (ad blocker vulnerable)The output template MUST redact sensitive identifiers before the report is saved to disk or posted to Slack. These values are useful for diagnosis but must not persist in full:
| Field | Show | Redact |
|-------|------|--------|
| Project Token | First 8 chars + ... | Full token |
| Distinct ID | Type (anonymous/identified) + first 8 chars | Full ID |
| Session ID | Present: yes/no + first 8 chars | Full ID |
| API Key | Never | Everything |
| Feature flag payloads | Keys only | Values/JSON |
| Person properties | Property names | Values |
Do not claim certainty from browser inspection alone. Browser evidence supports the diagnosis; it does not replace project evidence.
tools
Diagnose PostHog web analytics issues including missing pageviews, incorrect bounce rates, broken channel attribution, missing UTM data, reverse proxy problems, and discrepancies with other analytics tools.
business
Final synthesis skill. Produce a structured, evidence-graded triage report with a clear root-cause assessment, honest confidence, and a ready-to-send customer response.
tools
Normalize an incoming support ticket into structured investigation inputs: product area, identifiers, scope clues, URLs, timeframe, and likely first diagnostic path.
development
Diagnose PostHog survey issues including surveys not appearing, targeting mismatches, response collection failures, display timing problems, and API-mode survey integration issues.