From 6018de47e551e052450f257431f4497f079a4208 Mon Sep 17 00:00:00 2001 From: rogutkuba Date: Sat, 25 Jul 2026 17:53:13 -0400 Subject: [PATCH 1/2] Add PostHog analytics with session replay to web app Initialize posthog-js alongside Sentry, identify users and their active org, and enable session replay with inputs masked by default. The project key is env-var only (VITE_PUBLIC_POSTHOG_KEY) with no baked default, so PostHog stays disabled in local dev and until a key is configured. Co-Authored-By: Claude Opus 4.8 --- apps/web/.env.example | 7 +++- apps/web/package.json | 1 + apps/web/src/lib/posthog.ts | 34 +++++++++++++++ apps/web/src/main.tsx | 20 +++++++++ apps/web/vite.config.ts | 9 ++++ pnpm-lock.yaml | 83 +++++++++++++++++++++++++++++++++++++ 6 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/lib/posthog.ts diff --git a/apps/web/.env.example b/apps/web/.env.example index 097042e..0d32423 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,2 +1,7 @@ VITE_APP_URL="http://localhost:3000" -VITE_API_URL="http://localhost:3000" \ No newline at end of file +VITE_API_URL="http://localhost:3000" + +# PostHog analytics + session replay (public client-side key). +# Leave empty to disable PostHog. Host defaults to https://us.i.posthog.com. +VITE_PUBLIC_POSTHOG_KEY="" +VITE_PUBLIC_POSTHOG_HOST="https://us.i.posthog.com" \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 0d1943b..9dd5d5a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -100,6 +100,7 @@ "motion": "^12.23.6", "partysocket": "1.1.4", "postcss": "^8.5.6", + "posthog-js": "^1.407.2", "react": "^19.2.1", "react-cookie": "^8.0.1", "react-day-picker": "^9.7.0", diff --git a/apps/web/src/lib/posthog.ts b/apps/web/src/lib/posthog.ts new file mode 100644 index 0000000..5caeea1 --- /dev/null +++ b/apps/web/src/lib/posthog.ts @@ -0,0 +1,34 @@ +import posthog from 'posthog-js'; + +// PostHog is disabled in local dev (MODE === 'development') and whenever no +// project key is configured. The key/host are baked in at build time via +// `import.meta.env.VITE_PUBLIC_POSTHOG_KEY` / `VITE_PUBLIC_POSTHOG_HOST`. +const posthogKey = import.meta.env.VITE_PUBLIC_POSTHOG_KEY; +const posthogHost = import.meta.env.VITE_PUBLIC_POSTHOG_HOST; + +export const isPostHogEnabled = import.meta.env.MODE !== 'development' && Boolean(posthogKey); + +export function initPostHog() { + if (!isPostHogEnabled) { + return; + } + + posthog.init(posthogKey, { + api_host: posthogHost, + // Only fire pageviews once we've bootstrapped; we let the router drive them. + capture_pageview: true, + capture_pageleave: true, + person_profiles: 'identified_only', + // Session replay. + disable_session_recording: false, + session_recording: { + // Mask user-entered text by default so we don't record sensitive input + // (candidate code, credentials, etc.). Loosen per-element with + // `ph-no-mask` if you want specific fields recorded verbatim. + maskAllInputs: true, + maskTextSelector: '[data-ph-mask]', + }, + }); +} + +export { posthog }; diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 646134b..616b048 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -11,6 +11,7 @@ import './App.css'; import NotFound from '@/components/common/NotFound.tsx'; import { PendingView } from '@/components/common/PendingView.tsx'; import { AuthContext, AuthProvider, useAuth } from '@/contexts/AuthContext.tsx'; +import { initPostHog, isPostHogEnabled, posthog } from '@/lib/posthog.ts'; import { TanstackQueryClient } from '@/query/client.ts'; import reportWebVitals from './reportWebVitals.ts'; @@ -48,6 +49,10 @@ Sentry.init({ sendDefaultPii: true, }); +// Initialize PostHog analytics + session replay. Disabled in local dev and when +// no project key is configured (see @/lib/posthog). +initPostHog(); + const authClient = Promise.withResolvers(); function InnerApp() { @@ -61,6 +66,21 @@ function InnerApp() { // Tie Sentry events to the current user (cleared on sign-out). Sentry.setUser(auth.user ? { id: auth.user.id, email: auth.user.email } : null); + // Tie PostHog events + session replays to the current user (reset on sign-out). + if (isPostHogEnabled) { + if (auth.user) { + posthog.identify(auth.user.id, { + email: auth.user.email, + name: auth.user.name, + }); + if (auth.session?.activeOrganizationId) { + posthog.group('organization', auth.session.activeOrganizationId); + } + } else { + posthog.reset(); + } + } + authClient.resolve(auth); }, [auth, auth.isInitalLoading]); diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 15c4f4a..cb4fcaa 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -29,6 +29,15 @@ export default defineConfig({ process.env.VITE_SENTRY_DSN ?? 'https://d3de88682d569cc42655cfd5bc148001@o4510473855959040.ingest.us.sentry.io/4511725503512576' ), + // PostHog project API keys are public (shipped in the client bundle), but we + // intentionally keep the key env-var only with no baked default so PostHog + // stays off unless VITE_PUBLIC_POSTHOG_KEY is set at build time. + 'import.meta.env.VITE_PUBLIC_POSTHOG_KEY': JSON.stringify( + process.env.VITE_PUBLIC_POSTHOG_KEY ?? '' + ), + 'import.meta.env.VITE_PUBLIC_POSTHOG_HOST': JSON.stringify( + process.env.VITE_PUBLIC_POSTHOG_HOST ?? 'https://us.i.posthog.com' + ), }, resolve: { alias: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d9b824..b3bacd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -454,6 +454,9 @@ importers: postcss: specifier: ^8.5.6 version: 8.5.6 + posthog-js: + specifier: ^1.407.2 + version: 1.407.2 react: specifier: ^19.2.1 version: 19.2.1 @@ -3336,6 +3339,15 @@ packages: '@poppinss/exception@1.2.2': resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} + '@posthog/browser-common@0.2.1': + resolution: {integrity: sha512-FTVPsRw6GBKWvFwN26TNIQNNYPR9FsUj+B+CKhk/ht63IRzn4yYFtFOjcmW+bfXvNHuRADs6APbZ6Haz1kpoAw==} + + '@posthog/core@1.45.1': + resolution: {integrity: sha512-tLtvzomavb2PPWdGYKsusyIzIeL2Px47v348Smibkay7sMy/83TyPk+Ptsp2NdeOgJsbuwSxWkR2+XA0aSCAaA==} + + '@posthog/types@1.398.0': + resolution: {integrity: sha512-sJMkl4k+u8yS/0fjHsKqE9xTdsAh30a2WvgChiptellnVoE0e8QJKFgqOMD2sk8FaEArPdeFklAhXvmENAt3Sg==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -5264,6 +5276,9 @@ packages: '@types/tar@6.1.13': resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -5965,6 +5980,9 @@ packages: core-js@3.44.0: resolution: {integrity: sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -6184,6 +6202,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dompurify@3.4.12: + resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} + dotenv-expand@11.0.7: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} @@ -6691,6 +6712,9 @@ packages: picomatch: optional: true + fflate@0.4.9: + resolution: {integrity: sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -8239,6 +8263,17 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} + posthog-js@1.407.2: + resolution: {integrity: sha512-5deTvXopn+NJWEmCUw8Ix/ms3cv2+60WJ73Vgbvu2wSkZY/FIj2uqAgCnq7IewGpGC+tH7CAbPYFzH6hw8eW1w==} + + preact@10.29.7: + resolution: {integrity: sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==} + peerDependencies: + preact-render-to-string: '>=5' + peerDependenciesMeta: + preact-render-to-string: + optional: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -8364,6 +8399,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} 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==} @@ -9460,6 +9498,9 @@ packages: web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + web-vitals@5.3.0: + resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -12443,6 +12484,17 @@ snapshots: '@poppinss/exception@1.2.2': {} + '@posthog/browser-common@0.2.1': + dependencies: + '@posthog/core': 1.45.1 + '@posthog/types': 1.398.0 + + '@posthog/core@1.45.1': + dependencies: + '@posthog/types': 1.398.0 + + '@posthog/types@1.398.0': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.2': {} @@ -15226,6 +15278,9 @@ snapshots: '@types/node': 24.1.0 minipass: 4.2.8 + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -15997,6 +16052,8 @@ snapshots: core-js@3.44.0: {} + core-js@3.49.0: {} + crelt@1.0.6: {} cross-spawn@7.0.6: @@ -16177,6 +16234,10 @@ snapshots: '@babel/runtime': 7.28.2 csstype: 3.1.3 + dompurify@3.4.12: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dotenv-expand@11.0.7: dependencies: dotenv: 16.6.1 @@ -16892,6 +16953,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.4.9: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -18699,6 +18762,22 @@ snapshots: postgres@3.4.7: {} + posthog-js@1.407.2: + dependencies: + '@posthog/browser-common': 0.2.1 + '@posthog/core': 1.45.1 + '@posthog/types': 1.398.0 + core-js: 3.49.0 + dompurify: 3.4.12 + fflate: 0.4.9 + preact: 10.29.7 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.3.0 + transitivePeerDependencies: + - preact-render-to-string + + preact@10.29.7: {} + prelude-ls@1.2.1: {} prettier@3.6.2: {} @@ -18861,6 +18940,8 @@ snapshots: dependencies: side-channel: 1.1.0 + query-selector-shadow-dom@1.0.1: {} + queue-microtask@1.2.3: {} radix-ui@1.4.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): @@ -20308,6 +20389,8 @@ snapshots: web-vitals@4.2.4: {} + web-vitals@5.3.0: {} + webidl-conversions@7.0.0: {} webpack-virtual-modules@0.6.2: {} From 9813343b9c511e2fbddcd3494a632339eeab1ad5 Mon Sep 17 00:00:00 2001 From: rogutkuba Date: Sat, 25 Jul 2026 18:01:26 -0400 Subject: [PATCH 2/2] Shorten PostHog config comment Co-Authored-By: Claude Opus 4.8 --- apps/web/vite.config.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index cb4fcaa..66da16b 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -29,9 +29,7 @@ export default defineConfig({ process.env.VITE_SENTRY_DSN ?? 'https://d3de88682d569cc42655cfd5bc148001@o4510473855959040.ingest.us.sentry.io/4511725503512576' ), - // PostHog project API keys are public (shipped in the client bundle), but we - // intentionally keep the key env-var only with no baked default so PostHog - // stays off unless VITE_PUBLIC_POSTHOG_KEY is set at build time. + // Env-var only, no default: PostHog stays off unless the key is set. 'import.meta.env.VITE_PUBLIC_POSTHOG_KEY': JSON.stringify( process.env.VITE_PUBLIC_POSTHOG_KEY ?? '' ),