analytics

Event tracking architecture, Plausible Analytics, PostHog, Google Analytics 4, custom analytics pipelines, privacy-first tracking, and A/B testing integration

39 stars

Best use case

analytics is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Event tracking architecture, Plausible Analytics, PostHog, Google Analytics 4, custom analytics pipelines, privacy-first tracking, and A/B testing integration

Teams using analytics should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/analytics/SKILL.md --create-dirs "https://raw.githubusercontent.com/InugamiDev/ultrathink-oss/main/.claude/skills/analytics/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/analytics/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How analytics Compares

Feature / AgentanalyticsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Event tracking architecture, Plausible Analytics, PostHog, Google Analytics 4, custom analytics pipelines, privacy-first tracking, and A/B testing integration

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Analytics & Event Tracking Skill

## Purpose

Analytics transforms user behavior into actionable data. This skill covers integrating analytics providers (Plausible, PostHog, GA4), designing event taxonomies, building privacy-first tracking, setting up conversion funnels, and implementing A/B testing. The focus is on collecting only what matters while respecting user privacy and complying with GDPR/CCPA.

## Key Concepts

### Analytics Provider Comparison

| Feature | Plausible | PostHog | GA4 | Custom |
|---------|-----------|---------|-----|--------|
| Privacy-first | Yes (no cookies) | Configurable | No (requires consent) | You control it |
| Self-hostable | Yes | Yes | No | Yes |
| Session replay | No | Yes | No | Build it |
| Feature flags | No | Yes | No | Build it |
| A/B testing | No | Yes | Yes (Optimize sunset) | Build it |
| Funnels | Basic | Yes | Yes | Build it |
| Cost | $9+/mo or self-host | Free tier + paid | Free (with limits) | Infrastructure cost |
| Cookie-free | Yes | Optional | No | Your choice |
| GDPR compliant (no consent) | Yes (EU hosting) | Self-hosted only | No | If self-hosted |

### Event Taxonomy

Use a consistent naming convention across your entire application:

```
Format: object.action

Examples:
  page.viewed
  button.clicked
  form.submitted
  signup.started
  signup.completed
  checkout.step_completed
  payment.succeeded
  payment.failed
  feature.used
  search.performed
  error.encountered
```

**Rules:**
- Lowercase, dot-separated: `object.action`
- Past tense for completed actions: `form.submitted` not `form.submit`
- Include context as properties, not in the event name: `button.clicked { label: "Buy Now" }` not `buy_now_button.clicked`

## Workflow

### Step 1: Choose Your Provider

```
Need cookie-free, GDPR-compliant out of the box?
  -> Plausible

Need session replay, feature flags, and funnels?
  -> PostHog

Need to integrate with Google Ads / Search Console?
  -> GA4 (with consent banner)

Need full control and own your data?
  -> Custom analytics pipeline
```

### Step 2: Implement the Analytics Layer

#### Plausible (Privacy-First, No Cookies)

```typescript
// lib/analytics/plausible.ts
const PLAUSIBLE_DOMAIN = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN!;
const PLAUSIBLE_API = process.env.NEXT_PUBLIC_PLAUSIBLE_API_HOST ?? 'https://plausible.io';

export function trackEvent(
  eventName: string,
  props?: Record<string, string | number | boolean>
) {
  if (typeof window === 'undefined') return;
  if (window.location.hostname === 'localhost') return;

  // Uses the Plausible Events API — no cookies, no consent needed
  fetch(`${PLAUSIBLE_API}/api/event`, {
    method: 'POST',
    headers: { 'Content-Type': 'text/plain' },
    body: JSON.stringify({
      n: eventName,     // event name
      u: window.location.href,
      d: PLAUSIBLE_DOMAIN,
      r: document.referrer || null,
      p: props ? JSON.stringify(props) : undefined,
    }),
  }).catch(() => {
    // Silently fail — analytics should never break the app
  });
}

export function trackPageview() {
  trackEvent('pageview');
}
```

```typescript
// app/layout.tsx — Script tag approach (simpler)
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        {process.env.NODE_ENV === 'production' && (
          <Script
            defer
            data-domain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
            data-api={`${process.env.NEXT_PUBLIC_PLAUSIBLE_API_HOST}/api/event`}
            src={`${process.env.NEXT_PUBLIC_PLAUSIBLE_API_HOST}/js/script.js`}
            strategy="afterInteractive"
          />
        )}
      </head>
      <body>{children}</body>
    </html>
  );
}
```

#### PostHog (Full Product Analytics)

