Skip to content

Commit

Permalink
Merge pull request #2121 from pcbailey/feature-monitor-ui-clicks
Browse files Browse the repository at this point in the history
CNV-38571: Monitor user interaction in InstanceTypes and Templates flow
  • Loading branch information
openshift-merge-bot[bot] authored Aug 19, 2024
2 parents e9ae4b8 + dfc0eb0 commit 7fbe37f
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 15 deletions.
8 changes: 8 additions & 0 deletions console-extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -810,5 +810,13 @@
}
},
"type": "console.dashboards/overview/activity/resource"
},
{
"properties": {
"listener": {
"$codeRef": "telemetry.eventMonitor"
}
},
"type": "console.telemetry/listener"
}
]
1 change: 1 addition & 0 deletions plugin-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const metadata: ConsolePluginBuildMetadata = {
pvcSelectors: 'src/views/cdi-upload-provider/utils/selectors.ts',
pvcUploadStatus: 'src/views/cdi-upload-provider/popover/UploadPVCPopover.tsx',
pvcUploadUtils: 'src/views/cdi-upload-provider/utils/utils.tsx',
telemetry: 'src/utils/extensions/telemetry/telemetry.ts',
TemplateNavPage: './views/templates/details/TemplateNavPage.tsx',
UploadPVC: 'src/views/cdi-upload-provider/upload-pvc-form/UploadPVC.tsx',
useCDIUpload: 'src/views/cdi-upload-provider/hooks/useCDIUpload.tsx',
Expand Down
138 changes: 138 additions & 0 deletions src/utils/extensions/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { V1Template } from '@kubevirt-ui/kubevirt-api/console';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { createVMFlowTypes } from '@kubevirt-utils/extensions/telemetry/utils/constants';
import { getName } from '@kubevirt-utils/resources/shared';
import { kubevirtConsole } from '@kubevirt-utils/utils/utils';

const SEGMENT_KEY =
(window as any).SERVER_FLAGS?.telemetry?.DEVSANDBOX_SEGMENT_API_KEY ||
(window as any).SERVER_FLAGS?.telemetry?.SEGMENT_API_KEY ||
(window as any).SERVER_FLAGS?.telemetry?.SEGMENT_PUBLIC_API_KEY ||
'';

const initSegment = () => {
const analytics = ((window as any).analytics = (window as any).analytics || []);
if (analytics.initialize) {
return;
}

if (analytics.invoked)
window.console &&
kubevirtConsole.error &&
kubevirtConsole.error('Segment snippet included twice.');
else {
analytics.invoked = true;
analytics.methods = [
'trackSubmit',
'trackClick',
'trackLink',
'trackForm',
'pageview',
'identify',
'reset',
'group',
'track',
'ready',
'alias',
'debug',
'page',
'once',
'off',
'on',
'addSourceMiddleware',
'addIntegrationMiddleware',
'setAnonymousId',
'addDestinationMiddleware',
];
analytics.factory = function (e: string) {
return function (...args) {
const t = Array.prototype.slice.call(args);
t.unshift(e);
analytics.push(t);
return analytics;
};
};
for (let e = 0; e < analytics.methods.length; e++) {
const key = analytics.methods[e];
analytics[key] = analytics.factory(key);
}
analytics.load = function (key: string, e: Event) {
const t = document.createElement('script');
t.type = 'text/javascript';
t.async = true;
t.src =
'https://cdn.segment.com/analytics.js/v1/' + encodeURIComponent(key) + '/analytics.min.js';
const n = document.getElementsByTagName('script')[0];
if (n.parentNode) {
n.parentNode.insertBefore(t, n);
}
analytics._loadOptions = e;
};
analytics.SNIPPET_VERSION = '4.13.1';
if (SEGMENT_KEY) {
analytics.load(SEGMENT_KEY);
}
}
};

initSegment();

export const eventMonitor = async (eventType: string, properties?: any) => {
const anonymousIP = {
context: {
ip: '0.0.0.0',
},
};
switch (eventType) {
case 'identify': {
const { user, ...otherProperties } = properties;
// With 4.15+ we can use the user object directly, but on older releases (<4.15)
// we need to extract the user object from the metadata.
// All properties are defined in the UserInfo interface and marked as optional.
const uid = user?.uid || user?.metadata?.uid;
const username = user?.username || user?.metadata?.name;
const id = uid || `${location.host}-${username}`;
// Use SHA1 hash algorithm to anonymize the user
const anonymousIdBuffer = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(id));
const anonymousIdArray = Array.from(new Uint8Array(anonymousIdBuffer));
const anonymousId = anonymousIdArray.map((b) => b.toString(16).padStart(2, '0')).join('');
(window as any).analytics.identify(anonymousId, otherProperties, anonymousIP);
break;
}
case 'page': {
(window as any).analytics.page(undefined, properties, anonymousIP);
break;
}
default: {
(window as any).analytics.track(eventType, properties, anonymousIP);
}
}
};

