diff --git a/apps/dashboard/.eslintrc.js b/apps/dashboard/.eslintrc.js index d19656a5eb7..3c12095ad34 100644 --- a/apps/dashboard/.eslintrc.js +++ b/apps/dashboard/.eslintrc.js @@ -110,6 +110,11 @@ module.exports = { message: 'This is likely a mistake. If you really want to import this - postfix the imported name with Icon. Example - "LinkIcon"', }, + { + name: "posthog-js", + message: + 'Import "posthog-js" directly only within the analytics helpers ("src/@/analytics/*"). Use the exported helpers from "@/analytics/track" elsewhere.', + }, ], }, ], @@ -139,6 +144,13 @@ module.exports = { "no-restricted-imports": ["off"], }, }, + // allow direct PostHog imports inside analytics helpers + { + files: "src/@/analytics/**/*", + rules: { + "no-restricted-imports": ["off"], + }, + }, // enable rule specifically for TypeScript files { files: ["*.ts", "*.tsx"], diff --git a/apps/dashboard/src/@/analytics/hooks/identify-account.ts b/apps/dashboard/src/@/analytics/hooks/identify-account.ts new file mode 100644 index 00000000000..34a9b0122af --- /dev/null +++ b/apps/dashboard/src/@/analytics/hooks/identify-account.ts @@ -0,0 +1,20 @@ +"use client"; + +import posthog from "posthog-js"; +import { useEffect } from "react"; + +export function useIdentifyAccount(opts?: { + accountId: string; + email?: string; +}) { + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + // if no accountId, don't identify + if (!opts?.accountId) { + return; + } + + // if email is provided, add it to the identify + posthog.identify(opts.accountId, opts.email ? { email: opts.email } : {}); + }, [opts?.accountId, opts?.email]); +} diff --git a/apps/dashboard/src/@/analytics/hooks/identify-team.ts b/apps/dashboard/src/@/analytics/hooks/identify-team.ts new file mode 100644 index 00000000000..80ae6da0b28 --- /dev/null +++ b/apps/dashboard/src/@/analytics/hooks/identify-team.ts @@ -0,0 +1,19 @@ +"use client"; + +import posthog from "posthog-js"; +import { useEffect } from "react"; + +export function useIdentifyTeam(opts?: { + teamId: string; +}) { + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + // if no teamId, don't identify + if (!opts?.teamId) { + return; + } + + // identify the team + posthog.group("team", opts.teamId); + }, [opts?.teamId]); +} diff --git a/apps/dashboard/src/@/analytics/reset.ts b/apps/dashboard/src/@/analytics/reset.ts new file mode 100644 index 00000000000..61f4b0acf40 --- /dev/null +++ b/apps/dashboard/src/@/analytics/reset.ts @@ -0,0 +1,7 @@ +"use client"; + +import posthog from "posthog-js"; + +export function resetAnalytics() { + posthog.reset(); +} diff --git a/apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx b/apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx index 76b19ad283f..85a552821f1 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx +++ b/apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx @@ -1,5 +1,6 @@ "use client"; +import { resetAnalytics } from "@/analytics/reset"; import { Button } from "@/components/ui/button"; import { useStore } from "@/lib/reactive"; import { getSDKTheme } from "app/(app)/components/sdk-component-theme"; @@ -156,6 +157,7 @@ export const CustomConnectWallet = (props: { onDisconnect={async () => { try { await doLogout(); + resetAnalytics(); } catch (err) { console.error("Failed to log out", err); } diff --git a/apps/dashboard/src/app/(app)/account/components/AccountHeader.tsx b/apps/dashboard/src/app/(app)/account/components/AccountHeader.tsx index aef54edfbee..895cc022a12 100644 --- a/apps/dashboard/src/app/(app)/account/components/AccountHeader.tsx +++ b/apps/dashboard/src/app/(app)/account/components/AccountHeader.tsx @@ -1,6 +1,7 @@ "use client"; import { createTeam } from "@/actions/createTeam"; +import { resetAnalytics } from "@/analytics/reset"; import type { Project } from "@/api/projects"; import type { Team } from "@/api/team"; import { useDashboardRouter } from "@/lib/DashboardRouter"; @@ -35,6 +36,7 @@ export function AccountHeader(props: { const logout = useCallback(async () => { try { await doLogout(); + resetAnalytics(); if (wallet) { disconnect(wallet); } diff --git a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPage.tsx b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPage.tsx index 6cd50b51f00..027903d2fde 100644 --- a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPage.tsx +++ b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPage.tsx @@ -2,6 +2,7 @@ import { confirmEmailWithOTP } from "@/actions/confirmEmail"; import { apiServerProxy } from "@/actions/proxies"; import { updateAccount } from "@/actions/updateAccount"; +import { resetAnalytics } from "@/analytics/reset"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; import type { ThirdwebClient } from "thirdweb"; @@ -46,6 +47,7 @@ export function AccountSettingsPage(props: { }} onAccountDeleted={async () => { await doLogout(); + resetAnalytics(); if (activeWallet) { disconnect(activeWallet); } diff --git a/apps/dashboard/src/app/(app)/login/LoginPage.tsx b/apps/dashboard/src/app/(app)/login/LoginPage.tsx index bcae0f110ea..b24959da84c 100644 --- a/apps/dashboard/src/app/(app)/login/LoginPage.tsx +++ b/apps/dashboard/src/app/(app)/login/LoginPage.tsx @@ -1,6 +1,7 @@ "use client"; import { getRawAccountAction } from "@/actions/getAccount"; +import { resetAnalytics } from "@/analytics/reset"; import { ClientOnly } from "@/components/blocks/client-only"; import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; import { ToggleThemeButton } from "@/components/color-mode-toggle"; @@ -313,7 +314,10 @@ function CustomConnectEmbed(props: { throw e; } }, - doLogout, + doLogout: async () => { + await doLogout(); + resetAnalytics(); + }, isLoggedIn: async (x) => { const isLoggedInResult = await isLoggedIn(x); if (isLoggedInResult) { diff --git a/apps/dashboard/src/app/(app)/team/components/TeamHeader/team-header-logged-in.client.tsx b/apps/dashboard/src/app/(app)/team/components/TeamHeader/team-header-logged-in.client.tsx index ff3e2261675..b1f18dc7c72 100644 --- a/apps/dashboard/src/app/(app)/team/components/TeamHeader/team-header-logged-in.client.tsx +++ b/apps/dashboard/src/app/(app)/team/components/TeamHeader/team-header-logged-in.client.tsx @@ -1,6 +1,9 @@ "use client"; import { createTeam } from "@/actions/createTeam"; +import { useIdentifyAccount } from "@/analytics/hooks/identify-account"; +import { useIdentifyTeam } from "@/analytics/hooks/identify-team"; +import { resetAnalytics } from "@/analytics/reset"; import type { Project } from "@/api/projects"; import type { Team } from "@/api/team"; import { useDashboardRouter } from "@/lib/DashboardRouter"; @@ -12,7 +15,6 @@ import { toast } from "sonner"; import type { ThirdwebClient } from "thirdweb"; import { useActiveWallet, useDisconnect } from "thirdweb/react"; import { doLogout } from "../../../login/auth-actions"; - import { type TeamHeaderCompProps, TeamHeaderDesktopUI, @@ -27,6 +29,16 @@ export function TeamHeaderLoggedIn(props: { accountAddress: string; client: ThirdwebClient; }) { + // identify the account + useIdentifyAccount({ + accountId: props.account.id, + email: props.account.email, + }); + + // identify the team + useIdentifyTeam({ + teamId: props.currentTeam.id, + }); const [createProjectDialogState, setCreateProjectDialogState] = useState< { team: Team; isOpen: true } | { isOpen: false } >({ isOpen: false }); @@ -38,6 +50,7 @@ export function TeamHeaderLoggedIn(props: { // log out the user try { await doLogout(); + resetAnalytics(); if (activeWallet) { disconnect(activeWallet); } diff --git a/apps/dashboard/src/instrumentation-client.ts b/apps/dashboard/src/instrumentation-client.ts index fd21d85797c..6a9414eba40 100644 --- a/apps/dashboard/src/instrumentation-client.ts +++ b/apps/dashboard/src/instrumentation-client.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-restricted-imports import posthog from "posthog-js"; const NEXT_PUBLIC_POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY;