posthog-data-handling
SKILL.md
PostHog Data Handling
Overview
Manage analytics data privacy in PostHog. Covers property sanitization before event capture, user opt-out/consent management, data deletion for GDPR compliance, and configuring PostHog's built-in privacy controls.
Prerequisites
- PostHog project (Cloud or self-hosted)
posthog-jsand/orposthog-nodeSDKs- Understanding of GDPR data subject rights
- Privacy policy covering analytics data
Instructions
Step 1: Configure Privacy-Safe Event Capture
import posthog from 'posthog-js';
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: 'https://us.i.posthog.com',
autocapture: false, // Disable to control what's captured
capture_pageview: true,
capture_pageleave: true,
mask_all_text: false,
mask_all_element_attributes: false,
// Sanitize properties before sending
sanitize_properties: (properties, eventName) => {
// Remove PII from all events
delete properties['$ip'];
delete properties['email'];
// Redact URLs containing tokens
if (properties['$current_url']) {
properties['$current_url'] = properties['$current_url']
.replace(/token=[^&]+/g, 'token=[REDACTED]')
.replace(/key=[^&]+/g, 'key=[REDACTED]');
}
return properties;
},
// Respect Do Not Track
respect_dnt: true,
opt_out_capturing_by_default: false,
});
Step 2: Consent-Based Tracking
// Cookie consent integration
function handleConsentChange(consent: {
analytics: boolean;
marketing: boolean;
}) {
if (consent.analytics) {
posthog.opt_in_capturing();
} else {
posthog.opt_out_capturing();
posthog.reset(); // Clear local data
}
}
// Check consent before identifying users
function identifyWithConsent(
userId: string,
traits: Record<string, any>,
hasConsent: boolean
) {
if (!hasConsent) return;
// Only send non-PII traits
const safeTraits: Record<string, any> = {
plan: traits.plan,
signup_date: traits.signupDate,
account_type: traits.accountType,
};
// Explicitly exclude PII
// Do NOT send: email, name, phone, address
posthog.identify(userId, safeTraits);
}
Step 3: GDPR Data Deletion
// Server-side: delete user data for GDPR requests
async function deleteUserData(distinctId: string) {
const response = await fetch(
`https://us.i.posthog.com/api/person/${distinctId}/delete/`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTHOG_PERSONAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
delete_events: true, // Also delete all events from this user
}),
}
);
if (!response.ok) {
throw new Error(`Failed to delete user data: ${response.status}`);
}
return { deletedUser: distinctId, status: 'completed' };
}
// Find person by property for deletion lookup
async function findPersonByEmail(email: string) {
const response = await fetch(
`https://us.i.posthog.com/api/projects/${process.env.POSTHOG_PROJECT_ID}/persons/?properties=[{"key":"email","value":"${email}","type":"person"}]`,
{
headers: {
Authorization: `Bearer ${process.env.POSTHOG_PERSONAL_API_KEY}`,
},
}
);
const data = await response.json();
return data.results?.[0]?.distinct_ids?.[0];
}
Step 4: Property Filtering for Exports
// Filter sensitive properties from HogQL exports
async function safeExport(query: string) {
const BLOCKED_PROPERTIES = ['$ip', 'email', 'phone', 'name', 'address'];
const response = await fetch(
`https://us.i.posthog.com/api/projects/${process.env.POSTHOG_PROJECT_ID}/query/`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTHOG_PERSONAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: { kind: 'HogQLQuery', query },
}),
}
);
const data = await response.json();
// Strip blocked columns from results
if (data.columns && data.results) {
const blockedIndexes = data.columns
.map((col: string, i: number) => BLOCKED_PROPERTIES.some(b => col.includes(b)) ? i : -1)
.filter((i: number) => i >= 0);
data.results = data.results.map((row: any[]) =>
row.filter((_: any, i: number) => !blockedIndexes.includes(i))
);
data.columns = data.columns.filter((_: string, i: number) => !blockedIndexes.includes(i));
}
return data;
}
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| PII in events | Autocapture sending form data | Disable autocapture, use manual capture |
| Consent not respected | opt_out not called | Check consent state on every page load |
| Deletion failed | Wrong distinct_id | Look up person by email first |
| IP in events | Not stripped | Use sanitize_properties to remove $ip |
Examples
GDPR Subject Access Request
async function handleSAR(email: string) {
const distinctId = await findPersonByEmail(email);
if (!distinctId) return { found: false };
// Export their data (filtered)
const data = await safeExport(
`SELECT event, timestamp, properties FROM events WHERE distinct_id = '${distinctId}' LIMIT 1000` # 1000: 1 second in ms
);
return { found: true, events: data.results.length };
}
Resources
Output
- Configuration files or code changes applied to the project
- Validation report confirming correct implementation
- Summary of changes made and their rationale
Weekly Installs
16
Repository
jeremylongshore…s-skillsGitHub Stars
1.6K
First Seen
Jan 30, 2026
Security Audits
Installed on
codex16
opencode15
antigravity15
kilo15
qwen-code15
github-copilot15