```typescript
// lib/analytics/posthog.ts
import posthog from 'posthog-js';

let initialized = false;

export function initPostHog() {
  if (initialized) return;
  if (typeof window === 'undefined') return;
  if (process.env.NODE_ENV !== 'production') return;

  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://us.i.posthog.com',
    person_profiles: 'identified_only', // Only create profiles for identified users
    capture_pageview: false,             // We handle this manually for SPA
    capture_pageleave: true,
    persistence: 'localStorage',         // Avoid cookies if possible
    autocapture: false,                  // Explicit tracking only — less noise
    loaded: (ph) => {
      if (process.env.NODE_ENV === 'development') {
        ph.debug();
      }
    },
  });

  initialized = true;
}

export function identifyUser(userId: string, traits?: Record<string, unknown>) {
  posthog.identify(userId, traits);
}

export function trackEvent(event: string, properties?: Record<string, unknown>) {
  posthog.capture(event, properties);
}

export function trackPageview() {
  posthog.capture('$pageview');
}

export function resetUser() {
  posthog.reset();
}

// Feature flags
export function isFeatureEnabled(flag: string): boolean {
  return posthog.isFeatureEnabled(flag) ?? false;
}

export function getFeatureFlagPayload(flag: string): unknown {
  return posthog.getFeatureFlagPayload(flag);
}
```

```typescript
// components/providers/posthog-provider.tsx
'use client';

import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { initPostHog, trackPageview } from '@/lib/analytics/posthog';

export function PostHogProvider({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    initPostHog();
  }, []);

  useEffect(() => {
    trackPageview();
  }, [pathname, searchParams]);

  return <>{children}</>;
}
```

#### GA4 (With Consent Management)

```typescript
// lib/analytics/ga4.ts
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID!;

// Only load GA after user consents
export function loadGA4() {
  if (typeof window === 'undefined') return;

  const script = document.createElement('script');
  script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`;
  script.async = true;
  document.head.appendChild(script);

  window.dataLayer = window.dataLayer || [];
  function gtag(...args: unknown[]) {
    window.dataLayer.push(args);
  }
  window.gtag = gtag;

  gtag('js', new Date());
  gtag('config', GA_MEASUREMENT_ID, {
    send_page_view: false, // Manual page views for SPA
  });
}

export function trackPageview(url: string) {
  window.gtag?.('event', 'page_view', {
    page_location: url,
  });
}

export function trackEvent(
  eventName: string,
  params?: Record<string, string | number>
) {
  window.gtag?.('event', eventName, params);
}

export function trackConversion(conversionId: string, value?: number) {
  window.gtag?.('event', 'conversion', {
    send_to: conversionId,
    value,
    currency: 'USD',
  });
}

// Type augmentation
declare global {
  interface Window {
    dataLayer: unknown[];
    gtag: (...args: unknown[]) => void;
  }
}
```

#### Custom Analytics (Own Your Data)

```typescript
// lib/analytics/custom.ts
interface AnalyticsEvent {
  name: string;
  properties?: Record<string, unknown>;
  timestamp: number;
  sessionId: string;
  pathname: string;
  referrer: string;
}

const BATCH_SIZE = 10;
const FLUSH_INTERVAL = 5000; // 5 seconds

class AnalyticsClient {
  private queue: AnalyticsEvent[] = [];
  private sessionId: string;
  private flushTimer: ReturnType<typeof setInterval> | null = null;

  constructor(private endpoint: string) {
    this.sessionId = crypto.randomUUID();
    this.startFlushTimer();

    // Flush on page unload
    if (typeof window !== 'undefined') {
      window.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') {
          this.flush();
        }
      });
    }
  }

  track(name: string, properties?: Record<string, unknown>) {
    this.queue.push({
      name,
      properties,
      timestamp: Date.now(),
      sessionId: this.sessionId,
      pathname: typeof window !== 'undefined' ? window.location.pathname : '',
      referrer: typeof document !== 'undefined' ? document.referrer : '',
    });

    if (this.queue.length >= BATCH_SIZE) {
      this.flush();
    }
  }

  private startFlushTimer() {
    this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL);
  }

  private flush() {
    if (this.queue.length === 0) return;

    const events = [...this.queue];
    this.queue = [];

    // Use sendBeacon for reliability on page unload
    if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
      navigator.sendBeacon(
        this.endpoint,
        JSON.stringify({ events })
      );
    } else {
      fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ events }),
        keepalive: true,
      }).catch(() => {
        // Re-queue on failure (with limit to prevent memory leak)
        if (this.queue.length < 100) {
          this.queue.push(...events);
        }
      });
    }
  }

  destroy() {
    if (this.flushTimer) clearInterval(this.flushTimer);
    this.flush();
  }
}

