YugenYugen

Example Feature 1

Example feature implementation guide

This is a placeholder feature guide. Replace this content with your actual feature documentation.

Overview

This is an example feature guide that demonstrates how to document features in your Kaizen application.

Architecture Flow

Data Flow Diagram

Feature Integration Architecture

Prerequisites

  • ✅ Feature is implemented and tested locally
  • ✅ Database schema changes (if any) are deployed
  • ✅ Environment variables are configured
  • ✅ Feature flags are enabled in config.ts

Implementation Plan

Phase 1: Database Schema & Backend Functions

Database Schema Updates

// convex/schema.ts
export const notifications = defineTable({
  userId: v.id("users"),
  type: v.union(
    v.literal("feature_alert"),
    v.literal("system_update"),
    v.literal("user_action")
  ),
  title: v.string(),
  message: v.string(),
  isRead: v.boolean(),
  metadata: v.optional(v.object({
    featureId: v.optional(v.id("features")),
    actionUrl: v.optional(v.string()),
    priority: v.optional(v.union(v.literal("low"), v.literal("medium"), v.literal("high")))
  })),
  createdAt: v.number(),
  expiresAt: v.optional(v.number())
})
  .index("by_user", ["userId"])
  .index("by_user_unread", ["userId", "isRead"])
  .index("by_expiry", ["expiresAt"]);

export const featureUsage = defineTable({
  userId: v.id("users"),
  featureId: v.id("features"),
  action: v.string(),
  metadata: v.optional(v.object({
    duration: v.optional(v.number()),
    success: v.optional(v.boolean()),
    errorMessage: v.optional(v.string())
  })),
  timestamp: v.number(),
  sessionId: v.optional(v.string())
})
  .index("by_user_feature", ["userId", "featureId"])
  .index("by_timestamp", ["timestamp"]);

Convex Functions Implementation

// convex/notifications.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";

export const getUserNotifications = query({
  args: { 
    userId: v.id("users"),
    limit: v.optional(v.number()),
    unreadOnly: v.optional(v.boolean())
  },
  handler: async (ctx, args) => {
    const { userId, limit = 50, unreadOnly = false } = args;
    
    let query = ctx.db
      .query("notifications")
      .withIndex("by_user", (q) => q.eq("userId", userId))
      .filter((q) => q.or(
        q.eq(q.field("expiresAt"), undefined),
        q.gt(q.field("expiresAt"), Date.now())
      ));

    if (unreadOnly) {
      query = query.filter((q) => q.eq(q.field("isRead"), false));
    }

    return await query
      .order("desc")
      .take(limit);
  },
});

export const markNotificationAsRead = mutation({
  args: { notificationId: v.id("notifications") },
  handler: async (ctx, args) => {
    await ctx.db.patch(args.notificationId, { isRead: true });
  },
});

export const createNotification = mutation({
  args: {
    userId: v.id("users"),
    type: v.union(
      v.literal("feature_alert"),
      v.literal("system_update"),
      v.literal("user_action")
    ),
    title: v.string(),
    message: v.string(),
    metadata: v.optional(v.object({
      featureId: v.optional(v.id("features")),
      actionUrl: v.optional(v.string()),
      priority: v.optional(v.union(v.literal("low"), v.literal("medium"), v.literal("high")))
    })),
    expiresAt: v.optional(v.number())
  },
  handler: async (ctx, args) => {
    return await ctx.db.insert("notifications", {
      ...args,
      isRead: false,
      createdAt: Date.now()
    });
  },
});

// convex/analytics.ts
export const trackFeatureUsage = mutation({
  args: {
    userId: v.id("users"),
    featureId: v.id("features"),
    action: v.string(),
    metadata: v.optional(v.object({
      duration: v.optional(v.number()),
      success: v.optional(v.boolean()),
      errorMessage: v.optional(v.string())
    })),
    sessionId: v.optional(v.string())
  },
  handler: async (ctx, args) => {
    return await ctx.db.insert("featureUsage", {
      ...args,
      timestamp: Date.now()
    });
  },
});

