From 6b0355e9831a3a658b02d1caef490d290d8f5125 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 11 Feb 2026 15:54:11 +0000 Subject: [PATCH] feat: add PostHog tracking for docs feedback - Add posthog-node for server-side event capture - Create combined feedback adapter (Slack + PostHog) - Track docs_feedback_submitted, docs_feedback_helpful, docs_feedback_not_helpful events - Include page URL, category, and message in event properties - Enable session recording with password masking Amp-Thread-ID: https://ampcode.com/threads/T-019c4d62-38c2-7005-bbd3-3ad64e139c61 Co-authored-by: Amp --- package.json | 1 + pnpm-lock.yaml | 19 +++++++++ src/components/PostHogSetup.tsx | 6 +++ src/lib/feedback-adapter.ts | 70 +++++++++++++++++++++++++++++++++ src/lib/posthog.ts | 11 ++++++ vocs.config.ts | 5 ++- 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/lib/feedback-adapter.ts diff --git a/package.json b/package.json index 86bb9fc9..10958aff 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "monaco-editor": "^0.55.1", "ox": "^0.11.3", "posthog-js": "^1.333.0", + "posthog-node": "^5.24.15", "prool": "^0.2.2", "react": "^19.2.3", "react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5dfb305a..a750282f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,9 @@ importers: posthog-js: specifier: ^1.333.0 version: 1.333.0 + posthog-node: + specifier: ^5.24.15 + version: 5.24.15 prool: specifier: ^0.2.2 version: 0.2.2 @@ -732,6 +735,9 @@ packages: '@posthog/core@1.13.0': resolution: {integrity: sha512-knjncrk7qRmssFRbGzBl1Tunt21GRpe0Wv+uVelyL0Rh7PdQUsgguulzXFTps8hA6wPwTU4kq85qnbAJ3eH6Wg==} + '@posthog/core@1.22.0': + resolution: {integrity: sha512-WkmOnq95aAOu6yk6r5LWr5cfXsQdpVbWDCwOxQwxSne8YV6GuZET1ziO5toSQXgrgbdcjrSz2/GopAfiL6iiAA==} + '@posthog/types@1.333.0': resolution: {integrity: sha512-9Wg/2ez+EZh6NmtOjhtYSkBHz/yIq8WMS0QSIizUoggh35hHVg4BTMXl3rz/tPearJNKU/8oRjEyuZ0OYTEDOA==} @@ -3143,6 +3149,10 @@ packages: posthog-js@1.333.0: resolution: {integrity: sha512-c7vquERMedjuGE2GnaDDJW/V1BIMMQG7BlYKrH0z8O7fc3WpEsQ/IyQ+9aD9+DLxlDCFpzrwgoxVDWi9K37mdA==} + posthog-node@5.24.15: + resolution: {integrity: sha512-0QnWVOZAPwEAlp+r3r0jIGfk2IaNYM/2YnEJJhBMJZXs4LpHcTu7mX42l+e95o9xX87YpVuZU0kOkmtQUxgnOA==} + engines: {node: ^20.20.0 || >=22.22.0} + preact@10.28.2: resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} @@ -3224,6 +3234,7 @@ packages: react-server-dom-webpack@19.2.3: resolution: {integrity: sha512-ifo7aqqdNJyV6U2zuvvWX4rRQ51pbleuUFNG7ZYhIuSuWZzQPbfmYv11GNsyJm/3uGNbt8buJ9wmoISn/uOAfw==} engines: {node: '>=0.10.0'} + deprecated: High Security Vulnerability in React Server Components peerDependencies: react: ^19.2.3 react-dom: ^19.2.3 @@ -4619,6 +4630,10 @@ snapshots: dependencies: cross-spawn: 7.0.6 + '@posthog/core@1.22.0': + dependencies: + cross-spawn: 7.0.6 + '@posthog/types@1.333.0': {} '@protobufjs/aspromise@1.1.2': {} @@ -7182,6 +7197,10 @@ snapshots: query-selector-shadow-dom: 1.0.1 web-vitals: 4.2.4 + posthog-node@5.24.15: + dependencies: + '@posthog/core': 1.22.0 + preact@10.28.2: {} pretty-ms@9.3.0: diff --git a/src/components/PostHogSetup.tsx b/src/components/PostHogSetup.tsx index 19ec952a..ce17135b 100644 --- a/src/components/PostHogSetup.tsx +++ b/src/components/PostHogSetup.tsx @@ -16,6 +16,12 @@ function PostHogInitializer() { defaults: '2025-11-30', capture_exceptions: true, debug: import.meta.env.MODE === 'development', + session_recording: { + maskAllInputs: false, + maskInputOptions: { + password: true, + }, + }, }) posthog.register({ site: 'docs' }) diff --git a/src/lib/feedback-adapter.ts b/src/lib/feedback-adapter.ts new file mode 100644 index 00000000..3eb977ee --- /dev/null +++ b/src/lib/feedback-adapter.ts @@ -0,0 +1,70 @@ +import { PostHog } from 'posthog-node' +import { Feedback } from 'vocs/config' +import { POSTHOG_EVENTS, POSTHOG_PROPERTIES } from './posthog' + +type FeedbackData = { + helpful: boolean + category?: string | undefined + message?: string | undefined + pageUrl: string + timestamp: string +} + +export function createFeedbackAdapter() { + const slackAdapter = Feedback.slack() + + const posthogKey = process.env.VITE_POSTHOG_KEY + const posthogHost = process.env.VITE_POSTHOG_HOST || 'https://us.i.posthog.com' + + const posthog = posthogKey ? new PostHog(posthogKey, { host: posthogHost }) : null + + return Feedback.from({ + type: 'slack+posthog', + async submit(data: FeedbackData) { + const slackPromise = slackAdapter.submit(data) + + const posthogPromise = (async () => { + if (!posthog) return + + let pagePath: string | undefined + try { + pagePath = new URL(data.pageUrl).pathname + } catch { + pagePath = undefined + } + + const distinctId = `docs_feedback_${Date.now()}_${Math.random().toString(36).slice(2)}` + const ts = data.timestamp ? new Date(data.timestamp) : undefined + + const commonProperties = { + [POSTHOG_PROPERTIES.FEEDBACK_HELPFUL]: data.helpful, + [POSTHOG_PROPERTIES.FEEDBACK_CATEGORY]: data.category, + [POSTHOG_PROPERTIES.FEEDBACK_MESSAGE]: data.message, + [POSTHOG_PROPERTIES.FEEDBACK_PAGE_URL]: data.pageUrl, + [POSTHOG_PROPERTIES.PAGE_PATH]: pagePath, + [POSTHOG_PROPERTIES.SITE]: 'docs', + } + + posthog.capture({ + distinctId, + event: POSTHOG_EVENTS.FEEDBACK_SUBMITTED, + properties: commonProperties, + timestamp: ts, + }) + + posthog.capture({ + distinctId, + event: data.helpful + ? POSTHOG_EVENTS.FEEDBACK_HELPFUL + : POSTHOG_EVENTS.FEEDBACK_NOT_HELPFUL, + properties: commonProperties, + timestamp: ts, + }) + + await posthog.flush() + })().catch(() => {}) + + await Promise.allSettled([slackPromise, posthogPromise]) + }, + }) +} diff --git a/src/lib/posthog.ts b/src/lib/posthog.ts index f4310352..32878481 100644 --- a/src/lib/posthog.ts +++ b/src/lib/posthog.ts @@ -35,6 +35,11 @@ export const POSTHOG_EVENTS = { // Code interactions CODE_EXAMPLE_VIEW: 'docs_code_example_view', CODE_EXAMPLE_COPY: 'docs_code_example_copy', + + // Feedback + FEEDBACK_SUBMITTED: 'docs_feedback_submitted', + FEEDBACK_HELPFUL: 'docs_feedback_helpful', + FEEDBACK_NOT_HELPFUL: 'docs_feedback_not_helpful', } as const /** @@ -68,6 +73,12 @@ export const POSTHOG_PROPERTIES = { SEARCH_QUERY: 'search_query', SEARCH_RESULT_TITLE: 'search_result_title', SEARCH_RESULT_URL: 'search_result_url', + + // Feedback properties + FEEDBACK_HELPFUL: 'feedback_helpful', + FEEDBACK_CATEGORY: 'feedback_category', + FEEDBACK_MESSAGE: 'feedback_message', + FEEDBACK_PAGE_URL: 'feedback_page_url', } as const /** diff --git a/vocs.config.ts b/vocs.config.ts index 508b5c9d..6614236a 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -1,4 +1,5 @@ -import { Changelog, defineConfig, Feedback, McpSource } from 'vocs/config' +import { Changelog, defineConfig, McpSource } from 'vocs/config' +import { createFeedbackAdapter } from './src/lib/feedback-adapter' const baseUrl = (() => { if (URL.canParse(process.env.VITE_BASE_URL)) return process.env.VITE_BASE_URL @@ -19,7 +20,7 @@ export default defineConfig({ title: 'Tempo', titleTemplate: '%s ⋅ Tempo', description: 'Documentation for the Tempo network and protocol specifications', - feedback: Feedback.slack(), + feedback: createFeedbackAdapter(), mcp: { enabled: true, sources: [