Skip to content

Commit cdde458

Browse files
committed
feat: init posthog
1 parent 6ce9992 commit cdde458

File tree

10 files changed

+329
-245
lines changed

10 files changed

+329
-245
lines changed

packages/console/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"overlayscrollbars-react": "^0.5.0",
9090
"postcss": "^8.4.39",
9191
"postcss-modules": "^6.0.0",
92+
"posthog-js": "^1.268.6",
9293
"prettier": "^3.5.3",
9394
"prism-react-renderer": "^2.4.1",
9495
"prop-types": "^15.8.1",

packages/console/src/App.tsx

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
TenantScope,
88
} from '@logto/schemas';
99
import { conditionalArray } from '@silverhand/essentials';
10-
import { useContext, useMemo } from 'react';
10+
import { PostHogProvider, usePostHog } from 'posthog-js/react';
11+
import { useContext, useEffect, useMemo } from 'react';
1112
import { Helmet } from 'react-helmet';
1213
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
1314

@@ -21,7 +22,7 @@ import 'react-color-palette/css';
2122

2223
import CloudAppRoutes from '@/cloud/AppRoutes';
2324
import AppLoading from '@/components/AppLoading';
24-
import { isCloud } from '@/consts/env';
25+
import { isCloud, postHogHost, postHogKey } from '@/consts/env';
2526
import { cloudApi, getManagementApi, meApi } from '@/consts/resources';
2627
import { ConsoleRoutes } from '@/containers/ConsoleRoutes';
2728