export const logEventWithName = (
key: string,
vm?: V1VirtualMachine,
properties?: Record<string, any>,
) => {
eventMonitor(key, {
...properties,
...(vm && { vmName: getName(vm) }),
});
};

export const logITFlowEvent = (
key: string,
vm?: V1VirtualMachine,
properties?: Record<string, any>,
) => logEventWithName(key, vm, { ...properties, flow: createVMFlowTypes.InstanceTypes });

export const logTemplateFlowEvent = (
key: string,
template: V1Template,
properties?: Record<string, any>,
) =>
eventMonitor(key, {
...properties,
flow: createVMFlowTypes.Template,
templateName: getName(template),
});
21 changes: 21 additions & 0 deletions src/utils/extensions/telemetry/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const VIEW_YAML_AND_CLI_CLICKED = 'View YAML & CLI button clicked';

export const BOOTABLE_VOLUME_SELECTED = 'Bootable volume selected';
export const INSTANCETYPE_SELECTED = 'InstanceType selected';
export const TEMPLATE_SELECTED = 'Template selected';

export const CREATE_VM_BUTTON_CLICKED = 'Create VirtualMachine button clicked';
export const CANCEL_CREATE_VM_BUTTON_CLICKED = 'Cancel Create VirtualMachine button clicked';
export const CREATE_VM_SUCCEEDED = 'Create VirtualMachine succeeded';
export const CREATE_VM_FAILED = 'Create VirtualMachine failed';

export const CUSTOMIZE_VM_BUTTON_CLICKED = 'Customize VirtualMachine button clicked';
export const CANCEL_CUSTOMIZE_VM_BUTTON_CLICKED = 'Cancel Customize VirtualMachine button clicked';
export const CUSTOMIZE_PAGE_CREATE_VM_BUTTON_CLICKED = 'Customize VirtualMachine button clicked';
export const CUSTOMIZE_VM_SUCCEEDED = 'Customize VirtualMachine succeeded';
export const CUSTOMIZE_VM_FAILED = 'Customize InstanceTypes VirtualMachine flow failed';

