[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
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.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.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.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
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