export const getFeatureAnalytics = query({
  args: {
    featureId: v.id("features"),
    timeRange: v.optional(v.union(
      v.literal("24h"),
      v.literal("7d"),
      v.literal("30d")
    ))
  },
  handler: async (ctx, args) => {
    const { featureId, timeRange = "7d" } = args;
    
    const timeRanges = {
      "24h": 24 * 60 * 60 * 1000,
      "7d": 7 * 24 * 60 * 60 * 1000,
      "30d": 30 * 24 * 60 * 60 * 1000
    };
    
    const cutoff = Date.now() - timeRanges[timeRange];
    
    const usage = await ctx.db
      .query("featureUsage")
      .withIndex("by_user_feature", (q) => q.eq("featureId", featureId))
      .filter((q) => q.gt(q.field("timestamp"), cutoff))
      .collect();

    // Aggregate analytics
    const totalUsage = usage.length;
    const uniqueUsers = new Set(usage.map(u => u.userId)).size;
    const successRate = usage.filter(u => u.metadata?.success !== false).length / totalUsage;
    const avgDuration = usage
      .filter(u => u.metadata?.duration)
      .reduce((sum, u) => sum + (u.metadata?.duration || 0), 0) / totalUsage;

    return {
      totalUsage,
      uniqueUsers,
      successRate: Math.round(successRate * 100),
      avgDuration: Math.round(avgDuration),
      timeRange
    };
  },
});

Phase 2: Frontend Components & Hooks

Custom Hooks for Feature Management

// app/hooks/use-notifications.ts
import { useQuery, useMutation } from "convex/react";
import { api } from "~/convex/_generated/api";
import { useSession } from "better-auth/react";
import { useCallback } from "react";

export function useNotifications() {
  const { data: session } = useSession();
  const userId = session?.user?.id as Id<"users"> | undefined;

  const notifications = useQuery(
    api.notifications.getUserNotifications,
    userId ? { userId, unreadOnly: true } : "skip"
  );

  const markAsRead = useMutation(api.notifications.markNotificationAsRead);
  const createNotification = useMutation(api.notifications.createNotification);

  const markNotificationAsRead = useCallback(
    async (notificationId: Id<"notifications">) => {
      await markAsRead({ notificationId });
    },
    [markAsRead]
  );

  const sendNotification = useCallback(
    async (notification: {
      type: "feature_alert" | "system_update" | "user_action";
      title: string;
      message: string;
      metadata?: {
        featureId?: Id<"features">;
        actionUrl?: string;
        priority?: "low" | "medium" | "high";
      };
      expiresAt?: number;
    }) => {
      if (!userId) return;
      
      await createNotification({
        userId,
        ...notification
      });
    },
    [createNotification, userId]
  );

  return {
    notifications: notifications || [],
    unreadCount: notifications?.filter(n => !n.isRead).length || 0,
    markNotificationAsRead,
    sendNotification,
    isLoading: notifications === undefined
  };
}

// app/hooks/use-feature-analytics.ts
import { useQuery, useMutation } from "convex/react";
import { api } from "~/convex/_generated/api";
import { useCallback } from "react";

export function useFeatureAnalytics(featureId: Id<"features">) {
  const analytics = useQuery(api.analytics.getFeatureAnalytics, { featureId });
  const trackUsage = useMutation(api.analytics.trackFeatureUsage);

  const trackFeatureUsage = useCallback(
    async (action: string, metadata?: {
      duration?: number;
      success?: boolean;
      errorMessage?: string;
    }) => {
      const { data: session } = useSession();
      if (!session?.user) return;

      await trackUsage({
        userId: session.user.id as Id<"users">,
        featureId,
        action,
        metadata,
        sessionId: crypto.randomUUID()
      });
    },
    [trackUsage, featureId]
  );

  return {
    analytics,
    trackFeatureUsage,
    isLoading: analytics === undefined
  };
}

Notification Components

// app/components/notifications/notification-bell.tsx
import { Bell, Check } from "lucide-react";
import { Button } from "~/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuTrigger,
  DropdownMenuItem,
  DropdownMenuSeparator,
} from "~/components/ui/dropdown-menu";
import { Badge } from "~/components/ui/badge";
import { useNotifications } from "~/hooks/use-notifications";
import { formatDistanceToNow } from "date-fns";