@@ -71,8 +72,6 @@ export default App;
7172
* different components.
7273
*/
7374
function Providers() {
74-
const { currentTenantId } = useContext(TenantsContext);
75-
7675
// For Cloud, we use Management API proxy for accessing tenant data.
7776
// For OSS, we directly call the tenant API with the default tenant API resource.
7877
const resources = useMemo(
@@ -103,58 +102,85 @@ function Providers() {
103102
);
104103

105104
return (
106-
<LogtoProvider
107-
unstable_enableCache
108-
config={{
109-
endpoint: adminTenantEndpoint.href,
110-
appId: adminConsoleApplicationId,
111-
resources,
112-
scopes,
113-
prompt: [Prompt.Login, Prompt.Consent],
105+
<PostHogProvider
106+
apiKey={postHogKey ?? ''} // Empty key will disable PostHog
107+
options={{
108+
api_host: postHogHost,
109+
defaults: '2025-05-24',
114110
}}
115111
>
116-
<AppThemeProvider>
117-
<Helmet titleTemplate={`%s - ${mainTitle}`} defaultTitle={mainTitle} />
118-
<Toast />
119-
<AppConfirmModalProvider>
120-
<ErrorBoundary>
121-
<LogtoErrorBoundary>
122-
{/**
123-
* If it's not Cloud (OSS), render the tenant app container directly since only default tenant is available;
124-
* if it's Cloud, render the tenant app container only when a tenant ID is available (in a tenant context).
125-
*/}
126-
{!isCloud || currentTenantId ? (
112+
<LogtoProvider
113+
unstable_enableCache
114+
config={{
115+
endpoint: adminTenantEndpoint.href,
116+
appId: adminConsoleApplicationId,
117+
resources,
118+
scopes,
119+
prompt: [Prompt.Login, Prompt.Consent],
120+
}}
121+
>
122+
<AppThemeProvider>
123+
<Helmet titleTemplate={`%s - ${mainTitle}`} defaultTitle={mainTitle} />
124+
<Toast />
125+
<AppConfirmModalProvider>
126+
<ErrorBoundary>
127+
<LogtoErrorBoundary>
127128
<AppDataProvider>
128-
<AppRoutes />
129+
<GlobalScripts />
130+
<Content />
129131
</AppDataProvider>
130-
) : (
131-
<CloudAppRoutes />
132-
)}
133-
</LogtoErrorBoundary>
134-
</ErrorBoundary>
135-
</AppConfirmModalProvider>
136-
</AppThemeProvider>
137-
</LogtoProvider>
132+
</LogtoErrorBoundary>
133+
</ErrorBoundary>
134+
</AppConfirmModalProvider>
135+
</AppThemeProvider>
136+
</LogtoProvider>
137+
</PostHogProvider>
138138
);
139139
}
140140

141-
/** Renders different routes based on the user's onboarding status. */
142-
function AppRoutes() {
141+
function Content() {
143142
const { tenantEndpoint } = useContext(AppDataContext);
144-
const { isLoaded } = useCurrentUser();
143+
const { isLoaded, user } = useCurrentUser();
145144
const { isAuthenticated } = useLogto();
145+
const { currentTenantId, currentTenant } = useContext(TenantsContext);
146+
const postHog = usePostHog();
147+
148+
useEffect(() => {
149+
if (isLoaded) {
150+
postHog.identify(user?.id);
151+
}
152+
// We don't reset user info here because this component includes some anonymous pages.
153+
// Resetting user info may cause issues when the user switches between anonymous and
154+
// authenticated pages.
155+
// Reset user info in the sign-out logic instead.
156+
}, [isLoaded, postHog, user]);
157+
158+
useEffect(() => {
159+
if (currentTenant) {
160+
postHog.group('tenant', currentTenantId, {
161+
name: currentTenant.name,
162+
});
163+
} else if (currentTenantId) {
164+
postHog.group('tenant', currentTenantId);
165+
} else {
166+
postHog.resetGroups();
167+
}
168+
}, [postHog, currentTenantId, currentTenant]);
146169

147-
// Authenticated user should load onboarding data before rendering the app.
148-
// This looks weird and it will be refactored soon by merging the onboarding
149-
// routes with the console routes.
150-
if (!tenantEndpoint || (isCloud && isAuthenticated && !isLoaded)) {
151-
return <AppLoading />;
170+
/**
171+
* If it's not Cloud (OSS), render the tenant app container directly since only default tenant is available;
172+
* if it's Cloud, render the tenant app container only when a tenant ID is available (in a tenant context).
173+
*/
174+
if (!isCloud || currentTenantId) {
175+
// Authenticated user should load onboarding data before rendering the app.
176+
// This looks weird and it can be refactored by merging the onboarding
177+
// routes with the console routes.
178+
if (!tenantEndpoint || (isCloud && isAuthenticated && !isLoaded)) {
179+
return <AppLoading />;
180+
}
181+
182+
return <ConsoleRoutes />;
152183
}
153184

154-
return (
155-
<>
156-
<GlobalScripts />
157-
<ConsoleRoutes />
158-
</>
159-
);
185+
return <CloudAppRoutes />;
160186
}

packages/console/src/components/Topbar/UserInfo/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { builtInLanguageOptions as consoleBuiltInLanguageOptions } from '@logto/phrases';
2-
import { useLogto } from '@logto/react';
32
import { Theme } from '@logto/schemas';
43
import classNames from 'classnames';
54
import { useRef, useState } from 'react';
@@ -20,6 +19,7 @@ import Spacer from '@/ds-components/Spacer';
2019
import { Ring as Spinner } from '@/ds-components/Spinner';
2120
import useCurrentUser from '@/hooks/use-current-user';
2221
import useRedirectUri from '@/hooks/use-redirect-uri';
22+
import useSignOut from '@/hooks/use-sign-out';
2323
import useTenantPathname from '@/hooks/use-tenant-pathname';
2424
import useUserPreferences from '@/hooks/use-user-preferences';
2525
import { DynamicAppearanceMode } from '@/types/appearance-mode';
@@ -30,7 +30,7 @@ import UserInfoSkeleton from './UserInfoSkeleton';
3030
import styles from './index.module.scss';
3131

3232
function UserInfo() {
33-
const { signOut } = useLogto();
33+
const { signOut } = useSignOut();
3434
const { getUrl } = useTenantPathname();
3535
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
3636
const { user, isLoading: isLoadingUser } = useCurrentUser();

packages/console/src/consts/env.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { yes } from '@silverhand/essentials';
22

33
import { storageKeys } from './storage';
44

5-
const normalizeEnv = (value: unknown) =>
5+
export const normalizeEnv = (value: unknown) =>
66
value === null || value === undefined ? undefined : String(value);
77

88
const isProduction = import.meta.env.PROD;
@@ -23,3 +23,5 @@ export const isMultipleCustomDomainsEnabled = yes(
2323
);
2424

2525
export const inkeepApiKey = normalizeEnv(import.meta.env.INKEEP_API_KEY);
26+
export const postHogKey = normalizeEnv(import.meta.env.POSTHOG_PUBLIC_KEY);
27+
export const postHogHost = normalizeEnv(import.meta.env.POSTHOG_PUBLIC_HOST);

packages/console/src/contexts/TenantsProvider.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ type Tenants = {
6363
removeTenant: (tenantId: string) => void;
6464
/** Update a tenant by ID if it exists in the current tenants data. */
6565
updateTenant: (tenantId: string, data: Partial<TenantResponse>) => void;
66-
/** The current tenant ID parsed from the URL. */
66+
/**
67+
* The current tenant ID parsed from the URL.
68+
*
69+
* - If it's a non-cloud deployment, it will always be `default`.
70+
* - For cloud deployment, if it's `''`, the user is not in a tenant context (e.g. in onboarding
71+
* routes).
72+
*/
6773
currentTenantId: string;
6874
currentTenant?: TenantResponse;
6975
/** Indicates if the current tenant is a development tenant. */

packages/console/src/hooks/use-api.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { TenantsContext } from '@/contexts/TenantsProvider';
2727
import { useConfirmModal } from '@/hooks/use-confirm-modal';
2828
import useRedirectUri from '@/hooks/use-redirect-uri';
2929

30+
import useSignOut from './use-sign-out';
31+
3032
export class RequestError extends Error {
3133
constructor(
3234
public readonly status: number,
@@ -45,7 +47,7 @@ export type StaticApiProps = {
4547
};
4648

4749
const useGlobalRequestErrorHandler = (toastDisabledErrorCodes?: LogtoErrorCode[]) => {
48-
const { signOut } = useLogto();
50+
const { signOut } = useSignOut();
4951
const { show } = useConfirmModal();
5052
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
5153

@@ -135,10 +137,10 @@ export const useStaticApi = ({
135137
hooks: {
136138
beforeError: conditionalArray(
137139
!disableGlobalErrorHandling &&
138-
(async (error) => {
139-
await handleError(error.response);
140-
return error;
141-
})
140+
(async (error) => {
141+
await handleError(error.response);
142+
return error;
143+
})
142144
),
143145
beforeRequest: [
144146
async (request) => {
@@ -201,13 +203,13 @@ const useApi = (props: Omit<StaticApiProps, 'prefixUrl' | 'resourceIndicator'> =
201203
() =>
202204
isCloud
203205
? {
204-
prefixUrl: appendPath(new URL(window.location.origin), 'm', currentTenantId),
205-
resourceIndicator: buildOrganizationUrn(getTenantOrganizationId(currentTenantId)),
206-
}
206+
prefixUrl: appendPath(new URL(window.location.origin), 'm', currentTenantId),
207+
resourceIndicator: buildOrganizationUrn(getTenantOrganizationId(currentTenantId)),
208+
}
207209
: {
208-
prefixUrl: tenantEndpoint,
209-
resourceIndicator: getManagementApiResourceIndicator(currentTenantId),
210-
},
210+
prefixUrl: tenantEndpoint,
211+
resourceIndicator: getManagementApiResourceIndicator(currentTenantId),
212+
},
211213
[currentTenantId, tenantEndpoint]
212214
);
213215

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useLogto } from '@logto/react';
2+
import { usePostHog } from 'posthog-js/react';
3+
import { useCallback } from 'react';
4+
5+
/**
6+
* A hook that returns a wrapped `signOut` function from `useLogto` with necessary cleanup logic.
7+
*
8+
* Unless you have special needs, you should always use this hook instead of `useLogto` directly.
9+
*/
10+
const useSignOut = () => {
11+
const { signOut: logtoSignOut } = useLogto();
12+
const postHog = usePostHog();
13+
14+
const signOut = useCallback<ReturnType<typeof useLogto>['signOut']>(
15+
async (postSignOutRedirectUri) => {
16+
postHog.resetGroups(); // Not sure if this is needed, but just in case.
17+
postHog.reset();
18+
return logtoSignOut(postSignOutRedirectUri);
19+
},
20+
[logtoSignOut, postHog]
21+
);
22+
return {
23+
/** A wrapped version of `useLogto`'s `signOut` with necessary cleanup logic. */
24+
signOut,
25+
};
26+
};
27+
28+
export default useSignOut;

packages/console/src/pages/Profile/containers/DeleteAccountModal/components/FinalConfirmationModal/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useLogto } from '@logto/react';
21
import { ResponseError } from '@withtyped/client';
32
import { useContext, useState } from 'react';
43
import { useTranslation } from 'react-i18next';
@@ -10,6 +9,7 @@ import { TenantsContext } from '@/contexts/TenantsProvider';
109
import Button from '@/ds-components/Button';
1110
import ModalLayout from '@/ds-components/ModalLayout';
1211
import useRedirectUri from '@/hooks/use-redirect-uri';
12+
import useSignOut from '@/hooks/use-sign-out';
1313
import modalStyles from '@/scss/modal.module.scss';
1414

1515
import styles from '../../index.module.scss';
@@ -29,7 +29,7 @@ export default function FinalConfirmationModal({
2929
onClose,
3030
}: Props) {
3131
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.profile.delete_account' });
32-
const { signOut } = useLogto();
32+
const { signOut } = useSignOut();
3333
const { removeTenant } = useContext(TenantsContext);
3434
const postSignOutRedirectUri = useRedirectUri('signOut');
3535
const [isDeleting, setIsDeleting] = useState(false);

packages/console/vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const buildConfig = (mode: string): UserConfig => ({
5555
process.env.MULTIPLE_CUSTOM_DOMAINS_ENABLED
5656
),
5757
'import.meta.env.INKEEP_API_KEY': JSON.stringify(process.env.INKEEP_API_KEY),
58+
'import.meta.env.POSTHOG_PUBLIC_KEY': JSON.stringify(process.env.POSTHOG_PUBLIC_KEY),
59+
'import.meta.env.POSTHOG_PUBLIC_HOST': JSON.stringify(process.env.POSTHOG_PUBLIC_HOST),
5860
// `@withtyped/client` needs this to be defined. We can optimize this later.
5961
'process.env': {},
6062
},

0 commit comments

Comments
 (0)