export const analytics = new AnalyticsClient('/api/analytics/events');
```

```typescript
// app/api/analytics/events/route.ts — Server-side ingestion
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const { events } = await req.json();

  // Enrich with server-side data
  const enrichedEvents = events.map((event: Record<string, unknown>) => ({
    ...event,
    ip: req.headers.get('x-forwarded-for')?.split(',')[0] ?? 'unknown',
    userAgent: req.headers.get('user-agent'),
    country: req.headers.get('x-vercel-ip-country') ?? undefined,
    ingestedAt: Date.now(),
  }));

  // Insert into your database (Postgres, ClickHouse, BigQuery, etc.)
  await db.insert(analyticsEvents).values(enrichedEvents);

  return NextResponse.json({ ok: true }, { status: 202 });
}
```

### Step 3: Unified Analytics Abstraction

```typescript
// lib/analytics/index.ts
// Single interface — swap providers without changing app code
import { trackEvent as plausibleTrack } from './plausible';
import { trackEvent as posthogTrack, identifyUser as posthogIdentify } from './posthog';

type AnalyticsProvider = 'plausible' | 'posthog' | 'ga4' | 'custom';

const ACTIVE_PROVIDERS: AnalyticsProvider[] = ['plausible', 'posthog'];

export function track(event: string, properties?: Record<string, unknown>) {
  for (const provider of ACTIVE_PROVIDERS) {
    switch (provider) {
      case 'plausible':
        plausibleTrack(event, properties as Record<string, string | number | boolean>);
        break;
      case 'posthog':
        posthogTrack(event, properties);
        break;
    }
  }
}

export function identify(userId: string, traits?: Record<string, unknown>) {
  if (ACTIVE_PROVIDERS.includes('posthog')) {
    posthogIdentify(userId, traits);
  }
}

export { trackPageview } from './plausible';
```

### Step 4: Conversion Funnels

```typescript
// Define funnel steps as a typed constant
const SIGNUP_FUNNEL = [
  'signup.landing_viewed',
  'signup.form_started',
  'signup.email_entered',
  'signup.password_entered',
  'signup.submitted',
  'signup.email_verified',
  'signup.onboarding_started',
  'signup.onboarding_completed',
] as const;

// Track each step
function trackFunnelStep(step: (typeof SIGNUP_FUNNEL)[number], meta?: Record<string, unknown>) {
  track(step, {
    ...meta,
    funnel: 'signup',
    step_index: SIGNUP_FUNNEL.indexOf(step),
    total_steps: SIGNUP_FUNNEL.length,
  });
}

// Usage
trackFunnelStep('signup.form_started', { source: 'hero_cta' });
trackFunnelStep('signup.submitted', { plan: 'pro' });
```

### Step 5: A/B Testing (PostHog)

```typescript
// components/ab-test.tsx
'use client';

import { useEffect, useState } from 'react';
import { isFeatureEnabled, getFeatureFlagPayload } from '@/lib/analytics/posthog';

interface ABTestProps {
  flag: string;
  control: React.ReactNode;
  variant: React.ReactNode;
}

export function ABTest({ flag, control, variant }: ABTestProps) {
  const [isVariant, setIsVariant] = useState(false);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    // Wait for PostHog to load feature flags
    const check = () => {
      setIsVariant(isFeatureEnabled(flag));
      setReady(true);
    };

    // PostHog may not have loaded flags yet
    if (typeof window !== 'undefined' && window.__POSTHOG_READY__) {
      check();
    } else {
      // Poll briefly for PostHog readiness
      const timer = setTimeout(check, 500);
      return () => clearTimeout(timer);
    }
  }, [flag]);

  if (!ready) return <>{control}</>; // Default to control during load

  return <>{isVariant ? variant : control}</>;
}

// Usage
<ABTest
  flag="new-pricing-page"
  control={<PricingPageV1 />}
  variant={<PricingPageV2 />}
