From ff5afeffdf0aa1cd77e410b06f6edddf4f979c2e Mon Sep 17 00:00:00 2001 From: "Heiko W. Rupp" Date: Mon, 25 Mar 2024 19:51:37 +0100 Subject: [PATCH] [RHOAIENG-4226] Add additional tracking. --- docs/dev-setup.md | 2 +- frontend/src/components/ExternalLink.tsx | 6 +++- .../projects/notebook/NotebookRouteLink.tsx | 4 +++ .../notebook/NotebookStatusToggle.tsx | 2 +- .../screens/projects/DeleteProjectModal.tsx | 16 +++++++++ .../screens/projects/NewProjectButton.tsx | 7 ++++ .../screens/spawner/SpawnerFooter.tsx | 33 ++++++++++++++----- frontend/src/types.ts | 8 +++++ frontend/src/utilities/segmentIOUtils.tsx | 16 +++++++++ 9 files changed, 83 insertions(+), 11 deletions(-) diff --git a/docs/dev-setup.md b/docs/dev-setup.md index 37c690f112..eea4d051f8 100644 --- a/docs/dev-setup.md +++ b/docs/dev-setup.md @@ -46,7 +46,7 @@ npm run start > If you'd like to run "backend" and "frontend" separately for development, cd into each directory in two different terminals and run `npm run start:dev` from each. -For in-depth local run guidance review the [contribution guidelines](../CONTRIBUTING.md#Serving%20Content). +For in-depth local run guidance review the [contribution guidelines](../CONTRIBUTING.md). ### Testing diff --git a/frontend/src/components/ExternalLink.tsx b/frontend/src/components/ExternalLink.tsx index 97ab662356..a76cc72b4a 100644 --- a/frontend/src/components/ExternalLink.tsx +++ b/frontend/src/components/ExternalLink.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Button } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; +import { fireTrackingEventRaw } from '~/utilities/segmentIOUtils'; type ExternalLinkProps = { text: string; @@ -11,7 +12,10 @@ const ExternalLink: React.FC = ({ text, to }) => ( diff --git a/frontend/src/pages/projects/notebook/NotebookStatusToggle.tsx b/frontend/src/pages/projects/notebook/NotebookStatusToggle.tsx index 5066a0dbe8..60bfad167b 100644 --- a/frontend/src/pages/projects/notebook/NotebookStatusToggle.tsx +++ b/frontend/src/pages/projects/notebook/NotebookStatusToggle.tsx @@ -52,7 +52,7 @@ const NotebookStatusToggle: React.FC = ({ const fireNotebookTrackingEvent = React.useCallback( (action: 'started' | 'stopped') => { - fireTrackingEvent(`Workbench ${action}`, { + fireTrackingEvent(`Workbench ${action === 'started' ? 'Started' : 'Stopped'}`, { acceleratorCount: acceleratorProfile.useExisting ? undefined : acceleratorProfile.count, accelerator: acceleratorProfile.acceleratorProfile ? `${acceleratorProfile.acceleratorProfile.spec.displayName} (${acceleratorProfile.acceleratorProfile.metadata.name}): ${acceleratorProfile.acceleratorProfile.spec.identifier}` diff --git a/frontend/src/pages/projects/screens/projects/DeleteProjectModal.tsx b/frontend/src/pages/projects/screens/projects/DeleteProjectModal.tsx index e30a4f1a90..22d09032e0 100644 --- a/frontend/src/pages/projects/screens/projects/DeleteProjectModal.tsx +++ b/frontend/src/pages/projects/screens/projects/DeleteProjectModal.tsx @@ -3,17 +3,28 @@ import { ProjectKind } from '~/k8sTypes'; import { getProjectDisplayName } from '~/pages/projects/utils'; import { deleteProject } from '~/api'; import DeleteModal from '~/pages/projects/components/DeleteModal'; +import { fireTrackingEvent } from '~/utilities/segmentIOUtils'; +import { TrackingOutcome } from '~/types'; type DeleteProjectModalProps = { onClose: (deleted: boolean) => void; deleteData?: ProjectKind; }; +const deleteProjectEventType = 'Project Deleted'; const DeleteProjectModal: React.FC = ({ deleteData, onClose }) => { const [deleting, setDeleting] = React.useState(false); const [error, setError] = React.useState(); const onBeforeClose = (deleted: boolean) => { + if (!deleted) { + fireTrackingEvent(deleteProjectEventType, { outcome: TrackingOutcome.cancel }); + } else { + fireTrackingEvent(deleteProjectEventType, { + outcome: TrackingOutcome.submit, + success: true, + }); + } onClose(deleted); setDeleting(false); setError(undefined); @@ -34,6 +45,11 @@ const DeleteProjectModal: React.FC = ({ deleteData, onC deleteProject(deleteData.metadata.name) .then(() => onBeforeClose(true)) .catch((e) => { + fireTrackingEvent(deleteProjectEventType, { + outcome: TrackingOutcome.submit, + success: false, + error: e, + }); setError(e); setDeleting(false); }); diff --git a/frontend/src/pages/projects/screens/projects/NewProjectButton.tsx b/frontend/src/pages/projects/screens/projects/NewProjectButton.tsx index 72f5d5a79e..0222bd7d0b 100644 --- a/frontend/src/pages/projects/screens/projects/NewProjectButton.tsx +++ b/frontend/src/pages/projects/screens/projects/NewProjectButton.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; import { Button } from '@patternfly/react-core'; +import { fireTrackingEvent } from '~/utilities/segmentIOUtils'; +import { TrackingOutcome } from '~/types'; import ManageProjectModal from './ManageProjectModal'; type NewProjectButtonProps = { @@ -22,6 +24,11 @@ const NewProjectButton: React.FC = ({ closeOnCreate, onPr { + fireTrackingEvent('NewProject Created', { + outcome: newProjectName ? TrackingOutcome.submit : TrackingOutcome.cancel, + success: onProjectCreated != null, + projectName: newProjectName || '', + }); if (newProjectName) { if (onProjectCreated) { onProjectCreated(newProjectName); diff --git a/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx b/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx index 81a238f863..bafe7853bb 100644 --- a/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx +++ b/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx @@ -10,20 +10,21 @@ import { } from '@patternfly/react-core'; import { assembleSecret, createNotebook, createSecret, updateNotebook } from '~/api'; import { + DataConnectionData, + EnvVariable, StartNotebookData, StorageData, - EnvVariable, - DataConnectionData, } from '~/pages/projects/types'; import { useUser } from '~/redux/selectors'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; import { AppContext } from '~/app/AppContext'; -import { fireTrackingEvent } from '~/utilities/segmentIOUtils'; import usePreferredStorageClass from '~/pages/projects/screens/spawner/storage/usePreferredStorageClass'; import { ProjectSectionID } from '~/pages/projects/screens/detail/types'; +import { fireTrackingEvent, fireTrackingEventRaw } from '~/utilities/segmentIOUtils'; +import { TrackingOutcome } from '~/types'; import { - createPvcDataForNotebook, createConfigMapsAndSecretsForNotebook, + createPvcDataForNotebook, replaceRootVolumesForNotebook, updateConfigMapsAndSecretsForNotebook, } from './service'; @@ -83,7 +84,7 @@ const SpawnerFooter: React.FC = ({ const afterStart = (name: string, type: 'created' | 'updated') => { const { acceleratorProfile, notebookSize, image } = startNotebookData; - fireTrackingEvent(`Workbench ${type}`, { + fireTrackingEventRaw(`Workbench ${type === 'created' ? 'Created' : 'Updated'}`, { acceleratorCount: acceleratorProfile.useExisting ? undefined : acceleratorProfile.count, accelerator: acceleratorProfile.acceleratorProfile ? `${acceleratorProfile.acceleratorProfile.spec.displayName} (${acceleratorProfile.acceleratorProfile.metadata.name}): ${acceleratorProfile.acceleratorProfile.spec.identifier}` @@ -96,13 +97,26 @@ const SpawnerFooter: React.FC = ({ : `${image.imageStream?.metadata.name || 'unknown image'} - ${ image.imageVersion?.name || 'unknown version' }`, + imageName: image.imageStream?.metadata.name, projectName, notebookName: name, + storageType: storageData.storageType, + storageDataSize: storageData.creating.size, + dataConnectionType: dataConnection.creating?.type, + dataConnectionCategory: dataConnection.creating?.values?.category, + dataConnectionEnabled: dataConnection.enabled, + outcome: TrackingOutcome.submit, + success: true, }); refreshAllProjectData(); navigate(`/projects/${projectName}?section=${ProjectSectionID.WORKBENCHES}`); }; const handleError = (e: Error) => { + fireTrackingEvent('Workbench Created', { + outcome: TrackingOutcome.submit, + success: false, + error: e.message, + }); setErrorMessage(e.message || 'Error creating workbench'); setCreateInProgress(false); }; @@ -267,9 +281,12 @@ const SpawnerFooter: React.FC = ({ diff --git a/frontend/src/types.ts b/frontend/src/types.ts index f289f7c1ab..2d022a1bb2 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -266,6 +266,11 @@ export type Section = { actions: ApplicationAction[]; }; +export enum TrackingOutcome { + submit = 'submit', + cancel = 'cancel', +} + export type TrackingEventProperties = { name?: string; anonymousID?: string; @@ -278,6 +283,9 @@ export type TrackingEventProperties = { projectName?: string; notebookName?: string; lastActivity?: string; + outcome?: TrackingOutcome; + success?: boolean; + error?: string; }; export type NotebookPort = { diff --git a/frontend/src/utilities/segmentIOUtils.tsx b/frontend/src/utilities/segmentIOUtils.tsx index b353e57214..2ba4307ed4 100644 --- a/frontend/src/utilities/segmentIOUtils.tsx +++ b/frontend/src/utilities/segmentIOUtils.tsx @@ -1,6 +1,22 @@ import { TrackingEventProperties } from '~/types'; import { DEV_MODE } from './const'; +// The following is like the original method below, but allows for more 'free form' properties. +// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types +export const fireTrackingEventRaw = (eventType: string, properties?: any): void => { + const clusterID = window.clusterID ?? ''; + if (DEV_MODE) { + /* eslint-disable-next-line no-console */ + console.log( + `Telemetry event triggered: ${eventType}${ + properties ? ` - ${JSON.stringify(properties)}` : '' + }`, + ); + } else if (window.analytics) { + window.analytics.track(eventType, { ...properties, clusterID }); + } +}; + export const fireTrackingEvent = ( eventType: string, properties?: TrackingEventProperties,