posthog-data-handling by jeremylongshore
PostHog PII handling, GDPR compliance, consent management, data deletion,
Content & Writing
2.3K Stars
321 Forks
Updated Apr 30, 2026, 06:46 AM
Why Use This
This skill provides specialized capabilities for jeremylongshore's codebase.
Use Cases
- Developing new features in the jeremylongshore repository
- Refactoring existing code to follow jeremylongshore standards
- Understanding and working with jeremylongshore's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Repository claude-code-plugins-plus-skills
Skill Version
main
Community
2.3K 321
Updated At Apr 30, 2026, 06:46 AM
Skill Stats
SKILL.md 304 Lines
Total Files 1
Total Size 9.2 KB
License MIT
--- 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.0.0 license: MIT author: Jeremy Longshore <[email protected]> tags: - saas - posthog - compliance compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw --- # 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 ```typescript import posthog from 'posthog-js'; posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { api_host: 'https://us.i.posthog.com', // Disable autocapture to control exactly what's captured autocapture: false, // Respect browser Do Not Track setting respect_dnt: true, // Don't capture until user consents opt_out_capturing_by_default: false, // Set true for opt-in model // Sanitize ALL properties before they leave the browser sanitize_properties: (properties, eventName) => { // Remove IP address delete properties['$ip']; // Remove potentially identifying properties delete properties['$device_id']; // Redact URLs containing tokens or auth info 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]'); } // Redact referrer tokens if (properties['$referrer']) { properties['$referrer'] = properties['$referrer'] .replace(/token=[^&]+/g, 'token=[REDACTED]'); } return properties; }, // Session recording privacy session_recording: { maskAllInputs: true, // Mask all input fields maskTextSelector: '.pii-data', // Mask specific elements }, }); ``` ### Step 2: Consent-Based Tracking ```typescript // Cookie consent integration interface ConsentState { analytics: boolean; functional: boolean; marketing: boolean; } export function handleConsentChange(consent: ConsentState) { if (consent.analytics) { // User opted in — start capturing posthog.opt_in_capturing(); } else { // User opted out — stop capturing and clear local data posthog.opt_out_capturing(); posthog.reset(); // Clears distinct_id, device_id, session data } } // Check consent before identifying (PII) export function identifyWithConsent( userId: string, properties: Record<string, any>, hasAnalyticsConsent: boolean ) { if (!hasAnalyticsConsent) return; // Only send non-PII properties by default const safeProperties: Record<string, any> = { plan: properties.plan, signup_date: properties.signupDate, account_type: properties.accountType, // Do NOT include: email, name, phone, address }; posthog.identify(userId, safeProperties); } // On page load: restore consent state export function restoreConsent() { const consent = getCookieConsent(); // Your consent mechanism if (consent?.analytics === false) { posthog.opt_out_capturing(); } } ``` ### Step 3: GDPR Data Subject Access Request (SAR) ```typescript // Find a person by email and export their data async function handleSubjectAccessRequest(email: string) { const personalKey = process.env.POSTHOG_PERSONAL_API_KEY!; const projectId = process.env.POSTHOG_PROJECT_ID!; // 1. Find the person by email property 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]; // 2. Export their events (strip PII from export) const eventsResponse = await fetch( `https://app.posthog.com/api/projects/${projectId}/query/`, { method: 'POST', headers: { Authorization: `Bearer ${personalKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ query: { kind: 'HogQLQuery', query: `SELECT event, timestamp, properties FROM events WHERE distinct_id = '${distinctId}' ORDER BY timestamp DESC LIMIT 1000`, }, }), } ); const eventsData = await eventsResponse.json(); return { found: true, person: { distinct_ids: person.distinct_ids, properties: person.properties, created_at: person.created_at, }, events_count: eventsData.results?.length || 0, events: eventsData.results, }; } ``` ### Step 4: GDPR Right to Erasure (Data Deletion) ```typescript // Delete a person and all their events async function handleDeletionRequest(email: string) { const personalKey = process.env.POSTHOG_PERSONAL_API_KEY!; const projectId = process.env.POSTHOG_PROJECT_ID!; // 1. Find the person 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; // 2. Delete the person (PostHog also deletes associated events) const deleteResponse = await fetch( `https://app.posthog.com/api/projects/${projectId}/persons/${personId}/`, { method: 'DELETE', headers: { Authorization: `Bearer ${personalKey}` }, } ); if (!deleteResponse.ok) { throw new Error(`Deletion failed: ${deleteResponse.status}`); } return { deleted: true, personId, timestamp: new Date().toISOString(), }; } ``` ### Step 5: Property Filtering for Data Exports ```typescript // Strip PII from HogQL query results before exporting 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(); // Remove blocked columns from results if (data.columns && data.results) { const blockedIndexes = new Set( data.columns.map((col: string, i: number) => BLOCKED_PROPERTIES.some(b => col.toLowerCase().includes(b)) ? i : -1 ).filter((i: number) => i >= 0) ); data.columns = data.columns.filter((_: string, i: number) => !blockedIndexes.has(i)); data.results = data.results.map((row: any[]) => row.filter((_: any, i: number) => !blockedIndexes.has(i)) ); } return 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 - [ ] `sanitize_properties` strips PII before events leave browser - [ ] Consent mechanism with `opt_in_capturing` / `opt_out_capturing` - [ ] `respect_dnt: true` in PostHog init - [ ] Session recording masks all inputs - [ ] Subject Access Request handler implemented - [ ] Data Deletion handler implemented - [ ] Privacy policy updated to mention PostHog analytics ## 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 - [PostHog Privacy Controls](https://posthog.com/docs/privacy) - [PostHog GDPR Compliance](https://posthog.com/docs/privacy/gdpr-compliance) - [PostHog Persons API](https://posthog.com/docs/api/persons) - [PostHog Data Collection Controls](https://posthog.com/docs/privacy/data-collection)
Name Size