| name | posthog-data-handling |
| description | PostHog PII handling, GDPR compliance, consent management, data deletion,
property sanitization, and privacy-safe analytics configuration.
Trigger: "posthog data", "posthog PII", "posthog GDPR", "posthog data
retention", "posthog privacy", "posthog CCPA", "posthog consent".
|
| allowed-tools | Read, Write, Edit |
| version | 1.12.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","posthog","compliance"] |
| compatibility | Designed for Claude Code |
PostHog Data Handling
Overview
Privacy-safe analytics with PostHog. Covers property sanitization to strip PII before events leave the browser, consent-based tracking (opt-in/opt-out), GDPR data subject access requests and deletion, and PostHog's built-in privacy controls (IP masking, session recording masking).
Prerequisites
- PostHog project (Cloud or self-hosted)
posthog-js and/or posthog-node installed
- Privacy policy covering analytics data collection
- Cookie consent mechanism (e.g., CookieConsent banner)
Instructions
Step 1: Privacy-Safe Initialization
import posthog from 'posthog-js';
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: 'https://us.i.posthog.com',
autocapture: false,
respect_dnt: true,
opt_out_capturing_by_default: false,
sanitize_properties: (properties, eventName) => {
delete properties['$ip'];
delete properties['$device_id'];
if (properties['$current_url']) {
properties['$current_url'] = properties['$current_url']
.replace(/token=[^&]+/g, 'token=[REDACTED]')
.replace(/key=[^&]+/g, 'key=[REDACTED]')
.replace(/session=[^&]+/g, 'session=[REDACTED]');
}
if (properties['$referrer']) {
properties[] = properties[]
.(, );
}
properties;
},
: {
: ,
: ,
},
});
Step 2: Consent-Based Tracking
interface ConsentState {
analytics: boolean;
functional: boolean;
marketing: boolean;
}
export function handleConsentChange(consent: ConsentState) {
if (consent.analytics) {
posthog.opt_in_capturing();
} else {
posthog.opt_out_capturing();
posthog.reset();
}
}
export function identifyWithConsent(
userId: string,
properties: Record<string, any>,
hasAnalyticsConsent: boolean
) {
if (!hasAnalyticsConsent) return;
const safeProperties: Record<string, any> = {
plan: properties.plan,
signup_date: properties.signupDate,
: properties.,
};
posthog.(userId, safeProperties);
}
() {
consent = ();
(consent?. === ) {
posthog.();
}
}
Step 3: GDPR Data Subject Access Request (SAR)
async function handleSubjectAccessRequest(email: string) {
const personalKey = process.env.POSTHOG_PERSONAL_API_KEY!;
const projectId = process.env.POSTHOG_PROJECT_ID!;
const searchResponse = await fetch(
`https://app.posthog.com/api/projects/${projectId}/persons/?properties=[{"key":"email","value":"${encodeURIComponent(email)}","type":"person"}]`,
{ headers: { Authorization: `Bearer ${personalKey}` } }
);
const searchData = await searchResponse.json();
if (!searchData.results?.length) {
return { found: false, message: 'No person found with that email' };
}
const person = searchData.results[0];
const distinctId = person.distinct_ids[0];
const eventsResponse = await fetch(
`https://app.posthog.com/api/projects//query/`,
{
: ,
: {
: ,
: ,
},
: .({
: {
: ,
: ,
},
}),
}
);
eventsData = eventsResponse.();
{
: ,
: {
: person.,
: person.,
: person.,
},
: eventsData.?. || ,
: eventsData.,
};
}
Step 4: GDPR Right to Erasure (Data Deletion)
async function handleDeletionRequest(email: string) {
const personalKey = process.env.POSTHOG_PERSONAL_API_KEY!;
const projectId = process.env.POSTHOG_PROJECT_ID!;
const searchResponse = await fetch(
`https://app.posthog.com/api/projects/${projectId}/persons/?properties=[{"key":"email","value":"${encodeURIComponent(email)}","type":"person"}]`,
{ headers: { Authorization: `Bearer ${personalKey}` } }
);
const searchData = await searchResponse.json();
if (!searchData.results?.length) {
return { deleted: false, reason: 'Person not found' };
}
const personId = searchData.results[0].id;
const deleteResponse = await fetch(
`https://app.posthog.com/api/projects/${projectId}/persons/${personId}/`,
{
method: ,
: { : },
}
);
(!deleteResponse.) {
();
}
{
: ,
personId,
: ().(),
};
}
Step 5: Property Filtering for Data Exports
const BLOCKED_PROPERTIES = ['$ip', 'email', 'phone', 'name', 'address', 'ssn'];
async function safeExport(hogql: string) {
const response = await fetch(
`https://app.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: hogql } }),
}
);
const data = await response.json();
if (data.columns && data.results) {
const blockedIndexes = new Set(
data.columns.map((col: , : ) =>
.( col.().(b)) ? i : -
).( i >= )
);
data. = data..( !blockedIndexes.(i));
data. = data..(
row.( !blockedIndexes.(i))
);
}
data;
}
Error Handling
| Issue | Cause | Solution |
|---|
| PII in autocapture events | Form data captured automatically | Disable autocapture, use manual capture |
| IP address in events | Not stripped by sanitize_properties | Add delete properties['$ip'] |
| Consent not persisted | opt_out state lost on reload | Store consent in cookie, call opt_out on load |
| Deletion API returns 404 | Wrong person ID or already deleted | Search by email first, check response |
| Session recordings show PII | Text not masked | Add maskAllInputs: true and maskTextSelector |
GDPR Compliance Checklist
Output
- Privacy-safe PostHog initialization with property sanitization
- Consent-based tracking with opt-in/opt-out
- GDPR Subject Access Request handler
- GDPR Data Deletion handler
- PII-safe data export function
Resources