/>
```

## Best Practices

1. **Never block rendering for analytics** — Load scripts with `defer` or `afterInteractive`. Fire events asynchronously. Analytics failures must never break the app.
2. **Use a unified abstraction layer** — Wrap all providers behind a single `track()` function so you can swap providers without changing application code.
3. **Batch events and use `sendBeacon`** — Batching reduces HTTP requests. `sendBeacon` guarantees delivery on page unload, unlike `fetch`.
4. **Respect Do Not Track** — Check `navigator.globalPrivacyControl` and `navigator.doNotTrack`. For Plausible, this is handled automatically.
5. **Keep event names stable** — Changing event names breaks funnels and dashboards. Treat your event taxonomy like a public API.
6. **Track properties, not event variants** — Use `button.clicked { label: "Sign Up" }` instead of `signup_button_clicked`. This keeps your event count manageable.
7. **Separate analytics API from app API** — Analytics ingestion endpoints should not share rate limits or auth with your main API. Use a dedicated route.
8. **Test tracking in development** — Log events to console in dev mode instead of sending to the provider. Verify events fire before deploying.

## Common Pitfalls

| Pitfall | Symptom | Fix |
|---------|---------|-----|
| **Analytics blocking page load** | Lighthouse flags third-party scripts; LCP regresses | Load scripts with `defer` and `afterInteractive`; use `sendBeacon` for events |
| **No consent banner for GA4** | GDPR violation; fines from EU regulators | Use Plausible (no consent needed) or implement a consent banner that gates GA4 loading |
| **Event name proliferation** | Hundreds of uniquely named events, impossible to analyze | Use `object.action` taxonomy with properties for context, not separate event names |
| **Missing page views in SPA** | Only the initial page load is tracked | Manually fire `trackPageview()` on route changes using `usePathname()` |
| **Ad blockers blocking analytics** | 30-40% of events never arrive | Self-host Plausible/PostHog or proxy through your own domain (`/api/analytics/...`) |
| **Tracking PII in event properties** | Privacy violation; data breach liability | Never include email, name, or IP in client-side event properties; enrich server-side if needed |
| **No local development filtering** | Dev/test events pollute production dashboards | Check `process.env.NODE_ENV` or `window.location.hostname` before sending events |
| **Lost events on page unload** | Conversion events at end of flow are undercounted | Use `navigator.sendBeacon()` or `fetch` with `keepalive: true` for critical events |

Related Skills

ultrathink

39
from InugamiDev/ultrathink-oss

UltraThink Workflow OS — 4-layer skill mesh with persistent memory and privacy hooks for complex engineering tasks. Routes prompts through intent detection to activate the right domain skills automatically.

ultrathink_review

39
from InugamiDev/ultrathink-oss

Multi-pass code review powered by UltraThink's quality gate — checks correctness, security (OWASP), performance, readability, and project conventions in a single structured pass.

ultrathink_memory

39
from InugamiDev/ultrathink-oss

Persistent memory system for UltraThink — search, save, and recall project context, decisions, and patterns across sessions using Postgres-backed fuzzy search with synonym expansion.

ui-design

39
from InugamiDev/ultrathink-oss

Comprehensive UI design system: 230+ font pairings, 48 themes, 65 design systems, 23 design languages, 30 UX laws, 14 color systems, Swiss grid, Gestalt principles, Pencil.dev workflow. Inherits ui-ux-pro-max (99 UX rules) + impeccable-frontend-design (anti-AI-slop). Triggers on any design, UI, layout, typography, color, theme, or styling task.

Zod

39
from InugamiDev/ultrathink-oss

> TypeScript-first schema validation with static type inference.

webinar-registration-page

39
from InugamiDev/ultrathink-oss

Build a webinar or live event registration page as a self-contained HTML file with countdown timer, speaker bio, agenda, and registration form. Triggers on: "build a webinar registration page", "create a webinar sign-up page", "event registration landing page", "live training registration page", "workshop sign-up page", "create a webinar page", "build an event page", "free webinar landing page", "live demo registration page", "online event page", "create a registration page for my webinar", "build a training event page".

webhooks

39
from InugamiDev/ultrathink-oss

Webhook design patterns — delivery, retry with exponential backoff, HMAC signature verification, payload validation, idempotency keys

web-workers

39
from InugamiDev/ultrathink-oss

Offload heavy computation from the main thread using Web Workers, SharedWorkers, and Comlink — structured messaging, transferable objects, and off-main-thread architecture patterns

web-vitals

39
from InugamiDev/ultrathink-oss

Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js

web-components

39
from InugamiDev/ultrathink-oss

Native Web Components, custom elements API, Shadow DOM, HTML templates, slots, lifecycle callbacks, and framework-agnostic design patterns

wasm

39
from InugamiDev/ultrathink-oss

WebAssembly integration — Rust to WASM with wasm-pack/wasm-bindgen, WASI, browser usage, server-side WASM, and performance considerations

vue

39
from InugamiDev/ultrathink-oss

Vue 3 Composition API, Nuxt patterns, reactivity system, component architecture, and production development practices