77 TenantScope ,
88} from '@logto/schemas' ;
99import { 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' ;
1112import { Helmet } from 'react-helmet' ;
1213import { createBrowserRouter , RouterProvider } from 'react-router-dom' ;
1314
@@ -21,7 +22,7 @@ import 'react-color-palette/css';
2122
2223import CloudAppRoutes from '@/cloud/AppRoutes' ;
2324import AppLoading from '@/components/AppLoading' ;
24- import { isCloud } from '@/consts/env' ;
25+ import { isCloud , postHogHost , postHogKey } from '@/consts/env' ;
2526import { cloudApi , getManagementApi , meApi } from '@/consts/resources' ;
2627import { ConsoleRoutes } from '@/containers/ConsoleRoutes' ;
2728
@@ -71,8 +72,6 @@ export default App;
7172 * different components.
7273 */
7374function 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,123 @@ 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+ /**
159+ * The `useEffect` below sets the PostHog group properties based on:
160+ *
161+ * 1. If `currentTenant` is available, since it contains rich data, set the group with both ID
162+ * and necessary properties.
163+ * 2. If only `currentTenantId` is available, set the group with only ID. This usually happens
164+ * when the URL contains a tenant ID but the tenant data is not loaded yet or the tenant is
165+ * unavailable to the user.
166+ * 3. If neither is available, reset all group properties. This usually happens when the user is
167+ * not in a tenant context.
168+ *
169+ * @caveat
170+ * We need to identify group when window is reactivated (tab switch or window switch)
171+ * since one user may access different tenants in different tabs or windows.
172+ *
173+ * Currently, PostHog DOES capture the correct group when the user switches tabs or windows
174+ * since it reads the properties from the memory if existing, but just in case it doesn't work
175+ * in the future, we add this logic here.
176+ *
177+ * See {https://github.com/PostHog/posthog-js/blob/b5eb605/packages/core/src/posthog-core-stateless.ts#L778-L783 | posthog-js source code}
178+ * for details at the time of writing.
179+ */
180+ useEffect ( ( ) => {
181+ const captureGroups = ( ) => {
182+ if ( currentTenant ) {
183+ postHog . group ( 'tenant' , currentTenantId , {
184+ name : currentTenant . name ,
185+ } ) ;
186+ } else if ( currentTenantId ) {
187+ postHog . group ( 'tenant' , currentTenantId ) ;
188+ } else {
189+ postHog . resetGroups ( ) ;
190+ }
191+ } ;
192+
193+ captureGroups ( ) ;
194+
195+ const handleVisibilityChange = ( ) => {
196+ if ( document . visibilityState === 'visible' ) {
197+ captureGroups ( ) ;
198+ }
199+ } ;
146200
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 /> ;
201+ document . addEventListener ( 'visibilitychange' , handleVisibilityChange ) ;
202+
203+ return ( ) => {
204+ document . removeEventListener ( 'visibilitychange' , handleVisibilityChange ) ;
205+ } ;
206+ } , [ postHog , currentTenantId , currentTenant ] ) ;
207+
208+ /**
209+ * If it's not Cloud (OSS), render the tenant app container directly since only default tenant is available;
210+ * if it's Cloud, render the tenant app container only when a tenant ID is available (in a tenant context).
211+ */
212+ if ( ! isCloud || currentTenantId ) {
213+ // Authenticated user should load onboarding data before rendering the app.
214+ // This looks weird and it can be refactored by merging the onboarding
215+ // routes with the console routes.
216+ if ( ! tenantEndpoint || ( isCloud && isAuthenticated && ! isLoaded ) ) {
217+ return < AppLoading /> ;
218+ }
219+
220+ return < ConsoleRoutes /> ;
152221 }
153222
154- return (
155- < >
156- < GlobalScripts />
157- < ConsoleRoutes />
158- </ >
159- ) ;
223+ return < CloudAppRoutes /> ;
160224}
0 commit comments