Skip to content

Commit

Permalink
Merge pull request #824 from CareTogether/posthog
Browse files Browse the repository at this point in the history
PostHog.Identify(), unmask some UI components, and mask all events by default
  • Loading branch information
LarsKemmann authored Feb 17, 2025
2 parents 8f7df7b + 386e062 commit ab29cb4
Show file tree
Hide file tree
Showing 13 changed files with 125 additions and 48 deletions.
4 changes: 3 additions & 1 deletion src/b2c/CustomPolicies/Dev/SignUpOrSignin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="email" />
<!-- <OutputClaim ClaimTypeReferenceId="email" /> -->
<!-- Temporary workaround for missing email claims -->
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="identityProvider" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
Expand Down
26 changes: 4 additions & 22 deletions src/caretogether-pwa/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,8 @@ import { useLocalStorage } from './Hooks/useLocalStorage';
import { InboxScreen } from './Inbox/InboxScreen';
import { FamilyScreenV2 } from './Families/FamilyScreenV2';
import { familyScreenV2State } from './Families/familyScreenV2State';
import posthog from 'posthog-js';
import {
locationConfigurationQuery,
organizationConfigurationQuery,
} from './Model/ConfigurationModel';
import { usePostHogIdentify } from './Utilities/Instrumentation/usePostHogIdentify';
import { usePostHogGroups } from './Utilities/Instrumentation/usePostHogGroups';

const LAST_VISITED_LOCATION = 'lastVisitedLocation';

Expand Down Expand Up @@ -146,24 +143,9 @@ function LocationContextWrapper() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [organizationId, locationId, trace, setSelectedLocationContext]);

const organizationConfiguration = useLoadable(organizationConfigurationQuery);
const locationConfiguration = useLoadable(locationConfigurationQuery);
usePostHogGroups();

useEffect(() => {
if (organizationId && locationId) {
posthog.group('organization', organizationId, {
name: organizationConfiguration?.organizationName,
});
posthog.group('location', locationId, {
name: locationConfiguration?.name,
});
}
}, [
organizationId,
locationId,
organizationConfiguration?.organizationName,
locationConfiguration?.name,
]);
usePostHogIdentify();

