diff --git a/apps/client/package.json b/apps/client/package.json index 443fa4e6..9abc7c64 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -41,6 +41,7 @@ "lowlight": "^3.3.0", "mermaid": "^11.6.0", "mitt": "^3.0.1", + "posthog-js": "^1.255.1", "react": "^18.3.1", "react-arborist": "3.4.0", "react-clear-modal": "^2.0.15", diff --git a/apps/client/src/components/layouts/global/layout.tsx b/apps/client/src/components/layouts/global/layout.tsx index 26446327..e15149d7 100644 --- a/apps/client/src/components/layouts/global/layout.tsx +++ b/apps/client/src/components/layouts/global/layout.tsx @@ -1,6 +1,8 @@ import { UserProvider } from "@/features/user/user-provider.tsx"; import { Outlet } from "react-router-dom"; import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx"; +import { PosthogUser } from "@/ee/components/posthog-user.tsx"; +import { isCloud } from "@/lib/config.ts"; export default function Layout() { return ( @@ -8,6 +10,7 @@ export default function Layout() { + {isCloud() && } ); } diff --git a/apps/client/src/ee/components/posthog-user.tsx b/apps/client/src/ee/components/posthog-user.tsx new file mode 100644 index 00000000..893b0de9 --- /dev/null +++ b/apps/client/src/ee/components/posthog-user.tsx @@ -0,0 +1,41 @@ +import { usePostHog } from "posthog-js/react"; +import { useEffect } from "react"; +import { useAtom } from "jotai"; +import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts"; + +export function PosthogUser() { + const posthog = usePostHog(); + const [currentUser] = useAtom(currentUserAtom); + + useEffect(() => { + if (currentUser) { + const user = currentUser?.user; + const workspace = currentUser?.workspace; + if (!user || !workspace) return; + + posthog?.identify(user.id, { + name: user.name, + email: user.email, + workspaceId: user.workspaceId, + workspaceHostname: workspace.hostname, + lastActiveAt: new Date().toISOString(), + createdAt: user.createdAt, + source: "docmost-app", + }); + posthog?.group("workspace", workspace.id, { + name: workspace.name, + hostname: workspace.hostname, + plan: workspace?.plan, + status: workspace.status, + isOnTrial: !!workspace.trialEndAt, + hasStripeCustomerId: !!workspace.stripeCustomerId, + memberCount: workspace.memberCount, + lastActiveAt: new Date().toISOString(), + createdAt: workspace.createdAt, + source: "docmost-app", + }); + } + }, [posthog, currentUser]); + + return null; +} diff --git a/apps/client/src/features/workspace/types/workspace.types.ts b/apps/client/src/features/workspace/types/workspace.types.ts index 730106ce..c9df7f19 100644 --- a/apps/client/src/features/workspace/types/workspace.types.ts +++ b/apps/client/src/features/workspace/types/workspace.types.ts @@ -12,6 +12,7 @@ export interface IWorkspace { settings: any; status: string; enforceSso: boolean; + stripeCustomerId: string; billingEmail: string; trialEndAt: Date; createdAt: Date; diff --git a/apps/client/src/lib/config.ts b/apps/client/src/lib/config.ts index 717bf9ff..510eb25f 100644 --- a/apps/client/src/lib/config.ts +++ b/apps/client/src/lib/config.ts @@ -83,6 +83,18 @@ export function getBillingTrialDays() { return getConfigValue("BILLING_TRIAL_DAYS"); } +export function getPostHogHost() { + return getConfigValue("POSTHOG_HOST"); +} + +export function isPostHogEnabled(): boolean { + return Boolean(getPostHogHost() && getPostHogKey()); +} + +export function getPostHogKey() { + return getConfigValue("POSTHOG_KEY"); +} + function getConfigValue(key: string, defaultValue: string = undefined): string { const rawValue = import.meta.env.DEV ? process?.env?.[key] diff --git a/apps/client/src/main.tsx b/apps/client/src/main.tsx index dded807f..13bb7864 100644 --- a/apps/client/src/main.tsx +++ b/apps/client/src/main.tsx @@ -3,7 +3,7 @@ import "@mantine/spotlight/styles.css"; import "@mantine/notifications/styles.css"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; -import { mantineCssResolver, theme } from '@/theme'; +import { mantineCssResolver, theme } from "@/theme"; import { MantineProvider } from "@mantine/core"; import { BrowserRouter } from "react-router-dom"; import { ModalsProvider } from "@mantine/modals"; @@ -11,6 +11,14 @@ import { Notifications } from "@mantine/notifications"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { HelmetProvider } from "react-helmet-async"; import "./i18n"; +import { PostHogProvider } from "posthog-js/react"; +import { + getPostHogHost, + getPostHogKey, + isCloud, + isPostHogEnabled, +} from "@/lib/config.ts"; +import posthog from "posthog-js"; export const queryClient = new QueryClient({ defaultOptions: { @@ -23,9 +31,16 @@ export const queryClient = new QueryClient({ }, }); +if (isCloud() && isPostHogEnabled) { + posthog.init(getPostHogKey(), { + api_host: getPostHogHost(), + defaults: "2025-05-24", + disable_session_recording: true, + }); +} const root = ReactDOM.createRoot( - document.getElementById("root") as HTMLElement + document.getElementById("root") as HTMLElement, ); root.render( @@ -35,10 +50,12 @@ root.render( - + + + - + , ); diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts index cc8a01fd..a5fafce3 100644 --- a/apps/client/vite.config.ts +++ b/apps/client/vite.config.ts @@ -14,6 +14,8 @@ export default defineConfig(({ mode }) => { SUBDOMAIN_HOST, COLLAB_URL, BILLING_TRIAL_DAYS, + POSTHOG_HOST, + POSTHOG_KEY, } = loadEnv(mode, envPath, ""); return { @@ -27,6 +29,8 @@ export default defineConfig(({ mode }) => { SUBDOMAIN_HOST, COLLAB_URL, BILLING_TRIAL_DAYS, + POSTHOG_HOST, + POSTHOG_KEY, }, APP_VERSION: JSON.stringify(process.env.npm_package_version), }, diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts index 639113a6..3ce728ea 100644 --- a/apps/server/src/integrations/environment/environment.service.ts +++ b/apps/server/src/integrations/environment/environment.service.ts @@ -205,4 +205,12 @@ export class EnvironmentService { .toLowerCase(); return disable === 'true'; } + + getPostHogHost(): string { + return this.configService.get('POSTHOG_HOST'); + } + + getPostHogKey(): string { + return this.configService.get('POSTHOG_KEY'); + } } diff --git a/apps/server/src/integrations/static/static.module.ts b/apps/server/src/integrations/static/static.module.ts index 156fcf44..52c3c160 100644 --- a/apps/server/src/integrations/static/static.module.ts +++ b/apps/server/src/integrations/static/static.module.ts @@ -47,6 +47,8 @@ export class StaticModule implements OnModuleInit { BILLING_TRIAL_DAYS: this.environmentService.isCloud() ? this.environmentService.getBillingTrialDays() : undefined, + POSTHOG_HOST: this.environmentService.getPostHogHost(), + POSTHOG_KEY: this.environmentService.getPostHogKey(), }; const windowScriptContent = ``; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b248afbe..b8c0c88c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -296,6 +296,9 @@ importers: mitt: specifier: ^3.0.1 version: 3.0.1 + posthog-js: + specifier: ^1.255.1 + version: 1.255.1 react: specifier: ^18.3.1 version: 18.3.1 @@ -5213,6 +5216,9 @@ packages: core-js-compat@3.35.0: resolution: {integrity: sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==} + core-js@3.43.0: + resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -5988,6 +5994,9 @@ packages: picomatch: optional: true + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -7955,9 +7964,23 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + posthog-js@1.255.1: + resolution: {integrity: sha512-KMh0o9MhORhEZVjXpktXB5rJ8PfDk+poqBoTSoLzWgNjhJf6D8jcyB9jUMA6vVPfn4YeepVX5NuclDRqOwr5Mw==} + peerDependencies: + '@rrweb/types': 2.0.0-alpha.17 + rrweb-snapshot: 2.0.0-alpha.17 + peerDependenciesMeta: + '@rrweb/types': + optional: true + rrweb-snapshot: + optional: true + postmark@4.0.5: resolution: {integrity: sha512-nerZdd3TwOH4CgGboZnlUM/q7oZk0EqpZgJL+Y3Nup8kHeaukxouQ6JcFF3EJEijc4QbuNv1TefGhboAKtf/SQ==} + preact@10.26.9: + resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -9297,6 +9320,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + web-worker@1.5.0: resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} @@ -15194,6 +15220,8 @@ snapshots: dependencies: browserslist: 4.24.2 + core-js@3.43.0: {} + core-util-is@1.0.3: {} cors@2.8.5: @@ -16181,6 +16209,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.4.8: {} + fflate@0.8.2: {} figures@3.2.0: @@ -18482,12 +18512,21 @@ snapshots: postgres-range@1.1.4: {} + posthog-js@1.255.1: + dependencies: + core-js: 3.43.0 + fflate: 0.4.8 + preact: 10.26.9 + web-vitals: 4.2.4 + postmark@4.0.5: dependencies: axios: 1.9.0 transitivePeerDependencies: - debug + preact@10.26.9: {} + prelude-ls@1.2.1: {} prettier@3.4.1: {} @@ -19911,6 +19950,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-vitals@4.2.4: {} + web-worker@1.5.0: {} webidl-conversions@3.0.1: {}