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: {}