return (
// We need to wait for this to have a value before rendering the child tree; otherwise,
Expand Down
36 changes: 24 additions & 12 deletions src/caretogether-pwa/src/Authentication/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,19 @@ function displayableError(error: Error | unknown) {

function withDefaultScopes(scopes: string[]) {
// Add the default OpenID Connect scopes and then deduplicate the resulting entries.
return [...new Set(scopes.concat(['openid', 'profile', 'offline_access']))];
return [
...new Set(scopes.concat(['openid', 'profile', 'offline_access', 'email'])),
];
}
const scopes = withDefaultScopes([import.meta.env.VITE_APP_AUTH_SCOPES]);

async function loginAndSetActiveAccountAsync(): Promise<string> {
type AccountInfo = {
userId: string;
email?: string;
name?: string;
};

async function loginAndSetActiveAccountAsync(): Promise<AccountInfo> {
// The ultimate objective of this function is to obtain an AuthenticationResult from Azure AD via MSAL.js
// and return the local account ID of the authenticated account.
let result: AuthenticationResult | null = null;
Expand Down Expand Up @@ -127,7 +135,11 @@ async function loginAndSetActiveAccountAsync(): Promise<string> {
// This account ID becomes the root of the Recoil dataflow graph. That is, all other
// API queries should be designed to only execute once this atom is initialized.
if (result && result.account) {
return result.account.localAccountId;
return {
userId: result.account.localAccountId,
email: result.account.idTokenClaims?.email?.toString(),
name: result.account.name,
};
} else if (result && !result.account) {
throw displayableError(
new Error(
Expand Down Expand Up @@ -167,24 +179,24 @@ async function loginAndSetActiveAccountAsync(): Promise<string> {
}
}

let userIdStateInitialized = false;
const initializeUserIdStateAsync: AtomEffect<string> = (params) => {
trace(`InitializeUserIdStateAsync`, params.node.key);
if (!userIdStateInitialized) {
userIdStateInitialized = true;
let accountInfoStateInitialized = false;
const initializeAccountInfoStateAsync: AtomEffect<AccountInfo> = (params) => {
trace(`InitializeAccountInfoStateAsync`, params.node.key);
if (!accountInfoStateInitialized) {
accountInfoStateInitialized = true;
params.setSelf(loginAndSetActiveAccountAsync());
} else {
trace(
`InitializeUserIdStateAsync`,
`InitializeAccountInfoStateAsync`,
`Initialization has already started; skipping this atom effect invocation.`
);
}
};

// This will be set by AuthenticationWrapper once the user has authenticated and the default account is set.
export const userIdState = atom<string>({
key: 'userIdState',
effects: [loggingEffect, initializeUserIdStateAsync],
export const accountInfoState = atom<AccountInfo>({
key: 'accountInfoState',
effects: [loggingEffect, initializeAccountInfoStateAsync],
});

export async function tryAcquireAccessToken(): Promise<string | null> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { Suspense } from 'react';
import { ProgressBackdrop } from '../Shell/ProgressBackdrop';
import { useScopedTrace } from '../Hooks/useScopedTrace';
import { userIdState } from '../Authentication/Auth';
import { accountInfoState } from '../Authentication/Auth';
import { useLoadable } from '../Hooks/useLoadable';

function AuthenticatedUserWrapper({ children }: React.PropsWithChildren) {
const trace = useScopedTrace('AuthenticatedUserWrapper');

// This will suspend until a user ID has been set by the `userIdState` initialization logic.
const userId = useLoadable(userIdState);
// This will suspend until a user ID has been set by the `accountInfoState` initialization logic.
const userId = useLoadable(accountInfoState)?.userId;
trace(`userId: ${userId}`);

return <>{userId ? children : <></>}</>;
Expand Down
6 changes: 3 additions & 3 deletions src/caretogether-pwa/src/Model/Data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import {
RecordsAggregate,
} from '../GeneratedClient';
import { loggingEffect } from '../Utilities/loggingEffect';
import { userIdState } from '../Authentication/Auth';
import { accountInfoState } from '../Authentication/Auth';

// This will be available to query (asynchronously) after the userIdState is set (i.e., post-authentication).
// This will be available to query (asynchronously) after the accountInfoState is set (i.e., post-authentication).
export const userOrganizationAccessQuery = selector({
key: 'userOrganizationAccessQuery',
get: async ({ get }) => {
//HACK: Requiring the user ID state to be set is a workaround for fall-through issues with the AuthenticationWrapper
// and AuthenticatedUserWrapper. Removing this currently would cause runtime errors regarding the MsalProvider
// being updated while a child component is being rendered (e.g., the ShellContextSwitcher).
get(userIdState);
get(accountInfoState);
const userResponse = await api.users.getUserOrganizationAccess();
return userResponse;
},
Expand Down
3 changes: 2 additions & 1 deletion src/caretogether-pwa/src/Shell/ListItemLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface ListItemLinkProps {
to: string;
newTab?: boolean;
darkColor?: boolean;
className?: string;
}

function ListItemLink(props: ListItemLinkProps) {
Expand All @@ -44,7 +45,7 @@ function ListItemLink(props: ListItemLinkProps) {
);

return (
<li>
<li className={props.className}>
<ListItem
button
component={renderLink}
Expand Down
2 changes: 2 additions & 0 deletions src/caretogether-pwa/src/Shell/ShellContextSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function ShellContextSwitcher() {
<Stack sx={{ position: 'absolute' }}>
{organizationConfiguration ? (
<Typography
className="ph-unmask"
variant="subtitle1"
component="h1"
sx={{ position: 'relative', top: -4, left: 8 }}
Expand All @@ -68,6 +69,7 @@ export function ShellContextSwitcher() {
selectedLocationContext ? (
availableLocations.length >= 1 ? (
<Select
className="ph-unmask"
size={isDesktop ? 'small' : 'medium'}
variant="outlined"
sx={{
Expand Down
7 changes: 7 additions & 0 deletions src/caretogether-pwa/src/Shell/ShellSideNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ function SideNavigationMenu({ open }: SideNavigationMenuProps) {
) : (
<>
<ListItemLink
className="ph-unmask"
to={`${locationPrefix}`}
primary="Dashboard"
icon={<DashboardIcon sx={{ color: '#fff8' }} />}
/>
<ListItemLink
className="ph-unmask"
to={`${locationPrefix}/inbox`}
primary="Inbox"
icon={
Expand All @@ -91,20 +93,23 @@ function SideNavigationMenu({ open }: SideNavigationMenuProps) {
/>
{permissions(Permission.AccessPartneringFamiliesScreen) && (
<ListItemLink
className="ph-unmask"
to={`${locationPrefix}/referrals`}
primary="Referrals"
icon={<PermPhoneMsgIcon sx={{ color: '#fff8' }} />}
/>
)}
{permissions(Permission.AccessVolunteersScreen) && (
<ListItemLink
className="ph-unmask"
to={`${locationPrefix}/volunteers`}
primary="Volunteers"
icon={<PeopleIcon sx={{ color: '#fff8' }} />}
/>
)}
{permissions(Permission.AccessCommunitiesScreen) && (
<ListItemLink
className="ph-unmask"
to={`${locationPrefix}/communities`}
primary="Communities"
icon={<Diversity3Icon sx={{ color: '#fff8' }} />}
Expand All @@ -114,11 +119,13 @@ function SideNavigationMenu({ open }: SideNavigationMenuProps) {
<>
<Divider />
<ListItemLink
className="ph-unmask"
to={`${locationPrefix}/settings`}
primary="Settings"
icon={<SettingsIcon sx={{ color: '#fff8' }} />}
/>
<ListItemLink
className="ph-unmask"
to="http://support.caretogether.io"
newTab
primary="Support"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PostHogConfig } from 'posthog-js';

export const postHogOptions: Partial<PostHogConfig> = {
session_recording: {
// `maskTextFn` only applies to selected elements, so make sure to set to "*" if you want this to work globally.
maskTextSelector: '*',
maskTextFn: (text, element) => {
// To unmask text in a specific element, add the class `ph-unmask` to that element or the closest possible parent.
// Sometimes adding the class to the element itself is difficult due to how the 3rd party component was implemented,
// so you may need to add it to a parent.
// Adding it to an element too high up in the DOM tree may unmask more text than you want and cause performance issues,
// as the browser will have to work more to find an element with that class in the elements tree.
if (element?.closest('.ph-unmask') !== null) {
return text;
}

return '*'.repeat(text.length);
},
},
mask_all_text: true,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect } from 'react';
import { useLoadable } from '../../Hooks/useLoadable';
import posthog from 'posthog-js';
import {
locationConfigurationQuery,
organizationConfigurationQuery,
} from '../../Model/ConfigurationModel';
import { useParams } from 'react-router-dom';

export const usePostHogGroups = () => {
const { organizationId, locationId } = useParams<{
organizationId: string;
locationId: string;
}>();

const organizationConfiguration = useLoadable(organizationConfigurationQuery);
const locationConfiguration = useLoadable(locationConfigurationQuery);

useEffect(() => {
if (organizationId) {
posthog.group('organization', organizationId, {
name: organizationConfiguration?.organizationName,
});
}

if (organizationId && locationId) {
posthog.group('location', locationId, {
name: locationConfiguration?.name,
});
}
}, [
organizationId,
locationId,
organizationConfiguration?.organizationName,
locationConfiguration?.name,
]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useEffect } from 'react';
import { accountInfoState } from '../../Authentication/Auth';
import { useLoadable } from '../../Hooks/useLoadable';
import posthog from 'posthog-js';

export const usePostHogIdentify = () => {
const accountInfo = useLoadable(accountInfoState);

// Passing the properties to the deps array avoids calling the effect twice.
useEffect(() => {
if (accountInfo?.userId) {
posthog.identify(accountInfo.userId, {
email: accountInfo.email,
name: accountInfo.name,
});
}
}, [accountInfo?.userId, accountInfo?.email, accountInfo?.name]);
};
5 changes: 0 additions & 5 deletions src/caretogether-pwa/src/Utilities/instrumentation.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/caretogether-pwa/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import RequestBackdrop from './Shell/RequestBackdrop';
import { ProgressBackdrop } from './Shell/ProgressBackdrop';

import { PostHogProvider } from 'posthog-js/react';
import { postHogOptions } from './Utilities/instrumentation';
import { postHogOptions } from './Utilities/Instrumentation/postHogOptions';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
Expand Down

0 comments on commit ab29cb4

Please sign in to comment.