Skip to content

Commit 7849791

Browse files
authored
Merge pull request #3663 from Northeastern-Electric-Racing/microsoft-clarity
Microsoft clarity
2 parents 9c1f74b + 6d3a9ba commit 7849791

11 files changed

Lines changed: 2075 additions & 931 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"@types/react-dom": "17.0.1"
6767
},
6868
"dependencies": {
69+
"@microsoft/clarity": "^1.0.0",
6970
"@types/multer": "^1.4.12",
7071
"canvas-confetti": "^1.9.3",
7172
"mitt": "^3.0.1",

src/frontend/src/app/AppContextUser.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { v4 as uuidv4 } from 'uuid';
2+
13
/*
24
* This file is part of NER's FinishLine and licensed under GNU AGPLv3.
35
* See the LICENSE file in the repository root folder for details.
@@ -7,13 +9,33 @@ import { createContext } from 'react';
79
import { AuthenticatedUser } from 'shared';
810
import LoadingIndicator from '../components/LoadingIndicator';
911
import { useAuth } from '../hooks/auth.hooks';
12+
import { useClarity } from '../hooks/misc.hooks';
13+
import { fullNamePipe } from '../utils/pipes';
14+
import { useGetUsersTeams } from '../hooks/teams.hooks';
15+
import ErrorPage from '../pages/ErrorPage';
1016

1117
export const UserContext = createContext<AuthenticatedUser | undefined>(undefined);
1218

1319
const AppContextUser: React.FC = (props) => {
1420
const auth = useAuth();
21+
const clarity = useClarity();
22+
const { data: teams, isLoading: teamsIsLoading, isError: teamsIsError, error: teamsError } = useGetUsersTeams();
23+
24+
if (!auth.user || teamsIsLoading || !teams) return <LoadingIndicator />;
25+
26+
if (teamsIsError) return <ErrorPage message={teamsError.message} />;
1527

16-
if (!auth.user) return <LoadingIndicator />;
28+
if (import.meta.env.VITE_REACT_APP_CLARITY_PROJECT_ID) {
29+
clarity('consent');
30+
clarity('identify', auth.user.email, uuidv4(), undefined, fullNamePipe(auth.user));
31+
clarity('set', 'role', auth.user.role);
32+
clarity('set', 'finishlineUserId', auth.user.userId);
33+
clarity(
34+
'set',
35+
'team',
36+
teams.map((team) => team.teamName)
37+
);
38+
}
1739

1840
return <UserContext.Provider value={auth.user}>{props.children}</UserContext.Provider>;
1941
};

src/frontend/src/app/AppMain.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ import { BrowserRouter } from 'react-router-dom';
77
import AppContext from './AppContext';
88
import AppPublic from './AppPublic';
99
import { ToastProvider } from '../components/Toast/ToastProvider';
10+
import ClarityProvider from './ClarityProvider';
1011
import AppOAuthProvider from './AppOauthProvider';
1112

1213
const AppMain: React.FC = () => {
1314
return (
14-
<AppContext>
15-
<ToastProvider>
16-
<BrowserRouter>
17-
<AppOAuthProvider>
18-
<AppPublic />
19-
</AppOAuthProvider>
20-
</BrowserRouter>
21-
</ToastProvider>
22-
</AppContext>
15+
<ClarityProvider>
16+
<AppContext>
17+
<ToastProvider>
18+
<BrowserRouter>
19+
<AppOAuthProvider>
20+
<AppPublic />
21+
</AppOAuthProvider>
22+
</BrowserRouter>
23+
</ToastProvider>
24+
</AppContext>
25+
</ClarityProvider>
2326
);
2427
};
2528

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { useEffect, createContext, useCallback } from 'react';
2+
3+
// Extend the Window interface to include the Clarity function
4+
declare global {
5+
interface Window {
6+
clarity?: (...args: any[]) => void;
7+
}
8+
}
9+
10+
const CLARITY_PROJECT_ID = import.meta.env.VITE_REACT_APP_CLARITY_PROJECT_ID as string | undefined;
11+
12+
export type ClarityFn = (...args: any[]) => void;
13+
14+
export const ClarityContext = createContext<ClarityFn | undefined>(undefined);
15+
16+
/**
17+
* ClarityProvider component
18+
*
19+
* Injects the Clarity script on mount and provides the Clarity function via context.
20+
*/
21+
const ClarityProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
22+
useEffect(() => {
23+
// Inject the Clarity script only once, if not already present
24+
if (typeof window !== 'undefined' && !window.clarity && CLARITY_PROJECT_ID) {
25+
(function (c: any, l: Document, a: string, r: string, i: string) {
26+
c[a] =
27+
c[a] ||
28+
function (...args: any[]) {
29+
(c[a].q = c[a].q || []).push(args);
30+
};
31+
const t = l.createElement(r) as HTMLScriptElement;
32+
t.async = true;
33+
t.src = 'https://www.clarity.ms/tag/' + i;
34+
const [y] = l.getElementsByTagName(r);
35+
if (y && y.parentNode) {
36+
y.parentNode.insertBefore(t, y);
37+
}
38+
})(window, document, 'clarity', 'script', CLARITY_PROJECT_ID);
39+
}
40+
}, []);
41+
42+
// Memoized clarity function that calls window.clarity if available
43+
const clarity = useCallback<ClarityFn>((...args) => {
44+
if (typeof window !== 'undefined' && typeof window.clarity === 'function') {
45+
window.clarity(...args);
46+
}
47+
}, []);
48+
49+
return <ClarityContext.Provider value={clarity}>{children}</ClarityContext.Provider>;
50+
};
51+
52+
export default ClarityProvider;

src/frontend/src/hooks/misc.hooks.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { useQuery } from 'react-query';
77
import { VersionObject } from '../utils/types';
88
import { getReleaseInfo } from '../apis/misc.api';
99
import { useHistory } from 'react-router-dom';
10-
import { useState } from 'react';
10+
import { useContext, useState } from 'react';
11+
import { ClarityContext } from '../app/ClarityProvider';
1112

1213
export const useGetVersionNumber = () => {
1314
return useQuery<VersionObject, Error>(['version'], async () => {
@@ -34,3 +35,17 @@ export const useHistoryState = <T>(key: string, initialValue: T): [T, (t: T) =>
3435
}
3536
return [rawState, setState];
3637
};
38+
39+
/**
40+
* useClarity hook
41+
*
42+
* Returns the Clarity function from context. Use this to call Clarity API methods.
43+
* Example: const clarity = useClarity();
44+
*/
45+
export const useClarity = () => {
46+
const context = useContext(ClarityContext);
47+
if (context === undefined) {
48+
throw new Error('useClarity must be used within a ClarityProvider');
49+
}
50+
return context;
51+
};

src/frontend/src/tests/pages/ChangeRequestDetailPage/ChangeRequestDetailsView.test.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { mockAuth, mockUseMutationResult, mockUseQueryResult } from '../../test-
1313
import { exampleProject1 } from '../../test-support/test-data/projects.stub';
1414
import { ToastProvider } from '../../../components/Toast/ToastProvider';
1515
import * as authHooks from '../../../hooks/auth.hooks';
16-
import AppContextUser from '../../../app/AppContextUser';
1716
import { useAllUsers, useLogUserIn } from '../../../hooks/users.hooks';
1817
import * as userHooks from '../../../hooks/users.hooks';
1918
import {
@@ -47,18 +46,16 @@ const mockUseLogUserInHook = (isLoading: boolean, isError: boolean, error?: Erro
4746
const renderComponent = (cr: ChangeRequest, allowed: boolean = false) => {
4847
const RouterWrapper = routerWrapperBuilder({});
4948
return render(
50-
<AppContextUser>
51-
<ToastProvider>
52-
<RouterWrapper>
53-
<ChangeRequestDetailsView
54-
changeRequest={cr}
55-
isUserAllowedToReview={allowed}
56-
isUserAllowedToImplement={allowed}
57-
isUserAllowedToDelete={allowed}
58-
/>
59-
</RouterWrapper>
60-
</ToastProvider>
61-
</AppContextUser>
49+
<ToastProvider>
50+
<RouterWrapper>
51+
<ChangeRequestDetailsView
52+
changeRequest={cr}
53+
isUserAllowedToReview={allowed}
54+
isUserAllowedToImplement={allowed}
55+
isUserAllowedToDelete={allowed}
56+
/>
57+
</RouterWrapper>
58+
</ToastProvider>
6259
);
6360
};
6461

src/frontend/src/tests/pages/ChangeRequestDetailPage/ProposedSolutionsList.test.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ToastProvider } from '../../../components/Toast/ToastProvider';
1111
import AppContextUser from '../../../app/AppContextUser';
1212
import * as userHooks from '../../../hooks/users.hooks';
1313
import { exampleAuthenticatedAdminUser } from '../../test-support/test-data/authenticated-user.stub';
14+
import ClarityProvider from '../../../app/ClarityProvider';
1415

1516
const exampleProposedSolution1: ProposedSolution = {
1617
id: '1',
@@ -42,13 +43,15 @@ const exampleProposedSolutions = [exampleProposedSolution1, exampleProposedSolut
4243
const renderComponent = (proposedSolutions: ProposedSolution[] = [], crReviewed: boolean | undefined = undefined) => {
4344
const RouterWrapper = routerWrapperBuilder({});
4445
return render(
45-
<AppContextUser>
46-
<RouterWrapper>
47-
<ToastProvider>
48-
<ProposedSolutionsList proposedSolutions={proposedSolutions} crReviewed={crReviewed} crId={'0'} />{' '}
49-
</ToastProvider>
50-
</RouterWrapper>
51-
</AppContextUser>
46+
<ClarityProvider>
47+
<AppContextUser>
48+
<RouterWrapper>
49+
<ToastProvider>
50+
<ProposedSolutionsList proposedSolutions={proposedSolutions} crReviewed={crReviewed} crId={'0'} />{' '}
51+
</ToastProvider>
52+
</RouterWrapper>
53+
</AppContextUser>
54+
</ClarityProvider>
5255
);
5356
};
5457

src/frontend/src/tests/pages/CreateChangeRequestPage/CreateProposedSolutionsList.test.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import CreateProposedSolutionsList from '../../../pages/CreateChangeRequestPage/
88
import * as authHooks from '../../../hooks/auth.hooks';
99
import { mockAuth } from '../../test-support/test-data/test-utils.stub';
1010
import * as userHooks from '../../../hooks/users.hooks';
11-
import AppContextUser from '../../../app/AppContextUser';
1211
import { exampleAuthenticatedAdminUser } from '../../test-support/test-data/authenticated-user.stub';
1312

1413
/**
@@ -17,11 +16,9 @@ import { exampleAuthenticatedAdminUser } from '../../test-support/test-data/auth
1716
const renderComponent = () => {
1817
const RouterWrapper = routerWrapperBuilder({});
1918
return render(
20-
<AppContextUser>
21-
<RouterWrapper>
22-
<CreateProposedSolutionsList proposedSolutions={[]} setProposedSolutions={() => {}} />
23-
</RouterWrapper>
24-
</AppContextUser>
19+
<RouterWrapper>
20+
<CreateProposedSolutionsList proposedSolutions={[]} setProposedSolutions={() => {}} />
21+
</RouterWrapper>
2522
);
2623
};
2724

src/frontend/src/tests/pages/WorkPackageDetailPage/WorkPackagePage.test.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { mockAuth, mockUseQueryResult } from '../../test-support/test-data/test-
1313
import { exampleDesignWorkPackage, exampleResearchWorkPackage } from '../../test-support/test-data/work-packages.stub';
1414
import { exampleWbsProject1 } from '../../test-support/test-data/wbs-numbers.stub';
1515
import WorkPackagePage from '../../../pages/WorkPackageDetailPage/WorkPackagePage';
16-
import AppContextUser from '../../../app/AppContextUser';
1716
import { useCurrentUser } from '../../../hooks/users.hooks';
1817
import {
1918
exampleAuthenticatedAdminUser,
@@ -54,9 +53,7 @@ const renderComponent = () => {
5453
const RouterWrapper = routerWrapperBuilder({});
5554
return render(
5655
<RouterWrapper>
57-
<AppContextUser>
58-
<WorkPackagePage wbsNum={exampleWbsProject1} />
59-
</AppContextUser>
56+
<WorkPackagePage wbsNum={exampleWbsProject1} />
6057
</RouterWrapper>
6158
);
6259
};

src/frontend/src/tests/pages/WorkPackageDetailPage/WorkPackageViewContainer/WorkPackageViewContainer.test.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import AppContextUser from '../../../../app/AppContextUser';
1111
import * as userHooks from '../../../../hooks/users.hooks';
1212
import { mockManyWorkPackages } from '../../../test-support/mock-hooks';
1313
import { exampleAuthenticatedAdminUser } from '../../../test-support/test-data/authenticated-user.stub';
14+
import ClarityProvider from '../../../../app/ClarityProvider';
1415

1516
// Sets up the component under test with the desired values and renders it.
1617
const renderComponent = (
@@ -24,17 +25,19 @@ const renderComponent = (
2425
const RouterWrapper = routerWrapperBuilder({});
2526
return render(
2627
<RouterWrapper>
27-
<AppContextUser>
28-
<WorkPackageViewContainer
29-
workPackage={workPackage}
30-
enterEditMode={() => null}
31-
allowEdit={allowEdit}
32-
allowActivate={allowActivate}
33-
allowStageGate={allowStageGate}
34-
allowRequestChange={allowRequestChange}
35-
allowDelete={allowDelete}
36-
/>
37-
</AppContextUser>
28+
<ClarityProvider>
29+
<AppContextUser>
30+
<WorkPackageViewContainer
31+
workPackage={workPackage}
32+
enterEditMode={() => null}
33+
allowEdit={allowEdit}
34+
allowActivate={allowActivate}
35+
allowStageGate={allowStageGate}
36+
allowRequestChange={allowRequestChange}
37+
allowDelete={allowDelete}
38+
/>
39+
</AppContextUser>
40+
</ClarityProvider>
3841
</RouterWrapper>
3942
);
4043
};

0 commit comments

Comments
 (0)