export const createVMFlowTypes = {
InstanceTypes: 'InstanceTypes',
Template: 'Template',
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { V1beta1DataSource } from '@kubevirt-ui/kubevirt-api/containerized-data-
import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { V1beta1VirtualMachineClusterPreference } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { VolumeSnapshotKind } from '@kubevirt-utils/components/SelectSnapshot/types';
import { logITFlowEvent } from '@kubevirt-utils/extensions/telemetry/telemetry';
import { BOOTABLE_VOLUME_SELECTED } from '@kubevirt-utils/extensions/telemetry/utils/constants';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import {
getVolumeSnapshotSize,
Expand Down Expand Up @@ -57,12 +59,19 @@ const BootableVolumeRow: FC<BootableVolumeRowProps> = ({

const [isFavorite, addOrRemoveFavorite] = favorites;

const handleOnClick = () => {
setSelectedBootableVolume(bootableVolume, pvcSource, volumeSnapshotSource);
logITFlowEvent(BOOTABLE_VOLUME_SELECTED, null, {
selectedBootableVolume: getName(bootableVolume),
});
};

return (
<Tr
isClickable
isRowSelected={getName(selectedBootableVolume) === bootVolumeName}
isSelectable
onClick={() => setSelectedBootableVolume(bootableVolume, pvcSource, volumeSnapshotSource)}
onClick={() => handleOnClick()}
>
<TableData
favorites={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ import {
UNATTEND,
} from '@kubevirt-utils/components/SysprepModal/sysprep-utils';
import { VirtualMachineDetailsTab } from '@kubevirt-utils/constants/tabs-constants';
import { logITFlowEvent } from '@kubevirt-utils/extensions/telemetry/telemetry';
import {
CANCEL_CREATE_VM_BUTTON_CLICKED,
CREATE_VM_BUTTON_CLICKED,
CREATE_VM_FAILED,
CREATE_VM_SUCCEEDED,
CUSTOMIZE_VM_BUTTON_CLICKED,
VIEW_YAML_AND_CLI_CLICKED,
} from '@kubevirt-utils/extensions/telemetry/utils/constants';
import { useFeatures } from '@kubevirt-utils/hooks/useFeatures/useFeatures';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import useKubevirtUserSettings from '@kubevirt-utils/hooks/useKubevirtUserSettings/useKubevirtUserSettings';
Expand Down Expand Up @@ -71,10 +80,10 @@ const CreateVMFooter: FC = () => {
const { applyKeyToProject, secretOption, sshPubKey, sshSecretName } = sshSecretCredentials || {};
const isWindowsOSVolume = useIsWindowsBootableVolume();

const onCancel = useCallback(
() => navigate(getResourceUrl({ activeNamespace, model: VirtualMachineModel })),
[activeNamespace, navigate],
);
const onCancel = useCallback(() => {
logITFlowEvent(CANCEL_CREATE_VM_BUTTON_CLICKED, null, { vmName: vmName });
navigate(getResourceUrl({ activeNamespace, model: VirtualMachineModel }));
}, [activeNamespace, navigate, vmName]);

const [canCreateVM] = useAccessReview({
group: VirtualMachineModel.apiGroup,
Expand All @@ -100,6 +109,8 @@ const CreateVMFooter: FC = () => {
autoUpdateEnabled,
);

logITFlowEvent(CREATE_VM_BUTTON_CLICKED, vmToCreate);

if (
applyKeyToProject &&
!isEmpty(sshSecretName) &&
Expand Down Expand Up @@ -137,14 +148,20 @@ const CreateVMFooter: FC = () => {

createHeadlessService(createdVM);
navigate(getResourceUrl({ model: VirtualMachineModel, resource: createdVM }));

logITFlowEvent(CREATE_VM_SUCCEEDED, createdVM);
})
.catch((err) => {
setError(err);
logITFlowEvent(CREATE_VM_FAILED, null, { vmName: vmName });
})
.catch(setError)
.finally(() => setIsSubmitting(false));
};

const handleCustomize = async () => {
setIsSubmitting(true);
setError(null);

try {
await setVM(
generateVM(
Expand All @@ -163,6 +180,8 @@ const CreateVMFooter: FC = () => {
autoUpdateEnabled,
);

logITFlowEvent(CUSTOMIZE_VM_BUTTON_CLICKED, vmSignal.value);

navigate(
`/k8s/ns/${vmNamespaceTarget}/catalog/review/${VirtualMachineDetailsTab.Configurations}`,
);
Expand Down Expand Up @@ -237,7 +256,8 @@ const CreateVMFooter: FC = () => {
<SplitItem isFilled />
<SplitItem>
<Button
onClick={() =>
onClick={() => {
logITFlowEvent(VIEW_YAML_AND_CLI_CLICKED, null, { vmName: vmName });
createModal((props) => (
<YamlAndCLIViewerModal
vm={generateVM(
Expand All @@ -249,8 +269,8 @@ const CreateVMFooter: FC = () => {
)}
{...props}
/>
))
}
));
}}
isDisabled={isEmpty(selectedBootableVolume) || !hasNameAndInstanceType}
variant={ButtonVariant.secondary}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { MouseEvent, useRef, useState } from 'react';

import { useInstanceTypeVMStore } from '@catalog/CreateFromInstanceTypes/state/useInstanceTypeVMStore';
import { instanceTypeActionType } from '@catalog/CreateFromInstanceTypes/state/utils/types';
import { logITFlowEvent } from '@kubevirt-utils/extensions/telemetry/telemetry';
import { INSTANCETYPE_SELECTED } from '@kubevirt-utils/extensions/telemetry/utils/constants';
import { useClickOutside } from '@kubevirt-utils/hooks/useClickOutside/useClickOutside';

import { UseInstanceTypeCardMenuSectionValues } from '../utils/types';
Expand All @@ -24,6 +26,10 @@ const useInstanceTypeCardMenuSection = (): UseInstanceTypeCardMenuSectionValues
payload: { name: itName, namespace: null },
type: instanceTypeActionType.setSelectedInstanceType,
});

logITFlowEvent(INSTANCETYPE_SELECTED, null, {
selectedInstanceType: itName,
});
};

useClickOutside(menuRef, onMenuToggle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/Virtua
import ErrorAlert from '@kubevirt-utils/components/ErrorAlert/ErrorAlert';
import { SecretSelectionOption } from '@kubevirt-utils/components/SSHSecretModal/utils/types';
import { createSSHSecret } from '@kubevirt-utils/components/SSHSecretModal/utils/utils';
import { logITFlowEvent } from '@kubevirt-utils/extensions/telemetry/telemetry';
import {
CANCEL_CUSTOMIZE_VM_BUTTON_CLICKED,
CUSTOMIZE_VM_FAILED,
CUSTOMIZE_VM_SUCCEEDED,
} from '@kubevirt-utils/extensions/telemetry/utils/constants';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import useKubevirtUserSettings from '@kubevirt-utils/hooks/useKubevirtUserSettings/useKubevirtUserSettings';
import { getResourceUrl } from '@kubevirt-utils/resources/shared';
Expand Down Expand Up @@ -75,6 +81,7 @@ const CustomizeITVMFooter: FC = () => {
data: vmSignal.value || vm,
model: VirtualMachineModel,
});
logITFlowEvent(CUSTOMIZE_VM_SUCCEEDED, createdVM);
if (secretOption === SecretSelectionOption.addNew) {
createSSHSecret(sshPubKey, sshSecretName, vmNamespaceTarget);
}
Expand All @@ -83,6 +90,7 @@ const CustomizeITVMFooter: FC = () => {
navigate(getResourceUrl({ model: VirtualMachineModel, resource: createdVM }));
} catch (err) {
setError(err);
logITFlowEvent(CUSTOMIZE_VM_FAILED, vm);
} finally {
setIsSubmitting(false);
}
Expand All @@ -96,6 +104,7 @@ const CustomizeITVMFooter: FC = () => {
<SplitItem>
<Button
onClick={() => {
logITFlowEvent(CANCEL_CUSTOMIZE_VM_BUTTON_CLICKED, vm);
clearCustomizeInstanceType();
navigate(`/k8s/ns/${activeNamespace}/catalog`);
}}
Expand Down
3 changes: 3 additions & 0 deletions src/views/catalog/templatescatalog/TemplatesCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useParams } from 'react-router-dom-v5-compat';
import { clearSessionStorageVM } from '@catalog/utils/WizardVMContext';
import { V1Template } from '@kubevirt-ui/kubevirt-api/console';
import { DEFAULT_NAMESPACE } from '@kubevirt-utils/constants/constants';
import { logTemplateFlowEvent } from '@kubevirt-utils/extensions/telemetry/telemetry';
import { TEMPLATE_SELECTED } from '@kubevirt-utils/extensions/telemetry/utils/constants';
import { Stack, Toolbar, ToolbarContent } from '@patternfly/react-core';

import { TemplatesCatalogDrawer } from './components/TemplatesCatalogDrawer/TemplatesCatalogDrawer';
Expand Down Expand Up @@ -55,6 +57,7 @@ const TemplatesCatalog: FC = () => {
onTemplateClick={(template) => {
clearSessionStorageVM();
setSelectedTemplate(template);
logTemplateFlowEvent(TEMPLATE_SELECTED, template);
}}
availableDatasources={availableDatasources}
availableTemplatesUID={availableTemplatesUID}
Expand Down
Loading

0 comments on commit 7fbe37f

Please sign in to comment.