From 190a1f88f6abfbeff55833ab81ac00dc1005ce0e Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Mon, 14 Sep 2026 16:18:59 +0530 Subject: [PATCH 1/3] OpenConceptLab/ocl_online#137 | GA service and signup events with cross linked domains --- src/common/constants.js | 3 ++ src/common/utils.js | 35 ++++-------- src/components/app/App.jsx | 5 +- src/components/collections/CollectionForm.jsx | 4 +- src/components/collections/ExpansionForm.jsx | 4 +- .../common/ConceptContainerVersionForm.jsx | 4 +- src/components/concepts/ConceptForm.jsx | 5 +- src/components/concepts/ConceptHome.jsx | 11 ++-- src/components/mappings/MappingForm.jsx | 5 +- src/components/mappings/MappingHome.jsx | 5 +- src/components/orgs/OrgForm.jsx | 5 +- src/components/sources/Breadcrumbs.jsx | 7 +-- src/components/sources/SourceForm.jsx | 4 +- src/components/users/OIDLoginCallback.jsx | 3 ++ src/components/users/UserForm.jsx | 5 +- src/services/APIService.js | 4 +- src/services/GAService.js | 53 +++++++++++++++++++ 17 files changed, 106 insertions(+), 56 deletions(-) create mode 100644 src/services/GAService.js diff --git a/src/common/constants.js b/src/common/constants.js index 89a20577..5715613d 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -1,3 +1,6 @@ +import packageJson from '../../package.json'; + +export const OCL_CLIENT = `oclweb2/${packageJson.version}`; export const WHITE = '#FFF'; export const BLACK = '#000'; export const BLUE = 'rgb(51, 115, 170)'; diff --git a/src/common/utils.js b/src/common/utils.js index 936a14fd..416fe7f5 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -1,14 +1,13 @@ /*eslint no-process-env: 0*/ import 'core-js/features/url-search-params'; import React from 'react'; -import ReactGA from 'react-ga4'; import alertifyjs from 'alertifyjs'; 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, isNumber, uniq, flatten, pickBy, + uniqBy, cloneDeep, isEqual, without, capitalize, last, nth, isNumber, uniq, flatten, pickBy, isString } from 'lodash'; import { @@ -16,6 +15,7 @@ import { 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]; @@ -715,32 +715,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 = ''; @@ -1010,6 +987,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` } @@ -1030,6 +1010,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` } let url = '/#/accounts/signup' diff --git a/src/components/app/App.jsx b/src/components/app/App.jsx index e3eba31c..b122f5f8 100644 --- a/src/components/app/App.jsx +++ b/src/components/app/App.jsx @@ -4,9 +4,10 @@ import { Route, Switch, withRouter } from 'react-router-dom'; import { get, isEmpty } from 'lodash'; import { isFHIRServer, isLoggedIn, setUpRecentHistory, getAppliedServerConfig, getSiteTitle, - isDeprecatedBrowser, recordGAPageView, canViewOperationsPanel, isRedirectingToLoginViaReferrer, + isDeprecatedBrowser, canViewOperationsPanel, isRedirectingToLoginViaReferrer, isSSOEnabled, getLoginURL, isOtherOCLClientURL } from '../../common/utils'; +import GAService from '../../services/GAService'; import Search from '../search/Search'; import SourceHome from '../sources/SourceHome'; import CollectionHome from '../collections/CollectionHome'; @@ -132,7 +133,7 @@ const App = props => { if(!isFHIRServer()) fetchToggles() addLogoutListenerForAllTabs() - recordGAPageView() + GAService.recordPageView() setupHotJar() operationsPanelAccess && document.body.addEventListener("keydown", _listenKey) return () => operationsPanelAccess && document.body.removeEventListener("keydown", _listenKey) diff --git a/src/components/collections/CollectionForm.jsx b/src/components/collections/CollectionForm.jsx index 60b2903f..9a02784c 100644 --- a/src/components/collections/CollectionForm.jsx +++ b/src/components/collections/CollectionForm.jsx @@ -3,7 +3,7 @@ import alertifyjs from 'alertifyjs'; import { Divider, Button } from '@mui/material'; import { orderBy, map, merge, cloneDeep, get, isEmpty } from 'lodash'; import APIService from '../../services/APIService'; -import { recordGAUpsertEvent } from '../../common/utils'; +import GAService from '../../services/GAService'; import { COLLECTION_TYPES, WHITE } from '../../common/constants' import FormHeader from '../common/conceptContainerFormComponents/FormHeader'; import NameAndDescription from '../common/conceptContainerFormComponents/NameAndDescription'; @@ -43,7 +43,7 @@ class CollectionForm extends React.Component { form.reportValidity() const isFormValid = form.checkValidity() if(isFormValid) { - recordGAUpsertEvent('Collection', edit) + GAService.recordUpsertEvent('Collection', edit) const payload = this.getPayload() const service = APIService.new().overrideURL(this.state.owner.url + 'collections/') if(edit) diff --git a/src/components/collections/ExpansionForm.jsx b/src/components/collections/ExpansionForm.jsx index 4171c5ad..45fa6fea 100644 --- a/src/components/collections/ExpansionForm.jsx +++ b/src/components/collections/ExpansionForm.jsx @@ -8,7 +8,7 @@ import { has, forEach } from 'lodash'; import APIService from '../../services/APIService'; -import { recordGAUpsertEvent } from '../../common/utils'; +import GAService from '../../services/GAService'; import BetaLabel from '../common/BetaLabel'; const DEFAULT_TOOLTIP = 'This parameter is not yet supported.' @@ -191,7 +191,7 @@ class ExpansionForm extends React.Component { form.reportValidity() const isFormValid = form.checkValidity() if(selectedVersion && isFormValid) { - recordGAUpsertEvent('Expansion', false) + GAService.recordUpsertEvent('Expansion', false) this.alert = alertifyjs.warning('Starting Expansion Creation. This might take few seconds.', 0) fields = pickBy(fields, value => value) diff --git a/src/components/common/ConceptContainerVersionForm.jsx b/src/components/common/ConceptContainerVersionForm.jsx index b169af12..97bf1535 100644 --- a/src/components/common/ConceptContainerVersionForm.jsx +++ b/src/components/common/ConceptContainerVersionForm.jsx @@ -4,7 +4,7 @@ import moment from 'moment'; import { TextField, Button, FormControlLabel, Checkbox, Autocomplete } from '@mui/material'; import { set, get, cloneDeep, isEmpty, pickBy, startCase, isBoolean, isObject, values, map } from 'lodash'; import APIService from '../../services/APIService'; -import { recordGAUpsertEvent } from '../../common/utils'; +import GAService from '../../services/GAService'; class ConceptContainerVersionForm extends React.Component { @@ -65,7 +65,7 @@ class ConceptContainerVersionForm extends React.Component { const isFormValid = form.checkValidity() if(parentURL && isFormValid) { const isCollectionVersion = (resource || resourceType) === 'collection' - recordGAUpsertEvent(isCollectionVersion ? `Collection Version` : 'Source Version', edit) + GAService.recordUpsertEvent(isCollectionVersion ? `Collection Version` : 'Source Version', edit) this.alert = alertifyjs.warning('Starting Version Creation. This might take few seconds.', 0) fields = pickBy(fields, value => value) diff --git a/src/components/concepts/ConceptForm.jsx b/src/components/concepts/ConceptForm.jsx index b1a2da33..0c7bf093 100644 --- a/src/components/concepts/ConceptForm.jsx +++ b/src/components/concepts/ConceptForm.jsx @@ -7,9 +7,10 @@ import { set, get, map, cloneDeep, pullAt, filter, isEmpty, pick, isObject, values, startCase, } from 'lodash'; import APIService from '../../services/APIService'; +import GAService from '../../services/GAService'; import { arrayToObject, fetchDatatypes, fetchNameTypes, sortValuesBySourceSummary, - fetchDescriptionTypes, fetchConceptClasses, recordGAUpsertEvent, toParentURI + fetchDescriptionTypes, fetchConceptClasses, toParentURI } from '../../common/utils'; import { ERROR_RED } from '../../common/constants'; import LocaleForm from './LocaleForm'; @@ -279,7 +280,7 @@ class ConceptForm extends React.Component { const isFormValid = form.checkValidity() if(parentURL && isFormValid) { - recordGAUpsertEvent('Concept', edit) + GAService.recordUpsertEvent('Concept', edit) if(edit) fields.update_comment = fields.comment fields.extras = arrayToObject(fields.extras) diff --git a/src/components/concepts/ConceptHome.jsx b/src/components/concepts/ConceptHome.jsx index 33c67db6..5f7c3fca 100644 --- a/src/components/concepts/ConceptHome.jsx +++ b/src/components/concepts/ConceptHome.jsx @@ -4,7 +4,8 @@ import Split from 'react-split' import { CircularProgress } from '@mui/material'; import { get, isObject, isBoolean, has, flatten, values, isArray, find, map } from 'lodash'; import APIService from '../../services/APIService'; -import { fetchAllVersions, toParentURI, currentUserHasAccess, recordGAAction, highlightTexts } from '../../common/utils' +import GAService from '../../services/GAService'; +import { fetchAllVersions, toParentURI, currentUserHasAccess, highlightTexts } from '../../common/utils' import NotFound from '../common/NotFound'; import AccessDenied from '../common/AccessDenied'; import PermissionDenied from '../common/PermissionDenied'; @@ -102,7 +103,7 @@ class ConceptHome extends React.Component { .overrideURL(URL) .get(null, null, {includeReferences: this.props.scoped === 'collection'}) .then(response => { - recordGAAction('Concept', 'split_view', `Concept - ${URL}`) + GAService.recordEvent('split_view', { event_category: 'Concept', event_label: `Concept - ${URL}` }) if(get(response, 'detail') === "Not found.") this.setState({isLoading: false, concept: {}, notFound: true, accessDenied: false, permissionDenied: false}) else if(get(response, 'detail') === "Authentication credentials were not provided.") @@ -279,7 +280,7 @@ class ConceptHome extends React.Component { } retireMapping = (mapping, comment, isDirect) => { - recordGAAction('Mapping Inline', 'retired_mapping', 'Retried Mapping from Concept Details using Quick Actions') + GAService.recordEvent('retired_mapping', { event_category: 'Mapping Inline', event_label: 'Retried Mapping from Concept Details using Quick Actions' }) APIService.new().overrideURL(mapping.url).delete({comment: comment}).then(response => { if(get(response, 'status') === 204) { isDirect ? this.getMappings(true) : this.getInverseMappings(true) @@ -291,7 +292,7 @@ class ConceptHome extends React.Component { } unretireMapping = (mapping, comment, isDirect) => { - recordGAAction('Mapping Inline', 'unretired_mapping', 'Reactivated retired Mapping from Concept Details using Quick Actions') + GAService.recordEvent('unretired_mapping', { event_category: 'Mapping Inline', event_label: 'Reactivated retired Mapping from Concept Details using Quick Actions' }) APIService.new().overrideURL(mapping.url).appendToUrl('reactivate/').put({comment: comment}).then(response => { if(get(response, 'status') === 204) { isDirect ? this.getMappings(true) : this.getInverseMappings(true) @@ -303,7 +304,7 @@ class ConceptHome extends React.Component { } onCreateNewMapping = (payload, targetConcept, isDirect, successCallback) => { - recordGAAction('Mapping Inline', 'create_mapping', 'Created Mapping from Concept Details using Quick Actions') + GAService.recordEvent('create_mapping', { event_category: 'Mapping Inline', event_label: 'Created Mapping from Concept Details using Quick Actions' }) const { concept, mappings, reverseMappings } = this.state const URL = `${concept.owner_url}sources/${concept.source}/mappings/` const targetSourceURL = payload?.to_source_url || payload?.from_source_url || toParentURI(targetConcept.url) diff --git a/src/components/mappings/MappingForm.jsx b/src/components/mappings/MappingForm.jsx index 7b191848..db18ede3 100644 --- a/src/components/mappings/MappingForm.jsx +++ b/src/components/mappings/MappingForm.jsx @@ -10,7 +10,8 @@ import { set, get, cloneDeep, isEmpty, pickBy, pullAt, map } from 'lodash'; import APIService from '../../services/APIService'; -import { arrayToObject, fetchMapTypes, recordGAUpsertEvent } from '../../common/utils'; +import GAService from '../../services/GAService'; +import { arrayToObject, fetchMapTypes } from '../../common/utils'; import ExtrasForm from '../common/ExtrasForm'; import OwnerParentSelection from '../common/OwnerParentSelection'; @@ -221,7 +222,7 @@ class MappingForm extends React.Component { const isFormValid = form.checkValidity() if(parentURL && isFormValid) { - recordGAUpsertEvent('Mapping', edit) + GAService.recordUpsertEvent('Mapping', edit) fields.extras = arrayToObject(fields.extras) if(edit) fields.update_comment = fields.comment diff --git a/src/components/mappings/MappingHome.jsx b/src/components/mappings/MappingHome.jsx index e11cde6e..bd4d0421 100644 --- a/src/components/mappings/MappingHome.jsx +++ b/src/components/mappings/MappingHome.jsx @@ -2,7 +2,8 @@ import React from 'react'; import { CircularProgress } from '@mui/material'; import { get, isObject, has } from 'lodash'; import APIService from '../../services/APIService'; -import { fetchAllVersions, recordGAAction, highlightTexts } from '../../common/utils' +import GAService from '../../services/GAService'; +import { fetchAllVersions, highlightTexts } from '../../common/utils' import ScopeHeader from './ScopeHeader' import MappingHomeDetails from './MappingHomeDetails'; import NotFound from '../common/NotFound'; @@ -69,7 +70,7 @@ class MappingHome extends React.Component { .overrideURL(URL) .get(null, null, {includeReferences: this.props.scoped === 'collection'}) .then(response => { - recordGAAction('Mapping', 'split_view', `Mapping - ${URL}`) + GAService.recordEvent('split_view', { event_category: 'Mapping', event_label: `Mapping - ${URL}` }) if(get(response, 'detail') === "Not found.") this.setState({isLoading: false, mapping: {}, notFound: true, accessDenied: false, permissionDenied: false}) else if(get(response, 'detail') === "Authentication credentials were not provided.") diff --git a/src/components/orgs/OrgForm.jsx b/src/components/orgs/OrgForm.jsx index e3ad3630..f588db23 100644 --- a/src/components/orgs/OrgForm.jsx +++ b/src/components/orgs/OrgForm.jsx @@ -6,7 +6,8 @@ import { set, get, cloneDeep, isEmpty, pullAt, map } from 'lodash'; import APIService from '../../services/APIService'; -import { arrayToObject, toFullURL, refreshCurrentUserCache, recordGAUpsertEvent } from '../../common/utils'; +import GAService from '../../services/GAService'; +import { arrayToObject, toFullURL, refreshCurrentUserCache } from '../../common/utils'; import ExtrasForm from '../common/ExtrasForm'; import RTEditor from '../common/RTEditor'; @@ -85,7 +86,7 @@ class OrgForm extends React.Component { const isFormValid = form.checkValidity() if(isFormValid) { - recordGAUpsertEvent('Organization', edit) + GAService.recordUpsertEvent('Organization', edit) fields.extras = arrayToObject(fields.extras) if(edit) { APIService.orgs(fields.id).put(fields).then(response => this.handleSubmitResponse(response)) diff --git a/src/components/sources/Breadcrumbs.jsx b/src/components/sources/Breadcrumbs.jsx index 4d742899..0196a768 100644 --- a/src/components/sources/Breadcrumbs.jsx +++ b/src/components/sources/Breadcrumbs.jsx @@ -7,9 +7,10 @@ import { } from '@mui/icons-material'; import { IconButton } from '@mui/material'; import { isEmpty, get, startCase } from 'lodash'; -import { currentUserHasAccess, recordGAAction } from '../../common/utils'; +import { currentUserHasAccess } from '../../common/utils'; import { WHITE, ORANGE, GREEN, BLUE } from '../../common/constants'; import APIService from '../../services/APIService'; +import GAService from '../../services/GAService'; import OwnerButton from '../common/OwnerButton'; import SourceButton from '../common/SourceButton'; import CollectionButton from '../common/CollectionButton'; @@ -107,7 +108,7 @@ const Breadcrumbs = ({ const retire = comment => { const resourceLabel = startCase(resourceType) - recordGAAction(resourceLabel, `retired_${resourceType}`, `Retired ${resourceLabel}`) + GAService.recordEvent(`retired_${resourceType}`, { event_category: resourceLabel, event_label: `Retired ${resourceLabel}` }) APIService.new().overrideURL(resourceEncodedURL.replace('#', '')).delete({comment: comment}).then(response => { if(get(response, 'status') === 204) alertifyjs.success(`${resourceLabel} Retired`, 1, () => window.location.reload()) @@ -118,7 +119,7 @@ const Breadcrumbs = ({ const unretire = comment => { const resourceLabel = startCase(resourceType) - recordGAAction(resourceLabel, `unretired_${resourceType}`, `Reactivated ${resourceLabel}`) + GAService.recordEvent(`unretired_${resourceType}`, { event_category: resourceLabel, event_label: `Reactivated ${resourceLabel}` }) APIService.new().overrideURL(resourceEncodedURL.replace('#', '')).appendToUrl('reactivate/').put({comment: comment}).then(response => { if(get(response, 'status') === 204) alertifyjs.success(`${resourceLabel} Unretired`, 1, () => window.location.reload()) diff --git a/src/components/sources/SourceForm.jsx b/src/components/sources/SourceForm.jsx index b1eaeac9..95951988 100644 --- a/src/components/sources/SourceForm.jsx +++ b/src/components/sources/SourceForm.jsx @@ -3,7 +3,7 @@ import alertifyjs from 'alertifyjs'; import { Divider, Button } from '@mui/material'; import { orderBy, map, merge, cloneDeep, get, isEmpty } from 'lodash'; import APIService from '../../services/APIService'; -import { recordGAUpsertEvent } from '../../common/utils'; +import GAService from '../../services/GAService'; import { SOURCE_TYPES, WHITE } from '../../common/constants' import FormHeader from '../common/conceptContainerFormComponents/FormHeader'; import NameAndDescription from '../common/conceptContainerFormComponents/NameAndDescription'; @@ -35,7 +35,7 @@ class SourceForm extends React.Component { form.reportValidity() const isFormValid = form.checkValidity() if(isFormValid) { - recordGAUpsertEvent('Source', edit) + GAService.recordUpsertEvent('Source', edit) const payload = this.getPayload() const service = APIService.new().overrideURL(this.state.owner.url + 'sources/') if(edit) diff --git a/src/components/users/OIDLoginCallback.jsx b/src/components/users/OIDLoginCallback.jsx index bcd8f4f7..07a14131 100644 --- a/src/components/users/OIDLoginCallback.jsx +++ b/src/components/users/OIDLoginCallback.jsx @@ -6,6 +6,7 @@ import { refreshCurrentUserCache, consumeStoredPKCECodeVerifier, consumeAndValidateOAuthState } from '../../common/utils'; import APIService from '../../services/APIService' +import GAService from '../../services/GAService' class OIDLoginCallback extends React.Component { @@ -37,11 +38,13 @@ 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) this.cacheUserData() } else { + GAService.clearSignupFlow() alertifyjs.error(res.data) } }) diff --git a/src/components/users/UserForm.jsx b/src/components/users/UserForm.jsx index 642d8f06..988da494 100644 --- a/src/components/users/UserForm.jsx +++ b/src/components/users/UserForm.jsx @@ -9,7 +9,8 @@ import { set, get, isEmpty, cloneDeep, merge, map, pullAt, isEqual, uniq, without, includes, } from 'lodash'; import APIService from '../../services/APIService'; -import { arrayToObject, isAdminUser, getCurrentUserUsername, recordGAUpsertEvent } from '../../common/utils'; +import GAService from '../../services/GAService'; +import { arrayToObject, isAdminUser, getCurrentUserUsername } from '../../common/utils'; import { AUTH_GROUPS } from '../../common/constants'; import ExtrasForm from '../common/ExtrasForm'; const EXTRAS_MODEL = {key: '', value: ''} @@ -108,7 +109,7 @@ class UserForm extends React.Component { const isFormValid = form.checkValidity() if(isFormValid) { - recordGAUpsertEvent('User', edit) + GAService.recordUpsertEvent('User', edit) fields.extras = arrayToObject(fields.extras) if(!this.canEditAuthGroups() || (edit && isEqual(user.auth_groups, fields.auth_groups))) diff --git a/src/services/APIService.js b/src/services/APIService.js index eac3f560..d8c1a4e8 100644 --- a/src/services/APIService.js +++ b/src/services/APIService.js @@ -2,7 +2,7 @@ import axios from 'axios'; import {get, omit, isPlainObject, isString, defaults } from 'lodash'; import { currentUserToken, getAPIURL } from '../common/utils'; -import packageJson from '../../package.json'; +import { OCL_CLIENT } from '../common/constants'; const APIServiceProvider = {}; @@ -119,7 +119,7 @@ class APIService { token = token || (token !== false ? currentUserToken() : token); const obj = defaults(headers, this.headers); if (token) obj['Authorization'] = `Token ${token}`; - obj['X-OCL-CLIENT'] = `oclweb2/${packageJson.version}`; + obj['X-OCL-CLIENT'] = OCL_CLIENT; return obj; } diff --git a/src/services/GAService.js b/src/services/GAService.js new file mode 100644 index 00000000..536aaafa --- /dev/null +++ b/src/services/GAService.js @@ -0,0 +1,53 @@ +/*eslint no-process-env: 0*/ +import ReactGA from 'react-ga4'; +import { startCase } from 'lodash'; +import { OCL_CLIENT } from '../common/constants'; + +const LINKED_DOMAINS = ['openconceptlab.org', 'preview.openconceptlab.org', 'map.openconceptlab.org', 'app.v3.openconceptlab.org']; +const SIGNUP_FLOW_PENDING_KEY = 'signup_flow_pending'; + +const gaId = () => window.GA_ACCOUNT_ID || process.env.GA_ACCOUNT_ID; + +const initialize = options => { + /*eslint no-undef: 0*/ + ReactGA.initialize(gaId(), options); +}; + +const GAService = { + recordPageView() { + // eslint-disable-next-line spellcheck/spell-checker + initialize({ gtagOptions: { linker: { domains: LINKED_DOMAINS } } }); + 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; From d540c716bdb57114ee65a31204bfc8d76dad4a1c Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Mon, 14 Sep 2026 16:34:57 +0530 Subject: [PATCH 2/3] OpenConceptLab/ocl_online#137 | Add env specific domains --- src/services/GAService.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/services/GAService.js b/src/services/GAService.js index 536aaafa..0935fad2 100644 --- a/src/services/GAService.js +++ b/src/services/GAService.js @@ -3,11 +3,24 @@ import ReactGA from 'react-ga4'; import { startCase } from 'lodash'; import { OCL_CLIENT } from '../common/constants'; -const LINKED_DOMAINS = ['openconceptlab.org', 'preview.openconceptlab.org', 'map.openconceptlab.org', 'app.v3.openconceptlab.org']; 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./map. mirror this app's +// own env (e.g. app.qa. -> app.v3.qa., app.qa. -> map.qa.). +const linkedDomains = () => { + const host = window.location.host; + + return [ + 'openconceptlab.org', + 'preview.openconceptlab.org', + host.replace('app.', 'app.v3.'), + host.replace('app.', 'map.'), + ]; +}; + const initialize = options => { /*eslint no-undef: 0*/ ReactGA.initialize(gaId(), options); @@ -16,7 +29,7 @@ const initialize = options => { const GAService = { recordPageView() { // eslint-disable-next-line spellcheck/spell-checker - initialize({ gtagOptions: { linker: { domains: LINKED_DOMAINS } } }); + initialize({ gtagOptions: { linker: { domains: linkedDomains() } } }); ReactGA.send({ hitType: 'pageview', page: window.location.pathname + window.location.hash.split('?')[0] }); }, From 69551e23ee0ce624a02bc83c4106e1ac49e75b73 Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Tue, 22 Sep 2026 13:58:41 +0530 Subject: [PATCH 3/3] OpenConceptLab/ocl_online#137 | GA | review feedbacks --- src/services/GAService.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/services/GAService.js b/src/services/GAService.js index 0935fad2..571a758e 100644 --- a/src/services/GAService.js +++ b/src/services/GAService.js @@ -6,30 +6,27 @@ 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; +const enabled = () => Boolean(gaId() && gaId() !== 'UA-000000-01'); -// The marketing site only exists in prod, at these two domains, regardless -// of which env this app itself is running in. v3./map. mirror this app's -// own env (e.g. app.qa. -> app.v3.qa., app.qa. -> map.qa.). -const linkedDomains = () => { - const host = window.location.host; - - return [ - 'openconceptlab.org', - 'preview.openconceptlab.org', - host.replace('app.', 'app.v3.'), - host.replace('app.', 'map.'), - ]; -}; - -const initialize = options => { +// react-ga4 emits config only on the first initialize() call, so the +// options must be complete there. recordPageView sends page views explicitly. +let initialized = false; +const initialize = () => { /*eslint no-undef: 0*/ - ReactGA.initialize(gaId(), options); + if(initialized || !enabled()) + return; + + // eslint-disable-next-line spellcheck/spell-checker + ReactGA.initialize(gaId(), { gtagOptions: { send_page_view: false } }); + initialized = true; }; const GAService = { recordPageView() { - // eslint-disable-next-line spellcheck/spell-checker - initialize({ gtagOptions: { linker: { domains: linkedDomains() } } }); + initialize(); + if(!enabled()) + return; + ReactGA.send({ hitType: 'pageview', page: window.location.pathname + window.location.hash.split('?')[0] }); }, @@ -59,6 +56,9 @@ const GAService = { recordEvent(name, params) { initialize(); + if(!enabled()) + return; + ReactGA.event(name, { client: OCL_CLIENT, ...params }); }, };