diff --git a/src/storyMap/components/StoryMapForm/ShareDialog.js b/src/storyMap/components/StoryMapForm/ShareDialog.js index a80666e36..eda595c9a 100644 --- a/src/storyMap/components/StoryMapForm/ShareDialog.js +++ b/src/storyMap/components/StoryMapForm/ShareDialog.js @@ -40,6 +40,7 @@ import MembershipsList from 'collaboration/components/MembershipsList'; import ConfirmButton from 'common/components/ConfirmButton'; import ExternalLink from 'common/components/ExternalLink'; import UserEmailAutocomplete from 'common/components/UserEmailAutocomplete'; +import { useAnalytics } from 'monitoring/analytics'; import Restricted from 'permissions/components/Restricted'; import { MEMBERSHIP_ROLE_EDITOR, @@ -74,6 +75,7 @@ const RoleComponent = ({ member }) => { const RemoveButton = props => { const dispatch = useDispatch(); const navigate = useNavigate(); + const { trackEvent } = useAnalytics(); const { data: currentUser } = useSelector(state => state.account.currentUser); const processing = useSelector( state => @@ -100,9 +102,10 @@ const RemoveButton = props => { if (isOwnMembership) { navigate(-1); } + trackEvent('storymap.share.remove'); } }); - }, [dispatch, navigate, member, storyMap, isOwnMembership]); + }, [dispatch, navigate, trackEvent, member, storyMap, isOwnMembership]); const resource = useMemo(() => { if (!member?.userRole || member?.userRole === MEMBERSHIP_ROLE_OWNER) { @@ -162,6 +165,7 @@ const RemoveButton = props => { const ShareDialog = props => { const dispatch = useDispatch(); const { t } = useTranslation(); + const { trackEvent } = useAnalytics(); const processing = useSelector( state => state.storyMap.memberships.add.saving ); @@ -192,10 +196,15 @@ const ShareDialog = props => { const success = data?.meta?.requestStatus === 'fulfilled'; if (success) { setNewEditors([]); + trackEvent('storymap.share.invite', { + props: { + count: newEditors.length, + }, + }); onClose(); } }); - }, [dispatch, onClose, storyMap, newEditors]); + }, [dispatch, trackEvent, onClose, storyMap, newEditors]); const memberships = useMemo( () => [ diff --git a/src/storyMap/components/StoryMapInvite.js b/src/storyMap/components/StoryMapInvite.js index 4bf35b26b..7caa23c22 100644 --- a/src/storyMap/components/StoryMapInvite.js +++ b/src/storyMap/components/StoryMapInvite.js @@ -26,12 +26,14 @@ import { Alert } from '@mui/material'; import { useDocumentTitle } from 'common/document'; import PageContainer from 'layout/PageContainer'; import PageLoader from 'layout/PageLoader'; +import { useAnalytics } from 'monitoring/analytics'; import { approveMembershipToken } from 'storyMap/storyMapSlice'; const StoryMapInvite = () => { const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); + const { trackEvent } = useAnalytics(); const [searchParams] = useSearchParams(); const token = useMemo(() => searchParams.get('token'), [searchParams]); const decodedToken = useMemo(() => (token ? jwt(token) : null), [token]); @@ -82,7 +84,8 @@ const StoryMapInvite = () => { return; } navigate(`/tools/story-maps/${storyMap.storyMapId}/${storyMap.slug}/edit`); - }, [success, navigate, storyMap]); + trackEvent('storymap.share.accept'); + }, [success, navigate, trackEvent, storyMap]); useDocumentTitle(t('storyMap.invite_document_title')); diff --git a/src/storyMap/components/StoryMapInvite.test.js b/src/storyMap/components/StoryMapInvite.test.js index 7ce228c29..eee7b65dc 100644 --- a/src/storyMap/components/StoryMapInvite.test.js +++ b/src/storyMap/components/StoryMapInvite.test.js @@ -18,6 +18,8 @@ import { render, screen, waitFor, within } from 'tests/utils'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { mockTerrasoAPIrequestGraphQL } from 'tests/apiUtils'; +import { useAnalytics } from 'monitoring/analytics'; + import StoryMapInvite from './StoryMapInvite'; // Generated token with https://jwt.io/ @@ -38,11 +40,27 @@ jest.mock('react-router-dom', () => ({ useSearchParams: jest.fn(), })); +jest.mock('monitoring/analytics', () => ({ + ...jest.requireActual('monitoring/analytics'), + useAnalytics: jest.fn(), +})); + +beforeEach(() => { + useAnalytics.mockReturnValue({ + trackEvent: jest.fn(), + }); +}); + const setup = async initialState => { await render(, initialState); }; test('StoryMapInvite: Valid token', async () => { + const trackEvent = jest.fn(); + useAnalytics.mockReturnValue({ + trackEvent, + }); + const navigate = jest.fn(); useNavigate.mockReturnValue(navigate); @@ -79,6 +97,8 @@ test('StoryMapInvite: Valid token', async () => { expect(navigate.mock.calls[0]).toEqual([ '/tools/story-maps/story-map-id-1/hello-world/edit', ]); + + expect(trackEvent).toHaveBeenCalledWith('storymap.share.accept'); }); test('StoryMapInvite: Invalid token', async () => { diff --git a/src/storyMap/components/StoryMapUpdate.test.js b/src/storyMap/components/StoryMapUpdate.test.js index 7f7c51a4a..cf31d3d31 100644 --- a/src/storyMap/components/StoryMapUpdate.test.js +++ b/src/storyMap/components/StoryMapUpdate.test.js @@ -21,6 +21,7 @@ import { mockTerrasoAPIrequestGraphQL } from 'tests/apiUtils'; import { changeCombobox } from 'tests/uiUtils'; import i18n from 'localization/i18n'; +import { useAnalytics } from 'monitoring/analytics'; import { MEMBERSHIP_ROLE_EDITOR } from 'storyMap/storyMapConstants'; import StoryMapUpdate from './StoryMapUpdate'; @@ -29,6 +30,11 @@ jest.mock('terraso-client-shared/terrasoApi/api'); jest.mock('./StoryMap', () => props =>
Test
); +jest.mock('monitoring/analytics', () => ({ + ...jest.requireActual('monitoring/analytics'), + useAnalytics: jest.fn(), +})); + const CONFIG = { title: 'Story Map Title', subtitle: 'Story Map Subtitle', @@ -92,6 +98,12 @@ const API_STORY_MAP = { }, }; +beforeEach(() => { + useAnalytics.mockReturnValue({ + trackEvent: jest.fn(), + }); +}); + const setup = async user => { await render(, { account: { @@ -177,6 +189,10 @@ test('StoryMapUpdate: Show Share Dialog', async () => { }); test('StoryMapUpdate: Share Dialog invite members', async () => { + const trackEvent = jest.fn(); + useAnalytics.mockReturnValue({ + trackEvent, + }); mockTerrasoAPIrequestGraphQL({ 'query fetchStoryMap': Promise.resolve({ storyMaps: { @@ -245,9 +261,19 @@ test('StoryMapUpdate: Share Dialog invite members', async () => { expect( within(membersList).getByRole('listitem', { name: 'Manuel Perez' }) ).toBeInTheDocument(); + + expect(trackEvent).toHaveBeenCalledWith('storymap.share.invite', { + props: { + count: 2, + }, + }); }); test('StoryMapUpdate: Share Dialog remove members', async () => { + const trackEvent = jest.fn(); + useAnalytics.mockReturnValue({ + trackEvent, + }); mockTerrasoAPIrequestGraphQL({ 'query fetchStoryMap': Promise.resolve({ storyMaps: { @@ -290,6 +316,8 @@ test('StoryMapUpdate: Share Dialog remove members', async () => { storyMapSlug: 'test-slug', }, }); + + expect(trackEvent).toHaveBeenCalledWith('storymap.share.remove'); }); test('StoryMapUpdate: See story map as editor', async () => { diff --git a/src/storyMap/components/StoryMapsCard.js b/src/storyMap/components/StoryMapsCard.js index 67c47f798..627bc23cf 100644 --- a/src/storyMap/components/StoryMapsCard.js +++ b/src/storyMap/components/StoryMapsCard.js @@ -39,6 +39,7 @@ import { MEMBERSHIP_STATUS_PENDING } from 'collaboration/collaborationConstants' import RouterButton from 'common/components/RouterButton'; import RouterLink from 'common/components/RouterLink'; import { formatDate } from 'localization/utils'; +import { useAnalytics } from 'monitoring/analytics'; import Restricted from 'permissions/components/Restricted'; import HomeCard from 'home/components/HomeCard'; import { approveMembership, removeUserStoryMap } from 'storyMap/storyMapSlice'; @@ -75,6 +76,7 @@ const CollaborationIndicator = props => { const StoryMapListItem = props => { const dispatch = useDispatch(); const navigate = useNavigate(); + const { trackEvent } = useAnalytics(); const { t, i18n } = useTranslation(); const { storyMap } = props; @@ -106,9 +108,10 @@ const StoryMapListItem = props => { const storyMapId = data.payload.storyMap.storyMapId; const storyMapSlug = data.payload.storyMap.slug; navigate(`/tools/story-maps/${storyMapId}/${storyMapSlug}/edit`); + trackEvent('storymap.share.accept'); } }); - }, [dispatch, navigate, storyMap, accountMembership]); + }, [dispatch, navigate, trackEvent, storyMap, accountMembership]); const onDeleteSuccess = useCallback(() => { dispatch(removeUserStoryMap(storyMap.id)); diff --git a/src/storyMap/components/StoryMapsToolHome.test.js b/src/storyMap/components/StoryMapsToolHome.test.js index 8b1b51bcf..0c40b4318 100644 --- a/src/storyMap/components/StoryMapsToolHome.test.js +++ b/src/storyMap/components/StoryMapsToolHome.test.js @@ -14,13 +14,27 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { render, screen, within } from 'tests/utils'; +import { act, fireEvent, render, screen, within } from 'tests/utils'; import * as terrasoApi from 'terraso-client-shared/terrasoApi/api'; +import { mockTerrasoAPIrequestGraphQL } from 'tests/apiUtils'; + +import { useAnalytics } from 'monitoring/analytics'; import StoryMapsToolsHome from './StoryMapsToolHome'; jest.mock('terraso-client-shared/terrasoApi/api'); +jest.mock('monitoring/analytics', () => ({ + ...jest.requireActual('monitoring/analytics'), + useAnalytics: jest.fn(), +})); + +beforeEach(() => { + useAnalytics.mockReturnValue({ + trackEvent: jest.fn(), + }); +}); + test('StoryMapsToolHome: samples renders correctly', async () => { terrasoApi.requestGraphQL.mockReturnValue( Promise.resolve({ @@ -158,3 +172,77 @@ test('StoryMapsToolHome: user story maps render correctly', async () => { const link1 = within(items[1]).getByRole('link', { name: 'Story 1' }); expect(link1).toHaveAttribute('href', '/tools/story-maps/46h36we/id-1/edit'); }); + +test('StoryMapsToolHome: accept story map invite', async () => { + const trackEvent = jest.fn(); + useAnalytics.mockReturnValue({ + trackEvent, + }); + mockTerrasoAPIrequestGraphQL({ + 'query storyMapsHome': Promise.resolve({ + userStoryMaps: { + edges: [ + { + node: { + id: 'id-1', + slug: 'id-1', + storyMapId: '46h36we', + title: 'Story 1', + isPublished: false, + updatedAt: '2023-01-31T22:25:42.916303+00:00', + createdBy: { + userId: 'other-user-id', + firstName: 'Pablo', + lastName: 'Perez', + }, + membershipList: { + membershipsCount: 0, + accountMembership: { + id: '12eb041f-e847-4f78-89ec-46a6a6b7c5c6', + userRole: 'editor', + membershipStatus: 'PENDING', + }, + }, + }, + }, + ], + }, + }), + 'mutation approveMembership': Promise.resolve({ + approveStoryMapMembership: { + membership: { + id: 'membership-id-1', + }, + storyMap: { + id: 'story-map-id-1', + title: 'Hello world', + storyMapId: 'story-map-id-1', + slug: 'hello-world', + }, + }, + }), + }); + + await render(, { + account: { + currentUser: { + data: { + email: 'account@email.com', + firstName: 'Jodies', + }, + }, + }, + }); + + const storyMapItem = screen.getByRole('listitem', { name: 'Story 1' }); + + const acceptButton = within(storyMapItem).getByRole('button', { + name: 'Accept', + }); + + await act(async () => { + fireEvent.click(acceptButton); + }); + + expect(trackEvent).toHaveBeenCalledWith('storymap.share.accept'); +});