Adds support for pendo initialization across the app

This commit is contained in:
mabashian
2021-05-03 17:37:24 -04:00
parent 550a66553e
commit 3a56d2447c
10 changed files with 124 additions and 46 deletions

View File

@@ -19,6 +19,7 @@ import { MeAPI, RootAPI } from '../../api';
import { useConfig, useAuthorizedPath } from '../../contexts/Config'; import { useConfig, useAuthorizedPath } from '../../contexts/Config';
import { SESSION_TIMEOUT_KEY } from '../../constants'; import { SESSION_TIMEOUT_KEY } from '../../constants';
import { isAuthenticated } from '../../util/auth'; import { isAuthenticated } from '../../util/auth';
import issuePendoIdentity from '../../util/issuePendoIdentity';
import About from '../About'; import About from '../About';
import AlertModal from '../AlertModal'; import AlertModal from '../AlertModal';
import BrandLogo from './BrandLogo'; import BrandLogo from './BrandLogo';
@@ -138,6 +139,13 @@ function AppContainer({ navRouteConfig = [], children }) {
} }
}, [handleLogout, timeRemaining]); }, [handleLogout, timeRemaining]);
useEffect(() => {
if ('analytics_status' in config) {
issuePendoIdentity(config, 'foobar');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.analytics_status]);
const brandName = config?.license_info?.product_name; const brandName = config?.license_info?.product_name;
const alt = brandName ? t`${brandName} logo` : t`brand logo`; const alt = brandName ? t`${brandName} logo` : t`brand logo`;

View File

@@ -4,19 +4,25 @@ import {
mountWithContexts, mountWithContexts,
waitForElement, waitForElement,
} from '../../../testUtils/enzymeHelpers'; } from '../../../testUtils/enzymeHelpers';
import { ConfigAPI, MeAPI, RootAPI } from '../../api'; import { MeAPI, RootAPI } from '../../api';
import { useAuthorizedPath } from '../../contexts/Config'; import { useAuthorizedPath } from '../../contexts/Config';
import AppContainer from './AppContainer'; import AppContainer from './AppContainer';
jest.mock('../../api'); jest.mock('../../api');
jest.mock('../../util/bootstrapPendo');
global.pendo = {
initialize: jest.fn(),
};
describe('<AppContainer />', () => { describe('<AppContainer />', () => {
const version = '222'; const version = '222';
beforeEach(() => { beforeEach(() => {
ConfigAPI.read.mockResolvedValue({ RootAPI.readAssetVariables.mockResolvedValue({
data: { data: {
version, BRAND_NAME: 'AWX',
PENDO_API_KEY: '',
}, },
}); });
MeAPI.read.mockResolvedValue({ data: { results: [{}] } }); MeAPI.read.mockResolvedValue({ data: { results: [{}] } });
@@ -52,7 +58,22 @@ describe('<AppContainer />', () => {
{routeConfig.map(({ groupId }) => ( {routeConfig.map(({ groupId }) => (
<div key={groupId} id={groupId} /> <div key={groupId} id={groupId} />
))} ))}
</AppContainer> </AppContainer>,
{
context: {
config: {
analytics_status: 'detailed',
ansible_version: null,
custom_virtualenvs: [],
version: '9000',
me: { is_superuser: true },
toJSON: () => '/config/',
license_info: {
valid_key: true,
},
},
},
}
); );
}); });
wrapper.update(); wrapper.update();
@@ -70,6 +91,31 @@ describe('<AppContainer />', () => {
expect(wrapper.find('#group_one').length).toBe(1); expect(wrapper.find('#group_one').length).toBe(1);
expect(wrapper.find('#group_two').length).toBe(1); expect(wrapper.find('#group_two').length).toBe(1);
expect(global.pendo.initialize).toHaveBeenCalledTimes(1);
});
test('expected content is rendered', async () => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<AppContainer />, {
context: {
config: {
analytics_status: 'off',
ansible_version: null,
custom_virtualenvs: [],
version: '9000',
me: { is_superuser: true },
toJSON: () => '/config/',
license_info: {
valid_key: true,
},
},
},
});
});
wrapper.update();
expect(global.pendo.initialize).toHaveBeenCalledTimes(0);
}); });
test('opening the about modal renders prefetched config data', async () => { test('opening the about modal renders prefetched config data', async () => {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useContext, useEffect, useMemo } from 'react'; import React, { useCallback, useContext, useEffect, useMemo } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom'; import { useRouteMatch } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -22,8 +22,6 @@ export const useConfig = () => {
}; };
export const ConfigProvider = ({ children }) => { export const ConfigProvider = ({ children }) => {
const { pathname } = useLocation();
const { const {
error: configError, error: configError,
isLoading, isLoading,
@@ -48,10 +46,8 @@ export const ConfigProvider = ({ children }) => {
const { error, dismissError } = useDismissableError(configError); const { error, dismissError } = useDismissableError(configError);
useEffect(() => { useEffect(() => {
if (pathname !== '/login') { request();
request(); }, [request]);
}
}, [request, pathname]);
useEffect(() => { useEffect(() => {
if (error?.response?.status === 401) { if (error?.response?.status === 401) {

View File

@@ -18,7 +18,6 @@ import ContentLoading from '../../../../components/ContentLoading';
import ContentError from '../../../../components/ContentError'; import ContentError from '../../../../components/ContentError';
import { FormSubmitError } from '../../../../components/FormField'; import { FormSubmitError } from '../../../../components/FormField';
import { useConfig } from '../../../../contexts/Config'; import { useConfig } from '../../../../contexts/Config';
import issuePendoIdentity from './pendoUtils';
import SubscriptionStep from './SubscriptionStep'; import SubscriptionStep from './SubscriptionStep';
import AnalyticsStep from './AnalyticsStep'; import AnalyticsStep from './AnalyticsStep';
import EulaStep from './EulaStep'; import EulaStep from './EulaStep';
@@ -102,20 +101,18 @@ function SubscriptionEdit() {
isLoading: isContentLoading, isLoading: isContentLoading,
error: contentError, error: contentError,
request: fetchContent, request: fetchContent,
result: { brandName, pendoApiKey }, result: { brandName },
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const { const {
data: { BRAND_NAME, PENDO_API_KEY }, data: { BRAND_NAME },
} = await RootAPI.readAssetVariables(); } = await RootAPI.readAssetVariables();
return { return {
brandName: BRAND_NAME, brandName: BRAND_NAME,
pendoApiKey: PENDO_API_KEY,
}; };
}, []), }, []),
{ {
brandName: null, brandName: null,
pendoApiKey: null,
} }
); );
@@ -145,23 +142,11 @@ function SubscriptionEdit() {
}); });
} }
const [
{ data },
{
data: {
results: [me],
},
},
] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
const newConfig = { ...data, me };
setConfig(newConfig);
if (!hasValidKey) { if (!hasValidKey) {
if (form.pendo) { if (form.pendo) {
await SettingsAPI.updateCategory('ui', { await SettingsAPI.updateCategory('ui', {
PENDO_TRACKING_STATE: 'detailed', PENDO_TRACKING_STATE: 'detailed',
}); });
await issuePendoIdentity(newConfig, pendoApiKey);
} else { } else {
await SettingsAPI.updateCategory('ui', { await SettingsAPI.updateCategory('ui', {
PENDO_TRACKING_STATE: 'off', PENDO_TRACKING_STATE: 'off',
@@ -178,6 +163,18 @@ function SubscriptionEdit() {
}); });
} }
} }
const [
{ data },
{
data: {
results: [me],
},
},
] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
const newConfig = { ...data, me };
setConfig(newConfig);
return true; return true;
}, []) // eslint-disable-line react-hooks/exhaustive-deps }, []) // eslint-disable-line react-hooks/exhaustive-deps
); );

