CE-1123 - Update google analytics to work with events as well as page views; add calls to it to most actual pages.

This commit is contained in:
2024-04-16 16:33:27 -05:00
parent e3cbf9414b
commit da57226fe5
10 changed files with 81 additions and 40 deletions

View File

@ -49,7 +49,7 @@ import EntityEdit from "qqq/pages/records/edit/RecordEdit";
import RecordQuery from "qqq/pages/records/query/RecordQuery";
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
import RecordView from "qqq/pages/records/view/RecordView";
import GoogleAnalyticsUtils from "qqq/utils/GoogleAnalyticsUtils";
import GoogleAnalyticsUtils, {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import Client from "qqq/utils/qqq/Client";
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
@ -656,7 +656,7 @@ export default function App()
},
);
const [pageHeader, setPageHeaderState] = useState("" as string | JSX.Element);
const [pageHeader, setPageHeader] = useState("" as string | JSX.Element);
const [accentColor, setAccentColor] = useState("#0062FF");
const [accentColorLight, setAccentColorLight] = useState("#C0D6F7")
const [tableMetaData, setTableMetaData] = useState(null);
@ -671,26 +671,9 @@ export default function App()
/*******************************************************************************
**
*******************************************************************************/
function setPageHeader(header: string | JSX.Element)
function recordAnalytics(model: AnalyticsModel)
{
setPageHeaderState(header);
if(typeof header == "string")
{
recordAnalytics(header)
}
else
{
recordAnalytics("Title not available")
}
}
/*******************************************************************************
**
*******************************************************************************/
function recordAnalytics(title: string)
{
googleAnalyticsUtils.recordAnalytics(location, title)
googleAnalyticsUtils.recordAnalytics(model)
}

View File

@ -22,6 +22,7 @@
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import {createContext} from "react";
interface QContext
@ -50,7 +51,7 @@ interface QContext
///////////////////////////////////////////
// function to record an analytics event //
///////////////////////////////////////////
recordAnalytics?: (title: string) => void;
recordAnalytics?: (model: AnalyticsModel) => void;
///////////////////////////////////
// constants - no setters needed //

View File

