Wizard: add segment tracking

This commit is contained in:
Katarina Sieklova 2025-03-26 16:30:38 +01:00 committed by Klara Simickova
parent c99157216f
commit d18f25e331
9 changed files with 344 additions and 54 deletions

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Dropdown,
@ -15,10 +15,12 @@ import {
Button,
} from '@patternfly/react-core';
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { ChromeUser } from '@redhat-cloud-services/types';
import { skipToken } from '@reduxjs/toolkit/query';
import { targetOptions } from '../../constants';
import { AMPLITUDE_MODULE_NAME, targetOptions } from '../../constants';
import {
useGetBlueprintQuery,
useComposeBlueprintMutation,
@ -38,6 +40,16 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
const [buildBlueprint, { isLoading: imageBuildLoading }] =
useComposeBlueprintMutation();
const dispatch = useAppDispatch();
const { analytics, auth } = useChrome();
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
const onBuildHandler = async () => {
if (selectedBlueprintId) {
@ -50,6 +62,11 @@ export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => {
),
},
});
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Requested`, {
module: AMPLITUDE_MODULE_NAME,
trigger: 'synchronize images',
account_id: userData?.identity.internal?.account_id || 'Not found',
});
} catch (imageBuildError) {
dispatch(
addNotification({

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
ActionGroup,
@ -6,8 +6,14 @@ import {
Modal,
ModalVariant,
} from '@patternfly/react-core';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { PAGINATION_LIMIT, PAGINATION_OFFSET } from '../../constants';
import {
AMPLITUDE_MODULE_NAME,
PAGINATION_LIMIT,
PAGINATION_OFFSET,
} from '../../constants';
import {
backendApi,
useDeleteBlueprintMutation,
@ -36,6 +42,15 @@ export const DeleteBlueprintModal: React.FunctionComponent<
const blueprintsOffset = useAppSelector(selectOffset) || PAGINATION_OFFSET;
const blueprintsLimit = useAppSelector(selectLimit) || PAGINATION_LIMIT;
const dispatch = useAppDispatch();
const { analytics, auth } = useChrome();
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
const searchParams: GetBlueprintsApiArg = {
limit: blueprintsLimit,
@ -59,6 +74,10 @@ export const DeleteBlueprintModal: React.FunctionComponent<
});
const handleDelete = async () => {
if (selectedBlueprintId) {
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Deleted`, {
module: AMPLITUDE_MODULE_NAME,
account_id: userData?.identity.internal?.account_id || 'Not found',
});
setShowDeleteModal(false);
await deleteBlueprint({ id: selectedBlueprintId });
dispatch(setBlueprintId(undefined));

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
DropdownList,
@ -11,6 +11,7 @@ import {
Button,
} from '@patternfly/react-core';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
import { useCreateBlueprintMutation } from '../../../../../store/backendApi';
@ -21,6 +22,7 @@ import {
useComposeBlueprintMutation,
} from '../../../../../store/imageBuilderApi';
import { selectPackages } from '../../../../../store/wizardSlice';
import { createAnalytics } from '../../../../../Utilities/analytics';
import { useGetEnvironment } from '../../../../../Utilities/useGetEnvironment';
type CreateDropdownProps = {
@ -34,7 +36,15 @@ export const CreateSaveAndBuildBtn = ({
setIsOpen,
isDisabled,
}: CreateDropdownProps) => {
const { analytics, isBeta } = useChrome();
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth, isBeta } = useChrome();
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
const packages = useAppSelector(selectPackages);
const [buildBlueprint] = useComposeBlueprintMutation();
@ -47,15 +57,21 @@ export const CreateSaveAndBuildBtn = ({
const requestBody = await getBlueprintPayload();
setIsOpen(false);
if (!process.env.IS_ON_PREMISE && !isFedoraEnv) {
analytics.track(`${AMPLITUDE_MODULE_NAME}-blueprintCreated`, {
module: AMPLITUDE_MODULE_NAME,
isPreview: isBeta(),
if (!process.env.IS_ON_PREMISE && !isFedoraEnv && requestBody) {
const analyticsData = createAnalytics(requestBody, packages, isBeta);
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Created`, {
...analyticsData,
type: 'createBlueprintAndBuildImages',
packages: packages.map((pkg) => pkg.name),
account_id: userData?.identity.internal?.account_id || 'Not found',
});
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Requested`, {
module: AMPLITUDE_MODULE_NAME,
trigger: 'blueprint_created',
image_request_types: requestBody.image_requests.map(
(req) => req.image_type
),
});
}
const blueprint =
requestBody &&
(await createBlueprint({
@ -86,7 +102,15 @@ export const CreateSaveButton = ({
getBlueprintPayload,
isDisabled,
}: CreateDropdownProps) => {
const { analytics, isBeta } = useChrome();
const { analytics, auth, isBeta } = useChrome();
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
const packages = useAppSelector(selectPackages);
const { isFedoraEnv } = useGetEnvironment();
@ -141,12 +165,12 @@ export const CreateSaveButton = ({
const requestBody = await getBlueprintPayload();
setIsOpen(false);
if (!process.env.IS_ON_PREMISE && !isFedoraEnv) {
analytics.track(`${AMPLITUDE_MODULE_NAME}-blueprintCreated`, {
module: AMPLITUDE_MODULE_NAME,
isPreview: isBeta(),
if (!process.env.IS_ON_PREMISE && !isFedoraEnv && requestBody) {
const analyticsData = createAnalytics(requestBody, packages, isBeta);
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Created`, {
...analyticsData,
type: 'createBlueprint',
packages: packages.map((pkg) => pkg.name),
account_id: userData?.identity.internal?.account_id || 'Not found',
});
}
const blueprint =

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
DropdownList,
@ -8,12 +8,18 @@ import {
Flex,
FlexItem,
} from '@patternfly/react-core';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { AMPLITUDE_MODULE_NAME } from '../../../../../constants';
import { useUpdateBlueprintMutation } from '../../../../../store/backendApi';
import { useAppSelector } from '../../../../../store/hooks';
import {
CreateBlueprintRequest,
useComposeBlueprintMutation,
} from '../../../../../store/imageBuilderApi';
import { selectPackages } from '../../../../../store/wizardSlice';
import { createAnalytics } from '../../../../../Utilities/analytics';
type EditDropdownProps = {
getBlueprintPayload: () => Promise<'' | CreateBlueprintRequest | undefined>;
@ -28,13 +34,40 @@ export const EditSaveAndBuildBtn = ({
blueprintId,
isDisabled,
}: EditDropdownProps) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth, isBeta } = useChrome();
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
const [buildBlueprint] = useComposeBlueprintMutation();
const packages = useAppSelector(selectPackages);
const [updateBlueprint] = useUpdateBlueprintMutation({
fixedCacheKey: 'updateBlueprintKey',
});
const onSaveAndBuild = async () => {
const requestBody = await getBlueprintPayload();
if (!process.env.IS_ON_PREMISE && requestBody) {
const analyticsData = createAnalytics(requestBody, packages, isBeta);
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Updated`, {
...analyticsData,
type: 'editBlueprintAndBuildImages',
account_id: userData?.identity.internal?.account_id || 'Not found',
});
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Requested`, {
module: AMPLITUDE_MODULE_NAME,
trigger: 'blueprint_updated',
image_request_types: requestBody.image_requests.map(
(req) => req.image_type
),
});
}
setIsOpen(false);
if (requestBody) {
await updateBlueprint({
@ -64,11 +97,31 @@ export const EditSaveButton = ({
blueprintId,
isDisabled,
}: EditDropdownProps) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth, isBeta } = useChrome();
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
const packages = useAppSelector(selectPackages);
const [updateBlueprint, { isLoading }] = useUpdateBlueprintMutation({
fixedCacheKey: 'updateBlueprintKey',
});
const onSave = async () => {
const requestBody = await getBlueprintPayload();
if (!process.env.IS_ON_PREMISE && requestBody) {
const analyticsData = createAnalytics(requestBody, packages, isBeta);
analytics.track(`${AMPLITUDE_MODULE_NAME} - Blueprint Updated`, {
...analyticsData,
type: 'editBlueprint',
account_id: userData?.identity.internal?.account_id || 'Not found',
});
}
setIsOpen(false);
if (requestBody) {
updateBlueprint({ id: blueprintId, createBlueprintRequest: requestBody });

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
ClipboardCopy,
@ -12,9 +12,12 @@ import {
Skeleton,
} from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import ClonesTable from './ClonesTable';
import { AMPLITUDE_MODULE_NAME } from '../../constants';
import { useGetComposeStatusQuery } from '../../store/backendApi';
import { extractProvisioningList } from '../../store/helpers';
import {
@ -131,7 +134,15 @@ type AwsDetailsPropTypes = {
export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => {
const options = compose.request.image_requests[0].upload_request.options;
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
if (!isAwsUploadRequestOptions(options)) {
throw TypeError(
`Error: options must be of type AwsUploadRequestOptions, not ${typeof options}.`
@ -152,6 +163,15 @@ export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => {
clickTip="Copied"
variant="inline-compact"
ouiaId="aws-uuid"
onClick={() => {
analytics.track(`${AMPLITUDE_MODULE_NAME} - Button Clicked`, {
module: AMPLITUDE_MODULE_NAME,
link_name: compose.id,
current_path: window.location.pathname,
account_id:
userData?.identity.internal?.account_id || 'Not found',
});
}}
>
{compose.id}
</ClipboardCopy>
@ -183,6 +203,18 @@ export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => {
// the format of an account link is taken from
// https://docs.aws.amazon.com/signin/latest/userguide/sign-in-urls-defined.html
href={`https://${options.share_with_accounts[0]}.signin.aws.amazon.com/console/`}
onClick={() => {
analytics.track(`${AMPLITUDE_MODULE_NAME} - Link Clicked`, {
module: AMPLITUDE_MODULE_NAME,
link_name: options.share_with_accounts
? options.share_with_accounts[0]
: '',
current_path: window.location.pathname,
account_id:
userData?.identity.internal?.account_id || 'Not found',
});
}}
>
{options.share_with_accounts[0]}
</Button>

View file

@ -23,6 +23,8 @@ import {
Thead,
Tr,
} from '@patternfly/react-table';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { useDispatch } from 'react-redux';
import { NavigateFunction, useNavigate } from 'react-router-dom';
@ -47,6 +49,7 @@ import { ExpiringStatus, CloudStatus, LocalStatus } from './Status';
import { AwsTarget, Target } from './Target';
import {
AMPLITUDE_MODULE_NAME,
AWS_S3_EXPIRATION_TIME_IN_HOURS,
OCI_STORAGE_EXPIRATION_TIME_IN_DAYS,
PAGINATION_LIMIT,
@ -441,7 +444,14 @@ type AwsRowPropTypes = {
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
const navigate = useNavigate();
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
const target = <AwsTarget compose={compose} />;
const status = <CloudStatus compose={compose} />;
@ -451,7 +461,15 @@ const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
const details = <AwsDetails compose={compose} />;
const actions = (
<ActionsColumn items={awsActions(compose, composeStatus, navigate)} />
<ActionsColumn
items={awsActions(
compose,
composeStatus,
navigate,
analytics,
userData?.identity.internal?.account_id
)}
/>
);
return (
@ -497,6 +515,10 @@ type RowPropTypes = {
details: JSX.Element;
};
type Analytics = {
track: (event: string, props?: Record<string, unknown>) => void;
};
const Row = ({
compose,
rowIndex,
@ -506,6 +528,14 @@ const Row = ({
details,
instance,
}: RowPropTypes) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
const [isExpanded, setIsExpanded] = useState(false);
const handleToggle = () => setIsExpanded(!isExpanded);
const dispatch = useDispatch();
@ -568,7 +598,13 @@ const Row = ({
{actions ? (
actions
) : (
<ActionsColumn items={defaultActions(compose)} />
<ActionsColumn
items={defaultActions(
compose,
analytics,
userData?.identity.internal?.account_id
)}
/>
)}
</Td>
</Tr>
@ -581,33 +617,53 @@ const Row = ({
);
};
const defaultActions = (compose: ComposesResponseItem) => [
{
title: (
<a
className="ib-subdued-link"
href={`data:text/plain;charset=utf-8,${encodeURIComponent(
JSON.stringify(compose.request, null, ' ')
)}`}
download={`request-${compose.id}.json`}
>
Download compose request (.json)
</a>
),
},
];
const defaultActions = (
compose: ComposesResponseItem,
analytics: Analytics,
account_id: string | undefined
) => {
const name = `request-${compose.id}.json`;
return [
{
title: (
<a
className="ib-subdued-link"
href={`data:text/plain;charset=utf-8,${encodeURIComponent(
JSON.stringify(compose.request, null, ' ')
)}`}
download={name}
onClick={() => {
analytics.track(`${AMPLITUDE_MODULE_NAME} - File Downloaded`, {
module: AMPLITUDE_MODULE_NAME,
link_name: name,
current_path: window.location.pathname,
account_id: account_id || 'Not found',
});
}}
>
Download compose request (.json)
</a>
),
},
];
};
const awsActions = (
compose: ComposesResponseItem,
status: ComposeStatus | undefined,
navigate: NavigateFunction
) => [
{
title: 'Share to new region',
onClick: () => navigate(resolveRelPath(`share/${compose.id}`)),
isDisabled: status?.image_status.status === 'success' ? false : true,
},
...defaultActions(compose),
];
navigate: NavigateFunction,
analytics: Analytics,
account_id: string | undefined
) => {
return [
{
title: 'Share to new region',
onClick: () => navigate(resolveRelPath(`share/${compose.id}`)),
isDisabled: status?.image_status.status === 'success' ? false : true,
},
...defaultActions(compose, analytics, account_id),
];
};
export default ImagesTable;

View file

@ -1,4 +1,4 @@
import React, { Suspense, useState } from 'react';
import React, { Suspense, useEffect, useState } from 'react';
import path from 'path';
@ -20,11 +20,13 @@ import {
} from '@patternfly/react-core/dist/esm/components/List/List';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import { useLoadModule, useScalprum } from '@scalprum/react-core';
import cockpit from 'cockpit';
import { useNavigate } from 'react-router-dom';
import {
AMPLITUDE_MODULE_NAME,
FILE_SYSTEM_CUSTOMIZATION_URL,
MODAL_ANCHOR,
SEARCH_INPUT,
@ -93,6 +95,15 @@ const ProvisioningLink = ({
compose,
composeStatus,
}: ProvisioningLinkPropTypes) => {
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
const [wizardOpen, setWizardOpen] = useState(false);
const [exposedScalprumModule, error] = useLoadModule(
{
@ -155,7 +166,16 @@ const ProvisioningLink = ({
isLoading={isLoadingPermission}
variant="link"
isInline
onClick={() => setWizardOpen(true)}
onClick={() => {
analytics.track(`${AMPLITUDE_MODULE_NAME} - Link Clicked`, {
module: AMPLITUDE_MODULE_NAME,
image_name: compose.image_name,
current_path: window.location.pathname,
account_id: userData?.identity.internal?.account_id || 'Not found',
});
setWizardOpen(true);
}}
>
Launch
</Button>

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import './ImageBuildStatus.scss';
import {
@ -23,8 +23,11 @@ import {
OffIcon,
PendingIcon,
} from '@patternfly/react-icons';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeUser } from '@redhat-cloud-services/types';
import {
AMPLITUDE_MODULE_NAME,
AWS_S3_EXPIRATION_TIME_IN_HOURS,
OCI_STORAGE_EXPIRATION_TIME_IN_DAYS,
} from '../../constants';
@ -74,13 +77,21 @@ export const AwsDetailsStatus = ({ compose }: ComposeStatusPropTypes) => {
const { data, isSuccess } = useGetComposeStatusQuery({
composeId: compose.id,
});
const { analytics } = useChrome();
if (!isSuccess) {
return <></>;
}
switch (data?.image_status.status) {
case 'failure':
case 'failure': {
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Created`, {
module: AMPLITUDE_MODULE_NAME,
error: true,
error_id: data.image_status.error?.id,
error_details: data.image_status.error?.details,
error_reason: data.image_status.error?.reason,
});
return (
<ErrorStatus
icon={statuses[data.image_status.status].icon}
@ -88,6 +99,8 @@ export const AwsDetailsStatus = ({ compose }: ComposeStatusPropTypes) => {
error={data.image_status.error || ''}
/>
);
}
default:
return (
<Status
@ -106,13 +119,28 @@ export const CloudStatus = ({ compose }: CloudStatusPropTypes) => {
const { data, isSuccess } = useGetComposeStatusQuery({
composeId: compose.id,
});
const [userData, setUserData] = useState<ChromeUser | void>(undefined);
const { analytics, auth } = useChrome();
useEffect(() => {
(async () => {
const data = await auth?.getUser();
setUserData(data);
})();
}, [auth]);
if (!isSuccess) {
return <Skeleton />;
}
switch (data?.image_status.status) {
case 'failure':
case 'failure': {
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Created`, {
module: AMPLITUDE_MODULE_NAME,
error: true,
error_id: data.image_status.error?.id,
error_details: data.image_status.error?.details,
error_reason: data.image_status.error?.reason,
account_id: userData?.identity.internal?.account_id || 'Not found',
});
return (
<ErrorStatus
icon={statuses['failure'].icon}
@ -120,6 +148,7 @@ export const CloudStatus = ({ compose }: CloudStatusPropTypes) => {
error={data.image_status.error || ''}
/>
);
}
default:
return (
<Status
@ -135,8 +164,17 @@ type AzureStatusPropTypes = {
};
export const AzureStatus = ({ status }: AzureStatusPropTypes) => {
const { analytics } = useChrome();
switch (status.image_status.status) {
case 'failure':
case 'failure': {
analytics.track(`${AMPLITUDE_MODULE_NAME} - Image Created`, {
module: AMPLITUDE_MODULE_NAME,
error: true,
error_id: status.image_status.error?.id,
error_details: status.image_status.error?.details,
error_reason: status.image_status.error?.reason,
});
return (
<ErrorStatus
icon={statuses[status.image_status.status].icon}
@ -144,6 +182,7 @@ export const AzureStatus = ({ status }: AzureStatusPropTypes) => {
error={status.image_status.error || ''}
/>
);
}
default:
return (
<Status

View file

@ -0,0 +1,30 @@
import { IBPackageWithRepositoryInfo } from '../Components/CreateImageWizard/steps/Packages/Packages';
import { AMPLITUDE_MODULE_NAME } from '../constants';
import { CreateBlueprintRequest } from '../store/imageBuilderApi';
export const createAnalytics = (
requestBody: CreateBlueprintRequest,
packages: IBPackageWithRepositoryInfo[],
isBeta: () => boolean
) => {
const analyticsData = {
image_name: requestBody.name,
description: requestBody.description,
distribution: requestBody.distribution,
openscap: requestBody.customizations.openscap,
image_request_types: requestBody.image_requests.map(
(req) => req.image_type
),
image_request_architectures: requestBody.image_requests.map(
(req) => req.architecture
),
image_requests: requestBody.image_requests,
organization: requestBody.customizations.subscription?.organization,
metadata: requestBody.metadata,
packages: packages.map((pkg) => pkg.name),
file_system_configuration: requestBody.customizations.filesystem,
module: AMPLITUDE_MODULE_NAME,
is_preview: isBeta(),
};
return analyticsData;
};