From 0f07fdcb65ef9d55652eed7fe8b02b063f20ae47 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 20 Feb 2025 17:15:39 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=88(frontend)=20get=20user=20analytics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We will identify users by their email address. This will help us understand how users interact with the platform and improve the user experience. --- .../auth/hooks/__tests__/useAuth.test.tsx | 81 +++++++++++++++++++ .../src/features/auth/hooks/useAuth.tsx | 16 +++- .../impress/src/services/PosthogAnalytic.tsx | 8 +- 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/auth/hooks/__tests__/useAuth.test.tsx diff --git a/src/frontend/apps/impress/src/features/auth/hooks/__tests__/useAuth.test.tsx b/src/frontend/apps/impress/src/features/auth/hooks/__tests__/useAuth.test.tsx new file mode 100644 index 00000000..56c56df3 --- /dev/null +++ b/src/frontend/apps/impress/src/features/auth/hooks/__tests__/useAuth.test.tsx @@ -0,0 +1,81 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import fetchMock from 'fetch-mock'; +import { Fragment } from 'react'; + +import { AbstractAnalytic } from '@/libs'; +import { AppWrapper } from '@/tests/utils'; + +import { useAuth } from '../useAuth'; + +const trackEventMock = jest.fn(); +const flag = true; +class TestAnalytic extends AbstractAnalytic { + public constructor() { + super(); + } + + public Provider() { + return ; + } + + public trackEvent(props: any) { + trackEventMock(props); + } + + public isFeatureFlagActivated(flagName: string): boolean { + if (flagName === 'CopyAsHTML') { + return flag; + } + + return true; + } +} + +jest.mock('next/router', () => ({ + ...jest.requireActual('next/router'), + useRouter: () => ({ + pathname: '/dashboard', + replace: jest.fn(), + }), +})); + +const dummyUser = { id: '123', email: 'test@example.com' }; + +describe('useAuth hook - trackEvent effect', () => { + beforeEach(() => { + jest.clearAllMocks(); + fetchMock.restore(); + }); + + test('calls trackEvent when user exists, isSuccess is true, and event was not tracked yet', async () => { + new TestAnalytic(); + + fetchMock.get('http://test.jest/api/v1.0/users/me/', { + body: JSON.stringify(dummyUser), + }); + + renderHook(() => useAuth(), { + wrapper: AppWrapper, + }); + + await waitFor(() => { + expect(trackEventMock).toHaveBeenCalledWith({ + eventName: 'user', + id: dummyUser.id, + email: dummyUser.email, + }); + }); + }); + + test('does not call trackEvent if already tracked', () => { + fetchMock.get('http://test.jest/api/v1.0/users/me/', { + body: JSON.stringify(dummyUser), + }); + + renderHook(() => useAuth(), { + wrapper: AppWrapper, + }); + + expect(trackEventMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx b/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx index 92fa8f15..dc9054da 100644 --- a/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx +++ b/src/frontend/apps/impress/src/features/auth/hooks/useAuth.tsx @@ -1,6 +1,8 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; +import { useAnalytics } from '@/libs'; + import { useAuthQuery } from '../api'; const regexpUrlsAuth = [/\/docs\/$/g, /\/docs$/g, /^\/$/g]; @@ -8,7 +10,8 @@ const regexpUrlsAuth = [/\/docs\/$/g, /\/docs$/g, /^\/$/g]; export const useAuth = () => { const { data: user, ...authStates } = useAuthQuery(); const { pathname } = useRouter(); - + const { trackEvent } = useAnalytics(); + const [hasTracked, setHasTracked] = useState(authStates.isFetched); const [pathAllowed, setPathAllowed] = useState( !regexpUrlsAuth.some((regexp) => !!pathname.match(regexp)), ); @@ -17,6 +20,17 @@ export const useAuth = () => { setPathAllowed(!regexpUrlsAuth.some((regexp) => !!pathname.match(regexp))); }, [pathname]); + useEffect(() => { + if (!hasTracked && user && authStates.isSuccess) { + trackEvent({ + eventName: 'user', + id: user?.id || '', + email: user?.email || '', + }); + setHasTracked(true); + } + }, [hasTracked, authStates.isSuccess, user, trackEvent]); + return { user, authenticated: !!user && authStates.isSuccess, diff --git a/src/frontend/apps/impress/src/services/PosthogAnalytic.tsx b/src/frontend/apps/impress/src/services/PosthogAnalytic.tsx index deaf0b71..4b44cf29 100644 --- a/src/frontend/apps/impress/src/services/PosthogAnalytic.tsx +++ b/src/frontend/apps/impress/src/services/PosthogAnalytic.tsx @@ -3,7 +3,7 @@ import posthog from 'posthog-js'; import { PostHogProvider as PHProvider } from 'posthog-js/react'; import { JSX, PropsWithChildren, ReactNode, useEffect } from 'react'; -import { AbstractAnalytic } from '@/libs/'; +import { AbstractAnalytic, AnalyticEvent } from '@/libs/'; export class PostHogAnalytic extends AbstractAnalytic { private conf?: PostHogConf = undefined; @@ -18,7 +18,11 @@ export class PostHogAnalytic extends AbstractAnalytic { return {children}; } - public trackEvent(): void {} + public trackEvent(evt: AnalyticEvent): void { + if (evt.eventName === 'user') { + posthog.identify(evt.id, { email: evt.email }); + } + } public isFeatureFlagActivated(flagName: string): boolean { if (