From 5ec53c7fd597e3cc337c492df196867347cab25d Mon Sep 17 00:00:00 2001 From: Will McVay Date: Wed, 7 Sep 2022 11:12:49 +0100 Subject: [PATCH] feat: #7608 add do not track banner app market (#7652) * feat: #7608 add do not track banner app market * feat: #7608 tweak regex --- .../settings-profile.test.tsx.snap | 172 ++- .../__tests__/settings-profile.test.tsx | 20 +- .../components/settings/settings-profile.tsx | 64 +- .../core/__mocks__/use-apps-browse-state.tsx | 3 + .../app-market/src/core/__styles__/index.ts | 5 + .../analytics-banner.test.tsx.snap | 279 ++++ .../core/__tests__/analytics-banner.test.tsx | 162 ++ .../src/core/__tests__/analytics.test.tsx | 45 +- .../app-market/src/core/analytics-banner.tsx | 146 ++ packages/app-market/src/core/analytics.ts | 34 +- packages/app-market/src/core/index.tsx | 4 +- .../src/core/private-route-wrapper.tsx | 2 + .../src/core/use-apps-browse-state.tsx | 22 +- .../app-market/src/tests/__stubs__/user.ts | 45 + .../scripts/fetch-definitions.js | 7 + .../foundations-ts-definitions/types/index.ts | 2 + .../types/organisations-schema.ts | 1368 +++++++++++++++++ .../src/reapit-data/actions/api-constants.ts | 2 + .../src/reapit-data/actions/get.ts | 12 + .../src/reapit-data/actions/update.ts | 7 + 20 files changed, 2321 insertions(+), 80 deletions(-) create mode 100644 packages/app-market/src/core/__styles__/index.ts create mode 100644 packages/app-market/src/core/__tests__/__snapshots__/analytics-banner.test.tsx.snap create mode 100644 packages/app-market/src/core/__tests__/analytics-banner.test.tsx create mode 100644 packages/app-market/src/core/analytics-banner.tsx create mode 100644 packages/app-market/src/tests/__stubs__/user.ts create mode 100644 packages/foundations-ts-definitions/types/organisations-schema.ts diff --git a/packages/app-market/src/components/settings/__tests__/__snapshots__/settings-profile.test.tsx.snap b/packages/app-market/src/components/settings/__tests__/__snapshots__/settings-profile.test.tsx.snap index e20b6f51d1..9e75d8d935 100644 --- a/packages/app-market/src/components/settings/__tests__/__snapshots__/settings-profile.test.tsx.snap +++ b/packages/app-market/src/components/settings/__tests__/__snapshots__/settings-profile.test.tsx.snap @@ -101,7 +101,9 @@ Object { > Update Your Password -
+
@@ -181,6 +183,45 @@ Object {
+

+ Update Tracking Consent +

+

+ The App Market users mechanisms to track your use of the environment to provide an enhanced user experience and provide feedback to enable Reapit to improve the product. You can update your preferences below. +

+
+
+ + +
+
, "container":
@@ -280,7 +321,9 @@ Object { > Update Your Password -
+
@@ -360,6 +403,45 @@ Object {
+

+ Update Tracking Consent +

+

+ The App Market users mechanisms to track your use of the environment to provide an enhanced user experience and provide feedback to enable Reapit to improve the product. You can update your preferences below. +

+
+
+ + +
+
, "debug": [Function], "findAllByAltText": [Function], @@ -529,7 +611,9 @@ Object { > Update Your Password -
+
@@ -609,6 +693,45 @@ Object {
+

+ Update Tracking Consent +

+

+ The App Market users mechanisms to track your use of the environment to provide an enhanced user experience and provide feedback to enable Reapit to improve the product. You can update your preferences below. +

+
+
+ + +
+
, "container":
@@ -721,7 +844,9 @@ Object { > Update Your Password -
+
@@ -801,6 +926,45 @@ Object {
+

+ Update Tracking Consent +

+

+ The App Market users mechanisms to track your use of the environment to provide an enhanced user experience and provide feedback to enable Reapit to improve the product. You can update your preferences below. +

+
+
+ + +
+
, "debug": [Function], "findAllByAltText": [Function], diff --git a/packages/app-market/src/components/settings/__tests__/settings-profile.test.tsx b/packages/app-market/src/components/settings/__tests__/settings-profile.test.tsx index 82907c60d0..e0dd5b54f5 100644 --- a/packages/app-market/src/components/settings/__tests__/settings-profile.test.tsx +++ b/packages/app-market/src/components/settings/__tests__/settings-profile.test.tsx @@ -1,11 +1,15 @@ import React from 'react' import { render, setViewport } from '../../../tests/react-testing' -import { SettingsProfile, handleChangePassword } from '../settings-profile' +import { SettingsProfile, handleChangePassword, handleUserUpdate } from '../settings-profile' import { changePasswordService } from '../../../services/cognito-identity' import { UseSnack } from '@reapit/elements' import { TrackingEvent } from '../../../core/analytics-events' import { trackEvent } from '../../../core/analytics' +import { mockUserModel } from '../../../tests/__stubs__/user' +import { SendFunction } from '@reapit/utils-react' +import { UpdateUserModel } from '@reapit/foundations-ts-definitions' +jest.mock('../../../core/use-apps-browse-state') jest.mock('../../../core/analytics') jest.mock('../../../services/cognito-identity', () => ({ changePasswordService: jest.fn(), @@ -95,3 +99,17 @@ describe('handleChangePassword', () => { expect(snack.success).not.toHaveBeenCalled() }) }) + +describe('handleUserUpdate', () => { + it('should handle updating a user', async () => { + const updateUser = jest.fn(() => true) as unknown as SendFunction + const currentUserState = mockUserModel + const refreshCurrentUser = jest.fn() + const curried = handleUserUpdate(updateUser, currentUserState, refreshCurrentUser) + + await curried() + + expect(updateUser).toHaveBeenCalledWith({ ...mockUserModel, consentToTrack: false }) + expect(refreshCurrentUser).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/app-market/src/components/settings/settings-profile.tsx b/packages/app-market/src/components/settings/settings-profile.tsx index fc5b97006a..e1f846a727 100644 --- a/packages/app-market/src/components/settings/settings-profile.tsx +++ b/packages/app-market/src/components/settings/settings-profile.tsx @@ -5,6 +5,7 @@ import { ButtonGroup, Col, elMb11, + ElToggleItem, FlexContainer, FormLayout, Grid, @@ -12,6 +13,7 @@ import { InputWrap, Subtitle, Title, + Toggle, useMediaQuery, UseSnack, useSnack, @@ -26,6 +28,10 @@ import { RolesChip } from './__styles__' import { handleLogout } from '.' import { trackEventHandler, trackEvent } from '../../core/analytics' import { TrackingEvent } from '../../core/analytics-events' +import { UpdateUserModel, UserModel } from '@reapit/foundations-ts-definitions' +import { updateActions, UpdateActionNames } from '@reapit/utils-common' +import { SendFunction, useReapitUpdate } from '@reapit/utils-react' +import { useAppsBrowseState } from '../../core/use-apps-browse-state' export type ChangePasswordFormValues = { password: string @@ -50,11 +56,31 @@ export const handleChangePassword = } } +export const handleUserUpdate = + ( + updateUser: SendFunction, + currentUserState: UserModel | null, + refreshCurrentUser: () => void, + ) => + async () => { + const userUpdate = await updateUser({ + ...currentUserState, + name: currentUserState?.name ?? '', + consentToTrack: !currentUserState?.consentToTrack, + }) + + if (userUpdate) refreshCurrentUser() + } + export const SettingsProfile: FC = () => { + const { currentUserState, refreshCurrentUser } = useAppsBrowseState() const { connectSession, connectLogoutRedirect } = useReapitConnect(reapitConnectBrowserSession) const snacks = useSnack() const { isMobile } = useMediaQuery() const loginIdentity = connectSession?.loginIdentity ?? ({} as LoginIdentity) + const email = connectSession?.loginIdentity.email ?? '' + const userId = email ? window.btoa(email).replace(/=/g, '') : null + const { register, handleSubmit, @@ -68,11 +94,25 @@ export const SettingsProfile: FC = () => { }, }) + const [, , updateUser] = useReapitUpdate({ + reapitConnectBrowserSession, + action: updateActions(window.reapit.config.appEnv)[UpdateActionNames.updateUser], + method: 'PUT', + uriParams: { + userId, + }, + }) + useEffect(trackEventHandler(TrackingEvent.LoadProfile, true), []) const logoutUser = useCallback(handleLogout(connectLogoutRedirect), [connectLogoutRedirect]) + const userUpdate = useCallback(handleUserUpdate(updateUser, currentUserState, refreshCurrentUser), [ + currentUserState, + updateUser, + refreshCurrentUser, + ]) - const { name, email, orgName, clientId, groups } = loginIdentity + const { name, orgName, clientId, groups } = loginIdentity return ( <> @@ -130,7 +170,7 @@ export const SettingsProfile: FC = () => { )} Update Your Password -
+ { + Update Tracking Consent + + The App Market users mechanisms to track your use of the environment to provide an enhanced user experience and + provide feedback to enable Reapit to improve the product. You can update your preferences below. + + {currentUserState && ( + + + + Consent + Deny + + + + )} ) } diff --git a/packages/app-market/src/core/__mocks__/use-apps-browse-state.tsx b/packages/app-market/src/core/__mocks__/use-apps-browse-state.tsx index df780904e1..256dcf5850 100644 --- a/packages/app-market/src/core/__mocks__/use-apps-browse-state.tsx +++ b/packages/app-market/src/core/__mocks__/use-apps-browse-state.tsx @@ -1,4 +1,5 @@ import { mockCategoryModelPagedResult } from '../../tests/__stubs__/categories' +import { mockUserModel } from '../../tests/__stubs__/user' import { appsBrowseConfigCollection } from '../config' export const mockAppsBrowseState = { @@ -6,6 +7,8 @@ export const mockAppsBrowseState = { appsBrowseFilterState: null, appsBrowseConfigState: appsBrowseConfigCollection, appsBrowseCategoriesState: mockCategoryModelPagedResult, + currentUserState: mockUserModel, + refreshCurrentUser: jest.fn(), setAppsBrowseFilterState: jest.fn(), } diff --git a/packages/app-market/src/core/__styles__/index.ts b/packages/app-market/src/core/__styles__/index.ts new file mode 100644 index 0000000000..413e427887 --- /dev/null +++ b/packages/app-market/src/core/__styles__/index.ts @@ -0,0 +1,5 @@ +import { css } from '@linaria/core' + +export const cookieBannerPosition = css` + bottom: 1.25rem; +` diff --git a/packages/app-market/src/core/__tests__/__snapshots__/analytics-banner.test.tsx.snap b/packages/app-market/src/core/__tests__/__snapshots__/analytics-banner.test.tsx.snap new file mode 100644 index 0000000000..733136c0ba --- /dev/null +++ b/packages/app-market/src/core/__tests__/__snapshots__/analytics-banner.test.tsx.snap @@ -0,0 +1,279 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnalyticsBanner should match snapshot 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+
+ +
+ , + "container":
+
+ +
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`AnalyticsBanner should match snapshot when there is no current user 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
+
+ +
+ , + "container":
+
+ +
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/packages/app-market/src/core/__tests__/analytics-banner.test.tsx b/packages/app-market/src/core/__tests__/analytics-banner.test.tsx new file mode 100644 index 0000000000..051b179a3a --- /dev/null +++ b/packages/app-market/src/core/__tests__/analytics-banner.test.tsx @@ -0,0 +1,162 @@ +import { ReapitConnectSession } from '@reapit/connect-session' +import { + AnalyticsBanner, + handleTrackingBannerClick, + registerUserHandler, + handleSetUserConsent, + handleSetDoNotTrack, +} from '../analytics-banner' +import mixpanel from 'mixpanel-browser' +import { render } from '../../tests/react-testing' +import React, { MouseEvent } from 'react' +import { useAppsBrowseState } from '../use-apps-browse-state' +import { mockAppsBrowseState } from '../__mocks__/use-apps-browse-state' +import { mockUserModel } from '../../tests/__stubs__/user' +import { SendFunction } from '@reapit/utils-react' +import { UpdateUserModel } from '@reapit/foundations-ts-definitions' + +jest.mock('../use-apps-browse-state') +jest.mock('mixpanel-browser', () => ({ + track: jest.fn(), + identify: jest.fn(), + has_opted_in_tracking: jest.fn(() => true), + has_opted_out_tracking: jest.fn(() => false), + opt_in_tracking: jest.fn(), + opt_out_tracking: jest.fn(), + people: { + set: jest.fn(), + }, +})) + +const mockUseAppsBrowseState = useAppsBrowseState as jest.Mock + +describe('AnalyticsBanner', () => { + it('should match snapshot', () => { + expect(render()).toMatchSnapshot() + }) + + it('should match snapshot when there is no current user', () => { + mockUseAppsBrowseState.mockReturnValueOnce({ + ...mockAppsBrowseState, + currentUserState: null, + }) + expect(render()).toMatchSnapshot() + }) +}) + +describe('registerUserHandler', () => { + it('should register a user', () => { + window.reapit.config.appEnv = 'production' + const connectSession = { + loginIdentity: { + clientId: 'MOCK_CLIENT_ID', + developerId: 'MOCK_DEVELOPER_ID', + groups: ['OrganisationAdmin'], + name: 'MOCK_NAME', + email: 'foo@example.com', + orgName: 'MOCK_ORG_NAME', + }, + } as unknown as ReapitConnectSession + const analyticsRegistered = false + const setAnalyticsRegistered = jest.fn() + + const curried = registerUserHandler(connectSession, analyticsRegistered, setAnalyticsRegistered) + + curried() + + expect(mixpanel.identify).toHaveBeenCalledWith(connectSession.loginIdentity.email) + expect(mixpanel.people.set).toHaveBeenCalledWith({ + Name: connectSession.loginIdentity.name, + Email: connectSession.loginIdentity.email, + 'User Neg Code': connectSession.loginIdentity.userCode, + 'Organisation Name': connectSession.loginIdentity.orgName, + 'Organisation Client Code': connectSession.loginIdentity.clientId, + 'Developer Id': connectSession.loginIdentity.developerId, + 'User Roles': 'Group Organisation Admin', + }) + expect(setAnalyticsRegistered).toHaveBeenCalledWith(true) + }) +}) + +describe('handleTrackingBannerClick', () => { + it('handles tracking banner click', () => { + const setTrackingBannerVisible = jest.fn() + const trackingBannerVisibile = true + + const curried = handleTrackingBannerClick(setTrackingBannerVisible, trackingBannerVisibile) + + curried() + + expect(setTrackingBannerVisible).toBeCalledWith(false) + }) +}) + +describe('handleSetUserConsent', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should set user consent as true from user model', () => { + const currentUserState = mockUserModel + const setTrackingBannerVisible = jest.fn() + + const curried = handleSetUserConsent(currentUserState, setTrackingBannerVisible) + + curried() + + expect(mixpanel.opt_in_tracking).toHaveBeenCalledTimes(1) + expect(mixpanel.opt_out_tracking).not.toHaveBeenCalled() + expect(setTrackingBannerVisible).toHaveBeenCalledWith(true) + }) + + it('should set user consent as false from user model', () => { + const currentUserState = { ...mockUserModel, consentToTrack: false } + const setTrackingBannerVisible = jest.fn() + + const curried = handleSetUserConsent(currentUserState, setTrackingBannerVisible) + + curried() + + expect(mixpanel.opt_in_tracking).not.toHaveBeenCalled() + expect(mixpanel.opt_out_tracking).toHaveBeenCalledTimes(1) + expect(setTrackingBannerVisible).not.toHaveBeenCalled() + }) + + it('should set user consent as false if no user model', () => { + const currentUserState = null + const setTrackingBannerVisible = jest.fn() + + const curried = handleSetUserConsent(currentUserState, setTrackingBannerVisible) + + curried() + + expect(mixpanel.opt_in_tracking).not.toHaveBeenCalled() + expect(mixpanel.opt_out_tracking).not.toHaveBeenCalled() + expect(setTrackingBannerVisible).not.toHaveBeenCalled() + }) +}) + +describe('handleSetDoNotTrack', () => { + it('should set opt out tracking', async () => { + const setTrackingBannerVisible = jest.fn() + const updateUser = jest.fn(() => true) as unknown as SendFunction + const currentUserState = mockUserModel + const refreshCurrentUser = jest.fn() + const event = { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + } as unknown as MouseEvent + + const curried = handleSetDoNotTrack(setTrackingBannerVisible, updateUser, currentUserState, refreshCurrentUser) + + await curried(event) + + expect(mixpanel.opt_out_tracking).toHaveBeenCalledTimes(1) + expect(setTrackingBannerVisible).toHaveBeenCalledWith(false) + expect(updateUser).toHaveBeenCalledWith({ + ...mockUserModel, + consentToTrack: false, + }) + expect(refreshCurrentUser).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/app-market/src/core/__tests__/analytics.test.tsx b/packages/app-market/src/core/__tests__/analytics.test.tsx index 177d068d5c..e54d7998e1 100644 --- a/packages/app-market/src/core/__tests__/analytics.test.tsx +++ b/packages/app-market/src/core/__tests__/analytics.test.tsx @@ -1,11 +1,12 @@ -import { ReapitConnectSession } from '@reapit/connect-session' import mixpanel from 'mixpanel-browser' -import { trackEvent, getRoleFromGroups, trackEventHandler, registerUserHandler } from '../analytics' +import { trackEvent, getRoleFromGroups, trackEventHandler } from '../analytics' import { TrackingEvent } from '../analytics-events' jest.mock('mixpanel-browser', () => ({ track: jest.fn(), identify: jest.fn(), + has_opted_in_tracking: jest.fn(() => true), + has_opted_out_tracking: jest.fn(() => false), people: { set: jest.fn(), }, @@ -20,6 +21,13 @@ describe('trackEvent', () => { expect(mixpanel.track).not.toHaveBeenCalled() }) + it('should not track an event when has opted out with mixpanel', () => { + mixpanel.has_opted_in_tracking.mockReturnValueOnce(false) + trackEvent(TrackingEvent.ChangePassword, true) + + expect(mixpanel.track).not.toHaveBeenCalled() + }) + it('should not track an event when app env is local', () => { window.reapit.config.appEnv = 'local' trackEvent(TrackingEvent.ChangePassword, false) @@ -83,36 +91,3 @@ describe('onPageLoadHandler', () => { expect(mixpanel.track).toHaveBeenLastCalledWith(TrackingEvent.LoadAppDetail, data) }) }) - -describe('registerUserHandler', () => { - it('should register a user', () => { - const connectSession = { - loginIdentity: { - clientId: 'MOCK_CLIENT_ID', - developerId: 'MOCK_DEVELOPER_ID', - groups: ['OrganisationAdmin'], - name: 'MOCK_NAME', - email: 'foo@example.com', - orgName: 'MOCK_ORG_NAME', - }, - } as unknown as ReapitConnectSession - const analyticsRegistered = false - const setAnalyticsRegistered = jest.fn() - - const curried = registerUserHandler(connectSession, analyticsRegistered, setAnalyticsRegistered) - - curried() - - expect(mixpanel.identify).toHaveBeenCalledWith(connectSession.loginIdentity.email) - expect(mixpanel.people.set).toHaveBeenCalledWith({ - Name: connectSession.loginIdentity.name, - Email: connectSession.loginIdentity.email, - 'User Neg Code': connectSession.loginIdentity.userCode, - 'Organisation Name': connectSession.loginIdentity.orgName, - 'Organisation Client Code': connectSession.loginIdentity.clientId, - 'Developer Id': connectSession.loginIdentity.developerId, - 'User Roles': 'Group Organisation Admin', - }) - expect(setAnalyticsRegistered).toHaveBeenCalledWith(true) - }) -}) diff --git a/packages/app-market/src/core/analytics-banner.tsx b/packages/app-market/src/core/analytics-banner.tsx new file mode 100644 index 0000000000..c512a753bf --- /dev/null +++ b/packages/app-market/src/core/analytics-banner.tsx @@ -0,0 +1,146 @@ +import React, { Dispatch, FC, MouseEvent, SetStateAction, useCallback, useEffect, useState } from 'react' +import { reapitConnectBrowserSession } from './connect-session' +import { ReapitConnectSession, useReapitConnect } from '@reapit/connect-session' +import { getRoleFromGroups } from './analytics' +import mixpanel from 'mixpanel-browser' +import { PersistentNotification } from '@reapit/elements' +import { SendFunction, useReapitUpdate } from '@reapit/utils-react' +import { UpdateActionNames, updateActions } from '@reapit/utils-common' +import { UserModel, UpdateUserModel } from '@reapit/foundations-ts-definitions' +import { cookieBannerPosition } from './__styles__' +import { useAppsBrowseState } from './use-apps-browse-state' +import { useLocation } from 'react-router' +import { Routes } from '../constants/routes' + +export const registerUserHandler = + ( + connectSession: ReapitConnectSession | null, + analyticsRegistered: boolean, + setAnalyticsRegistered: Dispatch>, + ) => + () => { + const hasTrackingConsent = mixpanel.has_opted_in_tracking() + const isLocal = window.reapit.config.appEnv !== 'production' + + if (connectSession && !analyticsRegistered && !isLocal && hasTrackingConsent) { + const { email, name, clientId, userCode, orgName, groups, developerId } = connectSession?.loginIdentity ?? {} + const userRoles = getRoleFromGroups(groups) + + mixpanel.identify(email) + + mixpanel.people.set({ + Name: name, + Email: email, + 'User Neg Code': userCode, + 'Organisation Name': orgName, + 'Organisation Client Code': clientId, + 'Developer Id': developerId, + 'User Roles': userRoles, + }) + + setAnalyticsRegistered(true) + } + } + +export const handleSetUserConsent = + (currentUserState: UserModel | null, setTrackingBannerVisible: Dispatch>) => () => { + if (!currentUserState) return + + if (currentUserState.consentToTrack) { + mixpanel.opt_in_tracking() + setTrackingBannerVisible(true) + } else { + mixpanel.opt_out_tracking() + } + } + +export const handleSetDoNotTrack = + ( + setTrackingBannerVisible: Dispatch>, + updateUser: SendFunction, + currentUserState: UserModel | null, + refreshCurrentUser: () => void, + ) => + async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + + mixpanel.opt_out_tracking() + setTrackingBannerVisible(false) + + if (currentUserState) { + const userUpdate = await updateUser({ + ...currentUserState, + name: currentUserState.name ?? '', + consentToTrack: false, + }) + + if (userUpdate) refreshCurrentUser() + } + } + +export const handleTrackingBannerTimeout = + (setTrackingBannerVisible: Dispatch>, trackingBannerVisible: boolean) => () => { + if (trackingBannerVisible) { + const timeout = setTimeout(() => { + setTrackingBannerVisible(false) + }, 5000) + + return () => clearTimeout(timeout) + } + } + +export const handleTrackingBannerClick = + (setTrackingBannerVisible: Dispatch>, trackingBannerVisible: boolean) => () => { + setTrackingBannerVisible(!trackingBannerVisible) + } + +export const AnalyticsBanner: FC = () => { + const { pathname } = useLocation() + const { currentUserState, refreshCurrentUser } = useAppsBrowseState() + const [analyticsRegistered, setAnalyticsRegistered] = useState(false) + const [trackingBannerVisible, setTrackingBannerVisible] = useState(false) + const { connectSession } = useReapitConnect(reapitConnectBrowserSession) + const email = connectSession?.loginIdentity.email + const userId = email ? window.btoa(email).replace(/=/g, '') : null + + const [, , updateUser] = useReapitUpdate({ + reapitConnectBrowserSession, + action: updateActions(window.reapit.config.appEnv)[UpdateActionNames.updateUser], + method: 'PUT', + uriParams: { + userId, + }, + }) + + useEffect(registerUserHandler(connectSession, analyticsRegistered, setAnalyticsRegistered), [ + connectSession, + currentUserState, + ]) + + useEffect(handleSetUserConsent(currentUserState, setTrackingBannerVisible), [currentUserState]) + + useEffect(handleTrackingBannerTimeout(setTrackingBannerVisible, trackingBannerVisible), [trackingBannerVisible]) + + const doNotTrack = useCallback( + handleSetDoNotTrack(setTrackingBannerVisible, updateUser, currentUserState, refreshCurrentUser), + [currentUserState, trackingBannerVisible], + ) + + const trackingBannerClick = useCallback(handleTrackingBannerClick(setTrackingBannerVisible, trackingBannerVisible), [ + trackingBannerVisible, + ]) + + return currentUserState && currentUserState.consentToTrack && pathname !== Routes.SETTINGS_PROFILE ? ( + + The App Market users mechanisms to track your use of the environment to provide an enhanced user experience and + provide feedback to enable Reapit to improve the product - to opt out of this tracking{' '} + click here, if you do not opt out you can at a later date. + + ) : null +} diff --git a/packages/app-market/src/core/analytics.ts b/packages/app-market/src/core/analytics.ts index 5bf033a42e..b6b0307692 100644 --- a/packages/app-market/src/core/analytics.ts +++ b/packages/app-market/src/core/analytics.ts @@ -1,7 +1,5 @@ import { isTruthy } from '@reapit/utils-common' -import { ReapitConnectSession } from '@reapit/connect-session' import mixpanel from 'mixpanel-browser' -import { Dispatch, SetStateAction } from 'react' import { TrackingEvent } from './analytics-events' export interface TrackingEventData { @@ -10,7 +8,9 @@ export interface TrackingEventData { export const trackEvent = (event: TrackingEvent, shouldTrack: boolean, data?: TrackingEventData) => { const isLocal = window.reapit.config.appEnv !== 'production' - if (!shouldTrack || isLocal) return + const hasTrackingConsent = mixpanel.has_opted_in_tracking() + + if (!shouldTrack || isLocal || !hasTrackingConsent) return if (data) { mixpanel.track(event, data) @@ -44,31 +44,3 @@ export const getRoleFromGroups = (groups: string[]) => { export const trackEventHandler = (event: TrackingEvent, shouldTrack: boolean, data?: TrackingEventData) => () => { trackEvent(event, shouldTrack, data) } - -export const registerUserHandler = - ( - connectSession: ReapitConnectSession | null, - analyticsRegistered: boolean, - setAnalyticsRegistered: Dispatch>, - ) => - () => { - const isLocal = window.reapit.config.appEnv !== 'production' - if (connectSession && !analyticsRegistered && !isLocal) { - const { email, name, clientId, userCode, orgName, groups, developerId } = connectSession?.loginIdentity ?? {} - const userRoles = getRoleFromGroups(groups) - - mixpanel.identify(email) - - mixpanel.people.set({ - Name: name, - Email: email, - 'User Neg Code': userCode, - 'Organisation Name': orgName, - 'Organisation Client Code': clientId, - 'Developer Id': developerId, - 'User Roles': userRoles, - }) - - setAnalyticsRegistered(true) - } - } diff --git a/packages/app-market/src/core/index.tsx b/packages/app-market/src/core/index.tsx index c61c2f09c0..58d6ca849e 100644 --- a/packages/app-market/src/core/index.tsx +++ b/packages/app-market/src/core/index.tsx @@ -40,9 +40,9 @@ const run = async () => { try { const configRes = await fetch('config.json') const config = (await configRes.json()) as Config - const isLocal = config.appEnv !== 'production' + const isLocal = config.appEnv === 'production' - if (!isLocal && config.sentryDns && !window.location.hostname.includes('prod.paas')) { + if (!isLocal && config.sentryDns) { Sentry.init({ integrations: [new BrowserTracing()], release: process.env.APP_VERSION, diff --git a/packages/app-market/src/core/private-route-wrapper.tsx b/packages/app-market/src/core/private-route-wrapper.tsx index 9fa916b447..504515e941 100644 --- a/packages/app-market/src/core/private-route-wrapper.tsx +++ b/packages/app-market/src/core/private-route-wrapper.tsx @@ -6,6 +6,7 @@ import { useLocation, Redirect } from 'react-router' import { Loader, MainContainer, PageContainer } from '@reapit/elements' import { Routes } from '../constants/routes' import { AppsBrowseProvider } from './use-apps-browse-state' +import { AnalyticsBanner } from './analytics-banner' export type PrivateRouteWrapperProps = {} @@ -36,6 +37,7 @@ export const PrivateRouteWrapper: FC = ({ children }) return ( +