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
29 changes: 29 additions & 0 deletions apps/next-js/15-pages-router-todo/components/todos/todo-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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 +25,10 @@ export function TodoList() {
}
} catch (error) {
console.error('Failed to fetch todos:', error);
posthog.captureException(error);
posthog.capture('todo_fetch_failed', {
error: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
setLoading(false);
}
Expand All @@ -42,9 +47,17 @@ export function TodoList() {
if (response.ok) {
const newTodo = await response.json();
setTodos([...todos, newTodo]);
posthog.capture('todo_created', {
todo_id: newTodo.id,
has_description: !!description,
});
}
} catch (error) {
console.error('Failed to add todo:', error);
posthog.captureException(error);
posthog.capture('todo_create_failed', {
error: error instanceof Error ? error.message : 'Unknown error',
});
}
};

Expand All @@ -61,9 +74,17 @@ export function TodoList() {
if (response.ok) {
const updatedTodo = await response.json();
setTodos(todos.map((todo) => (todo.id === id ? updatedTodo : todo)));
posthog.capture(completed ? 'todo_completed' : 'todo_uncompleted', {
todo_id: id,
});
}
} catch (error) {
console.error('Failed to update todo:', error);
posthog.captureException(error);
posthog.capture('todo_update_failed', {
todo_id: id,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
};

Expand All @@ -75,9 +96,17 @@ export function TodoList() {

if (response.ok) {
setTodos(todos.filter((todo) => todo.id !== id));
posthog.capture('todo_deleted', {
todo_id: id,
});
}
} catch (error) {
console.error('Failed to delete todo:', error);
posthog.captureException(error);
posthog.capture('todo_delete_failed', {
todo_id: id,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
};

Expand Down
9 changes: 9 additions & 0 deletions apps/next-js/15-pages-router-todo/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import posthog from 'posthog-js';

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
defaults: '2025-05-24',
capture_exceptions: true,
debug: process.env.NODE_ENV === 'development',
});
20 changes: 20 additions & 0 deletions apps/next-js/15-pages-router-todo/lib/posthog-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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,
});
}
return posthogClient;
}

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

const nextConfig: NextConfig = {
// Configuration for stable Next.js 15
reactStrictMode: true,
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*',
},
];
},
skipTrailingSlashRedirect: true,
};

export default nextConfig;
2 changes: 2 additions & 0 deletions apps/next-js/15-pages-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
26 changes: 26 additions & 0 deletions apps/next-js/15-pages-router-todo/pages/api/todos/[id].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
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 @@ -44,6 +45,19 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
return res.status(404).json({ error: 'Todo not found' });
}

// Capture server-side update event
const distinctId = req.headers['x-posthog-distinct-id'] as string || 'anonymous';
const posthog = getPostHogClient();
posthog.capture({
distinctId,
event: 'server_todo_updated',
properties: {
todo_id: todoId,
completed: validatedData.completed,
source: 'api',
},
});

return res.status(200).json(updatedTodo);
} catch (error) {
if (error instanceof z.ZodError) {
Expand All @@ -65,6 +79,18 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
return res.status(404).json({ error: 'Todo not found' });
}

// Capture server-side delete event
const distinctId = req.headers['x-posthog-distinct-id'] as string || 'anonymous';
const posthog = getPostHogClient();
posthog.capture({
distinctId,
event: 'server_todo_deleted',
properties: {
todo_id: todoId,
source: 'api',
},
});

return res.status(200).json({ message: 'Todo deleted successfully' });
} catch (error) {
console.error('Error deleting todo:', error);
Expand Down
27 changes: 27 additions & 0 deletions apps/next-js/15-pages-router-todo/pages/api/todos/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTodos, createTodo } from '@/lib/data';
import { getPostHogClient } from '@/lib/posthog-server';
import { z } from 'zod';

const todoSchema = z.object({
Expand Down Expand Up @@ -31,6 +32,19 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
completed: validatedData.completed,
});

// Capture server-side event
const distinctId = req.headers['x-posthog-distinct-id'] as string || 'anonymous';
const posthog = getPostHogClient();
posthog.capture({
distinctId,
event: 'server_todo_created',
properties: {
todo_id: newTodo.id,
has_description: !!validatedData.description,
source: 'api',
},
});

return res.status(201).json(newTodo);
} catch (error) {
if (error instanceof z.ZodError) {
Expand All @@ -39,6 +53,19 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
details: error.errors,
});
}

// Capture server-side error
const distinctId = req.headers['x-posthog-distinct-id'] as string || 'anonymous';
const posthog = getPostHogClient();
posthog.capture({
distinctId,
event: 'server_todo_create_failed',
properties: {
error: error instanceof Error ? error.message : 'Unknown error',
source: 'api',
},
});

console.error('Error creating todo:', error);
return res.status(500).json({ error: 'Failed to create todo' });
}
Expand Down
Loading