agentsclimarketplace

Posthog data handling

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/posthog-pack/skills/posthog-data-handling

425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill posthog-data-handling

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

'PostHog PII handling, GDPR compliance, consent management, data deletion,

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

9.2 KB, as published. Nobody here has run it

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

IssueCauseSolution
PII in autocapture eventsForm data captured automaticallyDisable autocapture, use manual capture
IP address in eventsNot stripped by sanitize_propertiesAdd delete properties['$ip']
Consent not persistedopt_out state lost on reloadStore consent in cookie, call opt_out on load
Deletion API returns 404Wrong person ID or already deletedSearch by email first, check response
Session recordings show PIIText not maskedAdd 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

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.