diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx index 18bed7a3a..b98ae741f 100644 --- a/web/components/labels/create-label-modal.tsx +++ b/web/components/labels/create-label-modal.tsx @@ -6,13 +6,13 @@ import { Controller, useForm } from "react-hook-form"; import { ChevronDown } from "lucide-react"; import { Dialog, Popover, Transition } from "@headlessui/react"; import type { IIssueLabel, IState } from "@plane/types"; -// hooks -import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; -import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "@/constants/label"; -import { useLabel } from "@/hooks/store"; // ui -// types +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // constants +import { E_STATES, LABEL_CREATED } from "constants/event-tracker"; +import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "@/constants/label"; +// hooks +import { useLabel, useEventTracker } from "@/hooks/store"; // types type Props = { @@ -34,6 +34,7 @@ export const CreateLabelModal: React.FC = observer((props) => { const { workspaceSlug } = router.query; // store hooks const { createLabel } = useLabel(); + const { captureEvent } = useEventTracker(); // form info const { formState: { errors, isSubmitting }, @@ -70,6 +71,13 @@ export const CreateLabelModal: React.FC = observer((props) => { .then((res) => { onClose(); if (onSuccess) onSuccess(res); + captureEvent(LABEL_CREATED, { + label_id: res.id, + color: res.color, + parent: res.parent, + element: E_STATES, + state: "SUCCESS", + }); }) .catch((error) => { setToast({ @@ -78,6 +86,9 @@ export const CreateLabelModal: React.FC = observer((props) => { message: error?.detail ?? "Something went wrong. Please try again later.", }); reset(formData); + captureEvent(LABEL_CREATED, { + state: "FAILED", + }); }); }; diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index 280ae55d9..8fb470755 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -8,9 +8,10 @@ import { IIssueLabel } from "@plane/types"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // constants +import { E_Labels, LABEL_CREATED, LABEL_UPDATED } from "constants/event-tracker"; import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "@/constants/label"; // hooks -import { useLabel } from "@/hooks/store"; +import { useLabel, useEventTracker } from "@/hooks/store"; // types type Props = { @@ -34,12 +35,13 @@ export const CreateUpdateLabelInline = observer( const { workspaceSlug, projectId } = router.query; // store hooks const { createLabel, updateLabel } = useLabel(); + const { captureEvent } = useEventTracker(); // form info const { handleSubmit, control, reset, - formState: { errors, isSubmitting }, + formState: { errors, isSubmitting, dirtyFields }, watch, setValue, setFocus, @@ -57,7 +59,14 @@ export const CreateUpdateLabelInline = observer( if (!workspaceSlug || !projectId || isSubmitting) return; await createLabel(workspaceSlug.toString(), projectId.toString(), formData) - .then(() => { + .then((res) => { + captureEvent(LABEL_CREATED, { + label_id: res.id, + color: res.color, + parent: res.parent, + element: E_Labels, + state: "SUCCESS", + }); handleClose(); reset(defaultValues); }) @@ -76,9 +85,17 @@ export const CreateUpdateLabelInline = observer( // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain await updateLabel(workspaceSlug.toString(), projectId.toString(), labelToUpdate?.id!, formData) - .then(() => { + .then((res) => { reset(defaultValues); handleClose(); + captureEvent(LABEL_UPDATED, { + label_id: res.id, + color: res.color, + parent: res.parent, + change_details: Object.keys(dirtyFields), + element: E_Labels, + state: "SUCCESS", + }); }) .catch((error) => { setToast({ diff --git a/web/components/labels/delete-label-modal.tsx b/web/components/labels/delete-label-modal.tsx index cb1943f05..21794e746 100644 --- a/web/components/labels/delete-label-modal.tsx +++ b/web/components/labels/delete-label-modal.tsx @@ -7,8 +7,10 @@ import type { IIssueLabel } from "@plane/types"; import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { AlertModalCore } from "@/components/core"; +// constants +import { E_Labels, LABEL_DELETED, LABEL_GROUP_DELETED } from "constants/event-tracker"; // hooks -import { useLabel } from "@/hooks/store"; +import { useLabel, useEventTracker } from "@/hooks/store"; type Props = { isOpen: boolean; @@ -22,7 +24,8 @@ export const DeleteLabelModal: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; // store hooks - const { deleteLabel } = useLabel(); + const { deleteLabel, projectLabelsTree } = useLabel(); + const { captureEvent } = useEventTracker(); // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -39,6 +42,21 @@ export const DeleteLabelModal: React.FC = observer((props) => { await deleteLabel(workspaceSlug.toString(), projectId.toString(), data.id) .then(() => { handleClose(); + const labelChildCount = projectLabelsTree?.find((label) => label.id === data.id)?.children?.length || 0; + if (labelChildCount > 0) { + captureEvent(LABEL_GROUP_DELETED, { + group_id: data.id, + children_count: labelChildCount, + element: E_Labels, + state: "SUCCESS", + }); + } else { + captureEvent(LABEL_DELETED, { + label_id: data.id, + element: E_Labels, + state: "SUCCESS", + }); + } }) .catch((err) => { setIsDeleteLoading(false); diff --git a/web/components/labels/label-drag-n-drop-HOC.tsx b/web/components/labels/label-drag-n-drop-HOC.tsx index 3898d6b7a..1f4d58058 100644 --- a/web/components/labels/label-drag-n-drop-HOC.tsx +++ b/web/components/labels/label-drag-n-drop-HOC.tsx @@ -1,6 +1,10 @@ import { MutableRefObject, useEffect, useRef, useState } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; -import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { + ElementDragPayload, + draggable, + dropTargetForElements, +} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview"; import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; @@ -10,6 +14,10 @@ import { createRoot } from "react-dom/client"; import { IIssueLabel, InstructionType } from "@plane/types"; // ui import { DropIndicator } from "@plane/ui"; +// constants +import { LABEL_ADDED_G, LABEL_REMOVED_G } from "@/constants/event-tracker"; +// hooks +import { useEventTracker } from "@/hooks/store"; // components import { LabelName } from "./label-block/label-name"; import { TargetData, getCanDrop, getInstructionFromPayload } from "./label-utils"; @@ -52,10 +60,28 @@ export const LabelDndHOC = observer((props: Props) => { const [isDragging, setIsDragging] = useState(false); const [instruction, setInstruction] = useState(undefined); + // hooks + const { captureEvent } = useEventTracker(); // refs const labelRef = useRef(null); const dragHandleRef = useRef(null); + const captureLabelDropEvent = (source: TargetData, destination: TargetData) => { + + if (source?.parentId != destination.id) { + source?.parentId && + captureEvent(LABEL_REMOVED_G, { + group_id: source?.parentId, + child_id: source?.id, + }); + + captureEvent(LABEL_ADDED_G, { + group_id: destination.id, + child_id: source?.id, + }); + } + }; + useEffect(() => { const element = labelRef.current; const dragHandleElement = dragHandleRef.current; @@ -144,6 +170,8 @@ export const LabelDndHOC = observer((props: Props) => { const sourceData = source.data as TargetData; if (sourceData.id) onDrop(sourceData.id as string, parentId, droppedLabelId, dropAtEndOfList); + + captureLabelDropEvent(sourceData, dropTargetData); }, }) ); diff --git a/web/components/labels/project-setting-label-item.tsx b/web/components/labels/project-setting-label-item.tsx index 0a596fe41..8aaf5b3d3 100644 --- a/web/components/labels/project-setting-label-item.tsx +++ b/web/components/labels/project-setting-label-item.tsx @@ -4,7 +4,9 @@ import { X, Pencil } from "lucide-react"; // types import { IIssueLabel } from "@plane/types"; // hooks -import { useLabel } from "@/hooks/store"; +import { useLabel, useEventTracker } from "@/hooks/store"; +// constants +import { E_Labels, LABEL_REMOVED_G } from "@/constants/event-tracker"; // components import { CreateUpdateLabelInline } from "./create-update-label-inline"; import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block"; @@ -34,6 +36,7 @@ export const ProjectSettingLabelItem: React.FC = (props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { updateLabel } = useLabel(); + const { captureEvent } = useEventTracker(); const removeFromGroup = (label: IIssueLabel) => { if (!workspaceSlug || !projectId) return; @@ -41,6 +44,12 @@ export const ProjectSettingLabelItem: React.FC = (props) => { updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, { parent: null, }); + + captureEvent(LABEL_REMOVED_G, { + group_id: label.id, + child_id: label.id, + element: E_Labels, + }); }; const customMenuItems: ICustomMenuItem[] = [ diff --git a/web/constants/event-tracker.ts b/web/constants/event-tracker.ts index 7b5a5c6ca..bb34ccbd3 100644 --- a/web/constants/event-tracker.ts +++ b/web/constants/event-tracker.ts @@ -175,6 +175,13 @@ export const ISSUE_OPENED = "Issue opened"; export const STATE_CREATED = "State created"; export const STATE_UPDATED = "State updated"; export const STATE_DELETED = "State deleted"; +// Label Events +export const LABEL_CREATED = "Label created"; +export const LABEL_UPDATED = "Label updated"; +export const LABEL_DELETED = "Label deleted"; +export const LABEL_GROUP_DELETED = "Label group deleted"; +export const LABEL_ADDED_G = "Label added to group"; +export const LABEL_REMOVED_G = "Label removed from group"; // Project Page Events export const PAGE_CREATED = "Page created"; export const PAGE_UPDATED = "Page updated"; @@ -225,4 +232,5 @@ export const ARCHIVED_NOTIFICATIONS = "Archived notifications viewed"; export const GROUP_WORKSPACE = "Workspace_metrics"; // Elements -export const E_STATES = "Project states page"; \ No newline at end of file +export const E_STATES = "Project states page"; +export const E_Labels = "Project labels page";