Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/common/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import packageJson from '../../package.json';

export const OCL_CLIENT = `oclmap/${packageJson.version}`;
export const LANGUAGES = [
{locale: 'en', name: 'English'},
{locale: 'es', name: "Español"},
Expand Down
35 changes: 9 additions & 26 deletions src/common/utils.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
/*eslint no-process-env: 0*/
import 'core-js/features/url-search-params';
import React from 'react';
import ReactGA from 'react-ga4';
import moment from 'moment';
import { Tooltip } from '@mui/material';
import {
filter, difference, compact, find, reject, intersectionBy, size, keys, omitBy, isEmpty,
get, includes, map, isArray, values, pick, sortBy, zipObject, orderBy, isObject, merge,
uniqBy, cloneDeep, isEqual, without, capitalize, last, nth, startCase, uniq, flatten, pickBy, upperFirst
uniqBy, cloneDeep, isEqual, without, capitalize, last, nth, uniq, flatten, pickBy, upperFirst
} from 'lodash';
import {
DATE_FORMAT, TIME_FORMAT, DATETIME_FORMAT, OCL_SERVERS_GROUP, OCL_FHIR_SERVERS_GROUP, HAPI_FHIR_SERVERS_GROUP,
OPENMRS_URL, DEFAULT_FHIR_SERVER_FOR_LOCAL_ID, OPERATIONS_PANEL_GROUP
} from './constants';
import APIService from '../services/APIService';
import GAService from '../services/GAService';
import { SERVER_CONFIGS } from './serverConfigs';

export const currentPath = () => window.location.hash.split('?')[0];
Expand Down Expand Up @@ -561,32 +561,9 @@ export const getOpenMRSURL = () => {
return OPENMRS_URL.replace('openmrs.', `openmrs.${env}`);
}

export const recordGAPageView = () => {
/*eslint no-undef: 0*/
ReactGA.initialize(window.GA_ACCOUNT_ID || process.env.GA_ACCOUNT_ID);
// Strip the query string from the hash so auth params (code, state, session_state) on the OIDC callback are not sent to GA
ReactGA.send({ hitType: "pageview", page: window.location.pathname + window.location.hash.split('?')[0] });
}

export const recordGAAction = (category, action, label) => {
/*eslint no-undef: 0*/
if(category && action) {
ReactGA.initialize(window.GA_ACCOUNT_ID || process.env.GA_ACCOUNT_ID);
ReactGA.event({category: category, action: action, label: label || action, transport: "xhr"});
}
}

export const recordGAUpsertEvent = (category, edit, resource) => {
const actionPrefix = edit ? 'update' : 'create'
resource = resource || category.replaceAll(' ', '_').toLowerCase()
let action = `${actionPrefix}_${resource}`
let label = `${startCase(actionPrefix)} ${startCase(resource)}`
recordGAAction(category, action, label)
}

export const setUpRecentHistory = history => {
history.listen(location => {
recordGAPageView()
GAService.recordPageView()
let visits = JSON.parse(get(localStorage, 'visits', '[]'));
let urlParts = compact(location.pathname.split('/'));
let type = '';
Expand Down Expand Up @@ -873,6 +850,9 @@ export const getLoginURL = async returnTo => {
const codeChallenge = await preparePKCECodeChallenge()
const state = prepareOAuthState()
const nonce = generateSecureRandomString(32)

GAService.clearSignupFlow()

return `${getAPIURL()}/users/login/?client_id=${oidClientID}&state=${state}&nonce=${nonce}&redirect_uri=${redirectURL}&code_challenge=${codeChallenge}&code_challenge_method=S256`
}

Expand All @@ -895,6 +875,9 @@ export const getRegisterURL = async returnTo => {
const codeChallenge = await preparePKCECodeChallenge()
const state = prepareOAuthState()
const nonce = generateSecureRandomString(32)

GAService.recordSignupStart()

return `${getAPIURL()}/users/signup/?client_id=${oidClientID}&state=${state}&nonce=${nonce}&redirect_uri=${redirectURL}&code_challenge=${codeChallenge}&code_challenge_method=S256`
}

Expand Down
5 changes: 3 additions & 2 deletions src/components/app/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import React from 'react';
import { Route, Switch, withRouter } from 'react-router-dom';
import {
recordGAPageView, isLoggedIn, getLoginURL, isOtherOCLClientURL, isRedirectingToLoginViaReferrer,
isLoggedIn, getLoginURL, isOtherOCLClientURL, isRedirectingToLoginViaReferrer,
isInWaitlist, getEnv
} from '../../common/utils';
import GAService from '../../services/GAService';
import Error404 from '../errors/Error404';
import Error403 from '../errors/Error403';
import Error401 from '../errors/Error401';
Expand Down Expand Up @@ -108,7 +109,7 @@ const App = props => {
forceLoginUser()
fetchToggles()
addLogoutListenerForAllTabs()
recordGAPageView()
GAService.recordPageView()
setupHotJar()

return () => unsubscribe()
Expand Down
3 changes: 3 additions & 0 deletions src/components/users/OIDLoginCallback.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
refreshCurrentUserCache, consumeStoredPKCECodeVerifier, consumeAndValidateOAuthState
} from '../../common/utils';
import APIService from '../../services/APIService'
import GAService from '../../services/GAService'
import { OperationsContext } from '../app/LayoutContext';

class OIDLoginCallback extends React.Component {
Expand Down Expand Up @@ -39,6 +40,7 @@ class OIDLoginCallback extends React.Component {

APIService.users().appendToUrl('oidc/code-exchange/').post({code: code, redirect_uri: redirectURL, client_id: clientId, code_verifier: codeVerifier}).then(res => {
if(res.data?.access_token) {
GAService.recordSignupComplete()
localStorage.removeItem('server_configs')
localStorage.setItem('token', res.data.access_token)
localStorage.setItem('id_token', res.data.id_token)
Expand All @@ -51,6 +53,7 @@ class OIDLoginCallback extends React.Component {
})
this.cacheUserData()
} else {
GAService.clearSignupFlow()
setAlert({severity: 'error', message: res.data?.error_description || this.props.t('auth.sign_in_error')})
}
})
Expand Down
5 changes: 2 additions & 3 deletions src/services/APIService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import axios from 'axios';
import {get, omit, isPlainObject, isString, defaults } from 'lodash';
import { currentUserToken, getAPIURL, logoutUser, sleep } from '../common/utils';

import packageJson from '../../package.json';
import { OCL_CLIENT } from '../common/constants';


const APIServiceProvider = {};
Expand Down Expand Up @@ -177,7 +176,7 @@ class APIService {
const obj = defaults(headers, this.headers);
if (token) obj['Authorization'] = `Token ${token}`;
obj['INCLUDESEARCHLATEST'] = true
obj['X-OCL-CLIENT'] = `oclmap/${packageJson.version}`;
obj['X-OCL-CLIENT'] = OCL_CLIENT;
return obj;
}

Expand Down
65 changes: 65 additions & 0 deletions src/services/GAService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*eslint no-process-env: 0*/
import ReactGA from 'react-ga4';
import { startCase } from 'lodash';
import { OCL_CLIENT } from '../common/constants';

const SIGNUP_FLOW_PENDING_KEY = 'signup_flow_pending';

const gaId = () => window.GA_ACCOUNT_ID || process.env.GA_ACCOUNT_ID;

// The marketing site only exists in prod, at these two domains, regardless
// of which env this app itself is running in. v3./app. mirror this app's
// own env (e.g. map.qa. -> app.v3.qa., map.qa. -> app.qa.).
const linkedDomains = () => {
const host = window.location.host;

return [
'openconceptlab.org',
'preview.openconceptlab.org',
host.replace('map.', 'app.v3.'),
host.replace('map.', 'app.'),
];
};

const initialize = options => {
/*eslint no-undef: 0*/
ReactGA.initialize(gaId(), options);
};

const GAService = {
recordPageView() {
initialize({ gtagOptions: { linker: { domains: linkedDomains() } } });
ReactGA.send({ hitType: 'pageview', page: window.location.pathname + window.location.hash.split('?')[0] });
},

recordUpsertEvent(category, edit, resource) {
const actionPrefix = edit ? 'update' : 'create';
resource = resource || category.replaceAll(' ', '_').toLowerCase();
const action = `${actionPrefix}_${resource}`;
const label = `${startCase(actionPrefix)} ${startCase(resource)}`;
this.recordEvent(action, { event_category: category, event_label: label });
},

recordSignupStart() {
sessionStorage.setItem(SIGNUP_FLOW_PENDING_KEY, '1');
this.recordEvent('signup_start', { event_category: 'auth', event_label: 'signup_start' });
},

clearSignupFlow() {
sessionStorage.removeItem(SIGNUP_FLOW_PENDING_KEY);
},

recordSignupComplete() {
const pending = sessionStorage.getItem(SIGNUP_FLOW_PENDING_KEY) === '1';
this.clearSignupFlow();
if(pending)
this.recordEvent('signup_complete', { event_category: 'auth', event_label: 'signup_complete' });
},

recordEvent(name, params) {
initialize();
ReactGA.event(name, { client: OCL_CLIENT, ...params });
},
};

export default GAService;