View File

@@ -14,7 +14,6 @@ import {
} from '../../../../api'; } from '../../../../api';
import SubscriptionEdit from './SubscriptionEdit'; import SubscriptionEdit from './SubscriptionEdit';
jest.mock('./bootstrapPendo');
jest.mock('../../../../api'); jest.mock('../../../../api');
const mockConfig = { const mockConfig = {

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom'; import { Link, useHistory, useLocation } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core'; import { Button } from '@patternfly/react-core';
@@ -19,6 +19,12 @@ import { SettingDetail } from '../../shared';
function UIDetail() { function UIDetail() {
const { me } = useConfig(); const { me } = useConfig();
const { GET: options } = useSettings(); const { GET: options } = useSettings();
const history = useHistory();
const { hardReload } = useLocation();
if (hardReload) {
history.go();
}
const { isLoading, error, request, result: ui } = useRequest( const { isLoading, error, request, result: ui } = useRequest(
useCallback(async () => { useCallback(async () => {

View File

@@ -49,9 +49,18 @@ function UIEdit() {
useCallback( useCallback(
async values => { async values => {
await SettingsAPI.updateAll(values); await SettingsAPI.updateAll(values);
history.push('/settings/ui/details'); if (
values?.PENDO_TRACKING_STATE !== uiData?.PENDO_TRACKING_STATE?.value
) {
history.push({
pathname: '/settings/ui/details',
hardReload: true,
});
} else {
history.push('/settings/ui/details');
}
}, },
[history] [history, uiData]
), ),
null null
); );

View File

@@ -111,6 +111,21 @@ describe('<UIEdit />', () => {
wrapper.find('Form').invoke('onSubmit')(); wrapper.find('Form').invoke('onSubmit')();
}); });
expect(history.location.pathname).toEqual('/settings/ui/details'); expect(history.location.pathname).toEqual('/settings/ui/details');
expect(history.location.hardReload).toEqual(undefined);
});
test('should navigate to ui detail with reload param on successful submission where PENDO_TRACKING_STATE changes', async () => {
act(() => {
wrapper.find('select#PENDO_TRACKING_STATE').simulate('change', {
target: { value: 'off', name: 'CUSTOM_LOGIN_INFO' },
});
});
wrapper.update();
await act(async () => {
wrapper.find('Form').invoke('onSubmit')();
});
expect(history.location.pathname).toEqual('/settings/ui/details');
expect(history.location.hardReload).toEqual(true);
}); });
test('should navigate to ui detail when cancel is clicked', async () => { test('should navigate to ui detail when cancel is clicked', async () => {

View File

@@ -1,30 +1,26 @@
import { UsersAPI } from '../../../../api'; import { RootAPI, UsersAPI } from '../api';
import bootstrapPendo from './bootstrapPendo'; import bootstrapPendo from './bootstrapPendo';
function buildPendoOptions(config, pendoApiKey) { function buildPendoOptions(config, pendoApiKey) {
const tower_version = config.version.split('-')[0]; const towerVersion = config.version.split('-')[0];
const trial = config.trial ? config.trial : false; const trial = config.trial ? config.trial : false;
const options = {
return {
apiKey: pendoApiKey, apiKey: pendoApiKey,
visitor: { visitor: {
id: null, id: 0,
role: null, role: null,
}, },
account: { account: {
id: null, id: 'tower.ansible.com',
planLevel: config.license_type, planLevel: config.license_type,
planPrice: config.instance_count, planPrice: config.instance_count,
creationDate: config.license_date, creationDate: config.license_date,
trial, trial,
tower_version, tower_version: towerVersion,
ansible_version: config.ansible_version, ansible_version: config.ansible_version,
}, },
}; };
options.visitor.id = 0;
options.account.id = 'tower.ansible.com';
return options;
} }
async function buildPendoOptionsRole(options, config) { async function buildPendoOptionsRole(options, config) {
@@ -45,14 +41,20 @@ async function buildPendoOptionsRole(options, config) {
} }
} }
async function issuePendoIdentity(config, pendoApiKey) { async function issuePendoIdentity(config) {
if (!('license_info' in config)) {
config.license_info = {};
}
config.license_info.analytics_status = config.analytics_status; config.license_info.analytics_status = config.analytics_status;
config.license_info.version = config.version; config.license_info.version = config.version;
config.license_info.ansible_version = config.ansible_version; config.license_info.ansible_version = config.ansible_version;
if (config.analytics_status !== 'off') { if (config.analytics_status !== 'off') {
bootstrapPendo(pendoApiKey); const {
const pendoOptions = buildPendoOptions(config, pendoApiKey); data: { PENDO_API_KEY },
} = await RootAPI.readAssetVariables();
bootstrapPendo(PENDO_API_KEY);
const pendoOptions = buildPendoOptions(config, PENDO_API_KEY);
const pendoOptionsWithRole = await buildPendoOptionsRole( const pendoOptionsWithRole = await buildPendoOptionsRole(
pendoOptions, pendoOptions,
config config