From 731eab7136b78c10e8e048410b3dd75d81d0b4ba Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 15 Apr 2024 12:52:59 -0500 Subject: [PATCH 1/5] CE-1123 update exception status to be number (for qfc change) --- src/qqq/components/audits/AuditBody.tsx | 4 ++-- src/qqq/components/widgets/misc/DataBagViewer.tsx | 2 +- src/qqq/components/widgets/misc/ScriptViewer.tsx | 2 +- src/qqq/pages/processes/ProcessRun.tsx | 8 ++++---- src/qqq/pages/records/view/RecordDeveloperView.tsx | 8 ++++---- src/qqq/pages/records/view/RecordView.tsx | 4 ++-- src/qqq/utils/qqq/Client.ts | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/qqq/components/audits/AuditBody.tsx b/src/qqq/components/audits/AuditBody.tsx index 3d4b4a2..f87b6a6 100644 --- a/src/qqq/components/audits/AuditBody.tsx +++ b/src/qqq/components/audits/AuditBody.tsx @@ -34,10 +34,10 @@ import ToggleButton from "@mui/material/ToggleButton"; import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; -import React, {useContext, useEffect, useState} from "react"; import QContext from "QContext"; import Client from "qqq/utils/qqq/Client"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; +import React, {useContext, useEffect, useState} from "react"; interface Props { @@ -217,7 +217,7 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element { if (e instanceof QException) { - if ((e as QException).status === "403") + if ((e as QException).status === 403) { setStatusString("You do not have permission to view audits"); return; diff --git a/src/qqq/components/widgets/misc/DataBagViewer.tsx b/src/qqq/components/widgets/misc/DataBagViewer.tsx index b1d1e48..edb384a 100644 --- a/src/qqq/components/widgets/misc/DataBagViewer.tsx +++ b/src/qqq/components/widgets/misc/DataBagViewer.tsx @@ -119,7 +119,7 @@ export default function DataBagViewer({dataBagId}: Props): JSX.Element { if (e instanceof QException) { - if ((e as QException).status === "404") + if ((e as QException).status === 404) { setNotFoundMessage("Data bag data could not be found."); return; diff --git a/src/qqq/components/widgets/misc/ScriptViewer.tsx b/src/qqq/components/widgets/misc/ScriptViewer.tsx index 0c51e6e..e754d7c 100644 --- a/src/qqq/components/widgets/misc/ScriptViewer.tsx +++ b/src/qqq/components/widgets/misc/ScriptViewer.tsx @@ -169,7 +169,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc { if (e instanceof QException) { - if ((e as QException).status === "404") + if ((e as QException).status === 404) { setNotFoundMessage("Script code could not be found."); return; diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx index 254396f..623d491 100644 --- a/src/qqq/pages/processes/ProcessRun.tsx +++ b/src/qqq/pages/processes/ProcessRun.tsx @@ -47,9 +47,6 @@ import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro"; import FormData from "form-data"; import {Form, Formik} from "formik"; import parse from "html-react-parser"; -import React, {useContext, useEffect, useState} from "react"; -import {useLocation, useNavigate, useParams} from "react-router-dom"; -import * as Yup from "yup"; import QContext from "QContext"; import {QCancelButton, QSubmitButton} from "qqq/components/buttons/DefaultButtons"; import QDynamicForm from "qqq/components/forms/DynamicForm"; @@ -66,6 +63,9 @@ import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/Reco import Client from "qqq/utils/qqq/Client"; import TableUtils from "qqq/utils/qqq/TableUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; +import React, {useContext, useEffect, useState} from "react"; +import {useLocation, useNavigate, useParams} from "react-router-dom"; +import * as Yup from "yup"; interface Props @@ -1068,7 +1068,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is const handlePermissionDenied = (e: any): boolean => { - if ((e as QException).status === "403") + if ((e as QException).status === 403) { setProcessError(`You do not have permission to run this ${isReport ? "report" : "process"}.`, true); return (true); diff --git a/src/qqq/pages/records/view/RecordDeveloperView.tsx b/src/qqq/pages/records/view/RecordDeveloperView.tsx index cec5bf7..470d237 100644 --- a/src/qqq/pages/records/view/RecordDeveloperView.tsx +++ b/src/qqq/pages/records/view/RecordDeveloperView.tsx @@ -28,9 +28,6 @@ import Button from "@mui/material/Button"; import Card from "@mui/material/Card"; import Grid from "@mui/material/Grid"; import Snackbar from "@mui/material/Snackbar"; -import React, {useContext, useReducer, useState} from "react"; -import AceEditor from "react-ace"; -import {useParams} from "react-router-dom"; import QContext from "QContext"; import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer"; import BaseLayout from "qqq/layouts/BaseLayout"; @@ -41,6 +38,9 @@ import "ace-builds/src-noconflict/mode-java"; import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/mode-json"; import "ace-builds/src-noconflict/theme-github"; +import React, {useContext, useReducer, useState} from "react"; +import AceEditor from "react-ace"; +import {useParams} from "react-router-dom"; import "ace-builds/src-noconflict/ext-language_tools"; const qController = Client.getInstance(); @@ -121,7 +121,7 @@ function RecordDeveloperView({table}: Props): JSX.Element { if (e instanceof QException) { - if ((e as QException).status === "404") + if ((e as QException).status === 404) { setNotFoundMessage(`${tableMetaData.label} ${id} could not be found.`); return; diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx index 7b71b81..3127965 100644 --- a/src/qqq/pages/records/view/RecordView.tsx +++ b/src/qqq/pages/records/view/RecordView.tsx @@ -447,13 +447,13 @@ function RecordView({table, launchProcess}: Props): JSX.Element if (e instanceof QException) { - if ((e as QException).status === "404") + if ((e as QException).status === 404) { setNotFoundMessage(`${tableMetaData.label} ${id} could not be found.`); historyPurge(location.pathname); return; } - else if ((e as QException).status === "403") + else if ((e as QException).status === 403) { setNotFoundMessage(`You do not have permission to view ${tableMetaData.label} records`); historyPurge(location.pathname); diff --git a/src/qqq/utils/qqq/Client.ts b/src/qqq/utils/qqq/Client.ts index d75aa08..0c008fc 100644 --- a/src/qqq/utils/qqq/Client.ts +++ b/src/qqq/utils/qqq/Client.ts @@ -35,7 +35,7 @@ class Client { console.log(`Caught Exception: ${JSON.stringify(exception)}`); - if(exception && exception.status == "401" && Client.unauthorizedCallback) + if(exception && exception.status == 401 && Client.unauthorizedCallback) { console.log("This is a 401 - calling the unauthorized callback."); Client.unauthorizedCallback(); From 68d3119c6a17badd84843b114ab2b2abf37cabd5 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 15 Apr 2024 12:53:43 -0500 Subject: [PATCH 2/5] CE-1123 store values in localStorage from backend from manageSession call; add googleAnalytics utils & function to recordAnalytics --- src/App.tsx | 48 ++++++++++++++++++++++++++++++++++++++++-------- src/QContext.tsx | 5 +++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 269bb62..1980eb3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,12 +33,8 @@ import CssBaseline from "@mui/material/CssBaseline"; import Icon from "@mui/material/Icon"; import {ThemeProvider} from "@mui/material/styles"; import {LicenseInfo} from "@mui/x-license-pro"; -import jwt_decode from "jwt-decode"; -import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react"; -import {useCookies} from "react-cookie"; -import {Navigate, Route, Routes, useLocation, useSearchParams,} from "react-router-dom"; -import {Md5} from "ts-md5/dist/md5"; import CommandMenu from "CommandMenu"; +import jwt_decode from "jwt-decode"; import QContext from "QContext"; import Sidenav from "qqq/components/horseshoe/sidenav/SideNav"; import theme from "qqq/components/legacy/Theme"; @@ -53,8 +49,13 @@ 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 Client from "qqq/utils/qqq/Client"; import ProcessUtils from "qqq/utils/qqq/ProcessUtils"; +import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react"; +import {useCookies} from "react-cookie"; +import {Navigate, Route, Routes, useLocation, useSearchParams,} from "react-router-dom"; +import {Md5} from "ts-md5/dist/md5"; const qController = Client.getInstance(); @@ -160,7 +161,7 @@ export default function App() if (shouldStoreNewToken(accessToken, lsAccessToken)) { console.log("Sending accessToken to backend, requesting a sessionUUID..."); - const newSessionUuid = await qController.manageSession(accessToken, null); + const {uuid: newSessionUuid, values} = await qController.manageSession(accessToken, null); ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. // @@ -168,6 +169,7 @@ export default function App() // setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"}); localStorage.setItem("accessToken", accessToken); + localStorage.setItem("sessionValues", JSON.stringify(values)); console.log("Got new sessionUUID from backend, and stored new accessToken"); } else @@ -575,7 +577,7 @@ export default function App() console.error(e); if (e instanceof QException) { - if ((e as QException).status === "401") + if ((e as QException).status === 401) { console.log("Exception is a QException with status = 401. Clearing some of localStorage & cookies"); qController.clearAuthenticationMetaDataLocalStorage(); @@ -654,7 +656,7 @@ export default function App() }, ); - const [pageHeader, setPageHeader] = useState("" as string | JSX.Element); + const [pageHeader, setPageHeaderState] = useState("" as string | JSX.Element); const [accentColor, setAccentColor] = useState("#0062FF"); const [accentColorLight, setAccentColorLight] = useState("#C0D6F7") const [tableMetaData, setTableMetaData] = useState(null); @@ -663,6 +665,35 @@ export default function App() const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false); const [helpHelpActive] = useState(queryParams.has("helpHelp")); + const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils()); + + + /******************************************************************************* + ** + *******************************************************************************/ + function setPageHeader(header: string | JSX.Element) + { + setPageHeaderState(header); + if(typeof header == "string") + { + recordAnalytics(header) + } + else + { + recordAnalytics("Title not available") + } + } + + + /******************************************************************************* + ** + *******************************************************************************/ + function recordAnalytics(title: string) + { + googleAnalyticsUtils.recordAnalytics(location, title) + } + + return ( appRoutes && ( @@ -682,6 +713,7 @@ export default function App() setTableProcesses: (tableProcesses: QProcessMetaData[]) => setTableProcesses(tableProcesses), setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent), setKeyboardHelpOpen: (keyboardHelpOpen: boolean) => setKeyboardHelpOpen(keyboardHelpOpen), + recordAnalytics: recordAnalytics, pathToLabelMap: pathToLabelMap, branding: branding }}> diff --git a/src/QContext.tsx b/src/QContext.tsx index 797c970..3352816 100644 --- a/src/QContext.tsx +++ b/src/QContext.tsx @@ -47,6 +47,11 @@ interface QContext tableProcesses?: QProcessMetaData[]; setTableProcesses?: (tableProcesses: QProcessMetaData[]) => void; + /////////////////////////////////////////// + // function to record an analytics event // + /////////////////////////////////////////// + recordAnalytics?: (title: string) => void; + /////////////////////////////////// // constants - no setters needed // /////////////////////////////////// From 6282723ff659a6d88e5a69f06814b79c7b832eb2 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 15 Apr 2024 12:53:58 -0500 Subject: [PATCH 3/5] CE-1123 Update qfc to 1.0.96; add react-ga4 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 71f4964..f7dacc1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.90", + "@kingsrook/qqq-frontend-core": "1.0.96", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", @@ -40,6 +40,7 @@ "react-chartjs-2": "3.0.4", "react-cookie": "4.1.1", "react-dom": "18.0.0", + "react-ga4": "2.1.0", "react-github-btn": "1.2.1", "react-google-drive-picker": "^1.2.0", "react-markdown": "9.0.1", From e3cbf9414b1892ea8ca11795ab25613b9fc30a74 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 15 Apr 2024 14:51:03 -0500 Subject: [PATCH 4/5] CE-1123 Initial checkin --- src/qqq/utils/GoogleAnalyticsUtils.ts | 115 ++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/qqq/utils/GoogleAnalyticsUtils.ts diff --git a/src/qqq/utils/GoogleAnalyticsUtils.ts b/src/qqq/utils/GoogleAnalyticsUtils.ts new file mode 100644 index 0000000..3f5a894 --- /dev/null +++ b/src/qqq/utils/GoogleAnalyticsUtils.ts @@ -0,0 +1,115 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; +import Client from "qqq/utils/qqq/Client"; +import ReactGA from "react-ga4"; + + +const qController = Client.getInstance(); + +/******************************************************************************* + ** Utilities for working with Google Analytics (through react-ga4)^ + *******************************************************************************/ +export default class GoogleAnalyticsUtils +{ + private metaData: QInstance = null; + private active: boolean = false; + + + /******************************************************************************* + ** + *******************************************************************************/ + constructor() + { + } + + + /******************************************************************************* + ** + *******************************************************************************/ + private send = (location: Location, title: string) => + { + if(!this.active) + { + return; + } + + ReactGA.send({hitType: "pageview", page: location.pathname + location.search, title: title}); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + private setup = async (): Promise => + { + this.metaData = await qController.loadMetaData(); + + let sessionValues: {[key: string]: any} = null; + try + { + sessionValues = JSON.parse(localStorage.getItem("sessionValues")); + } + catch(e) + { + console.log("Error reading session values from localStorage: " + e); + } + + if (this.metaData.environmentValues.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues.get("GOOGLE_ANALYTICS_TRACKING_ID")) + { + this.active = true; + + if(sessionValues && sessionValues["googleAnalyticsValues"]) + { + ReactGA.gtag("set", "user_properties", sessionValues["googleAnalyticsValues"]); + } + + ReactGA.initialize(this.metaData.environmentValues.get("GOOGLE_ANALYTICS_TRACKING_ID"), + { + gaOptions: {}, + gtagOptions: {} + }); + } + else + { + this.active = false; + } + } + + + /******************************************************************************* + ** + *******************************************************************************/ + public recordAnalytics = (location: Location, title: string) => + { + if(this.metaData == null) + { + (async () => + { + await this.setup(); + })() + } + + this.send(location, title); + } + +} \ No newline at end of file From da57226fe5ad048eb5a18bd18956a9049622d7a8 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 16 Apr 2024 16:33:27 -0500 Subject: [PATCH 5/5] CE-1123 - Update google analytics to work with events as well as page views; add calls to it to most actual pages. --- src/App.tsx | 25 +++---------- src/QContext.tsx | 3 +- src/qqq/components/forms/EntityForm.tsx | 9 ++++- src/qqq/components/query/ExportMenuItem.tsx | 7 +++- src/qqq/pages/apps/Home.tsx | 9 ++--- src/qqq/pages/processes/ProcessRun.tsx | 8 ++++- src/qqq/pages/records/query/RecordQuery.tsx | 9 ++++- .../records/view/RecordDeveloperView.tsx | 8 ++--- src/qqq/pages/records/view/RecordView.tsx | 7 +++- src/qqq/utils/GoogleAnalyticsUtils.ts | 36 ++++++++++++++++--- 10 files changed, 81 insertions(+), 40 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1980eb3..e26f88d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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) } diff --git a/src/QContext.tsx b/src/QContext.tsx index 3352816..88db98d 100644 --- a/src/QContext.tsx +++ b/src/QContext.tsx @@ -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 // diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 2d6cbc0..af83903 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -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 // diff --git a/src/qqq/components/query/ExportMenuItem.tsx b/src/qqq/components/query/ExportMenuItem.tsx index f1a1004..19a251a 100644 --- a/src/qqq/components/query/ExportMenuItem.tsx +++ b/src/qqq/components/query/ExportMenuItem.tsx @@ -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 ( { - // setPageHeader(app.label); setPageHeader(null); + recordAnalytics({location: window.location, title: "App: " + app.label}); + recordAnalytics({category: "appEvents", action: "loadAppScreen", label: app.label}); if (!qInstance) { diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx index 623d491..a9de89b 100644 --- a/src/qqq/pages/processes/ProcessRun.tsx +++ b/src/qqq/pages/processes/ProcessRun.tsx @@ -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, diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 66c84bb..5bcedfe 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -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) diff --git a/src/qqq/pages/records/view/RecordDeveloperView.tsx b/src/qqq/pages/records/view/RecordDeveloperView.tsx index 470d237..11e13a6 100644 --- a/src/qqq/pages/records/view/RecordDeveloperView.tsx +++ b/src/qqq/pages/records/view/RecordDeveloperView.tsx @@ -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 // ////////////////////////////// diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx index 3127965..e08a890 100644 --- a/src/qqq/pages/records/view/RecordView.tsx +++ b/src/qqq/pages/records/view/RecordView.tsx @@ -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(() => { diff --git a/src/qqq/utils/GoogleAnalyticsUtils.ts b/src/qqq/utils/GoogleAnalyticsUtils.ts index 3f5a894..bbcc6e9 100644 --- a/src/qqq/utils/GoogleAnalyticsUtils.ts +++ b/src/qqq/utils/GoogleAnalyticsUtils.ts @@ -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); } } \ No newline at end of file