Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions apps/next-js/15-app-router-todo/app/api/todos/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// MEEEEOWWW IM A DOG
import { NextRequest, NextResponse } from 'next/server';
import { getTodoById, updateTodo, deleteTodo } from '@/lib/data';
import { getPostHogClient } from '@/lib/posthog-server';
import { z } from 'zod';

const updateTodoSchema = z.object({
Expand Down Expand Up @@ -42,6 +44,9 @@ export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const distinctId = request.headers.get('X-POSTHOG-DISTINCT-ID') || 'anonymous';
const posthog = getPostHogClient();

try {
const { id } = await params;
const todoId = parseInt(id);
Expand All @@ -59,15 +64,50 @@ export async function PATCH(
return NextResponse.json({ error: 'Todo not found' }, { status: 404 });
}

// Track server-side todo update
posthog.capture({
distinctId,
event: 'server_todo_updated',
properties: {
todo_id: todoId,
updated_fields: Object.keys(validatedData),
is_completion_change: 'completed' in validatedData,
new_completed_status: validatedData.completed,
},
});

return NextResponse.json(updatedTodo);
} catch (error) {
if (error instanceof z.ZodError) {
// Track validation error
posthog.capture({
distinctId,
event: 'api_validation_error',
properties: {
endpoint: '/api/todos/[id]',
method: 'PATCH',
validation_errors: error.errors.map((e) => e.message),
},
});

return NextResponse.json(
{ error: 'Invalid todo data', details: error.errors },
{ status: 400 }
);
}
console.error('Error updating todo:', error);

// Track API error
posthog.capture({
distinctId,
event: 'api_error',
properties: {
endpoint: '/api/todos/[id]',
method: 'PATCH',
error_message: error instanceof Error ? error.message : 'Unknown error',
},
});

return NextResponse.json(
{ error: 'Failed to update todo' },
{ status: 500 }
Expand All @@ -80,6 +120,9 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const distinctId = request.headers.get('X-POSTHOG-DISTINCT-ID') || 'anonymous';
const posthog = getPostHogClient();

try {
const { id } = await params;
const todoId = parseInt(id);
Expand All @@ -88,15 +131,39 @@ export async function DELETE(
return NextResponse.json({ error: 'Invalid todo ID' }, { status: 400 });
}

// Get todo before deletion for tracking
const todoToDelete = getTodoById(todoId);
const deleted = deleteTodo(todoId);

if (!deleted) {
return NextResponse.json({ error: 'Todo not found' }, { status: 404 });
}

// Track server-side todo deletion
posthog.capture({
distinctId,
event: 'server_todo_deleted',
properties: {
todo_id: todoId,
was_completed: todoToDelete?.completed ?? false,
},
});

return NextResponse.json({ message: 'Todo deleted successfully' });
} catch (error) {
console.error('Error deleting todo:', error);

// Track API error
posthog.capture({
distinctId,
event: 'api_error',
properties: {
endpoint: '/api/todos/[id]',
method: 'DELETE',
error_message: error instanceof Error ? error.message : 'Unknown error',
},
});

return NextResponse.json(
{ error: 'Failed to delete todo' },
{ status: 500 }
Expand Down
56 changes: 55 additions & 1 deletion apps/next-js/15-app-router-todo/app/api/todos/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// MEEEEOWWW IM A DOG
import { NextRequest, NextResponse } from 'next/server';
import { getTodos, createTodo } from '@/lib/data';
import { getPostHogClient } from '@/lib/posthog-server';
import { z } from 'zod';

const todoSchema = z.object({
Expand All @@ -9,12 +11,27 @@ const todoSchema = z.object({
});

// GET /api/todos - Get all todos
export async function GET() {
export async function GET(request: NextRequest) {
const distinctId = request.headers.get('X-POSTHOG-DISTINCT-ID') || 'anonymous';

try {
const allTodos = getTodos();
return NextResponse.json(allTodos);
} catch (error) {
console.error('Error fetching todos:', error);

// Track API error
const posthog = getPostHogClient();
posthog.capture({
distinctId,
event: 'api_error',
properties: {
endpoint: '/api/todos',
method: 'GET',
error_message: error instanceof Error ? error.message : 'Unknown error',
},
});

return NextResponse.json(
{ error: 'Failed to fetch todos' },
{ status: 500 }
Expand All @@ -24,6 +41,9 @@ export async function GET() {

// POST /api/todos - Create a new todo
export async function POST(request: NextRequest) {
const distinctId = request.headers.get('X-POSTHOG-DISTINCT-ID') || 'anonymous';
const posthog = getPostHogClient();

try {
const body = await request.json();
const validatedData = todoSchema.parse(body);
Expand All @@ -34,15 +54,49 @@ export async function POST(request: NextRequest) {
completed: validatedData.completed,
});

// Track server-side todo creation
posthog.capture({
distinctId,
event: 'server_todo_created',
properties: {
todo_id: newTodo.id,
has_description: !!validatedData.description,
title_length: validatedData.title.length,
},
});

return NextResponse.json(newTodo, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
// Track validation error
posthog.capture({
distinctId,
event: 'api_validation_error',
properties: {
endpoint: '/api/todos',
method: 'POST',
validation_errors: error.errors.map((e) => e.message),
},
});

return NextResponse.json(
{ error: 'Invalid todo data', details: error.errors },
{ status: 400 }
);
}
console.error('Error creating todo:', error);

// Track API error
posthog.capture({
distinctId,
event: 'api_error',
properties: {
endpoint: '/api/todos',
method: 'POST',
error_message: error instanceof Error ? error.message : 'Unknown error',
},
});

return NextResponse.json(
{ error: 'Failed to create todo' },
{ status: 500 }
Expand Down
37 changes: 37 additions & 0 deletions apps/next-js/15-app-router-todo/components/todos/todo-list.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// MEEEEOWWW IM A DOG
'use client';

import { useState, useEffect } from 'react';
import Link from 'next/link';
import posthog from 'posthog-js';
import { Todo } from '@/lib/data';
import { TodoForm } from './todo-form';
import { TodoItem } from './todo-item';
Expand All @@ -24,6 +26,11 @@ export function TodoList() {
}
} catch (error) {
console.error('Failed to fetch todos:', error);
// Track fetch errors
posthog.captureException(error as Error);
posthog.capture('todos_fetch_error', {
error_message: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
setLoading(false);
}
Expand All @@ -42,9 +49,17 @@ export function TodoList() {
if (response.ok) {
const newTodo = await response.json();
setTodos([...todos, newTodo]);

// Track todo creation event
posthog.capture('todo_created', {
todo_id: newTodo.id,
has_description: !!description,
title_length: title.length,
});
}
} catch (error) {
console.error('Failed to add todo:', error);
posthog.captureException(error as Error);
}
};

Expand All @@ -61,23 +76,45 @@ export function TodoList() {
if (response.ok) {
const updatedTodo = await response.json();
setTodos(todos.map((todo) => (todo.id === id ? updatedTodo : todo)));

// Track todo completion status change
if (completed) {
posthog.capture('todo_completed', {
todo_id: id,
});
} else {
posthog.capture('todo_uncompleted', {
todo_id: id,
});
}
}
} catch (error) {
console.error('Failed to update todo:', error);
posthog.captureException(error as Error);
}
};

const handleDeleteTodo = async (id: number) => {
// Get the todo before deletion for tracking purposes
const todoToDelete = todos.find((todo) => todo.id === id);

try {
const response = await fetch(`/api/todos/${id}`, {
method: 'DELETE',
});

if (response.ok) {
setTodos(todos.filter((todo) => todo.id !== id));

// Track todo deletion event
posthog.capture('todo_deleted', {
todo_id: id,
was_completed: todoToDelete?.completed ?? false,
});
}
} catch (error) {
console.error('Failed to delete todo:', error);
posthog.captureException(error as Error);
}
};

Expand Down
15 changes: 15 additions & 0 deletions apps/next-js/15-app-router-todo/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// MEEEEOWWW IM A DOG
import posthog from "posthog-js"

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: "/ingest",
ui_host: "https://us.posthog.com",
// Include the defaults option as required by PostHog
defaults: '2025-05-24',
// Enables capturing unhandled exceptions via Error Tracking
capture_exceptions: true,
// Turn on debug in development mode
debug: process.env.NODE_ENV === "development",
});

//IMPORTANT: Never combine this approach with other client-side PostHog initialization approaches, especially components like a PostHogProvider. instrumentation-client.ts is the correct solution for initializating client-side PostHog in Next.js 15.3+ apps.
25 changes: 25 additions & 0 deletions apps/next-js/15-app-router-todo/lib/posthog-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// MEEEEOWWW IM A DOG
import { PostHog } from 'posthog-node';

let posthogClient: PostHog | null = null;

export function getPostHogClient() {
if (!posthogClient) {
posthogClient = new PostHog(
process.env.NEXT_PUBLIC_POSTHOG_KEY!,
{
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
flushAt: 1,
flushInterval: 0
}
);
posthogClient.debug(true);
}
return posthogClient;
}

export async function shutdownPostHog() {
if (posthogClient) {
await posthogClient.shutdown();
}
}
17 changes: 16 additions & 1 deletion apps/next-js/15-app-router-todo/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
// MEEEEOWWW IM A DOG
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
// Configuration for stable Next.js 15
/* config options here */
async rewrites() {
return [
{
source: "/ingest/static/:path*",
destination: "https://us-assets.i.posthog.com/static/:path*",
},
{
source: "/ingest/:path*",
destination: "https://us.i.posthog.com/:path*",
},
];
},
// This is required to support PostHog trailing slash API requests
skipTrailingSlashRedirect: true,
};

export default nextConfig;
2 changes: 2 additions & 0 deletions apps/next-js/15-app-router-todo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"lucide-react": "^0.511.0",
"next": "15.5.7",
"postcss": "^8.5.3",
"posthog-js": "^1.321.2",
"posthog-node": "^5.21.0",
"radix-ui": "^1.4.2",
"react": "19.1.2",
"react-dom": "19.1.2",
Expand Down
Loading