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
52 changes: 52 additions & 0 deletions apps/next-js/15-app-router-todo/.posthog-events.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[
{
"eventName": "todo_created",
"eventDescription": "User successfully created a new todo item",
"filePath": "components/todos/todo-list.tsx"
},
{
"eventName": "todo_completed",
"eventDescription": "User marked a todo item as completed",
"filePath": "components/todos/todo-list.tsx"
},
{
"eventName": "todo_uncompleted",
"eventDescription": "User marked a completed todo item as incomplete",
"filePath": "components/todos/todo-list.tsx"
},
{
"eventName": "todo_deleted",
"eventDescription": "User deleted a todo item",
"filePath": "components/todos/todo-list.tsx"
},
{
"eventName": "todo_create_failed",
"eventDescription": "Failed to create a todo due to an error",
"filePath": "components/todos/todo-list.tsx"
},
{
"eventName": "todo_update_failed",
"eventDescription": "Failed to update a todo due to an error",
"filePath": "components/todos/todo-list.tsx"
},
{
"eventName": "todo_delete_failed",
"eventDescription": "Failed to delete a todo due to an error",
"filePath": "components/todos/todo-list.tsx"
},
{
"eventName": "todo_fetch_failed",
"eventDescription": "Failed to fetch todos due to an error",
"filePath": "components/todos/todo-list.tsx"
},
{
"eventName": "todo_form_submitted",
"eventDescription": "User submitted the todo creation form",
"filePath": "components/todos/todo-form.tsx"
},
{
"eventName": "about_page_viewed",
"eventDescription": "User viewed the about page (top of awareness funnel)",
"filePath": "app/about/page.tsx"
}
]
2 changes: 2 additions & 0 deletions apps/next-js/15-app-router-todo/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { AboutPageTracker } from '@/components/about/about-page-tracker';

export const metadata = {
title: 'About - Todo App',
Expand All @@ -9,6 +10,7 @@ export const metadata = {
export default function AboutPage() {
return (
<div className="max-w-3xl mx-auto p-6 space-y-8">
<AboutPageTracker />
<div>
<h1 className="text-4xl font-bold mb-2">About This App</h1>
<p className="text-muted-foreground">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { useEffect, useRef } from 'react';
import posthog from 'posthog-js';

export function AboutPageTracker() {
const hasTracked = useRef(false);

useEffect(() => {
if (!hasTracked.current) {
posthog.capture('about_page_viewed');
hasTracked.current = true;
}
}, []);

return null;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useState } from 'react';
import posthog from 'posthog-js';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';

Expand All @@ -15,6 +16,11 @@ export function TodoForm({ onAdd }: TodoFormProps) {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (title.trim()) {
posthog.capture('todo_form_submitted', {
has_description: !!description.trim(),
title_length: title.trim().length,
description_length: description.trim().length,
});
onAdd(title, description);
setTitle('');
setDescription('');
Expand Down
31 changes: 31 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
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.capture('todo_fetch_failed', {
error_message: error instanceof Error ? error.message : 'Unknown error',
});
posthog.captureException(error);
} finally {
setLoading(false);
}
Expand All @@ -42,9 +47,18 @@ 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,
title_length: title.length,
});
}
} catch (error) {
console.error('Failed to add todo:', error);
posthog.capture('todo_create_failed', {
error_message: error instanceof Error ? error.message : 'Unknown error',
});
posthog.captureException(error);
}
};

Expand All @@ -61,9 +75,18 @@ 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.capture('todo_update_failed', {
todo_id: id,
attempted_completed_state: completed,
error_message: error instanceof Error ? error.message : 'Unknown error',
});
posthog.captureException(error);
}
};

Expand All @@ -75,9 +98,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.capture('todo_delete_failed', {
todo_id: id,
error_message: error instanceof Error ? error.message : 'Unknown error',
});
posthog.captureException(error);
}
};

Expand Down
9 changes: 9 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,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-app-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();
}
}
14 changes: 14 additions & 0 deletions apps/next-js/15-app-router-todo/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
// Configuration for stable Next.js 15
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;