export function NotificationBell() {
  const { notifications, unreadCount, markNotificationAsRead } = useNotifications();

  const handleMarkAsRead = async (notificationId: Id<"notifications">) => {
    await markNotificationAsRead(notificationId);
  };

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon" className="relative">
          <Bell className="h-5 w-5" />
          {unreadCount > 0 && (
            <Badge 
              variant="destructive" 
              className="absolute -top-1 -right-1 h-5 w-5 p-0 text-xs flex items-center justify-center"
            >
              {unreadCount > 99 ? "99+" : unreadCount}
            </Badge>
          )}
        </Button>
      </DropdownMenuTrigger>
      
      <DropdownMenuContent align="end" className="w-80">
        <div className="p-2">
          <h4 className="font-semibold">Notifications</h4>
        </div>
        <DropdownMenuSeparator />
        
        {notifications.length === 0 ? (
          <div className="p-4 text-center text-muted-foreground">
            No new notifications
          </div>
        ) : (
          notifications.slice(0, 5).map((notification) => (
            <DropdownMenuItem
              key={notification._id}
              className="flex flex-col items-start gap-1 p-3"
              onClick={() => handleMarkAsRead(notification._id)}
            >
              <div className="flex items-center justify-between w-full">
                <span className="font-medium text-sm">{notification.title}</span>
                {!notification.isRead && (
                  <Check className="h-3 w-3 text-blue-500" />
                )}
              </div>
              <p className="text-xs text-muted-foreground line-clamp-2">
                {notification.message}
              </p>
              <span className="text-xs text-muted-foreground">
                {formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })}
              </span>
            </DropdownMenuItem>
          ))
        )}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

// app/components/notifications/notification-toast.tsx
import { toast } from "sonner";
import { useNotifications } from "~/hooks/use-notifications";
import { useEffect } from "react";

export function NotificationToast() {
  const { notifications } = useNotifications();

  useEffect(() => {
    const latestNotification = notifications[0];
    if (latestNotification && !latestNotification.isRead) {
      toast.info(latestNotification.title, {
        description: latestNotification.message,
        action: latestNotification.metadata?.actionUrl ? {
          label: "View",
          onClick: () => window.open(latestNotification.metadata?.actionUrl, "_blank")
        } : undefined,
        duration: latestNotification.metadata?.priority === "high" ? 10000 : 5000,
      });
    }
  }, [notifications]);

  return null;
}

Feature Analytics Dashboard

// app/components/analytics/feature-dashboard.tsx
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Progress } from "~/components/ui/progress";
import { useFeatureAnalytics } from "~/hooks/use-feature-analytics";
import { TrendingUp, Users, Clock, CheckCircle } from "lucide-react";

interface FeatureDashboardProps {
  featureId: Id<"features">;
  featureName: string;
}

