diff --git a/frontend/src/concepts/analyticsTracking/segmentIOUtils.tsx b/frontend/src/concepts/analyticsTracking/segmentIOUtils.tsx index 080ebeb379..f55556ba6b 100644 --- a/frontend/src/concepts/analyticsTracking/segmentIOUtils.tsx +++ b/frontend/src/concepts/analyticsTracking/segmentIOUtils.tsx @@ -86,6 +86,13 @@ export const firePageEvent = (): void => { } }; +// Stuff that gets send over as traits on an identify call. Must not include (anonymous) user Id. +type IdentifyTraits = { + isAdmin: boolean; + canCreateProjects: boolean; + clusterID: string; +}; + /* * This fires a call to associate further processing with the passed (anonymous) userId * in the properties. @@ -94,8 +101,13 @@ export const fireIdentifyEvent = (properties: IdentifyEventProperties): void => const clusterID = window.clusterID ?? ''; if (DEV_MODE) { /* eslint-disable-next-line no-console */ - console.log(`Identify event triggered`); + console.log(`Identify event triggered: ${JSON.stringify(properties)}`); } else if (window.analytics) { - window.analytics.identify(properties.anonymousID, { clusterID }); + const traits: IdentifyTraits = { + clusterID, + isAdmin: properties.isAdmin, + canCreateProjects: properties.canCreateProjects, + }; + window.analytics.identify(properties.anonymousID, traits); } }; diff --git a/frontend/src/concepts/analyticsTracking/trackingProperties.ts b/frontend/src/concepts/analyticsTracking/trackingProperties.ts index 81c26f44e9..17daeb3a16 100644 --- a/frontend/src/concepts/analyticsTracking/trackingProperties.ts +++ b/frontend/src/concepts/analyticsTracking/trackingProperties.ts @@ -3,7 +3,10 @@ export type ODHSegmentKey = { }; export type IdentifyEventProperties = { + isAdmin: boolean; anonymousID?: string; + userId?: string; + canCreateProjects: boolean; }; export const enum TrackingOutcome { diff --git a/frontend/src/concepts/analyticsTracking/useSegmentTracking.ts b/frontend/src/concepts/analyticsTracking/useSegmentTracking.ts index d609a9a2f9..2411a5b744 100644 --- a/frontend/src/concepts/analyticsTracking/useSegmentTracking.ts +++ b/frontend/src/concepts/analyticsTracking/useSegmentTracking.ts @@ -2,6 +2,7 @@ import React from 'react'; import { useAppContext } from '~/app/AppContext'; import { useAppSelector } from '~/redux/hooks'; import { fireIdentifyEvent, firePageEvent } from '~/concepts/analyticsTracking/segmentIOUtils'; +import { useTrackUser } from '~/concepts/analyticsTracking/useTrackUser'; import { useWatchSegmentKey } from './useWatchSegmentKey'; import { initSegment } from './initSegment'; @@ -10,28 +11,27 @@ export const useSegmentTracking = (): void => { const { dashboardConfig } = useAppContext(); const username = useAppSelector((state) => state.user); const clusterID = useAppSelector((state) => state.clusterID); + const [userProps, uPropsLoaded] = useTrackUser(username); React.useEffect(() => { - if (segmentKey && loaded && !loadError && username && clusterID) { - const computeUserId = async () => { - const anonymousIDBuffer = await crypto.subtle.digest( - 'SHA-1', - new TextEncoder().encode(username), - ); - const anonymousIDArray = Array.from(new Uint8Array(anonymousIDBuffer)); - return anonymousIDArray.map((b) => b.toString(16).padStart(2, '0')).join(''); - }; - + if (segmentKey && loaded && !loadError && username && clusterID && uPropsLoaded) { window.clusterID = clusterID; initSegment({ segmentKey, enabled: !dashboardConfig.spec.dashboardConfig.disableTracking, }).then(() => { - computeUserId().then((userId) => { - fireIdentifyEvent({ anonymousID: userId }); - firePageEvent(); - }); + fireIdentifyEvent(userProps); + firePageEvent(); }); } - }, [clusterID, loadError, loaded, segmentKey, username, dashboardConfig]); + }, [ + clusterID, + loadError, + loaded, + segmentKey, + username, + dashboardConfig, + userProps, + uPropsLoaded, + ]); }; diff --git a/frontend/src/concepts/analyticsTracking/useTrackUser.ts b/frontend/src/concepts/analyticsTracking/useTrackUser.ts new file mode 100644 index 0000000000..062334dd76 --- /dev/null +++ b/frontend/src/concepts/analyticsTracking/useTrackUser.ts @@ -0,0 +1,50 @@ +import React from 'react'; +import { useUser } from '~/redux/selectors'; +import { useAccessReview } from '~/api'; +import { AccessReviewResourceAttributes } from '~/k8sTypes'; +import { IdentifyEventProperties } from '~/concepts/analyticsTracking/trackingProperties'; + +export const useTrackUser = (username?: string): [IdentifyEventProperties, boolean] => { + const { isAdmin } = useUser(); + const [anonymousId, setAnonymousId] = React.useState(undefined); + + const [loaded, setLoaded] = React.useState(false); + const createReviewResource: AccessReviewResourceAttributes = { + group: 'project.openshift.io', + resource: 'projectrequests', + verb: 'create', + }; + const [allowCreate, acLoaded] = useAccessReview(createReviewResource); + + React.useEffect(() => { + const computeAnonymousUserId = async () => { + const anonymousIDBuffer = await crypto.subtle.digest( + 'SHA-1', + new TextEncoder().encode(username), + ); + const anonymousIDArray = Array.from(new Uint8Array(anonymousIDBuffer)); + const aId = anonymousIDArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + return aId; + }; + + if (!anonymousId) { + computeAnonymousUserId().then((val) => { + setAnonymousId(val); + }); + } + if (acLoaded && anonymousId) { + setLoaded(true); + } + }, [username, anonymousId, acLoaded]); + + const props: IdentifyEventProperties = React.useMemo( + () => ({ + isAdmin, + canCreateProjects: allowCreate, + anonymousID: anonymousId, + }), + [isAdmin, allowCreate, anonymousId], + ); + + return [props, loaded]; +};