diff --git a/web/components/cycles/active-cycle/cycle-stats.tsx b/web/components/cycles/active-cycle/cycle-stats.tsx index 2eb128763..b078cbd77 100644 --- a/web/components/cycles/active-cycle/cycle-stats.tsx +++ b/web/components/cycles/active-cycle/cycle-stats.tsx @@ -14,13 +14,14 @@ import { StateDropdown } from "@/components/dropdowns"; import { EmptyState } from "@/components/empty-state"; // constants import { EmptyStateType } from "@/constants/empty-state"; +import { ACYCLE_TAB_CHANGED } from "@/constants/event-tracker"; import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys"; import { EIssuesStoreType } from "@/constants/issue"; // helper import { cn } from "@/helpers/common.helper"; import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; // hooks -import { useIssues, useProject } from "@/hooks/store"; +import { useEventTracker, useIssues, useProject } from "@/hooks/store"; import useLocalStorage from "@/hooks/use-local-storage"; export type ActiveCycleStatsProps = { @@ -51,6 +52,7 @@ export const ActiveCycleStats: FC = observer((props) => { } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectDetails } = useProject(); + const { captureEvent } = useEventTracker(); const { data: activeCycleIssues } = useSWR( workspaceSlug && projectId && cycle.id ? CYCLE_ISSUES_WITH_PARAMS(cycle.id, { priority: "urgent,high" }) : null, @@ -65,17 +67,25 @@ export const ActiveCycleStats: FC = observer((props) => { as={Fragment} defaultIndex={currentValue(tab)} onChange={(i) => { + let tab: string; switch (i) { case 0: - return setTab("Priority-Issues"); + tab = "Priority-Issues"; + break; case 1: - return setTab("Assignees"); + tab = "Assignees"; + break; case 2: - return setTab("Labels"); - + tab = "Labels"; + break; default: - return setTab("Priority-Issues"); + tab = "Priority-Issues"; + break; } + setTab(tab); + captureEvent(ACYCLE_TAB_CHANGED, { + tab: tab, + }); }} > { // router @@ -21,6 +22,7 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => { const { workspaceSlug, projectId } = router.query; // hooks const { fetchArchivedCycles, currentProjectArchivedCycleIds, loader } = useCycle(); + const { captureEvent } = useEventTracker(); // cycle filters hook const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useCycleFilter(); // derived values @@ -43,6 +45,11 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => { if (!value) newValues = []; else newValues = newValues.filter((val) => val !== value); + captureEvent(CYCLES_FILTER_REMOVED, { + filter_type: key, + filter_property: value, + current_filters: currentProjectArchivedFilters, + }); updateFilters(projectId.toString(), { [key]: newValues }, "archived"); }; diff --git a/web/components/cycles/cycles-list-mobile-header.tsx b/web/components/cycles/cycles-list-mobile-header.tsx index 590cb794f..3830ae6d1 100644 --- a/web/components/cycles/cycles-list-mobile-header.tsx +++ b/web/components/cycles/cycles-list-mobile-header.tsx @@ -5,13 +5,16 @@ import { CustomMenu } from "@plane/ui"; // icon // constants import { CYCLE_VIEW_LAYOUTS } from "@/constants/cycle"; +import { CYCLE_LAYOUT_CHANGED } from "@/constants/event-tracker"; // hooks -import { useCycleFilter, useProject } from "@/hooks/store"; +import { useCycleFilter, useEventTracker, useProject } from "@/hooks/store"; const CyclesListMobileHeader = observer(() => { - const { currentProjectDetails } = useProject(); // hooks + const { currentProjectDetails } = useProject(); const { updateDisplayFilters } = useCycleFilter(); + const { captureEvent } = useEventTracker(); + return (
{ updateDisplayFilters(currentProjectDetails!.id, { layout: layout.key, }); + captureEvent(CYCLE_LAYOUT_CHANGED, { + layout: layout.key, + }); }} className="flex items-center gap-2" > diff --git a/web/components/cycles/cycles-view-header.tsx b/web/components/cycles/cycles-view-header.tsx index 50cf6df97..36e363b4c 100644 --- a/web/components/cycles/cycles-view-header.tsx +++ b/web/components/cycles/cycles-view-header.tsx @@ -11,10 +11,16 @@ import { CycleFiltersSelection } from "@/components/cycles"; import { FiltersDropdown } from "@/components/issues"; // constants import { CYCLE_TABS_LIST, CYCLE_VIEW_LAYOUTS } from "@/constants/cycle"; +import { + CYCLES_FILTER_APPLIED, + CYCLES_FILTER_REMOVED, + CYCLE_LAYOUT_CHANGED, + CYCLE_TAB_CHANGED, +} from "@/constants/event-tracker"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useCycleFilter } from "@/hooks/store"; +import { useCycleFilter, useEventTracker } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -36,6 +42,7 @@ export const CyclesViewHeader: React.FC = observer((props) => { updateSearchQuery, } = useCycleFilter(); const { isMobile } = usePlatformOS(); + const { captureEvent } = useEventTracker(); // states const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false); // outside click detector hook @@ -48,7 +55,7 @@ export const CyclesViewHeader: React.FC = observer((props) => { const handleFilters = useCallback( (key: keyof TCycleFilters, value: string | string[]) => { if (!projectId) return; - const newValues = currentProjectFilters?.[key] ?? []; + const newValues = Array.from(currentProjectFilters?.[key] ?? []); if (Array.isArray(value)) value.forEach((val) => { @@ -59,7 +66,14 @@ export const CyclesViewHeader: React.FC = observer((props) => { if (currentProjectFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); else newValues.push(value); } - + captureEvent( + (currentProjectFilters?.[key] ?? []).length > newValues.length ? CYCLES_FILTER_REMOVED : CYCLES_FILTER_APPLIED, + { + filter_type: key, + filter_property: value, + current_filters: currentProjectFilters, + } + ); updateFilters(projectId, { [key]: newValues }); }, [currentProjectFilters, projectId, updateFilters] @@ -81,6 +95,11 @@ export const CyclesViewHeader: React.FC = observer((props) => { {CYCLE_TABS_LIST.map((tab) => ( + captureEvent(CYCLE_TAB_CHANGED, { + tab: tab.key, + }) + } className={({ selected }) => `border-b-2 p-4 text-sm font-medium outline-none ${ selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent" @@ -146,11 +165,14 @@ export const CyclesViewHeader: React.FC = observer((props) => { className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${ activeLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : "" }`} - onClick={() => + onClick={() => { updateDisplayFilters(projectId, { layout: layout.key, - }) - } + }); + captureEvent(CYCLE_LAYOUT_CHANGED, { + layout: layout.key, + }); + }} > = observer((props) => { const [archiveCycleModal, setArchiveCycleModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); // store hooks - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent } = useEventTracker(); const { membership: { currentWorkspaceAllProjectsRole }, } = useUser(); @@ -59,7 +59,7 @@ export const CycleQuickActions: React.FC = observer((props) => { const handleEditCycle = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - setTrackElement(E_CYCLES_LIST_LAYOUT); + setTrackElement(E_CYCLES); setUpdateModal(true); }; @@ -67,6 +67,10 @@ export const CycleQuickActions: React.FC = observer((props) => { e.preventDefault(); e.stopPropagation(); setArchiveCycleModal(true); + captureEvent(CYCLE_ARCHIVED, { + cycleId: cycleId, + element: E_CYCLES, + }); }; const handleRestoreCycle = async (e: React.MouseEvent) => { @@ -79,6 +83,10 @@ export const CycleQuickActions: React.FC = observer((props) => { title: "Restore success", message: "Your cycle can be found in project cycles.", }); + captureEvent(CYCLE_RESTORED, { + cycleId: cycleId, + element: E_CYCLES, + }); router.push(`/${workspaceSlug}/projects/${projectId}/archives/cycles`); }) .catch(() => @@ -93,7 +101,7 @@ export const CycleQuickActions: React.FC = observer((props) => { const handleDeleteCycle = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - setTrackElement(E_CYCLES_LIST_LAYOUT); + setTrackElement(E_CYCLES); setDeleteModal(true); }; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 46612f2da..62aab5d60 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -120,7 +120,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const newValues = Array.from(issueFilters?.filters?.[key] ?? []); if (Array.isArray(value)) { // this validation is majorly for the filter start_date, target_date custom @@ -133,9 +133,10 @@ export const CycleIssuesHeader: React.FC = observer(() => { else newValues.push(value); } + const event = newValues.length > (issueFilters?.filters?.[key] ?? []).length ? FILTER_APPLIED : FILTER_REMOVED; updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId).then(() => captureIssuesFilterEvent({ - eventName: (issueFilters?.filters?.[key] ?? []).length > newValues.length ? FILTER_REMOVED : FILTER_APPLIED, + eventName: event, payload: { routePath: router.asPath, filters: issueFilters, diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index 8cb12aa0d..f1701f1f1 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -48,7 +48,7 @@ export const GlobalIssuesHeader: React.FC = observer(() => { const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !globalViewId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const newValues = Array.from(issueFilters?.filters?.[key] ?? []); if (Array.isArray(value)) { // this validation is majorly for the filter start_date, target_date custom @@ -61,6 +61,7 @@ export const GlobalIssuesHeader: React.FC = observer(() => { else newValues.push(value); } + const event = (issueFilters?.filters?.[key] ?? []).length > newValues.length ? FILTER_REMOVED : FILTER_APPLIED; updateFilters( workspaceSlug.toString(), undefined, @@ -69,7 +70,7 @@ export const GlobalIssuesHeader: React.FC = observer(() => { globalViewId.toString() ).then(() => { captureIssuesFilterEvent({ - eventName: (issueFilters?.filters?.[key] ?? []).length > newValues.length ? FILTER_REMOVED : FILTER_APPLIED, + eventName: event, payload: { routePath: router.asPath, filters: issueFilters, diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 891dd70bc..3a65cca8f 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -121,7 +121,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const newValues = Array.from(issueFilters?.filters?.[key] ?? []); if (Array.isArray(value)) { // this validation is majorly for the filter start_date, target_date custom @@ -134,9 +134,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => { else newValues.push(value); } + const event = (issueFilters?.filters?.[key] ?? []).length > newValues.length ? FILTER_REMOVED : FILTER_APPLIED; updateFilters(projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues }).then(() => { captureIssuesFilterEvent({ - eventName: (issueFilters?.filters?.[key] ?? []).length > newValues.length ? FILTER_REMOVED : FILTER_APPLIED, + eventName: event, payload: { routePath: router.asPath, filters: issueFilters, diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 67e064c99..dc6d50db9 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -67,7 +67,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const newValues = Array.from(issueFilters?.filters?.[key] ?? []); if (Array.isArray(value)) { // this validation is majorly for the filter start_date, target_date custom @@ -80,9 +80,10 @@ export const ProjectIssuesHeader: React.FC = observer(() => { else newValues.push(value); } + const event = (issueFilters?.filters?.[key] ?? []).length > newValues.length ? FILTER_REMOVED : FILTER_APPLIED; updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }).then(() => captureIssuesFilterEvent({ - eventName: (issueFilters?.filters?.[key] ?? []).length > newValues.length ? FILTER_REMOVED : FILTER_APPLIED, + eventName: event, payload: { routePath: router.asPath, filters: issueFilters, diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 6ea73586e..ca4caf156 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -86,7 +86,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !projectId || !viewId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const newValues = Array.from(issueFilters?.filters?.[key] ?? []); if (Array.isArray(value)) { // this validation is majorly for the filter start_date, target_date custom @@ -98,24 +98,24 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); else newValues.push(value); } - + const event = (issueFilters?.filters?.[key] ?? []).length > newValues.length ? FILTER_REMOVED : FILTER_APPLIED; updateFilters( workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues }, viewId.toString() - ).then(() => { + ).then(() => captureIssuesFilterEvent({ - eventName: (issueFilters?.filters?.[key] ?? []).length > newValues.length ? FILTER_REMOVED : FILTER_APPLIED, + eventName: event, payload: { routePath: router.asPath, filters: issueFilters, filter_property: value, filter_type: key, }, - }); - }); + }) + ); }, [workspaceSlug, projectId, viewId, issueFilters, updateFilters, captureIssuesFilterEvent, router.asPath] ); diff --git a/web/components/modules/quick-actions.tsx b/web/components/modules/quick-actions.tsx index 010a3caf6..44f0fdfeb 100644 --- a/web/components/modules/quick-actions.tsx +++ b/web/components/modules/quick-actions.tsx @@ -13,7 +13,7 @@ import { EUserProjectRoles } from "@/constants/project"; // helpers import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useModule, useEventTracker, useUser, useModuleFilter } from "@/hooks/store"; +import { useModule, useEventTracker, useUser } from "@/hooks/store"; type Props = { moduleId: string; diff --git a/web/constants/event-tracker.ts b/web/constants/event-tracker.ts index 71ae7d039..173fb7f34 100644 --- a/web/constants/event-tracker.ts +++ b/web/constants/event-tracker.ts @@ -196,6 +196,15 @@ export const CYCLE_UPDATED = "Cycle updated"; export const CYCLE_DELETED = "Cycle deleted"; export const CYCLE_FAVORITED = "Cycle favorited"; export const CYCLE_UNFAVORITED = "Cycle unfavorited"; +export const CYCLES_LAYOUT_CHANGED = "cycles layout changed"; +export const CYCLES_SORT_UPDATED = "cycle sort updated"; +export const CYCLES_FILTER_APPLIED = "Cycles filter applied"; +export const CYCLES_FILTER_REMOVED = "Cycles filter removed"; +export const CYCLE_ARCHIVED = "Cycle archived"; +export const CYCLE_RESTORED = "Cycle restored"; +export const CYCLE_LAYOUT_CHANGED = "Cycle layout changed"; +export const CYCLE_TAB_CHANGED = "Cycle tab changed"; +export const ACYCLE_TAB_CHANGED = "Active cycle tab changed"; // Module Events export const MODULE_CREATED = "Module created"; export const MODULE_UPDATED = "Module updated"; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 304c44d43..d73fb8de6 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -18,7 +18,7 @@ import { CyclesHeader } from "@/components/headers"; import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui"; import { CYCLE_TABS_LIST } from "@/constants/cycle"; import { EmptyStateType } from "@/constants/empty-state"; -import { E_CYCLES_EMPTY_STATE } from "@/constants/event-tracker"; +import { CYCLES_FILTER_REMOVED, E_CYCLES_EMPTY_STATE } from "@/constants/event-tracker"; import { calculateTotalFilters } from "@/helpers/filter.helper"; import { useEventTracker, useCycle, useProject, useCycleFilter } from "@/hooks/store"; // layouts @@ -37,6 +37,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { const { setTrackElement } = useEventTracker(); const { currentProjectCycleIds, loader } = useCycle(); const { getProjectById, currentProjectDetails } = useProject(); + const { captureEvent } = useEventTracker(); // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; @@ -58,6 +59,11 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { if (!value) newValues = []; else newValues = newValues.filter((val) => val !== value); + captureEvent(CYCLES_FILTER_REMOVED, { + filter_type: key, + filter_property: value, + current_filters: currentProjectFilters, + }); updateFilters(projectId.toString(), { [key]: newValues }); };