export function FeatureDashboard({ featureId, featureName }: FeatureDashboardProps) {
  const { analytics, isLoading } = useFeatureAnalytics(featureId);

  if (isLoading) {
    return (
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
        {Array.from({ length: 4 }).map((_, i) => (
          <Card key={i}>
            <CardHeader className="pb-2">
              <div className="h-4 bg-muted animate-pulse rounded" />
            </CardHeader>
            <CardContent>
              <div className="h-8 bg-muted animate-pulse rounded" />
            </CardContent>
          </Card>
        ))}
      </div>
    );
  }

  if (!analytics) return null;

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h2 className="text-2xl font-bold">{featureName} Analytics</h2>
        <Badge variant="outline" className="text-xs">
          Last {analytics.timeRange}
        </Badge>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Total Usage</CardTitle>
            <TrendingUp className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{analytics.totalUsage.toLocaleString()}</div>
            <p className="text-xs text-muted-foreground">
              +12% from last period
            </p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Unique Users</CardTitle>
            <Users className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{analytics.uniqueUsers}</div>
            <p className="text-xs text-muted-foreground">
              {Math.round((analytics.uniqueUsers / analytics.totalUsage) * 100)}% of total usage
            </p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Success Rate</CardTitle>
            <CheckCircle className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{analytics.successRate}%</div>
            <Progress value={analytics.successRate} className="mt-2" />
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Avg Duration</CardTitle>
            <Clock className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{analytics.avgDuration}ms</div>
            <p className="text-xs text-muted-foreground">
              Average response time
            </p>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

Phase 3: Route Integration & Layout Updates

Dashboard Route with Notifications

// app/routes/dashboard/index.tsx
import type { Route } from "./+types/index";
import { FeatureDashboard } from "~/components/analytics/feature-dashboard";
import { NotificationToast } from "~/components/notifications/notification-toast";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";

export default function DashboardRoute(props: Route.ComponentProps) {
  return (
    <div className="container mx-auto py-8 space-y-8">
      <NotificationToast />
      
      <div className="flex items-center justify-between">
        <h1 className="text-3xl font-bold">Dashboard</h1>
      </div>

      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        <div className="lg:col-span-2">
          <FeatureDashboard 
            featureId="feature_123" as Id<"features">
            featureName="Smart Notifications"
          />
        </div>
        
        <div className="space-y-4">
          <Card>
            <CardHeader>
              <CardTitle>Quick Actions</CardTitle>
            </CardHeader>
            <CardContent className="space-y-2">
              <Button className="w-full" variant="outline">
                Send Test Notification
              </Button>
              <Button className="w-full" variant="outline">
                View All Analytics
              </Button>
              <Button className="w-full" variant="outline">
                Export Data
              </Button>
            </CardContent>
          </Card>
        </div>
      </div>
    </div>
  );
}

Layout Updates with Notification Bell

// app/routes/dashboard/layout.tsx
import type { Route } from "./+types/layout";
import { Outlet } from "react-router";
import { NotificationBell } from "~/components/notifications/notification-bell";
import { Button } from "~/components/ui/button";
import { Settings, User } from "lucide-react";

export default function DashboardLayout(props: Route.ComponentProps) {
  return (
    <div className="min-h-screen bg-background">
      <header className="border-b">
        <div className="container mx-auto px-4 py-4">
          <div className="flex items-center justify-between">
            <h1 className="text-xl font-semibold">Kaizen Dashboard</h1>
            
            <div className="flex items-center gap-2">
              <NotificationBell />
              <Button variant="ghost" size="icon">
                <Settings className="h-5 w-5" />
              </Button>
              <Button variant="ghost" size="icon">
                <User className="h-5 w-5" />
              </Button>
            </div>
          </div>
        </div>
      </header>
      
      <main className="container mx-auto px-4 py-8">
        <Outlet />
      </main>
    </div>
  );
}

Testing

Local Testing

  1. Start your development server: bun run dev
  2. Navigate to the feature route
  3. Test all functionality
  4. Verify database operations work correctly

Production Testing

  1. Deploy to staging environment
  2. Test with real data
  3. Verify performance and error handling
  4. Check logs for any issues

Configuration

Feature Flags

Enable the feature in your configuration:

// config.ts
export const config: AppConfig = {
  features: {
    // ... other features
    featureExample: true, // Enable this feature
  },
  // ... rest of config
};

Environment Variables

Add any required environment variables:

# .env.local
FEATURE_EXAMPLE_API_KEY=your_api_key_here
FEATURE_EXAMPLE_WEBHOOK_SECRET=whsec_...

Architectural Changes

Before Implementation

After Implementation

Database Schema Evolution

Troubleshooting

Common Issues

Feature not appearing:

  • Check that the feature flag is enabled in config.ts
  • Verify the route is properly configured
  • Check browser console for errors

Database errors:

  • Ensure schema changes are deployed: bunx convex deploy
  • Check Convex dashboard for function errors
  • Verify user permissions

Performance issues:

  • Check query performance in Convex dashboard
  • Consider adding indexes for frequently queried fields
  • Optimize component re-renders

Next Steps

  • Add authentication checks
  • Implement error boundaries
  • Add loading states
  • Write unit tests
  • Add analytics tracking
  • Consider caching strategies

On this page