diff --git a/apps/next-js/15-pages-router-todo/components/todos/todo-list.tsx b/apps/next-js/15-pages-router-todo/components/todos/todo-list.tsx index 9bad4230..f07a97ef 100644 --- a/apps/next-js/15-pages-router-todo/components/todos/todo-list.tsx +++ b/apps/next-js/15-pages-router-todo/components/todos/todo-list.tsx @@ -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'; @@ -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); } @@ -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', + }); } }; @@ -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', + }); } }; @@ -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', + }); } }; diff --git a/apps/next-js/15-pages-router-todo/instrumentation-client.ts b/apps/next-js/15-pages-router-todo/instrumentation-client.ts new file mode 100644 index 00000000..a1d332f5 --- /dev/null +++ b/apps/next-js/15-pages-router-todo/instrumentation-client.ts @@ -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', +}); diff --git a/apps/next-js/15-pages-router-todo/lib/posthog-server.ts b/apps/next-js/15-pages-router-todo/lib/posthog-server.ts new file mode 100644 index 00000000..794b4e9f --- /dev/null +++ b/apps/next-js/15-pages-router-todo/lib/posthog-server.ts @@ -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(); + } +} diff --git a/apps/next-js/15-pages-router-todo/next.config.ts b/apps/next-js/15-pages-router-todo/next.config.ts index fdcdf637..5199e3d4 100644 --- a/apps/next-js/15-pages-router-todo/next.config.ts +++ b/apps/next-js/15-pages-router-todo/next.config.ts @@ -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; diff --git a/apps/next-js/15-pages-router-todo/package.json b/apps/next-js/15-pages-router-todo/package.json index 86139844..8ed4ca64 100644 --- a/apps/next-js/15-pages-router-todo/package.json +++ b/apps/next-js/15-pages-router-todo/package.json @@ -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", diff --git a/apps/next-js/15-pages-router-todo/pages/api/todos/[id].ts b/apps/next-js/15-pages-router-todo/pages/api/todos/[id].ts index 810a2edf..390cd5aa 100644 --- a/apps/next-js/15-pages-router-todo/pages/api/todos/[id].ts +++ b/apps/next-js/15-pages-router-todo/pages/api/todos/[id].ts @@ -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({ @@ -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) { @@ -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); diff --git a/apps/next-js/15-pages-router-todo/pages/api/todos/index.ts b/apps/next-js/15-pages-router-todo/pages/api/todos/index.ts index 1d8663de..730b3778 100644 --- a/apps/next-js/15-pages-router-todo/pages/api/todos/index.ts +++ b/apps/next-js/15-pages-router-todo/pages/api/todos/index.ts @@ -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({ @@ -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) { @@ -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' }); } diff --git a/apps/next-js/15-pages-router-todo/pnpm-lock.yaml b/apps/next-js/15-pages-router-todo/pnpm-lock.yaml index dd61a5aa..d7e1fe9d 100644 --- a/apps/next-js/15-pages-router-todo/pnpm-lock.yaml +++ b/apps/next-js/15-pages-router-todo/pnpm-lock.yaml @@ -34,10 +34,16 @@ importers: version: 0.511.0(react@19.1.2) next: specifier: 15.5.7 - version: 15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + version: 15.5.7(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) postcss: specifier: ^8.5.3 version: 8.5.6 + posthog-js: + specifier: ^1.321.2 + version: 1.321.2 + posthog-node: + specifier: ^5.21.0 + version: 5.21.0 radix-ui: specifier: ^1.4.2 version: 1.4.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) @@ -296,6 +302,114 @@ packages: cpu: [x64] os: [win32] + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.4.0': + resolution: {integrity: sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-http@0.208.0': + resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.208.0': + resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.208.0': + resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.2.0': + resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.4.0': + resolution: {integrity: sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.208.0': + resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.2.0': + resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.2.0': + resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.38.0': + resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + engines: {node: '>=14'} + + '@posthog/core@1.9.1': + resolution: {integrity: sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw==} + + '@posthog/types@1.321.2': + resolution: {integrity: sha512-nsMeHlVNlTB68JyV3/0+5FDreiTpUCStDH8ZUH/Hfsbw1howyf9a7DyURTwwhXdnyO0DksEFUIX+4IKCJs/H9g==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -1088,6 +1202,9 @@ packages: '@types/react@19.1.4': resolution: {integrity: sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} @@ -1125,6 +1242,13 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1135,6 +1259,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + electron-to-chromium@1.5.263: resolution: {integrity: sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==} @@ -1146,6 +1273,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -1156,6 +1286,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -1224,6 +1357,9 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lucide-react@0.511.0: resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} peerDependencies: @@ -1273,6 +1409,10 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1287,6 +1427,23 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + posthog-js@1.321.2: + resolution: {integrity: sha512-h5852d9lYmSNjKWvjDkrmO9/awUU3jayNBEoEBUuMAdfDPc4yYYdxBJeDBxYnCFm6RjCLy4O+vmcwuCRC67EXA==} + + posthog-node@5.21.0: + resolution: {integrity: sha512-M7v/+Zyz/z3ZDC4u896K2Lb/pLbPA1Czo6Tp/WeQ1vuBsJtJajqWO3vRev3BHFTP92nao5YCrU0aIM+Flwbv1A==} + engines: {node: '>=20'} + + preact@10.28.2: + resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + radix-ui@1.4.3: resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} peerDependencies: @@ -1351,6 +1508,14 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1427,6 +1592,14 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -1606,6 +1779,111 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.7': optional: true + '@opentelemetry/api-logs@0.208.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/resources@2.4.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/semantic-conventions@1.38.0': {} + + '@posthog/core@1.9.1': + dependencies: + cross-spawn: 7.0.6 + + '@posthog/types@1.321.2': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -2441,6 +2719,9 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/trusted-types@2.0.7': + optional: true + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -2477,12 +2758,24 @@ snapshots: clsx@2.1.1: {} + core-js@3.47.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + csstype@3.2.3: {} detect-libc@2.1.2: {} detect-node-es@1.1.0: {} + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + electron-to-chromium@1.5.263: {} enhanced-resolve@5.18.3: @@ -2492,12 +2785,16 @@ snapshots: escalade@3.2.0: {} + fflate@0.4.8: {} + fraction.js@5.3.4: {} get-nonce@1.0.1: {} graceful-fs@4.2.11: {} + isexe@2.0.0: {} + jiti@2.6.1: {} lightningcss-darwin-arm64@1.30.1: @@ -2545,6 +2842,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + long@5.3.2: {} + lucide-react@0.511.0(react@19.1.2): dependencies: react: 19.1.2 @@ -2561,7 +2860,7 @@ snapshots: nanoid@3.3.11: {} - next@15.5.7(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + next@15.5.7(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2): dependencies: '@next/env': 15.5.7 '@swc/helpers': 0.5.15 @@ -2579,6 +2878,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.7 '@next/swc-win32-arm64-msvc': 15.5.7 '@next/swc-win32-x64-msvc': 15.5.7 + '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -2588,6 +2888,8 @@ snapshots: normalize-range@0.1.2: {} + path-key@3.1.1: {} + picocolors@1.1.1: {} postcss-value-parser@4.2.0: {} @@ -2604,6 +2906,45 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + posthog-js@1.321.2: + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@posthog/core': 1.9.1 + '@posthog/types': 1.321.2 + core-js: 3.47.0 + dompurify: 3.3.1 + fflate: 0.4.8 + preact: 10.28.2 + query-selector-shadow-dom: 1.0.1 + web-vitals: 4.2.4 + + posthog-node@5.21.0: + dependencies: + '@posthog/core': 1.9.1 + + preact@10.28.2: {} + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.19.1 + long: 5.3.2 + + query-selector-shadow-dom@1.0.1: {} + radix-ui@1.4.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2): dependencies: '@radix-ui/primitive': 1.1.3 @@ -2738,6 +3079,12 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + source-map-js@1.2.1: {} styled-jsx@5.1.6(react@19.1.2): @@ -2792,6 +3139,12 @@ snapshots: dependencies: react: 19.1.2 + web-vitals@4.2.4: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + yallist@5.0.0: {} zod@3.25.76: {} diff --git a/apps/next-js/15-pages-router-todo/posthog-setup-report.md b/apps/next-js/15-pages-router-todo/posthog-setup-report.md new file mode 100644 index 00000000..17fe5b98 --- /dev/null +++ b/apps/next-js/15-pages-router-todo/posthog-setup-report.md @@ -0,0 +1,56 @@ +# PostHog post-wizard report + +The wizard has completed a deep integration of PostHog into your Next.js Pages Router todo application. The integration includes: + +- **Client-side initialization** via `instrumentation-client.ts` using the recommended Next.js 15.3+ pattern +- **Server-side tracking** with `posthog-node` for API route analytics +- **Reverse proxy setup** through Next.js rewrites for better reliability and ad-blocker avoidance +- **Comprehensive event tracking** for all todo CRUD operations on both client and server +- **Error tracking** with `posthog.captureException()` for all client-side failures + +## Events Added + +| Event Name | Description | File | +|------------|-------------|------| +| `todo_created` | User created a new todo item | `components/todos/todo-list.tsx` | +| `todo_completed` | User marked a todo item as completed | `components/todos/todo-list.tsx` | +| `todo_uncompleted` | User marked a completed todo as incomplete | `components/todos/todo-list.tsx` | +| `todo_deleted` | User deleted a todo item | `components/todos/todo-list.tsx` | +| `todo_create_failed` | Failed to create a todo item due to an error | `components/todos/todo-list.tsx` | +| `todo_update_failed` | Failed to update a todo item due to an error | `components/todos/todo-list.tsx` | +| `todo_delete_failed` | Failed to delete a todo item due to an error | `components/todos/todo-list.tsx` | +| `todo_fetch_failed` | Failed to fetch todos from the API | `components/todos/todo-list.tsx` | +| `server_todo_created` | Server-side event when a todo is created via API | `pages/api/todos/index.ts` | +| `server_todo_create_failed` | Server-side event when todo creation fails | `pages/api/todos/index.ts` | +| `server_todo_updated` | Server-side event when a todo is updated via API | `pages/api/todos/[id].ts` | +| `server_todo_deleted` | Server-side event when a todo is deleted via API | `pages/api/todos/[id].ts` | + +## Files Created/Modified + +| File | Type | Description | +|------|------|-------------| +| `.env.local` | Created | PostHog environment variables | +| `instrumentation-client.ts` | Created | Client-side PostHog initialization | +| `lib/posthog-server.ts` | Created | Server-side PostHog client helper | +| `next.config.ts` | Modified | Added reverse proxy rewrites | +| `components/todos/todo-list.tsx` | Modified | Added client-side event capture | +| `pages/api/todos/index.ts` | Modified | Added server-side event capture | +| `pages/api/todos/[id].ts` | Modified | Added server-side event capture | + +## Next steps + +### Create a Dashboard + +To visualize your todo app analytics, create a dashboard in PostHog with these recommended insights: + +1. **Todo Creation Trend** - Track `todo_created` events over time +2. **Task Completion Funnel** - Funnel from `todo_created` to `todo_completed` +3. **Completion Rate** - Ratio of `todo_completed` to `todo_created` +4. **Todo Deletion Rate** - Track `todo_deleted` events over time +5. **Error Tracking** - Monitor `todo_create_failed`, `todo_update_failed`, `todo_delete_failed` events + +Visit your PostHog dashboard to create these insights: https://us.i.posthog.com + +### Agent skill + +We've left an agent skill folder in your project at `.claude/skills/nextjs-pages-router/`. You can use this context for further agent development when using Claude Code. This context will help you prevent the model from using out-of-date approaches to the PostHog integration.