Agent Skills: PostHog Data Handling

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/posthog-data-handling

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/posthog-pack/skills/posthog-data-handling

Skill Files

Browse the full folder contents for posthog-data-handling.

Download Skill

Loading file tree…

plugins/saas-packs/posthog-pack/skills/posthog-data-handling/SKILL.md

Skill Metadata

Name
posthog-data-handling
Description
|

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',

  // 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

// 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)

// 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)

// 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

// 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