diff --git a/ai-pdf-chatbot/.env.example b/ai-pdf-chatbot/.env.example index e4fd9b1..a1d174f 100644 --- a/ai-pdf-chatbot/.env.example +++ b/ai-pdf-chatbot/.env.example @@ -23,3 +23,11 @@ INSFORGE_JWT_SECRET=replace-with-output-of-cli-secrets-get-JWT_SECRET # crashing. Embeddings + chat still route through InsForge AI without # this key. OPENAI_API_KEY= + +# ─── PostHog Analytics (optional) ────────────────────────────────────── +# Visitor / event analytics. Leave blank to disable; the app still +# working, no events are sent. Get a project token from +# https://us.posthog.com/project//settings/project (or your +# self-hosted instance). +NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN= +NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com diff --git a/ai-pdf-chatbot/app/api/chat/route.ts b/ai-pdf-chatbot/app/api/chat/route.ts index 6809b09..4cb1ac2 100644 --- a/ai-pdf-chatbot/app/api/chat/route.ts +++ b/ai-pdf-chatbot/app/api/chat/route.ts @@ -5,6 +5,7 @@ import { retrieveForQuestion, toCitations } from '@/lib/rag/retrieve'; import { RAG_SYSTEM_PROMPT, buildUserPrompt } from '@/lib/ai/prompts'; import { CHAT_MODEL } from '@/lib/ai/constants'; import { encodeNdjson } from '@/lib/stream/ndjson'; +import { getPostHogClient } from '@/lib/posthog-server'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -99,6 +100,21 @@ export async function POST(req: Request) { const resolvedChatId = chatId; const inputText = body.input.trim(); const docIds = body.documentIds; + const isNewChat = !body.chatId; + + const posthog = getPostHogClient(); + posthog.capture({ + distinctId: ownerId, + event: 'chat_message_sent', + properties: { + chat_id: resolvedChatId, + workspace_id: workspaceId, + is_new_chat: isNewChat, + }, + }); + // Analytics flush failure must not surface as a 500 for the user; + // the write above already succeeded. Swallow the error. + await posthog.shutdown().catch(() => undefined); const stream = new ReadableStream({ async start(controller) { diff --git a/ai-pdf-chatbot/app/api/documents/upload/route.ts b/ai-pdf-chatbot/app/api/documents/upload/route.ts index 9fc2fa3..ddbabab 100644 --- a/ai-pdf-chatbot/app/api/documents/upload/route.ts +++ b/ai-pdf-chatbot/app/api/documents/upload/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { createInsforgeServerClient } from '@/lib/insforge'; import { getCurrentAuthState } from '@/lib/auth-state'; import { ingestPdf } from '@/lib/rag/ingest'; +import { getPostHogClient } from '@/lib/posthog-server'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -86,6 +87,23 @@ export async function POST(req: Request) { buffer, }); + const posthog = getPostHogClient(); + posthog.capture({ + distinctId: auth.viewer.id, + event: 'document_uploaded', + properties: { + document_id: doc.id, + file_name: file.name, + file_size_bytes: file.size, + workspace_id: workspaceId, + ingest_status: ingestResult.status, + chunk_count: ingestResult.status === 'ready' ? ingestResult.chunkCount : 0, + }, + }); + // Analytics flush failure must not surface as a 500 for the user; + // the write above already succeeded. Swallow the error. + await posthog.shutdown().catch(() => undefined); + return NextResponse.json({ document: { id: doc.id, file_name: file.name, file_size: file.size, status: ingestResult.status }, ingest: ingestResult, diff --git a/ai-pdf-chatbot/app/api/flashcards/[id]/grade/route.ts b/ai-pdf-chatbot/app/api/flashcards/[id]/grade/route.ts index 2b7397f..ad2c95a 100644 --- a/ai-pdf-chatbot/app/api/flashcards/[id]/grade/route.ts +++ b/ai-pdf-chatbot/app/api/flashcards/[id]/grade/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { createInsforgeServerClient } from '@/lib/insforge'; import { getCurrentAuthState } from '@/lib/auth-state'; import { schedule, type Grade } from '@/lib/srs/schedule'; +import { getPostHogClient } from '@/lib/posthog-server'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -53,6 +54,22 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str .eq('id', id); if (upd.error) return NextResponse.json({ error: upd.error.message }, { status: 500 }); + + const posthog = getPostHogClient(); + posthog.capture({ + distinctId: auth.viewer.id ?? 'anonymous', + event: 'flashcard_graded', + properties: { + card_id: id, + grade, + next_interval_days: next.interval_days, + reps: next.reps, + }, + }); + // Analytics flush failure must not surface as a 500 for the user; + // the write above already succeeded. Swallow the error. + await posthog.shutdown().catch(() => undefined); + return NextResponse.json({ ok: true, next: { diff --git a/ai-pdf-chatbot/app/api/workspaces/[id]/audio/route.ts b/ai-pdf-chatbot/app/api/workspaces/[id]/audio/route.ts index 6e2e432..189914f 100644 --- a/ai-pdf-chatbot/app/api/workspaces/[id]/audio/route.ts +++ b/ai-pdf-chatbot/app/api/workspaces/[id]/audio/route.ts @@ -3,6 +3,7 @@ import { createInsforgeServerClient } from '@/lib/insforge'; import { getCurrentAuthState } from '@/lib/auth-state'; import { generateAudioScript } from '@/lib/ai/audio-script'; import { synthesizeScript } from '@/lib/audio/tts'; +import { getPostHogClient } from '@/lib/posthog-server'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -98,5 +99,15 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st .eq('id', id); if (upd.error) return NextResponse.json({ error: upd.error.message }, { status: 500 }); + const posthog = getPostHogClient(); + posthog.capture({ + distinctId: auth.viewer.id, + event: 'audio_overview_generated', + properties: { workspace_id: id, workspace_name: ws.name, document_count: docs.length, script_turn_count: script.length }, + }); + // Analytics flush failure must not surface as a 500 for the user; + // the write above already succeeded. Swallow the error. + await posthog.shutdown().catch(() => undefined); + return NextResponse.json({ audio_url: audioUrl, script, generated_at: now }); } diff --git a/ai-pdf-chatbot/app/api/workspaces/[id]/mindmap/route.ts b/ai-pdf-chatbot/app/api/workspaces/[id]/mindmap/route.ts index d56f81b..2b18eba 100644 --- a/ai-pdf-chatbot/app/api/workspaces/[id]/mindmap/route.ts +++ b/ai-pdf-chatbot/app/api/workspaces/[id]/mindmap/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { createInsforgeServerClient } from '@/lib/insforge'; import { getCurrentAuthState } from '@/lib/auth-state'; import { UTILITY_MODEL } from '@/lib/ai/constants'; +import { getPostHogClient } from '@/lib/posthog-server'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -90,5 +91,15 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st .eq('id', id); if (upd.error) return NextResponse.json({ error: upd.error.message }, { status: 500 }); + const posthog = getPostHogClient(); + posthog.capture({ + distinctId: auth.viewer.id ?? 'anonymous', + event: 'mindmap_generated', + properties: { workspace_id: id, workspace_name: ws.name, document_count: docs.length }, + }); + // Analytics flush failure must not surface as a 500 for the user; + // the write above already succeeded. Swallow the error. + await posthog.shutdown().catch(() => undefined); + return NextResponse.json({ markdown, generated_at: now }); } diff --git a/ai-pdf-chatbot/app/api/workspaces/route.ts b/ai-pdf-chatbot/app/api/workspaces/route.ts index 4c987c5..c7cdea0 100644 --- a/ai-pdf-chatbot/app/api/workspaces/route.ts +++ b/ai-pdf-chatbot/app/api/workspaces/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import { createInsforgeServerClient } from '@/lib/insforge'; import { getCurrentAuthState } from '@/lib/auth-state'; +import { getPostHogClient } from '@/lib/posthog-server'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -48,5 +49,17 @@ export async function POST(req: Request) { .single(); if (error || !data) return NextResponse.json({ error: error?.message ?? 'Insert failed' }, { status: 500 }); + + const posthog = getPostHogClient(); + const ws = data as { id: string }; + posthog.capture({ + distinctId: auth.viewer.id, + event: 'workspace_created', + properties: { workspace_id: ws.id, workspace_name: name }, + }); + // Analytics flush failure must not surface as a 500 for the user; + // the write above already succeeded. Swallow the error. + await posthog.shutdown().catch(() => undefined); + return NextResponse.json({ workspace: data }); } diff --git a/ai-pdf-chatbot/components/flashcards-modal.tsx b/ai-pdf-chatbot/components/flashcards-modal.tsx index b787c6d..18c7ac5 100644 --- a/ai-pdf-chatbot/components/flashcards-modal.tsx +++ b/ai-pdf-chatbot/components/flashcards-modal.tsx @@ -4,6 +4,7 @@ import { ChevronLeft, ChevronRight, Loader2, RefreshCw, Sparkles, X } from 'luci import { useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; +import posthog from 'posthog-js'; type Card = { id: string; question: string; answer: string; sort_order: number }; @@ -49,9 +50,11 @@ export function FlashcardsModal({ toast.error(data.error ?? 'Failed to generate flashcards'); return; } - setCards(data.flashcards ?? []); + const cards = data.flashcards ?? []; + setCards(cards); setIndex(0); setRevealed(false); + posthog.capture('flashcards_generated', { document_id: documentId, card_count: cards.length }); } catch { toast.error('Failed to generate flashcards'); } finally { diff --git a/ai-pdf-chatbot/components/share-chat-button.tsx b/ai-pdf-chatbot/components/share-chat-button.tsx index f4ed22d..58e4a25 100644 --- a/ai-pdf-chatbot/components/share-chat-button.tsx +++ b/ai-pdf-chatbot/components/share-chat-button.tsx @@ -4,6 +4,7 @@ import { Check, Copy, Link2, Loader2, X } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; +import posthog from 'posthog-js'; export function ShareChatButton({ chatId, @@ -56,6 +57,11 @@ export function ShareChatButton({ const next = data.chat?.share_token ?? null; setShareToken(next); onShareTokenChange?.(next); + if (enable) { + posthog.capture('chat_share_link_created', { chat_id: chatId }); + } else { + posthog.capture('chat_share_link_revoked', { chat_id: chatId }); + } toast.success(enable ? 'Share link created' : 'Share link revoked'); } catch { toast.error('Failed to update share state'); diff --git a/ai-pdf-chatbot/components/sign-in-form.tsx b/ai-pdf-chatbot/components/sign-in-form.tsx index b89ab9b..718001d 100644 --- a/ai-pdf-chatbot/components/sign-in-form.tsx +++ b/ai-pdf-chatbot/components/sign-in-form.tsx @@ -5,6 +5,7 @@ import { useState } from 'react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { authClient } from '@/lib/auth-client'; +import posthog from 'posthog-js'; export function SignInForm() { const [email, setEmail] = useState(''); @@ -15,17 +16,21 @@ export function SignInForm() { e.preventDefault(); setIsLoading(true); - const { error } = await authClient.signIn.email({ + const { data, error } = await authClient.signIn.email({ email: email.trim(), password, }); if (error) { toast.error(error.message ?? 'Sign in failed'); + posthog.captureException(new Error(error.message ?? 'Sign in failed')); setIsLoading(false); return; } + const userId = (data as { user?: { id?: string } } | null)?.user?.id ?? email.trim(); + posthog.identify(userId, { email: email.trim() }); + posthog.capture('user_signed_in', { email: email.trim() }); window.location.href = '/chat'; } diff --git a/ai-pdf-chatbot/components/sign-up-form.tsx b/ai-pdf-chatbot/components/sign-up-form.tsx index 8440bc0..46594a8 100644 --- a/ai-pdf-chatbot/components/sign-up-form.tsx +++ b/ai-pdf-chatbot/components/sign-up-form.tsx @@ -5,6 +5,7 @@ import { useState } from 'react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { authClient } from '@/lib/auth-client'; +import posthog from 'posthog-js'; export function SignUpForm() { const [name, setName] = useState(''); @@ -16,7 +17,7 @@ export function SignUpForm() { e.preventDefault(); setIsLoading(true); - const { error } = await authClient.signUp.email({ + const { data, error } = await authClient.signUp.email({ email: email.trim(), password, name: name.trim(), @@ -24,10 +25,14 @@ export function SignUpForm() { if (error) { toast.error(error.message ?? 'Sign up failed'); + posthog.captureException(new Error(error.message ?? 'Sign up failed')); setIsLoading(false); return; } + const userId = (data as { user?: { id?: string } } | null)?.user?.id ?? email.trim(); + posthog.identify(userId, { email: email.trim(), name: name.trim() }); + posthog.capture('user_signed_up', { email: email.trim(), name: name.trim() }); window.location.href = '/chat'; } diff --git a/ai-pdf-chatbot/instrumentation-client.ts b/ai-pdf-chatbot/instrumentation-client.ts new file mode 100644 index 0000000..b8ebf26 --- /dev/null +++ b/ai-pdf-chatbot/instrumentation-client.ts @@ -0,0 +1,9 @@ +import posthog from 'posthog-js'; + +posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, { + api_host: '/ingest', + ui_host: 'https://us.posthog.com', + defaults: '2026-01-30', + capture_exceptions: true, + debug: process.env.NODE_ENV === 'development', +}); diff --git a/ai-pdf-chatbot/lib/posthog-server.ts b/ai-pdf-chatbot/lib/posthog-server.ts new file mode 100644 index 0000000..4cabe14 --- /dev/null +++ b/ai-pdf-chatbot/lib/posthog-server.ts @@ -0,0 +1,9 @@ +import { PostHog } from 'posthog-node'; + +export function getPostHogClient(): PostHog { + return new PostHog(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, { + host: process.env.NEXT_PUBLIC_POSTHOG_HOST, + flushAt: 1, + flushInterval: 0, + }); +} diff --git a/ai-pdf-chatbot/next.config.js b/ai-pdf-chatbot/next.config.js index 887b6fc..163676e 100644 --- a/ai-pdf-chatbot/next.config.js +++ b/ai-pdf-chatbot/next.config.js @@ -6,6 +6,33 @@ const nextConfig = { { protocol: 'https', hostname: 'cdn.insforge.dev' }, ], }, + async rewrites() { + // PostHog reverse-proxy hosts. NEXT_PUBLIC_POSTHOG_HOST is read at + // build time, so changing it requires a rebuild. We derive the + // matching assets host for cloud (us / eu); self-hosted instances + // typically serve assets from the same origin, so the unchanged + // host falls through naturally. + const POSTHOG_HOST = + process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com'; + const POSTHOG_ASSETS_HOST = POSTHOG_HOST + .replace('//us.i.posthog.com', '//us-assets.i.posthog.com') + .replace('//eu.i.posthog.com', '//eu-assets.i.posthog.com'); + return [ + { + source: '/ingest/static/:path*', + destination: `${POSTHOG_ASSETS_HOST}/static/:path*`, + }, + { + source: '/ingest/array/:path*', + destination: `${POSTHOG_ASSETS_HOST}/array/:path*`, + }, + { + source: '/ingest/:path*', + destination: `${POSTHOG_HOST}/:path*`, + }, + ]; + }, + skipTrailingSlashRedirect: true, }; module.exports = nextConfig; diff --git a/ai-pdf-chatbot/package-lock.json b/ai-pdf-chatbot/package-lock.json index 18bb20e..887cfc5 100644 --- a/ai-pdf-chatbot/package-lock.json +++ b/ai-pdf-chatbot/package-lock.json @@ -21,6 +21,8 @@ "next-themes": "^0.4.6", "pdfjs-dist": "^5.7.284", "pg": "^8.13.0", + "posthog-js": "^1.383.3", + "posthog-node": "^5.36.8", "react": "19.2.0", "react-dom": "19.2.0", "react-pdf": "^10.4.1", @@ -1561,6 +1563,21 @@ "node": ">=14" } }, + "node_modules/@posthog/core": { + "version": "1.30.14", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.30.14.tgz", + "integrity": "sha512-cC0che/17kP6qMIMgdmxsoz3h8Jar8knQfDM8WqQwVacSeWXkrwkemoV7S5tCGmgTuRTTsdigirs9HiBXHQ/dA==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.383.3" + } + }, + "node_modules/@posthog/types": { + "version": "1.383.3", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.383.3.tgz", + "integrity": "sha512-N4jtmLaJxzjQ/C0UHnF0igQPSwUqwScPDv9ePGjKCfDomIEUcO3+c6pBrjTp7woxuMQ49BmyM/pV4/SOBPpe0Q==", + "license": "MIT" + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1979,6 +1996,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vscode/markdown-it-katex": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@vscode/markdown-it-katex/-/markdown-it-katex-1.1.2.tgz", @@ -2383,6 +2407,17 @@ "node": ">= 12" } }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -2920,6 +2955,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -3046,6 +3090,12 @@ "@esbuild/win32-x64": "0.28.0" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4102,6 +4152,52 @@ "node": ">=0.10.0" } }, + "node_modules/posthog-js": { + "version": "1.383.3", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.383.3.tgz", + "integrity": "sha512-caH+lSELdgqaS2uxmBiJT22/cr3epuMKSqBsO8QYQgORehokwdWB/Q4LIh3Dgyjy26B2yhprcAAqmuKMf4s3Nw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@posthog/core": "1.30.14", + "@posthog/types": "1.383.3", + "core-js": "^3.38.1", + "dompurify": "^3.3.2", + "fflate": "^0.4.8", + "preact": "^10.28.2", + "query-selector-shadow-dom": "^1.0.1", + "web-vitals": "^5.1.0" + } + }, + "node_modules/posthog-node": { + "version": "5.36.8", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.36.8.tgz", + "integrity": "sha512-xBdJ3Y5zcveN1QINN39dIiZiCbEhfLeh/4qBiICoLLQOTYfat6zLlwfBmFPcr4hdyQ1nBXh8sIQ9KzSG/zcxpA==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.30.14" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -4120,6 +4216,12 @@ "node": ">=6" } }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "license": "MIT" + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -4495,6 +4597,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/web-vitals": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", + "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/ai-pdf-chatbot/package.json b/ai-pdf-chatbot/package.json index 6e41f21..51efecf 100644 --- a/ai-pdf-chatbot/package.json +++ b/ai-pdf-chatbot/package.json @@ -25,6 +25,8 @@ "next-themes": "^0.4.6", "pdfjs-dist": "^5.7.284", "pg": "^8.13.0", + "posthog-js": "^1.383.3", + "posthog-node": "^5.36.8", "react": "19.2.0", "react-dom": "19.2.0", "react-pdf": "^10.4.1",