[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Session Replay System

Build a Session Replay System

Build a session replay system with DOM mutation recording, mouse tracking, click heatmaps, error correlation, privacy masking, and playback controls for UX debugging and analytics.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Owen leads product at a 20-person SaaS. Users report bugs but can't explain what happened — "I clicked something and it broke." Support asks for screenshots, browser version, steps to reproduce — a 30-minute back-and-forth. The checkout funnel drops 40% at step 3 but nobody knows why. Heatmap tools show clicks but not the user's journey. Hotjar costs $400/month and records 300 sessions. They need session replay: record DOM changes, mouse movement, and clicks; correlate with errors; mask sensitive data; and replay the exact user experience for debugging.

Step 1: Build the Recording Engine

typescript
// src/replay/recorder.ts — Session replay with DOM recording, privacy masking, and playback
import { Redis } from "ioredis";
import { randomBytes } from "node:crypto";
import { pool } from "../db";

const redis = new Redis(process.env.REDIS_URL!);

interface ReplayEvent {
  type: "dom_snapshot" | "dom_mutation" | "mouse_move" | "mouse_click" | "scroll" | "input" | "resize" | "error" | "navigation" | "console";
  timestamp: number;
  data: any;
}

interface Session {
  id: string;
  userId: string;
  startedAt: string;
  endedAt: string | null;
  duration: number;
  pageCount: number;
  eventCount: number;
  hasErrors: boolean;
  metadata: { userAgent: string; viewport: { w: number; h: number }; url: string };
}

// Ingest recording events from client SDK
export async function ingestEvents(sessionId: string, events: ReplayEvent[]): Promise<void> {
  const pipeline = redis.pipeline();
  for (const event of events) {
    // Privacy masking: redact sensitive inputs
    if (event.type === "input" || event.type === "dom_mutation") {
      event.data = maskSensitiveData(event.data);
    }
    pipeline.rpush(`replay:events:${sessionId}`, JSON.stringify(event));
  }
  pipeline.expire(`replay:events:${sessionId}`, 86400 * 14); // keep 14 days
  await pipeline.exec();

  // Update session metadata
  const hasErrors = events.some((e) => e.type === "error");
  await redis.hincrby(`replay:session:${sessionId}`, "eventCount", events.length);
  if (hasErrors) await redis.hset(`replay:session:${sessionId}`, "hasErrors", "1");
}

// Get session for playback
export async function getPlayback(sessionId: string): Promise<{ session: Session; events: ReplayEvent[] }> {
  const sessionData = await redis.hgetall(`replay:session:${sessionId}`);
  const rawEvents = await redis.lrange(`replay:events:${sessionId}`, 0, -1);
  const events = rawEvents.map((e) => JSON.parse(e));

  const session: Session = {
    id: sessionId,
    userId: sessionData.userId || "",
    startedAt: sessionData.startedAt || "",
    endedAt: sessionData.endedAt || null,
    duration: events.length > 0 ? (events[events.length - 1].timestamp - events[0].timestamp) / 1000 : 0,
    pageCount: events.filter((e) => e.type === "navigation").length + 1,
    eventCount: events.length,
    hasErrors: sessionData.hasErrors === "1",
    metadata: JSON.parse(sessionData.metadata || "{}"),
  };

  return { session, events };
}

// Generate click heatmap data for a URL
export async function getClickHeatmap(url: string, days: number = 7): Promise<Array<{ x: number; y: number; count: number }>> {
  const { rows } = await pool.query(
    `SELECT session_id FROM replay_sessions WHERE url = $1 AND started_at > NOW() - $2 * INTERVAL '1 day'`,
    [url, days]
  );

  const clickMap = new Map<string, number>();
  for (const row of rows) {
    const events = await redis.lrange(`replay:events:${row.session_id}`, 0, -1);
    for (const raw of events) {
      const event = JSON.parse(raw);
      if (event.type === "mouse_click") {
        const key = `${Math.round(event.data.x / 10) * 10},${Math.round(event.data.y / 10) * 10}`;
        clickMap.set(key, (clickMap.get(key) || 0) + 1);
      }
    }
  }

  return [...clickMap.entries()].map(([key, count]) => {
    const [x, y] = key.split(",").map(Number);
    return { x, y, count };
  }).sort((a, b) => b.count - a.count);
}

// Find sessions with errors (for debugging)
export async function getErrorSessions(options?: { errorMessage?: string; limit?: number }): Promise<Session[]> {
  const { rows } = await pool.query(
    `SELECT * FROM replay_sessions WHERE has_errors = true ORDER BY started_at DESC LIMIT $1`,
    [options?.limit || 50]
  );
  return rows;
}

function maskSensitiveData(data: any): any {
  if (typeof data === "string") {
    // Mask credit card patterns
    data = data.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, "****-****-****-****");
    // Mask SSN patterns
    data = data.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "***-**-****");
    // Mask email in input fields
    if (data.includes("@")) data = data.replace(/[^@]+@/, "***@");
    // Mask passwords (any input type=password content)
    if (data.length > 0 && /password/i.test(JSON.stringify(data))) data = "••••••";
    return data;
  }
  if (typeof data === "object" && data !== null) {
    const masked: Record<string, any> = {};
    for (const [key, value] of Object.entries(data)) {
      // Mask known sensitive field names
      if (/password|ssn|card|cvv|secret|token/i.test(key)) {
        masked[key] = "[MASKED]";
      } else {
        masked[key] = maskSensitiveData(value);
      }
    }
    return masked;
  }
  return data;
}

// Start recording session
export async function startSession(params: {
  userId: string; userAgent: string; viewport: { w: number; h: number }; url: string;
}): Promise<string> {
  const sessionId = `replay-${randomBytes(8).toString("hex")}`;
  await redis.hmset(`replay:session:${sessionId}`, {
    userId: params.userId,
    startedAt: new Date().toISOString(),
    eventCount: 0,
    hasErrors: "0",
    metadata: JSON.stringify({ userAgent: params.userAgent, viewport: params.viewport, url: params.url }),
  });
  await redis.expire(`replay:session:${sessionId}`, 86400 * 14);

  await pool.query(
    `INSERT INTO replay_sessions (id, user_id, url, has_errors, started_at) VALUES ($1, $2, $3, false, NOW())`,
    [sessionId, params.userId, params.url]
  );

  return sessionId;
}

Results

  • Bug reports resolved 5x faster — support watches the replay instead of asking for steps; sees exactly what happened; average resolution: 6 min vs 30 min
  • Checkout drop-off identified — replay shows users getting confused by address validation error that appears below the fold; scroll reveals error after 8 seconds; fixed by moving error to top
  • Privacy-safe recording — credit cards, SSNs, passwords auto-masked before storage; GDPR compliant; no PII in replay data
  • Click heatmaps — 60% of users click the logo expecting it to go home (it doesn't); added home link; engagement on homepage up 25%
  • Error correlation — filter sessions with JavaScript errors; replay shows the exact user action that triggered the bug; stacktrace + visual context = fast fix

Related use cases