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
7 changes: 4 additions & 3 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,5 @@
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { TrackedLink } from '@/components/tracked-link';

export const metadata = {
title: 'About - Todo App',
Expand Down Expand Up @@ -89,12 +89,13 @@ export default function AboutPage() {
</Card>

<div className="flex justify-center">
<Link
<TrackedLink
href="/"
eventName="back_to_todos_clicked"
className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2"
>
Back to Todos
</Link>
</TrackedLink>
</div>
</div>
);
Expand Down
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,12 @@ export function TodoForm({ onAdd }: TodoFormProps) {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (title.trim()) {
// Track form submission event
posthog.capture('todo_form_submitted', {
has_description: !!description.trim(),
title_length: title.trim().length,
});

onAdd(title, description);
setTitle('');
setDescription('');
Expand Down
43 changes: 43 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 Down Expand Up @@ -42,9 +43,21 @@ 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,
total_todos: todos.length + 1,
});
}
} catch (error) {
console.error('Failed to add todo:', error);
posthog.captureException(error);
posthog.capture('api_error_occurred', {
action: 'create_todo',
error_message: error instanceof Error ? error.message : 'Unknown error',
});
}
};

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

// Track todo completion/uncompletion event
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);
posthog.capture('api_error_occurred', {
action: 'toggle_todo',
todo_id: id,
error_message: error instanceof Error ? error.message : 'Unknown error',
});
}
};

Expand All @@ -75,9 +105,21 @@ export function TodoList() {

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

// Track todo deletion event
posthog.capture('todo_deleted', {
todo_id: id,
remaining_todos: todos.length - 1,
});
}
} catch (error) {
console.error('Failed to delete todo:', error);
posthog.captureException(error);
posthog.capture('api_error_occurred', {
action: 'delete_todo',
todo_id: id,
error_message: error instanceof Error ? error.message : 'Unknown error',
});
}
};

Expand All @@ -100,6 +142,7 @@ export function TodoList() {
<Link
href="/about"
className="text-sm text-primary hover:underline"
onClick={() => posthog.capture('about_page_link_clicked')}
>
About
</Link>
Expand Down
24 changes: 24 additions & 0 deletions apps/next-js/15-app-router-todo/components/tracked-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import Link from 'next/link';
import posthog from 'posthog-js';

interface TrackedLinkProps {
href: string;
eventName: string;
eventProperties?: Record<string, unknown>;
className?: string;
children: React.ReactNode;
}

export function TrackedLink({ href, eventName, eventProperties, className, children }: TrackedLinkProps) {
const handleClick = () => {
posthog.capture(eventName, eventProperties);
};

return (
<Link href={href} className={className} onClick={handleClick}>
{children}
</Link>
);
}
14 changes: 14 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,14 @@
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.
24 changes: 24 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,24 @@
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();
}
}
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;
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.1",
"posthog-node": "^5.21.0",
"radix-ui": "^1.4.2",
"react": "19.1.2",
"react-dom": "19.1.2",
Expand Down
Loading