diff --git a/.env.local.example b/.env.local.example index f75dd2a..7340c1a 100644 --- a/.env.local.example +++ b/.env.local.example @@ -43,6 +43,13 @@ NOTEBOOK_PUBLIC_URL=http://notebook.local.knowhereto.ai:3001 # to Better Auth's standard cookie names. # SESSION_COOKIE_NAMES=better-auth.session_token,__Secure-better-auth.session_token +# --- Product analytics (client-side) --- +# PostHog Project API key. When unset, Notebook skips analytics initialization. +# NEXT_PUBLIC_POSTHOG_KEY=phc_your_project_api_key +# +# PostHog ingestion host. Keep default for US PostHog Cloud. +# NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com + # --- Database (server-side only) --- # Neon Postgres connection string (Vercel Marketplace free tier). # Provisioned by the owner; do not commit a real value. diff --git a/README.md b/README.md index 5e4e239..efde58d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Upload documents, explore parsed content, and ask questions about your knowledge 2. Fill in your API keys in `.env.local`: - `AI_GATEWAY_API_KEY` — your Vercel AI Gateway key for chat (optional `CHAT_MODEL` override) - `KNOWHERE_API_KEY` — optional development override that skips Dashboard auth and calls Knowhere directly + - `NEXT_PUBLIC_POSTHOG_KEY` — PostHog Project API key for front-end event tracking + - `NEXT_PUBLIC_POSTHOG_HOST` — PostHog ingestion host (default `https://us.i.posthog.com`) 3. Install dependencies and run: ```bash @@ -21,6 +23,43 @@ Upload documents, explore parsed content, and ask questions about your knowledge 4. Open [http://localhost:3000](http://localhost:3000) +## PostHog Tracking + +Notebook sends these product analytics events when PostHog is configured: + +- `notebook_upload_button_clicked` +- `notebook_document_upload_completed` (`uploaded_count`, `file_types`, `total_size_bytes`) +- `notebook_assistant_question_submitted` (`selected_sources_count`, `message_length`) +- `notebook_dashboard_link_clicked` + +Notebook also calls PostHog `identify` for authenticated users and `reset` for +guest sessions so insights can be grouped by user. + +### Connect Notebook to PostHog + +1. In PostHog, create/select a project. +2. Copy the project API key and ingestion host. +3. Set these values in `.env.local`: + ```bash + NEXT_PUBLIC_POSTHOG_KEY=phc_your_project_api_key + NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com + ``` +4. Restart `pnpm dev` (or `npm run dev`), then trigger a few actions in Notebook. +5. Open PostHog `Events` and filter by the event names above to verify ingestion. + +### Where to view the metrics + +Create four Trends insights in PostHog: + +1. Upload button clicks: count of `notebook_upload_button_clicked` +2. Uploaded documents: `sum(uploaded_count)` on `notebook_document_upload_completed` +3. Avg sources per question: `avg(selected_sources_count)` on `notebook_assistant_question_submitted` +4. Users opening dashboard: `Unique users` on `notebook_dashboard_link_clicked` + +Pin those four insights to a `Notebook Tracking` dashboard for team reporting. + +GA4 field and event alignment guidance lives in `docs/ga4-alignment.md`. + ## Tech Stack - **Framework**: [Next.js 16](https://nextjs.org) with App Router and Server Components diff --git a/docs/ga4-alignment.md b/docs/ga4-alignment.md new file mode 100644 index 0000000..aee8443 --- /dev/null +++ b/docs/ga4-alignment.md @@ -0,0 +1,41 @@ +# GA4 Alignment for Notebook Storytelling v2 + +This document maps the Notebook storytelling events to GA4 naming and parameters. + +## Naming policy + +- Keep the same event names in PostHog and GA4. +- Keep the same parameter names and meanings in both systems. + +## Shared user properties + +- `workspace_id` +- `workspace_namespace` +- `is_guest` + +## Event mapping + +- `notebook_upload_button_clicked` + - params: `source_count_snapshot`, `surface` +- `notebook_document_upload_completed` + - params: `uploaded_count`, `file_types`, `total_size_bytes`, `source_count_before`, `source_count_after` +- `notebook_document_upload_failed` + - params: `file_type`, `file_size_bytes`, `error_type`, `error_message` +- `notebook_assistant_question_submitted` + - params: `thread_id`, `selected_sources_count`, `source_count_snapshot`, `message_length` +- `notebook_assistant_answer_completed` + - params: `thread_id`, `latency_ms` +- `notebook_assistant_answer_failed` + - params: `thread_id`, `latency_ms`, `error_type`, `error_message` +- `notebook_dashboard_link_clicked` + - params: `from_page`, `target_url`, `has_sources`, `has_chats` +- `notebook_workspace_first_document_uploaded` + - params: `surface` +- `notebook_workspace_first_question_asked` + - params: `selected_sources_count`, `surface` + +## GA4 implementation notes + +- Send GA4 events from the same trigger points as PostHog. +- Attach `workspace_id`, `workspace_namespace`, and `is_guest` as GA4 user properties after auth state resolves. +- Keep `error_message` truncated to avoid oversized payloads. diff --git a/docs/posthog-notebook-tracking.md b/docs/posthog-notebook-tracking.md new file mode 100644 index 0000000..499a276 --- /dev/null +++ b/docs/posthog-notebook-tracking.md @@ -0,0 +1,34 @@ +# PostHog Notebook Tracking Setup + +This runbook configures the four baseline insights for Knowhere Notebook usage. + +## Events + +- `notebook_upload_button_clicked` +- `notebook_document_upload_completed` +- `notebook_assistant_question_submitted` +- `notebook_dashboard_link_clicked` + +## Create the Insights + +1. Open PostHog `Product Analytics -> Insights`. +2. Create a `Trends` insight named `Notebook - Upload Button Clicks`. + - Series: event `notebook_upload_button_clicked` + - Math: `Total count` +3. Create `Notebook - Uploaded Documents`. + - Series: event `notebook_document_upload_completed` + - Math: `Sum` + - Property: `uploaded_count` +4. Create `Notebook - Avg Sources Per Question`. + - Series: event `notebook_assistant_question_submitted` + - Math: `Average` + - Property: `selected_sources_count` +5. Create `Notebook - Dashboard Click Users`. + - Series: event `notebook_dashboard_link_clicked` + - Math: `Unique users` + +## Dashboard + +1. Open PostHog `Dashboards` and create `Notebook Tracking`. +2. Add all four insights above to this dashboard. +3. Use a rolling `Last 30 days` filter for product reviews. diff --git a/package-lock.json b/package-lock.json index d5a79bb..f543f10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@ai-sdk/react": "^3.0.177", "@effect/platform": "^0.96.1", "@neondatabase/serverless": "^1.1.0", - "@ontos-ai/knowhere-sdk": "^0.4.0", + "@ontos-ai/knowhere-sdk": "^0.6.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -39,6 +39,7 @@ "next-themes": "^0.4.6", "pdfjs-dist": "5.4.296", "postgres": "^3.4.9", + "posthog-js": "^1.386.8", "react": "19.2.4", "react-dom": "19.2.4", "react-markdown": "^10.1.0", @@ -2251,7 +2252,9 @@ } }, "node_modules/@ontos-ai/knowhere-sdk": { - "version": "0.4.0", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@ontos-ai/knowhere-sdk/-/knowhere-sdk-0.6.0.tgz", + "integrity": "sha512-YqDgg+BKwGp7NepD50aquhEOmUgdDAQxpoyttqh0d+4tWjOgkSo74vEGiJVxt8v+LAcA/5CmxPeghX1Hx0v3wQ==", "license": "MIT", "dependencies": { "axios": "^1.15.2", @@ -2310,6 +2313,21 @@ "node": ">=18" } }, + "node_modules/@posthog/core": { + "version": "1.32.5", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.32.5.tgz", + "integrity": "sha512-Vmr6LZms7QEP1ljfTRRBVtLkeEHMrTmLECt0FXPLwUIgPwxOgvBdAYwRMWyP13WuN/XQweo1tJ9J1DxHNAF3qg==", + "license": "MIT", + "dependencies": { + "@posthog/types": "^1.386.4" + } + }, + "node_modules/@posthog/types": { + "version": "1.386.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.386.4.tgz", + "integrity": "sha512-UgE9+5+wkZg7yc2MpW5G32/6zYW8fAbXJkG8WGnrb4X8OKWzNa9cWeNCd82vKok+TSoRpM4qiYQFtBSpJS1dsw==", + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "license": "MIT" @@ -5114,6 +5132,17 @@ "node": ">=6.6.0" } }, + "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/core-util-is": { "version": "1.0.3", "license": "MIT" @@ -7567,6 +7596,12 @@ "node": "^12.20 || >= 14.13" } }, + "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/figures": { "version": "6.1.0", "dev": true, @@ -11462,6 +11497,22 @@ "url": "https://github.com/sponsors/porsager" } }, + "node_modules/posthog-js": { + "version": "1.386.8", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.386.8.tgz", + "integrity": "sha512-L4cJD1sleUULE7K4mZQI5wpS24NHGtNmFw1rPHruc579unVzHuooHiVDEHp/qyNnhNxtNE6alBTiVORbYnRTSw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@posthog/core": "^1.32.4", + "@posthog/types": "^1.386.4", + "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/powershell-utils": { "version": "0.1.0", "dev": true, @@ -11473,6 +11524,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/prelude-ls": { "version": "1.2.1", "dev": true, @@ -11623,6 +11684,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/queue-microtask": { "version": "1.2.3", "dev": true, @@ -14917,6 +14984,12 @@ "node": ">= 8" } }, + "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": "8.0.1", "dev": true, @@ -15268,111 +15341,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", - "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", - "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", - "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", - "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", - "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", - "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", - "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index aca7377..8449b3b 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "next-themes": "^0.4.6", "pdfjs-dist": "5.4.296", "postgres": "^3.4.9", + "posthog-js": "^1.386.8", "react": "19.2.4", "react-dom": "19.2.4", "react-markdown": "^10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6213a9..484f2fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: postgres: specifier: ^3.4.9 version: 3.4.9 + posthog-js: + specifier: ^1.386.8 + version: 1.386.8 react: specifier: 19.2.4 version: 19.2.4 @@ -1434,6 +1437,12 @@ packages: engines: {node: '>=18'} hasBin: true + '@posthog/core@1.32.5': + resolution: {integrity: sha512-Vmr6LZms7QEP1ljfTRRBVtLkeEHMrTmLECt0FXPLwUIgPwxOgvBdAYwRMWyP13WuN/XQweo1tJ9J1DxHNAF3qg==} + + '@posthog/types@1.386.4': + resolution: {integrity: sha512-UgE9+5+wkZg7yc2MpW5G32/6zYW8fAbXJkG8WGnrb4X8OKWzNa9cWeNCd82vKok+TSoRpM4qiYQFtBSpJS1dsw==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2716,6 +2725,9 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3312,6 +3324,9 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -4520,10 +4535,16 @@ packages: resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} engines: {node: '>=12'} + posthog-js@1.386.8: + resolution: {integrity: sha512-L4cJD1sleUULE7K4mZQI5wpS24NHGtNmFw1rPHruc579unVzHuooHiVDEHp/qyNnhNxtNE6alBTiVORbYnRTSw==} + powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} + preact@10.29.2: + resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4568,6 +4589,9 @@ packages: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5310,6 +5334,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-vitals@5.3.0: + resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==} + webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -6396,6 +6423,12 @@ snapshots: dependencies: playwright: 1.58.2 + '@posthog/core@1.32.5': + dependencies: + '@posthog/types': 1.386.4 + + '@posthog/types@1.386.4': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -7579,6 +7612,8 @@ snapshots: cookie@1.1.1: {} + core-js@3.49.0: {} + core-util-is@1.0.3: {} cors@2.8.6: @@ -8317,6 +8352,8 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fflate@0.4.8: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -9709,8 +9746,21 @@ snapshots: postgres@3.4.9: {} + posthog-js@1.386.8: + dependencies: + '@posthog/core': 1.32.5 + '@posthog/types': 1.386.4 + core-js: 3.49.0 + dompurify: 3.4.2 + fflate: 0.4.8 + preact: 10.29.2 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.3.0 + powershell-utils@0.1.0: {} + preact@10.29.2: {} + prelude-ls@1.2.1: {} pretty-format@27.5.1: @@ -9753,6 +9803,8 @@ snapshots: dependencies: side-channel: 1.1.0 + query-selector-shadow-dom@1.0.1: {} + queue-microtask@1.2.3: {} range-parser@1.2.1: {} @@ -10654,6 +10706,8 @@ snapshots: web-streams-polyfill@3.3.3: {} + web-vitals@5.3.0: {} + webidl-conversions@8.0.1: {} whatwg-mimetype@5.0.0: {} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index afbda8b..6520102 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import { ThemeProvider } from "@/components/theme-provider"; import { appMetadata } from "@/lib/app-metadata"; +import { PostHogInitializer } from "@/providers/posthog-initializer"; import "./globals.css"; const geistSans = Geist({ @@ -34,7 +35,7 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - {children} + {children} diff --git a/src/components/chat-composer.tsx b/src/components/chat-composer.tsx index 807f298..8f1de60 100644 --- a/src/components/chat-composer.tsx +++ b/src/components/chat-composer.tsx @@ -21,7 +21,6 @@ import { import { Spinner } from "@/components/ui/spinner"; import { Textarea } from "@/components/ui/textarea"; import { chatPromptTemplates } from "@/domains/chat/prompt-templates"; - const chatComposerId = "chat-composer"; const placeholderPattern = /(\[[^\]\r\n]{1,80}\])/gu; const placeholderSegmentPattern = /^\[[^\]\r\n]{1,80}\]$/u; diff --git a/src/components/chat-panel.test.ts b/src/components/chat-panel.test.ts index 1d9736e..cc39fc2 100644 --- a/src/components/chat-panel.test.ts +++ b/src/components/chat-panel.test.ts @@ -4,6 +4,15 @@ import { cleanup, render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +const analyticsMocks = vi.hoisted(() => ({ + trackNotebookAssistantQuestionSubmitted: vi.fn(), +})); + +vi.mock("@/lib/posthog", () => ({ + trackNotebookAssistantQuestionSubmitted: + analyticsMocks.trackNotebookAssistantQuestionSubmitted, +})); + import { workspaceClient } from "@/domains/workspace/client"; import { ChatPanel } from "./chat-panel"; @@ -23,6 +32,7 @@ describe("ChatPanel", () => { disconnect() {} }; vi.mocked(workspaceClient.createChatDiagram).mockReset(); + analyticsMocks.trackNotebookAssistantQuestionSubmitted.mockReset(); mockVisibleVirtualViewport(); }); @@ -242,11 +252,56 @@ describe("ChatPanel", () => { await user.click(screen.getByRole("button", { name: "Send message" })); expect(onSend).not.toHaveBeenCalled(); + expect( + analyticsMocks.trackNotebookAssistantQuestionSubmitted, + ).not.toHaveBeenCalled(); expect(workspaceClient.createChatDiagram).toHaveBeenCalledWith({ answer: "Cloud revenue was 42 and Ads revenue was 28.", }); }); + it("tracks normal composer sends as assistant questions", async () => { + const user = userEvent.setup(); + const onSend = vi.fn(); + + render( + React.createElement(C, { + activeThreadId: "thread_1", + analyticsContext: { + workspaceId: "workspace_1", + workspaceNamespace: "demo", + userId: "user_1", + isGuest: false, + }, + selectedSourcesCount: 2, + sourceCount: 4, + onSend, + }), + ); + + await user.type( + screen.getByPlaceholderText("Ask a question about your documents…"), + "Summarize revenue", + ); + await user.click(screen.getByRole("button", { name: "Send message" })); + + expect(onSend).toHaveBeenCalledWith("Summarize revenue"); + expect( + analyticsMocks.trackNotebookAssistantQuestionSubmitted, + ).toHaveBeenCalledWith({ + context: { + workspaceId: "workspace_1", + workspaceNamespace: "demo", + userId: "user_1", + isGuest: false, + }, + threadId: "thread_1", + selectedSourcesCount: 2, + sourceCountSnapshot: 4, + messageLength: "Summarize revenue".length, + }); + }); + it("uses Notebook source titles for generated Knowhere citation filenames", () => { const { container } = render( React.createElement(C, { diff --git a/src/components/chat-panel.tsx b/src/components/chat-panel.tsx index 1aeb897..7600cf0 100644 --- a/src/components/chat-panel.tsx +++ b/src/components/chat-panel.tsx @@ -38,6 +38,10 @@ import type { ChatThreadView, } from "@/domains/chat/types"; import { workspaceClient } from "@/domains/workspace/client"; +import { + trackNotebookAssistantQuestionSubmitted, + type AnalyticsContext, +} from "@/lib/posthog"; export type ChatPanelProps = { messages: ChatMessageView[]; @@ -51,6 +55,8 @@ export type ChatPanelProps = { onLoginClick?: () => void; sourceTitlesByDocumentId?: Readonly>; sourceCount?: number; + selectedSourcesCount?: number; + analyticsContext?: AnalyticsContext; isSending?: boolean; isHistoryLoading?: boolean; isCreatingThread?: boolean; @@ -73,6 +79,8 @@ export function ChatPanel({ onLoginClick, sourceTitlesByDocumentId = {}, sourceCount = 0, + selectedSourcesCount = 0, + analyticsContext, isSending = false, isHistoryLoading = false, isCreatingThread = false, @@ -150,6 +158,13 @@ export function ChatPanel({ return; } + void trackNotebookAssistantQuestionSubmitted({ + context: analyticsContext, + threadId: activeThreadId, + selectedSourcesCount, + sourceCountSnapshot: sourceCount, + messageLength: text.length, + }); onSend?.(text); } diff --git a/src/components/chunks-panel.tsx b/src/components/chunks-panel.tsx index 7d5b853..fd9e7a0 100644 --- a/src/components/chunks-panel.tsx +++ b/src/components/chunks-panel.tsx @@ -43,6 +43,7 @@ import { useSourceOriginalPreviewWarmup } from "@/components/source-original-pre import { sourceOriginalPreviewModel } from "@/components/source-original-preview-model"; import type { ParsedChunkView } from "@/domains/chunks/types"; import type { SourceOriginalFileView, SourceView } from "@/domains/sources/types"; +import type { AnalyticsContext } from "@/lib/posthog"; import { cn } from "@/lib/utils"; export type ChunksPanelProps = { @@ -60,6 +61,8 @@ export type ChunksPanelProps = { onLoadAllChunks?: () => void; onLoginClick?: () => void; onSourceUploaded?: (source: SourceView) => void; + analyticsContext?: AnalyticsContext; + sourceCountSnapshot?: number; }; type ChunkDisplayMode = "list" | "tree"; @@ -85,6 +88,8 @@ export function ChunksPanel({ onLoadAllChunks, onLoginClick, onSourceUploaded, + analyticsContext, + sourceCountSnapshot = 0, }: Partial = {}) { const originalPreviewCacheKey = selectedSourceFile?.url ?? null; const isOriginalPreviewAvailable = @@ -362,6 +367,8 @@ export function ChunksPanel({ ) ) : isTreeModeVisible ? ( @@ -967,11 +974,15 @@ function truncateTreeLabel(value: string): string { } function EmptySourceUploadState({ + analyticsContext, onLoginClick, onSourceUploaded, + sourceCountSnapshot = 0, }: { + readonly analyticsContext?: AnalyticsContext; readonly onLoginClick?: () => void; readonly onSourceUploaded?: (source: SourceView) => void; + readonly sourceCountSnapshot?: number; }): ReactNode { if (onLoginClick) { return ( @@ -999,6 +1010,8 @@ function EmptySourceUploadState({ return ( (