diff --git a/core/tracking/TrackingInitializer.tsx b/core/tracking/TrackingInitializer.tsx index cf5e91e63229c165a2e46624a50a1e5abe69e320..0d284d6c2a73b2cde2bd678a7e64d4cd3bb102a3 100644 --- a/core/tracking/TrackingInitializer.tsx +++ b/core/tracking/TrackingInitializer.tsx @@ -1,25 +1,27 @@ -import { ApolloClient, ReactiveVar, makeVar, useApolloClient, useReactiveVar } from '@apollo/client' +import { type ApolloClient, makeVar, type ReactiveVar, useApolloClient, useReactiveVar } from '@apollo/client' import React, { memo, useEffect, useMemo } from 'react' import { - LoginStateChangeListener, listenOnLoginStateChange, loginState, + type LoginStateChangeListener, loginStateUnknown, } from '@holi/core/auth/loginState' -import { AuthenticatedUserV2Response, authenticatedUserV2Query } from '@holi/core/domain/shared/queries' -import { AuthenticatedUser } from '@holi/core/domain/shared/types' +import { authenticatedUserV2Query, type AuthenticatedUserV2Response } from '@holi/core/domain/shared/queries' +import type { AuthenticatedUser } from '@holi/core/domain/shared/types' import { isApp } from '@holi/core/helpers/isApp' import { isSSR } from '@holi/core/helpers/isSSR' import { getLogger } from '@holi/core/helpers/logging' import { - UpdateAuthenticatedUserInput, - UpdateAuthenticatedUserV2Response, + type UpdateAuthenticatedUserInput, updateAuthenticatedUserV2Mutation, + type UpdateAuthenticatedUserV2Response, } from '@holi/core/screens/userprofile/queries' import { consentStore } from '@holi/core/tracking/ConsentStore' import { usePosthogCrossPlatform } from '@holi/core/tracking/PosthogCrossPlatform' -import { Consent, ConsentState, PosthogCrossPlatform, TrackingPurpose } from '@holi/core/tracking/types' +import { Consent, ConsentState, type PosthogCrossPlatform, TrackingPurpose } from '@holi/core/tracking/types' +import { TrackingEvent } from '@holi/core/tracking/events' +import useTracking from '@holi/core/tracking/hooks/useTracking' const logger = getLogger('Tracking') @@ -243,6 +245,7 @@ const TrackingInitializerInner = ({ const apollo: ApolloClient<object> = useApolloClient() const tracking = useMemo(() => new Tracking(posthog, apollo), [posthog, apollo]) const state = useReactiveVar(initializerStateVar) + const { track } = useTracking() useEffect(() => { const loginStateChangeListener: LoginStateChangeListener = (curState, prevState) => { @@ -379,6 +382,7 @@ const TrackingInitializerInner = ({ state: 'loggedIn', processing: null, }) + track(TrackingEvent.Auth.LoginCompleted) onUserLoggedInCompleted?.() }) }) @@ -411,7 +415,7 @@ const TrackingInitializerInner = ({ break } } - }, [tracking, state, onUpdateCompleted, onUserLoggedInCompleted, onUserLoggedOutCompleted]) + }, [tracking, state, onUpdateCompleted, onUserLoggedInCompleted, onUserLoggedOutCompleted, track]) return null } diff --git a/core/tracking/__tests__/TrackingInitializer.test.tsx b/core/tracking/__tests__/TrackingInitializer.test.tsx index 49aa1ac15b914032f56d7d272228e7bb08aecfcb..90495100e38b6800269a95b4b32f2e3a36201dbc 100644 --- a/core/tracking/__tests__/TrackingInitializer.test.tsx +++ b/core/tracking/__tests__/TrackingInitializer.test.tsx @@ -11,17 +11,17 @@ import { setLoginStateLoggedIn, setLoginStateLoggedOut, } from '@holi/core/auth/loginState' -import { type AuthenticatedUserV2Response, authenticatedUserV2Query } from '@holi/core/domain/shared/queries' +import { authenticatedUserV2Query, type AuthenticatedUserV2Response } from '@holi/core/domain/shared/queries' import type { AuthenticatedUser } from '@holi/core/domain/shared/types' import { type UpdateAuthenticatedUserInput, - type UpdateAuthenticatedUserV2Response, updateAuthenticatedUserV2Mutation, + type UpdateAuthenticatedUserV2Response, } from '@holi/core/screens/userprofile/queries' import { usePosthogCrossPlatform } from '@holi/core/tracking/PosthogCrossPlatform' import TrackingInitializer, { - initialState, exportedForTesting as trackingInitializerTesting, + initialState, updateConsentState, } from '@holi/core/tracking/TrackingInitializer' import { Consent, ConsentState, type PosthogCrossPlatform, TrackingPurpose } from '@holi/core/tracking/types' @@ -343,6 +343,7 @@ describe('TrackingInitializer', () => { expect(posthogCrossPlatformMock.identify).not.toHaveBeenCalled() const [consentState] = trackingInitializerTesting.consentStateVar() expect(consentState).toEqual(expectedConsentState) + expect(mockTrack).not.toHaveBeenCalled() }) }) describe('given only analytics consent', () => { @@ -375,6 +376,7 @@ describe('TrackingInitializer', () => { }) const [consentState] = trackingInitializerTesting.consentStateVar() expect(consentState).toEqual(expectedConsentState) + expect(mockTrack).not.toHaveBeenCalled() }) }) describe('given only personalization consent', () => { @@ -401,6 +403,7 @@ describe('TrackingInitializer', () => { expect(posthogCrossPlatformMock.identify).not.toHaveBeenCalled() const [consentState] = trackingInitializerTesting.consentStateVar() expect(consentState).toEqual(expectedConsentState) + expect(mockTrack).not.toHaveBeenCalled() }) }) describe('given analytics + personalization consent', () => { @@ -433,6 +436,7 @@ describe('TrackingInitializer', () => { }) const [consentState] = trackingInitializerTesting.consentStateVar() expect(consentState).toEqual(expectedConsentState) + expect(mockTrack).not.toHaveBeenCalled() }) describe('when the user retracts personalization consent', () => { it('updates consent state on PostHogs person properties', async () => { @@ -467,6 +471,7 @@ describe('TrackingInitializer', () => { await act(async () => await setLoginStateLoggedIn(mockSession)) rerender(component) await waitFor(() => expect(onUserLoggedInCompleted).toHaveBeenCalledTimes(1)) + expect(mockTrack).not.toHaveBeenCalled() expect(posthogCrossPlatformMock.optIn).toHaveBeenCalledTimes(1) expect(posthogCrossPlatformMock.identify).toHaveBeenCalledTimes(1) @@ -533,6 +538,7 @@ describe('TrackingInitializer', () => { trackingConsentPersonalization: true, }) expect(posthogCrossPlatformMock.flush).toHaveBeenCalledTimes(1) + expect(mockTrack).not.toHaveBeenCalled() }) }) }) @@ -588,6 +594,7 @@ describe('TrackingInitializer', () => { }) const [consentStateAfter] = trackingInitializerTesting.consentStateVar() expect(consentStateAfter).toEqual(updatedConsentState) + expect(mockTrack).not.toHaveBeenCalled() }) }) }) @@ -633,6 +640,7 @@ describe('TrackingInitializer', () => { expect(posthogCrossPlatformMock.identify).toHaveBeenCalledTimes(1) const [consentStateAfter] = trackingInitializerTesting.consentStateVar() expect(consentStateAfter).toEqual(ConsentState.unknown) + expect(mockTrack).not.toHaveBeenCalled() }) }) }) @@ -677,6 +685,12 @@ describe('TrackingInitializer', () => { await act(async () => await setLoginStateLoggedIn(mockSession)) rerender(component) await waitFor(() => expect(onUserLoggedInCompleted).toHaveBeenCalledTimes(1)) + expect(mockTrack).toHaveBeenCalledWith({ + name: 'loginCompleted', + event_version__major: 1, + event_version__minor: 0, + event_version__patch: 0, + }) expect(posthogCrossPlatformMock.optIn).toHaveBeenCalledTimes(2) expect(posthogCrossPlatformMock.identify).toHaveBeenCalledWith(user.id, { @@ -728,6 +742,12 @@ describe('TrackingInitializer', () => { await act(async () => await setLoginStateLoggedIn(mockSession)) rerender(component) await waitFor(() => expect(onUserLoggedInCompleted).toHaveBeenCalledTimes(1)) + expect(mockTrack).toHaveBeenCalledWith({ + name: 'loginCompleted', + event_version__major: 1, + event_version__minor: 0, + event_version__patch: 0, + }) expect(posthogCrossPlatformMock.optOut).toHaveBeenCalledTimes(1) expect(posthogCrossPlatformMock.identify).not.toHaveBeenCalled() @@ -773,6 +793,12 @@ describe('TrackingInitializer', () => { await act(async () => await setLoginStateLoggedIn(mockSession)) rerender(component) await waitFor(() => expect(onUserLoggedInCompleted).toHaveBeenCalledTimes(1)) + expect(mockTrack).toHaveBeenCalledWith({ + name: 'loginCompleted', + event_version__major: 1, + event_version__minor: 0, + event_version__patch: 0, + }) expect(posthogCrossPlatformMock.optOut).toHaveBeenCalledTimes(1) expect(posthogCrossPlatformMock.identify).not.toHaveBeenCalled() @@ -818,6 +844,12 @@ describe('TrackingInitializer', () => { await act(async () => await setLoginStateLoggedIn(mockSession)) rerender(component) await waitFor(() => expect(onUserLoggedInCompleted).toHaveBeenCalledTimes(1)) + expect(mockTrack).toHaveBeenCalledWith({ + name: 'loginCompleted', + event_version__major: 1, + event_version__minor: 0, + event_version__patch: 0, + }) expect(posthogCrossPlatformMock.optOut).toHaveBeenCalledTimes(0) expect(posthogCrossPlatformMock.optIn).toHaveBeenCalledTimes(2) @@ -919,6 +951,12 @@ describe('TrackingInitializer', () => { await act(async () => await setLoginStateLoggedIn(mockSession)) rerender(component) await waitFor(() => expect(onUserLoggedInCompleted).toHaveBeenCalledTimes(1)) + expect(mockTrack).toHaveBeenCalledWith({ + name: 'loginCompleted', + event_version__major: 1, + event_version__minor: 0, + event_version__patch: 0, + }) expect(posthogCrossPlatformMock.optIn).not.toHaveBeenCalled() expect(posthogCrossPlatformMock.identify).not.toHaveBeenCalled() @@ -964,6 +1002,12 @@ describe('TrackingInitializer', () => { await act(async () => await setLoginStateLoggedIn(mockSession)) rerender(component) await waitFor(() => expect(onUserLoggedInCompleted).toHaveBeenCalledTimes(1)) + expect(mockTrack).toHaveBeenCalledWith({ + name: 'loginCompleted', + event_version__major: 1, + event_version__minor: 0, + event_version__patch: 0, + }) expect(posthogCrossPlatformMock.optIn).toHaveBeenCalledTimes(1) expect(posthogCrossPlatformMock.identify).toHaveBeenCalledTimes(1) @@ -1009,6 +1053,12 @@ describe('TrackingInitializer', () => { await act(async () => await setLoginStateLoggedIn(mockSession)) rerender(component) await waitFor(() => expect(onUserLoggedInCompleted).toHaveBeenCalledTimes(1)) + expect(mockTrack).toHaveBeenCalledWith({ + name: 'loginCompleted', + event_version__major: 1, + event_version__minor: 0, + event_version__patch: 0, + }) expect(posthogCrossPlatformMock.optIn).not.toHaveBeenCalled() expect(posthogCrossPlatformMock.identify).not.toHaveBeenCalled() diff --git a/core/tracking/__tests__/useTracking.test.ts b/core/tracking/__tests__/useTracking.test.ts index 2152f0ad8b7a6e6faec45ade22351779e41103a4..7ddb3221bd309c62d61b0eae3490f87448acd84f 100644 --- a/core/tracking/__tests__/useTracking.test.ts +++ b/core/tracking/__tests__/useTracking.test.ts @@ -1,9 +1,10 @@ import { jest } from '@jest/globals' import { usePosthogCrossPlatform } from '@holi/core/tracking/PosthogCrossPlatform' -import { useConsentState } from '@holi/core/tracking/TrackingInitializer' +import { getConsentState } from '@holi/core/tracking/TrackingInitializer' import useTracking from '@holi/core/tracking/hooks/useTracking' -import { Consent, PosthogCrossPlatform, TrackingPurpose } from '@holi/core/tracking/types' +import { Consent, type PosthogCrossPlatform, TrackingPurpose } from '@holi/core/tracking/types' +import { renderHook } from '@testing-library/react-hooks' jest.mock('@holi/core/helpers/config', () => ({ posthogKey: 'some.key', @@ -11,7 +12,7 @@ jest.mock('@holi/core/helpers/config', () => ({ })) jest.mock('@holi/core/tracking/TrackingInitializer') -const useConsentStateMock = useConsentState as jest.Mock +const getConsentStateMock = getConsentState as jest.Mock jest.mock('@holi/core/tracking/PosthogCrossPlatform') const posthogCrossPlatformMock = { @@ -31,7 +32,7 @@ jest.unmock('@holi/core/tracking/hooks/useTracking') beforeEach(() => { usePosthogCrossPlatformMock.mockReset() - useConsentStateMock.mockReset() + getConsentStateMock.mockReset() }) describe('useTracking', () => { @@ -43,17 +44,14 @@ describe('useTracking', () => { it('does add trackingConsentPersonalization=true as an event property', async () => { // GIVEN usePosthogCrossPlatformMock.mockReturnValue(posthogCrossPlatformMock) - useConsentStateMock.mockReturnValue([ - { - [TrackingPurpose.Analytics]: Consent.Given, - [TrackingPurpose.Personalization]: Consent.Given, - }, - { loading: false }, - ]) + getConsentStateMock.mockReturnValue({ + [TrackingPurpose.Analytics]: Consent.Given, + [TrackingPurpose.Personalization]: Consent.Given, + }) // WHEN - const tracking = useTracking() - tracking.track({ + const { result } = renderHook(() => useTracking()) + result.current.track({ name: 'testEvent', event_version__major: 1, event_version__minor: 0, @@ -75,17 +73,14 @@ describe('useTracking', () => { it('does add trackingConsentPersonalization=false as an event property', async () => { // GIVEN usePosthogCrossPlatformMock.mockReturnValue(posthogCrossPlatformMock) - useConsentStateMock.mockReturnValue([ - { - [TrackingPurpose.Analytics]: Consent.Given, - [TrackingPurpose.Personalization]: Consent.Denied, - }, - { loading: false }, - ]) + getConsentStateMock.mockReturnValue({ + [TrackingPurpose.Analytics]: Consent.Given, + [TrackingPurpose.Personalization]: Consent.Denied, + }) // WHEN - const tracking = useTracking() - tracking.track({ + const { result } = renderHook(() => useTracking()) + result.current.track({ name: 'testEvent', event_version__major: 1, event_version__minor: 0, diff --git a/core/tracking/events.ts b/core/tracking/events.ts index 8f2a4c10b555278b9f2e586759ae81595a6ec6b9..60e2bffcf4a528cb9b0172504d6ad51967b225e5 100644 --- a/core/tracking/events.ts +++ b/core/tracking/events.ts @@ -249,6 +249,10 @@ export namespace TrackingEvent { name: 'loginButtonPressed', ...versionOne, } + export const LoginCompleted: TrackingEvent = { + name: 'loginCompleted', + ...versionOne, + } export const ContinueAsGuestButtonPressed: TrackingEvent = { name: 'continueAsGuestButtonPressed', ...versionOne, diff --git a/core/tracking/hooks/useTracking.ts b/core/tracking/hooks/useTracking.ts index 0a2577ad5fab52fcf99ae18ee9e83de9b13d21a4..d5e646e311ad2bab9fcf04bee3d6dff544561916 100644 --- a/core/tracking/hooks/useTracking.ts +++ b/core/tracking/hooks/useTracking.ts @@ -1,21 +1,21 @@ import { getLogger } from '@holi/core/helpers/logging' import { usePosthogCrossPlatform } from '@holi/core/tracking/PosthogCrossPlatform' -import { useConsentState } from '@holi/core/tracking/TrackingInitializer' -import { TrackingEvent } from '@holi/core/tracking/events' -import { ConsentState, TrackingHook } from '@holi/core/tracking/types' +import { getConsentState } from '@holi/core/tracking/TrackingInitializer' +import type { TrackingEvent } from '@holi/core/tracking/events' +import { ConsentState, type TrackingHook } from '@holi/core/tracking/types' +import { useCallback } from 'react' const logger = getLogger('Tracking') const useTracking = (): TrackingHook => { const posthog = usePosthogCrossPlatform() - const [consent] = useConsentState() - return { - track: (event: TrackingEvent) => { + const track = useCallback( + (event: TrackingEvent) => { logger.debug('trackEvent', `tracking "${event.name}" event`, event) try { posthog.capture(event.name, { ...event.properties, - ...ConsentState.toEventProperties(consent), + ...ConsentState.toEventProperties(getConsentState()), event_version__major: event.event_version__major, event_version__minor: event.event_version__minor, event_version__patch: event.event_version__patch, @@ -26,6 +26,11 @@ const useTracking = (): TrackingHook => { console.error('Error during evaluation of onTrack callback', e) } }, + [posthog] + ) + + return { + track, } }