[WEB-4405]chore: added event tracker for work item types (#3623)

* chore: added event tracker for custom properties

* chore: added event trackers for custom properties

* chore: handled conditional event names

* chore: revert search item changes

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
This commit is contained in:
Vamsi Krishna
2025-08-11 19:18:02 +05:30
committed by GitHub
parent b32bae457d
commit ef16c0ae69
12 changed files with 250 additions and 15 deletions

View File

@@ -4,6 +4,7 @@ import { v4 } from "uuid";
import { ChevronRight, Plus } from "lucide-react";
// plane constants
// plane i18n
import { CUSTOMER_PROPERTY_TRACKER_ELEMENTS, CUSTOMER_PROPERTY_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// plane types
import { EIssuePropertyType, TCreationListModes, TIssueProperty, TIssuePropertyPayload } from "@plane/types";
@@ -121,6 +122,18 @@ export const CustomerCustomPropertiesRoot: FC = observer(() => {
lastElementRef={lastElementRef}
properties={properties}
isUpdateAllowed={false}
trackers={{
create: {
button: CUSTOMER_PROPERTY_TRACKER_ELEMENTS.CREATE_PROPERTY_BUTTON,
eventName: CUSTOMER_PROPERTY_TRACKER_EVENTS.CREATE,
},
update: {
eventName: CUSTOMER_PROPERTY_TRACKER_EVENTS.UPDATE,
},
delete: {
eventName: CUSTOMER_PROPERTY_TRACKER_EVENTS.DELETE,
},
}}
/>
<div className={cn("flex items-center py-2 px-4", !isAnyPropertiesAvailable && "justify-center")}>
<Button
@@ -133,6 +146,7 @@ export const CustomerCustomPropertiesRoot: FC = observer(() => {
...defaultCustomProperty,
});
}}
data-ph-element={CUSTOMER_PROPERTY_TRACKER_ELEMENTS.CREATE_PROPERTY_BUTTON}
>
<Plus className="h-3.5 w-3.5" />
{t("customers.properties.add.primary_button")}

View File

@@ -4,6 +4,7 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
import { ChevronRight } from "lucide-react";
// plane imports
import { EPIC_CUSTOM_PROPERTY_TRACKER_ELEMENTS, EPIC_CUSTOM_PROPERTY_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TLoader, IIssueType } from "@plane/types";
import { Collapsible } from "@plane/ui";
@@ -85,6 +86,18 @@ export const EpicPropertiesRoot = observer((props: EpicPropertiesProps) => {
issueTypeId={epicId}
propertiesLoader={propertiesLoader}
getWorkItemTypeById={getWorkItemTypeById}
trackers={{
create: {
button: EPIC_CUSTOM_PROPERTY_TRACKER_ELEMENTS.CREATE_PROPERTY_BUTTON,
eventName: EPIC_CUSTOM_PROPERTY_TRACKER_EVENTS.CREATE,
},
update: {
eventName: EPIC_CUSTOM_PROPERTY_TRACKER_EVENTS.UPDATE,
},
delete: {
eventName: EPIC_CUSTOM_PROPERTY_TRACKER_EVENTS.DELETE,
},
}}
/>
</div>
</Collapsible>

View File

@@ -1,16 +1,17 @@
import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
// ui
import { WORK_ITEM_TYPE_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssueType } from "@plane/types";
import { EModalPosition, EModalWidth, getRandomIconName, ModalCore, setToast, TOAST_TYPE } from "@plane/ui";
// helpers
import { getRandomBackgroundColor } from "@plane/utils";
// plane web components
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { CreateOrUpdateIssueTypeForm } from "@/plane-web/components/issue-types/";
// plane web
import { useIssueType, useIssueTypes } from "@/plane-web/hooks/store";
// plane imports
type Props = {
issueTypeId: string | null;
@@ -69,13 +70,19 @@ export const CreateOrUpdateIssueTypeModal: FC<Props> = observer((props) => {
if (!issueTypeFormData) return;
setIsSubmitting(true);
await createType(issueTypeFormData)
.then(() => {
.then((res) => {
handleModalClearAndClose();
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("work_item_types.create.toast.success.title"),
message: t("work_item_types.create.toast.success.message"),
});
captureSuccess({
eventName: WORK_ITEM_TYPE_TRACKER_EVENTS.CREATE,
payload: {
work_item_type_id: res?.id,
},
});
})
.catch((error) => {
if (error.code === "ISSUE_TYPE_ALREADY_EXIST") {
@@ -91,6 +98,10 @@ export const CreateOrUpdateIssueTypeModal: FC<Props> = observer((props) => {
message: t("work_item_types.create.toast.error.message.default"),
});
}
captureError({
eventName: WORK_ITEM_TYPE_TRACKER_EVENTS.CREATE,
error: error as Error,
});
})
.finally(() => {
setIsSubmitting(false);
@@ -110,6 +121,12 @@ export const CreateOrUpdateIssueTypeModal: FC<Props> = observer((props) => {
title: t("work_item_types.update.toast.success.title"),
message: t("work_item_types.update.toast.success.message", { name: issueTypeFormData?.name }),
});
captureSuccess({
eventName: WORK_ITEM_TYPE_TRACKER_EVENTS.UPDATE,
payload: {
work_item_type_id: issueTypeFormData?.id,
},
});
})
.catch((error) => {
if (error.code === "ISSUE_TYPE_ALREADY_EXIST") {
@@ -125,6 +142,10 @@ export const CreateOrUpdateIssueTypeModal: FC<Props> = observer((props) => {
message: t("work_item_types.update.toast.error.message.default"),
});
}
captureError({
eventName: WORK_ITEM_TYPE_TRACKER_EVENTS.UPDATE,
error: error as Error,
});
})
.finally(() => {
setIsSubmitting(false);

View File

@@ -1,12 +1,14 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { WORK_ITEM_TYPE_TRACKER_ELEMENTS, WORK_ITEM_TYPE_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EProductSubscriptionEnum } from "@plane/types";
import { AlertModalCore, setToast, TOAST_TYPE } from "@plane/ui";
// helpers
import { DetailedEmptyState } from "@/components/empty-state";
// hooks
import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// plane web imports
import { useFlag, useIssueTypes, useWorkspaceSubscription } from "@/plane-web/hooks/store";
@@ -43,13 +45,26 @@ export const IssueTypeEmptyState: FC<TIssueTypeEmptyState> = observer((props) =>
title: "Success!",
message: "Work item types and custom properties are now enabled for this project",
});
captureSuccess({
eventName: WORK_ITEM_TYPE_TRACKER_EVENTS.TYPES_ENABLED,
payload: {
project_id: projectId,
},
});
})
.catch(() => {
.catch((error) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to enable work item types",
});
captureError({
eventName: WORK_ITEM_TYPE_TRACKER_EVENTS.TYPES_ENABLED,
payload: {
project_id: projectId,
},
error: error as Error,
});
})
.finally(() => {
setIsLoading(false);
@@ -66,7 +81,12 @@ export const IssueTypeEmptyState: FC<TIssueTypeEmptyState> = observer((props) =>
assetPath={resolvedPath}
primaryButton={{
text: t("work_item_types.empty_state.enable.primary_button.text"),
onClick: () => setEnableIssueTypeConfirmation(true),
onClick: () => {
captureClick({
elementName: WORK_ITEM_TYPE_TRACKER_ELEMENTS.HEADER_ENABLE_WORK_ITEM_TYPES_BUTTON,
});
setEnableIssueTypeConfirmation(true);
},
}}
size="md"
/>

View File

@@ -1,10 +1,11 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { ChevronRight } from "lucide-react";
// plane imports
import { WORK_ITEM_TYPE_TRACKER_ELEMENTS, WORK_ITEM_TYPE_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TLoader, IIssueType } from "@plane/types";
import { Collapsible, setToast, TOAST_TYPE, setPromiseToast } from "@plane/ui";
import { Collapsible } from "@plane/ui";
// helpers
import { cn } from "@plane/utils";
// plane web components
@@ -125,6 +126,18 @@ export const IssueTypeListItem = observer((props: TIssueTypeListItem) => {
issueTypeId={issueTypeId}
propertiesLoader={propertiesLoader}
getWorkItemTypeById={getWorkItemTypeById}
trackers={{
create: {
button: WORK_ITEM_TYPE_TRACKER_ELEMENTS.CREATE_PROPERTY_BUTTON,
eventName: WORK_ITEM_TYPE_TRACKER_EVENTS.CREATE_PROPERTY,
},
update: {
eventName: WORK_ITEM_TYPE_TRACKER_EVENTS.UPDATE,
},
delete: {
eventName: WORK_ITEM_TYPE_TRACKER_EVENTS.DELETE,
},
}}
/>
</div>
</Collapsible>

View File

@@ -11,6 +11,12 @@ export type TIssuePropertyCreateListItem = {
issuePropertyCreateListData?: TIssuePropertyCreateList;
customPropertyOperations: TCustomPropertyOperations;
isUpdateAllowed: boolean;
trackers?: {
[key in "create" | "update" | "delete" | "quickActions"]?: {
button?: string;
eventName?: string;
};
};
};
export const IssuePropertyCreateListItem = observer(
@@ -18,7 +24,7 @@ export const IssuePropertyCreateListItem = observer(
props: TIssuePropertyCreateListItem,
ref: React.Ref<HTMLDivElement>
) {
const { issuePropertyCreateListData, customPropertyOperations, isUpdateAllowed } = props;
const { issuePropertyCreateListData, customPropertyOperations, isUpdateAllowed, trackers } = props;
return (
<div ref={ref}>
@@ -27,6 +33,7 @@ export const IssuePropertyCreateListItem = observer(
operationMode="create"
customPropertyOperations={customPropertyOperations}
isUpdateAllowed={isUpdateAllowed}
trackers={trackers}
/>
</div>
);

View File

@@ -19,6 +19,7 @@ import {
import { Button, InfoIcon, TOAST_TYPE, Tooltip, setPromiseToast, setToast } from "@plane/ui";
import { getIssuePropertyAttributeDisplayNameKey, cn } from "@plane/utils";
// plane web components
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import {
AttributePill,
IssuePropertyLogo,
@@ -48,6 +49,12 @@ type TIssuePropertyListItem = {
operationMode?: TOperationMode;
customPropertyOperations: TCustomPropertyOperations;
isUpdateAllowed: boolean;
trackers?: {
[key in "create" | "update" | "delete" | "quickActions"]?: {
button?: string;
eventName?: string;
};
};
};
export type TIssuePropertyFormError = {
@@ -62,8 +69,14 @@ const defaultIssuePropertyError: TIssuePropertyFormError = {
};
export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) => {
const { customPropertyId, issuePropertyCreateListData, operationMode, customPropertyOperations, isUpdateAllowed } =
props;
const {
customPropertyId,
issuePropertyCreateListData,
operationMode,
customPropertyOperations,
isUpdateAllowed,
trackers,
} = props;
const {
getPropertyDetail,
getSortedActivePropertyOptions,
@@ -178,6 +191,14 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
options: optionsPayload,
})
.then(async (response) => {
if (trackers?.create?.eventName) {
captureSuccess({
eventName: trackers.create.eventName,
payload: {
name: issuePropertyData?.display_name,
},
});
}
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("work_item_types.settings.properties.toast.create.success.title"),
@@ -188,6 +209,15 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
return response;
})
.catch((error) => {
if (trackers?.create?.eventName) {
captureError({
eventName: trackers.create.eventName,
payload: {
name: issuePropertyData?.display_name,
},
error: error,
});
}
setToast({
type: TOAST_TYPE.ERROR,
title: t("work_item_types.settings.properties.toast.create.error.title"),
@@ -238,6 +268,14 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
options: optionsPayload,
})
.then(() => {
if (trackers?.update?.eventName) {
captureSuccess({
eventName: trackers.update.eventName,
payload: {
name: issuePropertyData?.display_name,
},
});
}
if (showToast)
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -248,6 +286,15 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
});
})
.catch((error) => {
if (trackers?.update?.eventName) {
captureError({
eventName: trackers.update.eventName,
payload: {
name: issuePropertyData?.display_name,
},
error: error,
});
}
if (showToast)
setToast({
type: TOAST_TYPE.ERROR,
@@ -282,6 +329,14 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
setIsSubmitting(true);
await deleteProperty(propertyId)
.then(() => {
if (trackers?.delete?.eventName) {
captureSuccess({
eventName: trackers.delete.eventName,
payload: {
name: issuePropertyData?.display_name,
},
});
}
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("work_item_types.settings.properties.toast.delete.success.title"),
@@ -291,6 +346,14 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
});
})
.catch(() => {
if (trackers?.delete?.eventName) {
captureError({
eventName: trackers.delete.eventName,
payload: {
name: issuePropertyData?.display_name,
},
});
}
setToast({
type: TOAST_TYPE.ERROR,
title: t("work_item_types.settings.properties.toast.delete.error.title"),
@@ -457,6 +520,7 @@ export const IssuePropertyListItem = observer((props: TIssuePropertyListItem) =>
onDisable={async () => handlePropertyDataChange("is_active", false, true)}
onDelete={handleDelete}
onIssuePropertyOperationMode={(mode) => setIssuePropertyOperationMode(mode)}
trackers={trackers}
/>
</div>
</div>

View File

@@ -19,6 +19,12 @@ type TIssuePropertyList = {
containerRef: React.RefObject<HTMLDivElement>;
lastElementRef: React.RefObject<HTMLDivElement>;
isUpdateAllowed: boolean;
trackers?: {
[key in "create" | "update" | "delete" | "quickActions"]?: {
button?: string;
eventName?: string;
};
};
};
export const IssuePropertyList: FC<TIssuePropertyList> = observer((props) => {
@@ -29,6 +35,7 @@ export const IssuePropertyList: FC<TIssuePropertyList> = observer((props) => {
containerRef,
lastElementRef,
isUpdateAllowed,
trackers,
} = props;
return (
@@ -45,6 +52,7 @@ export const IssuePropertyList: FC<TIssuePropertyList> = observer((props) => {
customPropertyId={property.id}
customPropertyOperations={customPropertyOperations}
isUpdateAllowed={isUpdateAllowed}
trackers={trackers}
/>
</IssuePropertyOptionsProvider>
))}
@@ -60,6 +68,7 @@ export const IssuePropertyList: FC<TIssuePropertyList> = observer((props) => {
issuePropertyCreateListData={issueProperty}
customPropertyOperations={customPropertyOperations}
isUpdateAllowed
trackers={trackers}
/>
</IssuePropertyOptionsProvider>
))}

View File

@@ -7,6 +7,7 @@ import { TOperationMode } from "@plane/types";
import { CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// plane web components
import { captureClick } from "@/helpers/event-tracker.helper";
import { DeleteConfirmationModal } from "@/plane-web/components/issue-types";
type TIssuePropertyQuickActions = {
@@ -14,10 +15,16 @@ type TIssuePropertyQuickActions = {
onDisable: () => Promise<void>;
onDelete: () => Promise<void>;
onIssuePropertyOperationMode: (mode: TOperationMode) => void;
trackers?: {
[key in "create" | "update" | "delete" | "quickActions"]?: {
button?: string;
eventName?: string;
};
};
};
export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickActions) => {
const { isPropertyDisabled, onDisable, onDelete, onIssuePropertyOperationMode } = props;
const { isPropertyDisabled, onDisable, onDelete, onIssuePropertyOperationMode, trackers } = props;
// plane hooks
const { t } = useTranslation();
// states
@@ -26,13 +33,23 @@ export const IssuePropertyQuickActions = observer((props: TIssuePropertyQuickAct
const MENU_ITEMS: TContextMenuItem[] = [
{
key: "edit",
action: () => onIssuePropertyOperationMode("update"),
action: () => {
captureClick({
elementName: trackers?.quickActions?.button || "",
});
onIssuePropertyOperationMode("update");
},
title: t("common.actions.edit"),
icon: Pencil,
},
{
key: "delete",
action: () => setIsDeleteModalOpen(true),
action: () => {
captureClick({
elementName: trackers?.quickActions?.button || "",
});
setIsDeleteModalOpen(true);
},
title: t("common.actions.delete"),
icon: Trash2,
},

View File

@@ -4,10 +4,12 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { Pencil, Trash2 } from "lucide-react";
// plane imports
import { WORK_ITEM_TYPE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IIssueType } from "@plane/types";
import { CustomMenu, setPromiseToast, TContextMenuItem, ToggleSwitch, Tooltip } from "@plane/ui";
import { cn } from "@plane/utils";
import { captureClick } from "@/helpers/event-tracker.helper";
type Props = {
issueTypeId: string;
@@ -38,13 +40,23 @@ export const IssueTypeQuickActions: React.FC<Props> = observer((props) => {
const MENU_ITEMS: (TContextMenuItem & { tooltipContent?: string })[] = [
{
key: "edit",
action: () => onEditIssueTypeIdChange(issueTypeId),
action: () => {
captureClick({
elementName: WORK_ITEM_TYPE_TRACKER_ELEMENTS.PROPERTY_QUICK_ACTIONS,
});
onEditIssueTypeIdChange(issueTypeId);
},
title: t("common.actions.edit"),
icon: Pencil,
},
{
key: "delete",
action: () => onDeleteIssueTypeIdChange(issueTypeId),
action: () => {
captureClick({
elementName: WORK_ITEM_TYPE_TRACKER_ELEMENTS.PROPERTY_QUICK_ACTIONS,
});
onDeleteIssueTypeIdChange(issueTypeId);
},
title: t("common.actions.delete"),
tooltipContent: issueTypeDetail?.is_default
? t("work_item_types.settings.cant_delete_default_message")

View File

@@ -3,9 +3,11 @@ import { useCallback, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { WORK_ITEM_TYPE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// plane web components
import { SettingsHeading } from "@/components/settings";
import { captureClick } from "@/helpers/event-tracker.helper";
import { IssueTypeEmptyState, IssueTypesList, CreateOrUpdateIssueTypeModal } from "@/plane-web/components/issue-types";
// plane web hooks
import { useIssueTypes } from "@/plane-web/hooks/store";
@@ -62,7 +64,12 @@ export const IssueTypesRoot = observer(() => {
showButton={isWorkItemTypeEnabled}
button={{
label: t("work_item_types.create.button"),
onClick: () => setIsModalOpen(true),
onClick: () => {
captureClick({
elementName: WORK_ITEM_TYPE_TRACKER_ELEMENTS.HEADER_CREATE_WORK_ITEM_TYPE_BUTTON,
});
setIsModalOpen(true);
},
}}
/>
<div className="my-2 h-full overflow-y-scroll vertical-scrollbar scrollbar-sm">

View File

@@ -198,6 +198,44 @@ export const TEAMSPACE_VIEW_TRACKER_ELEMENTS = {
CONTEXT_MENU: "teamspace_view_context_menu",
};
export const WORK_ITEM_TYPE_TRACKER_EVENTS = {
TYPES_ENABLED: "work_item_types_enabled",
CREATE: "work_item_type_created",
UPDATE: "work_item_type_updated",
DELETE: "work_item_type_deleted",
ENABLE: "work_item_type_enabled",
DISABLE: "work_item_type_disabled",
CREATE_PROPERTY: "work_item_type_create_property",
};
export const WORK_ITEM_TYPE_TRACKER_ELEMENTS = {
HEADER_CREATE_WORK_ITEM_TYPE_BUTTON: "work_item_type_header_create_button",
HEADER_ENABLE_WORK_ITEM_TYPES_BUTTON: "work_item_type_header_enable_work_item_types_button",
HEADER_ENABLE_DISABLE_WORK_ITEM_TYPE_BUTTON: "work_item_type_header_enable_disable_button",
CREATE_PROPERTY_BUTTON: "work_item_type_create_property_button",
PROPERTY_QUICK_ACTIONS: "work_item_type_property_quick_actions",
};
export const EPIC_CUSTOM_PROPERTY_TRACKER_EVENTS = {
CREATE: "epic_custom_property_created",
UPDATE: "epic_custom_property_updated",
DELETE: "epic_custom_property_deleted",
};
export const EPIC_CUSTOM_PROPERTY_TRACKER_ELEMENTS = {
CREATE_PROPERTY_BUTTON: "epic_custom_property_create_property_button",
PROPERTY_QUICK_ACTIONS: "epic_custom_property_property_quick_actions",
};
export const CUSTOMER_PROPERTY_TRACKER_EVENTS = {
CREATE: "customer_custom_property_created",
UPDATE: "customer_custom_property_updated",
DELETE: "customer_custom_property_deleted",
};
export const CUSTOMER_PROPERTY_TRACKER_ELEMENTS = {
CREATE_PROPERTY_BUTTON: "customer_custom_property_create_property_button",
PROPERTY_QUICK_ACTIONS: "customer_custom_property_property_quick_actions",
};
// Project Template Tracker Start
export const INTEGRATION_TRACKER_ELEMENTS = {
INTEGRATIONS_MAPPING_ENTITY_ITEM_BUTTON: "integrations_mapping_entity_item_button",
};