[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build Feature Usage Analytics

Build Feature Usage Analytics

Build feature usage analytics with event tracking, adoption funnels, cohort analysis, feature retention curves, and product decision dashboards for data-driven product development.

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

Sam leads product at a 25-person SaaS. They shipped 30 features last year but don't know which ones users actually use. The "advanced filters" feature took 3 months to build — 4% adoption. Nobody discovered it. The "quick export" took 2 weeks — 60% adoption. Product decisions are based on loudest customer requests, not usage data. Feature flags exist but there's no analytics on post-launch adoption. They need feature usage tracking: who uses what, adoption curves, feature retention, discovery funnel, and data to decide what to build (or kill) next.

Step 1: Build the Analytics Engine

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);

interface FeatureEvent { userId: string; feature: string; action: "viewed" | "activated" | "used" | "completed"; metadata?: Record<string, any>; timestamp: number; }
interface FeatureMetrics { feature: string; totalUsers: number; activeUsers: number; adoptionRate: number; avgUsagePerUser: number; retentionDay7: number; retentionDay30: number; discoveryRate: number; }

// Track feature usage event
export async function trackFeatureEvent(event: FeatureEvent): Promise<void> {
  const day = new Date(event.timestamp).toISOString().slice(0, 10);
  const pipeline = redis.pipeline();
  pipeline.sadd(`feature:users:${event.feature}`, event.userId);
  pipeline.sadd(`feature:users:${event.feature}:${day}`, event.userId);
  pipeline.hincrby(`feature:events:${event.feature}`, event.action, 1);
  pipeline.hincrby(`feature:events:${event.feature}:${day}`, event.action, 1);
  if (event.action === "activated") {
    pipeline.sadd(`feature:activated:${event.feature}`, event.userId);
    pipeline.set(`feature:first_use:${event.feature}:${event.userId}`, event.timestamp);
  }
  await pipeline.exec();

  await pool.query(
    `INSERT INTO feature_events (user_id, feature, action, metadata, created_at) VALUES ($1, $2, $3, $4, to_timestamp($5 / 1000.0))`,
    [event.userId, event.feature, event.action, JSON.stringify(event.metadata || {}), event.timestamp]
  );
}

// Get metrics for a feature
export async function getFeatureMetrics(feature: string): Promise<FeatureMetrics> {
  const totalUsers = await redis.scard(`feature:users:${feature}`);
  const today = new Date().toISOString().slice(0, 10);
  const activeUsers = await redis.scard(`feature:users:${feature}:${today}`);
  const activatedUsers = await redis.scard(`feature:activated:${feature}`);

  // Total registered users
  const { rows: [{ count: totalRegistered }] } = await pool.query("SELECT COUNT(*) as count FROM users WHERE status = 'active'");
  const adoptionRate = parseInt(totalRegistered) > 0 ? (totalUsers / parseInt(totalRegistered)) * 100 : 0;

  // Average usage per user
  const events = await redis.hgetall(`feature:events:${feature}`);
  const totalUsed = parseInt(events.used || "0");
  const avgUsage = totalUsers > 0 ? totalUsed / totalUsers : 0;

  // Discovery rate (viewed → activated)
  const viewed = parseInt(events.viewed || "0");
  const activated = parseInt(events.activated || "0");
  const discoveryRate = viewed > 0 ? (activated / viewed) * 100 : 0;

  // Retention
  const retentionDay7 = await calculateRetention(feature, 7);
  const retentionDay30 = await calculateRetention(feature, 30);

  return { feature, totalUsers, activeUsers, adoptionRate: Math.round(adoptionRate * 10) / 10, avgUsagePerUser: Math.round(avgUsage * 10) / 10, retentionDay7, retentionDay30, discoveryRate: Math.round(discoveryRate * 10) / 10 };
}

async function calculateRetention(feature: string, days: number): Promise<number> {
  const targetDate = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
  const firstUseDate = new Date(Date.now() - (days + 7) * 86400000).toISOString().slice(0, 10);

  // Users who first used the feature around firstUseDate
  const { rows: firstUsers } = await pool.query(
    `SELECT DISTINCT user_id FROM feature_events WHERE feature = $1 AND action = 'activated' AND created_at::date = $2`,
    [feature, firstUseDate]
  );
  if (firstUsers.length === 0) return 0;

  // Of those, how many used it again on targetDate or after
  const userIds = firstUsers.map((r: any) => r.user_id);
  const { rows: [{ count: retained }] } = await pool.query(
    `SELECT COUNT(DISTINCT user_id) as count FROM feature_events WHERE feature = $1 AND user_id = ANY($2) AND created_at::date >= $3`,
    [feature, userIds, targetDate]
  );

  return Math.round((parseInt(retained) / firstUsers.length) * 100);
}

// Get adoption funnel for a feature
export async function getAdoptionFunnel(feature: string): Promise<Array<{ stage: string; users: number; dropoff: number }>> {
  const events = await redis.hgetall(`feature:events:${feature}`);
  const stages = [
    { stage: "Viewed", users: parseInt(events.viewed || "0") },
    { stage: "Activated", users: parseInt(events.activated || "0") },
    { stage: "Used (1x)", users: parseInt(events.used || "0") > 0 ? await redis.scard(`feature:users:${feature}`) : 0 },
    { stage: "Used (5x+)", users: 0 },
    { stage: "Completed", users: parseInt(events.completed || "0") },
  ];

  return stages.map((s, i) => ({
    ...s,
    dropoff: i > 0 && stages[i - 1].users > 0 ? Math.round(((stages[i - 1].users - s.users) / stages[i - 1].users) * 100) : 0,
  }));
}

// Dashboard: all features ranked by usage
export async function getFeatureDashboard(): Promise<FeatureMetrics[]> {
  const { rows: features } = await pool.query("SELECT DISTINCT feature FROM feature_events");
  const metrics: FeatureMetrics[] = [];
  for (const f of features) {
    metrics.push(await getFeatureMetrics(f.feature));
  }
  return metrics.sort((a, b) => b.adoptionRate - a.adoptionRate);
}

// Middleware: auto-track feature views
export function featureTrackingMiddleware(featureMap: Record<string, string>) {
  return async (c: any, next: any) => {
    await next();
    const feature = featureMap[c.req.path];
    if (feature && c.get("userId")) {
      trackFeatureEvent({ userId: c.get("userId"), feature, action: "used", timestamp: Date.now() }).catch(() => {});
    }
  };
}

Results

  • Kill underperformers — "advanced filters": 4% adoption, 20% day-7 retention → deprioritized; "quick export": 60% adoption, 75% retention → invested more; data-driven roadmap
  • Discovery problem solved — feature funnel shows 500 viewed → 50 activated (90% dropoff!); added onboarding tooltip → 500 viewed → 250 activated; 5x improvement
  • Retention curves reveal quality — feature with high adoption but low retention = novelty; feature with moderate adoption but high retention = core value; different product strategies
  • Automatic tracking — middleware auto-tracks feature usage per endpoint; no manual event calls; every API endpoint mapped to a feature
  • Product decisions backed by data — stakeholder says "we need feature X"; dashboard shows 3% of users even tried similar feature Y; build something else

Related use cases