diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index 3cd578847..30b19b2e0 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -9,7 +9,8 @@ import { ProjectLogo } from "@/components/project"; import { ViewListHeader } from "@/components/views"; import { EUserProjectRoles } from "@/constants/project"; // constants -import { useCommandPalette, useProject, useUser } from "@/hooks/store"; +import { E_VIEWS } from "@/constants/event-tracker"; +import { useCommandPalette, useProject, useUser, useEventTracker } from "@/hooks/store"; export const ProjectViewsHeader: React.FC = observer(() => { // router @@ -21,6 +22,7 @@ export const ProjectViewsHeader: React.FC = observer(() => { membership: { currentProjectRole }, } = useUser(); const { currentProjectDetails } = useProject(); + const { setTrackElement } = useEventTracker(); const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); @@ -60,7 +62,14 @@ export const ProjectViewsHeader: React.FC = observer(() => {
-
diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 049867f84..748b6be88 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -3,14 +3,14 @@ import isEqual from "lodash/isEqual"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { IIssueFilterOptions } from "@plane/types"; -// hooks import { Button } from "@plane/ui"; -import { AppliedFiltersList } from "@/components/issues"; -import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; -import { useIssues, useLabel, useProjectState, useProjectView } from "@/hooks/store"; // components -// ui -// types +import { AppliedFiltersList } from "@/components/issues"; +// constants +import { VIEW_UPDATED } from "constants/event-tracker"; +import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; +// hooks +import { useIssues, useLabel, useProjectState, useProjectView, useEventTracker } from "@/hooks/store"; export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { // router @@ -23,6 +23,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { const { projectLabels } = useLabel(); const { projectStates } = useProjectState(); const { viewMap, updateView } = useProjectView(); + const { captureEvent } = useEventTracker(); // derived values const viewDetails = viewId ? viewMap[viewId.toString()] : null; const userFilters = issueFilters?.filters; diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index 38524f83b..d9cafb7a8 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -7,8 +7,10 @@ import { IProjectView } from "@plane/types"; import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { AlertModalCore } from "@/components/core"; +// constants +import { E_VIEWS, VIEW_DELETED } from "constants/event-tracker"; // hooks -import { useProjectView } from "@/hooks/store"; +import { useProjectView, useEventTracker } from "@/hooks/store"; type Props = { data: IProjectView; @@ -25,6 +27,7 @@ export const DeleteProjectViewModal: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store hooks const { deleteView } = useProjectView(); + const { captureEvent } = useEventTracker(); const handleClose = () => { onClose(); @@ -39,20 +42,29 @@ export const DeleteProjectViewModal: React.FC = observer((props) => { await deleteView(workspaceSlug.toString(), projectId.toString(), data.id) .then(() => { handleClose(); - + captureEvent(VIEW_DELETED, { + view_id: data.id, + element: E_VIEWS, + state: "SUCCESS", + }); setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View deleted successfully.", }); }) - .catch(() => + .catch(() => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", message: "View could not be deleted. Please try again.", - }) - ) + }); + captureEvent(VIEW_DELETED, { + view_id: data.id, + element: E_VIEWS, + state: "FAILED", + }); + }) .finally(() => { setIsDeleteLoading(false); }); diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index ab2d74e06..b87d58d60 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -1,5 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react"; +import { useRouter } from "next/router"; // types import { IProjectView } from "@plane/types"; // ui @@ -7,8 +8,10 @@ import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; import { ProjectViewForm } from "@/components/views"; +// constants +import { VIEW_CREATED, VIEW_UPDATED } from "constants/event-tracker"; // hooks -import { useProjectView } from "@/hooks/store"; +import { useProjectView, useEventTracker } from "@/hooks/store"; type Props = { data?: IProjectView | null; @@ -21,8 +24,11 @@ type Props = { export const CreateUpdateProjectViewModal: FC = observer((props) => { const { data, isOpen, onClose, preLoadedData, workspaceSlug, projectId } = props; + // router + const router = useRouter(); // store hooks const { createView, updateView } = useProjectView(); + const { captureEvent, trackElement } = useEventTracker(); const handleClose = () => { onClose(); @@ -30,33 +36,59 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { const handleCreateView = async (payload: IProjectView) => { await createView(workspaceSlug, projectId, payload) - .then(() => { + .then((res) => { handleClose(); setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", message: "View created successfully.", }); + captureEvent(VIEW_CREATED, { + view_id: res.id, + filters: res.filters, + // element_id: getElementFromPath(router.asPath), + // element: trackElement ? trackElement : getElementIdFromPath(router.asPath), + state: "SUCCESS", + }); }) - .catch(() => + .catch(() => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong. Please try again.", - }) - ); + }); + captureEvent(VIEW_CREATED, { + // element_id: getElementFromPath(router.asPath), + // element: trackElement ? trackElement : getElementIdFromPath(router.asPath), + state: "FAILED", + }); + }); }; const handleUpdateView = async (payload: IProjectView) => { await updateView(workspaceSlug, projectId, data?.id as string, payload) - .then(() => handleClose()) - .catch((err) => + .then((res) => { + handleClose(); + captureEvent(VIEW_UPDATED, { + view_id: res.id, + filters: res.filters, + // element: trackElement ? trackElement : getElementIdFromPath(router.asPath), + state: "SUCCESS", + }); + }) + .catch((err) => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", message: err?.detail ?? "Something went wrong. Please try again.", - }) - ); + }); + captureEvent(VIEW_UPDATED, { + view_id: payload.id, + filters: payload.filters, + // element: trackElement ? trackElement : getElementIdFromPath(router.asPath), + state: "FAILED", + }); + }); }; const handleFormSubmit = async (formData: IProjectView) => { diff --git a/web/components/views/quick-actions.tsx b/web/components/views/quick-actions.tsx index f3f9353e5..c8cfb73e3 100644 --- a/web/components/views/quick-actions.tsx +++ b/web/components/views/quick-actions.tsx @@ -8,12 +8,13 @@ import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from // components import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "@/components/views"; // constants +import { E_VIEWS } from "@/constants/event-tracker"; import { EUserProjectRoles } from "@/constants/project"; // helpers import { cn } from "@/helpers/common.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useUser } from "@/hooks/store"; +import { useUser, useEventTracker } from "@/hooks/store"; type Props = { parentRef: React.RefObject; @@ -31,6 +32,7 @@ export const ViewQuickActions: React.FC = observer((props) => { const { membership: { currentProjectRole }, } = useUser(); + const { setTrackElement } = useEventTracker(); // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -48,7 +50,10 @@ export const ViewQuickActions: React.FC = observer((props) => { const MENU_ITEMS: TContextMenuItem[] = [ { key: "edit", - action: () => setCreateUpdateViewModal(true), + action: () => { + setTrackElement(E_VIEWS); + setCreateUpdateViewModal(true); + }, title: "Edit", icon: Pencil, shouldRender: isEditingAllowed, @@ -67,7 +72,10 @@ export const ViewQuickActions: React.FC = observer((props) => { }, { key: "delete", - action: () => setDeleteViewModal(true), + action: () => { + setTrackElement(E_VIEWS); + setDeleteViewModal(true); + }, title: "Delete", icon: Trash2, shouldRender: isEditingAllowed, diff --git a/web/components/views/view-list-item-action.tsx b/web/components/views/view-list-item-action.tsx index 80ba5ba0c..4d61ecdff 100644 --- a/web/components/views/view-list-item-action.tsx +++ b/web/components/views/view-list-item-action.tsx @@ -7,11 +7,12 @@ import { IProjectView } from "@plane/types"; import { FavoriteStar } from "@/components/core"; import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views"; // constants +import { E_VIEWS, VIEW_FAVORITED, VIEW_UNFAVORITED } from "constants/event-tracker"; import { EUserProjectRoles } from "@/constants/project"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useMember, useProjectView, useUser } from "@/hooks/store"; +import { useMember, useProjectView, useUser, useEventTracker } from "@/hooks/store"; import { ButtonAvatars } from "../dropdowns/member/avatar"; type Props = { @@ -33,6 +34,7 @@ export const ViewListItemAction: FC = observer((props) => { } = useUser(); const { addViewToFavorites, removeViewFromFavorites } = useProjectView(); const { getUserDetails } = useMember(); + const { captureEvent } = useEventTracker(); // derived values const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -43,13 +45,23 @@ export const ViewListItemAction: FC = observer((props) => { const handleAddToFavorites = () => { if (!workspaceSlug || !projectId) return; - addViewToFavorites(workspaceSlug.toString(), projectId.toString(), view.id); + addViewToFavorites(workspaceSlug.toString(), projectId.toString(), view.id).then(() => { + captureEvent(VIEW_FAVORITED, { + view_id: view.id, + element: E_VIEWS, + }); + }); }; const handleRemoveFromFavorites = () => { if (!workspaceSlug || !projectId) return; - removeViewFromFavorites(workspaceSlug.toString(), projectId.toString(), view.id); + removeViewFromFavorites(workspaceSlug.toString(), projectId.toString(), view.id).then(() => { + captureEvent(VIEW_UNFAVORITED, { + view_id: view.id, + element: E_VIEWS, + }); + }); }; const createdByDetails = view.created_by ? getUserDetails(view.created_by) : undefined; diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index ea300678a..5f598e5af 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -6,13 +6,15 @@ import { ViewListLoader } from "@/components/ui"; import { ProjectViewListItem } from "@/components/views"; // constants import { EmptyStateType } from "@/constants/empty-state"; +import { E_VIEWS_EMPTY_STATE } from "@/constants/event-tracker"; // hooks -import { useCommandPalette, useProjectView } from "@/hooks/store"; +import { useCommandPalette, useProjectView, useEventTracker } from "@/hooks/store"; export const ProjectViewsList = observer(() => { // store hooks const { toggleCreateViewModal } = useCommandPalette(); const { projectViewIds, getViewById, loader, searchQuery } = useProjectView(); + const { setTrackElement } = useEventTracker(); if (loader || !projectViewIds) return ; @@ -34,7 +36,13 @@ export const ProjectViewsList = observer(() => {
) : ( - toggleCreateViewModal(true)} /> + { + setTrackElement(E_VIEWS_EMPTY_STATE); + toggleCreateViewModal(true); + }} + /> )} ); diff --git a/web/constants/event-tracker.ts b/web/constants/event-tracker.ts index 7edfccba5..5a279cab8 100644 --- a/web/constants/event-tracker.ts +++ b/web/constants/event-tracker.ts @@ -170,6 +170,12 @@ export const ISSUE_DELETED = "Issue deleted"; export const ISSUE_ARCHIVED = "Issue archived"; export const ISSUE_RESTORED = "Issue restored"; export const ISSUE_OPENED = "Issue opened"; +// Project View Events +export const VIEW_CREATED = "View created"; +export const VIEW_UPDATED = "View updated"; +export const VIEW_DELETED = "View deleted"; +export const VIEW_FAVORITED = "View favorited"; +export const VIEW_UNFAVORITED = "View unfavorited"; // Project State Events export const STATE_CREATED = "State created"; export const STATE_UPDATED = "State updated"; @@ -222,3 +228,7 @@ export const SNOOZED_NOTIFICATIONS = "Snoozed notifications viewed"; export const ARCHIVED_NOTIFICATIONS = "Archived notifications viewed"; // Groups export const GROUP_WORKSPACE = "Workspace_metrics"; + +// Elements +export const E_VIEWS = "Views page"; +export const E_VIEWS_EMPTY_STATE = "Views empty state"; \ No newline at end of file