@ -82,7 +82,7 @@ function EntityForm(props: Props): JSX.Element
const qController = Client.getInstance();
const tableNameParam = useParams().tableName;
const tableName = props.table === null ? tableNameParam : props.table.name;
const {accentColor} = useContext(QContext);
const {accentColor, recordAnalytics} = useContext(QContext);
const [formTitle, setFormTitle] = useState("");
const [validations, setValidations] = useState({});
@ -359,6 +359,7 @@ function EntityForm(props: Props): JSX.Element
{
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
recordAnalytics({location: window.location, title: (props.isCopy ? "Copy" : props.id ? "Edit" : "New") + ": " + tableMetaData.label});
const metaData = await qController.loadMetaData();
setMetaData(metaData);
@ -389,6 +390,7 @@ function EntityForm(props: Props): JSX.Element
{
record = await qController.get(tableName, props.id);
setRecord(record);
recordAnalytics({category: "tableEvents", action: props.isCopy ? "copy" : "edit", label: tableMetaData?.label + " / " + record?.recordLabel});
const titleVerb = props.isCopy ? "Copy" : "Edit";
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
@ -428,6 +430,7 @@ function EntityForm(props: Props): JSX.Element
// else handle preparing to do an insert //
///////////////////////////////////////////
setFormTitle(`Creating New ${tableMetaData?.label}`);
recordAnalytics({category: "tableEvents", action: "new", label: tableMetaData?.label});
if (!props.isModal)
{
@ -757,6 +760,8 @@ function EntityForm(props: Props): JSX.Element
if (props.id !== null && !props.isCopy)
{
recordAnalytics({category: "tableEvents", action: "saveEdit", label: tableMetaData?.label});
///////////////////////
// perform an update //
///////////////////////
@ -799,6 +804,8 @@ function EntityForm(props: Props): JSX.Element
}
else
{
recordAnalytics({category: "tableEvents", action: props.isCopy ? "saveCopy" : "saveNew", label: tableMetaData?.label});
/////////////////////////////////
// perform an insert //
// todo - audit if it's a dupe //

View File

@ -23,8 +23,9 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import MenuItem from "@mui/material/MenuItem";
import {GridColDef, GridExportMenuItemProps} from "@mui/x-data-grid-pro";
import React from "react";
import QContext from "QContext";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
import React, {useContext} from "react";
interface QExportMenuItemProps extends GridExportMenuItemProps<{}>
{
@ -43,6 +44,10 @@ export default function ExportMenuItem(props: QExportMenuItemProps)
{
const {format, tableMetaData, totalRecords, columnsModel, columnVisibilityModel, queryFilter, hideMenu} = props;
const {recordAnalytics} = useContext(QContext);
recordAnalytics({category: "tableEvents", action: "export", label: tableMetaData.label});
return (
<MenuItem
disabled={totalRecords === 0}

View File

@ -31,8 +31,6 @@ import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid";
import React, {useContext, useEffect, useState} from "react";
import {Link, useLocation} from "react-router-dom";
import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors";
import MDTypography from "qqq/components/legacy/MDTypography";
@ -41,6 +39,8 @@ import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import MiniStatisticsCard from "qqq/components/widgets/statistics/MiniStatisticsCard";
import BaseLayout from "qqq/layouts/BaseLayout";
import Client from "qqq/utils/qqq/Client";
import React, {useContext, useEffect, useState} from "react";
import {Link, useLocation} from "react-router-dom";
const qController = Client.getInstance();
@ -62,7 +62,7 @@ function AppHome({app}: Props): JSX.Element
const [updatedTableCounts, setUpdatedTableCounts] = useState(new Date());
const [widgets, setWidgets] = useState([] as any[]);
const {pageHeader, setPageHeader} = useContext(QContext);
const {pageHeader, recordAnalytics, setPageHeader} = useContext(QContext);
const location = useLocation();
@ -86,8 +86,9 @@ function AppHome({app}: Props): JSX.Element
useEffect(() =>
{
// setPageHeader(app.label);
setPageHeader(null);
recordAnalytics({location: window.location, title: "App: " + app.label});
recordAnalytics({category: "appEvents", action: "loadAppScreen", label: app.label});
if (!qInstance)
{

View File

@ -124,7 +124,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [showErrorDetail, setShowErrorDetail] = useState(false);
const [showFullHelpText, setShowFullHelpText] = useState(false);
const {pageHeader, setPageHeader} = useContext(QContext);
const {pageHeader, recordAnalytics, setPageHeader} = useContext(QContext);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for setting the processError state - call this function, which will also set the isUserFacingError state //
@ -1146,6 +1146,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const processMetaData = await Client.getInstance().loadProcessMetaData(processName);
setProcessMetaData(processMetaData);
setSteps(processMetaData.frontendSteps);
recordAnalytics({location: window.location, title: "Process: " + processMetaData?.label});
recordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label});
if (processMetaData.tableName && !tableMetaData)
{
try
@ -1251,6 +1255,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setTimeout(async () =>
{
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await Client.getInstance().processStep(
processName,
processUUID,

View File

@ -338,7 +338,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
/////////////////////////////
// page context references //
/////////////////////////////
const {accentColor, accentColorLight, setPageHeader, dotMenuOpen, keyboardHelpOpen} = useContext(QContext);
const {accentColor, accentColorLight, setPageHeader, recordAnalytics, dotMenuOpen, keyboardHelpOpen} = useContext(QContext);
//////////////////////////////////////////////////////////////////
// we use our own header - so clear out the context page header //
@ -875,6 +875,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
return;
}
recordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label});
console.log(`In updateTable for ${reason} ${JSON.stringify(queryFilter)}`);
setLoading(true);
setRows([]);
@ -1642,6 +1644,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
if (selectedSavedViewId != null)
{
recordAnalytics({category: "tableEvents", action: "activateSavedView", label: tableMetaData.label});
//////////////////////////////////////////////
// fetch, then activate the selected filter //
//////////////////////////////////////////////
@ -1657,6 +1661,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
/////////////////////////////////
// this is 'new view' - right? //
/////////////////////////////////
recordAnalytics({category: "tableEvents", action: "activateNewView", label: tableMetaData.label});
//////////////////////////////
// wipe away the saved view //
@ -2327,6 +2332,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setTableMetaData(tableMetaData);
setTableLabel(tableMetaData.label);
recordAnalytics({location: window.location, title: "Query: " + tableMetaData.label});
setTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)

View File

@ -69,13 +69,9 @@ function RecordDeveloperView({table}: Props): JSX.Element
const [associatedScripts, setAssociatedScripts] = useState([] as any[]);
const [notFoundMessage, setNotFoundMessage] = useState(null);
const [selectedTabs, setSelectedTabs] = useState({} as any);
const [viewingRevisions, setViewingRevisions] = useState({} as any);
const [scriptLogs, setScriptLogs] = useState({} as any);
const [alertText, setAlertText] = useState(null as string);
const {setPageHeader} = useContext(QContext);
const {setPageHeader, recordAnalytics} = useContext(QContext);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
if (!asyncLoadInited)
@ -90,6 +86,8 @@ function RecordDeveloperView({table}: Props): JSX.Element
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
recordAnalytics({location: window.location, title: "Developer Mode: " + tableMetaData.label});
//////////////////////////////
// load top-level meta-data //
//////////////////////////////

View File

@ -121,7 +121,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null);
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive} = useContext(QContext);
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics} = useContext(QContext);
if (localStorage.getItem(tableVariantLocalStorageKey))
{
@ -384,6 +384,8 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
recordAnalytics({location: window.location, title: "View: " + tableMetaData.label});
//////////////////////////////////////////////////////////////////
// load top-level meta-data (e.g., to find processes for table) //
//////////////////////////////////////////////////////////////////
@ -430,6 +432,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
{
record = await qController.get(tableName, id, tableVariant, null, queryJoins);
setRecord(record);
recordAnalytics({category: "tableEvents", action: "view", label: tableMetaData?.label + " / " + record?.recordLabel});
}
catch (e)
{
@ -631,6 +634,8 @@ function RecordView({table, launchProcess}: Props): JSX.Element
event?.preventDefault();
(async () =>
{
recordAnalytics({category: "tableEvents", action: "delete", label: tableMetaData?.label + " / " + record?.recordLabel});
await qController.delete(tableName, id)
.then(() =>
{

View File

@ -24,6 +24,21 @@ import Client from "qqq/utils/qqq/Client";
import ReactGA from "react-ga4";
export interface PageView
{
location: Location;
title: string;
}
export interface UserEvent
{
action: string;
category: string;
label?: string;
}
export type AnalyticsModel = PageView | UserEvent;
const qController = Client.getInstance();
/*******************************************************************************
@ -46,14 +61,27 @@ export default class GoogleAnalyticsUtils
/*******************************************************************************
**
*******************************************************************************/
private send = (location: Location, title: string) =>
private send = (model: AnalyticsModel) =>
{
if(!this.active)
{
return;
}
ReactGA.send({hitType: "pageview", page: location.pathname + location.search, title: title});
if(model.hasOwnProperty("location"))
{
const pageView = model as PageView;
ReactGA.send({hitType: "pageview", page: pageView.location.pathname + pageView.location.search, title: pageView.title});
}
else if(model.hasOwnProperty("action") || model.hasOwnProperty("category") || model.hasOwnProperty("label"))
{
const userEvent = model as UserEvent;
ReactGA.event({action: userEvent.action, category: userEvent.category, label: userEvent.label})
}
else
{
console.log("Unrecognizable analytics model", model);
}
}
@ -99,7 +127,7 @@ export default class GoogleAnalyticsUtils
/*******************************************************************************
**
*******************************************************************************/
public recordAnalytics = (location: Location, title: string) =>
public recordAnalytics = (model: AnalyticsModel) =>
{
if(this.metaData == null)
{
@ -109,7 +137,7 @@ export default class GoogleAnalyticsUtils
})()
}
this.send(location, title);
this.send(model);
}
}