From 7143c98b2eca9548fa1dc7186deb8379a2df9818 Mon Sep 17 00:00:00 2001 From: LAKHAN BAHETI Date: Fri, 23 Feb 2024 17:10:15 +0530 Subject: [PATCH] chore: added issues filters & layout related events --- web/components/headers/cycle-issues.tsx | 84 ++++++++++++++++--- web/components/headers/global-issues.tsx | 70 ++++++++++++++-- web/components/headers/module-issues.tsx | 84 ++++++++++++++++--- web/components/headers/project-issues.tsx | 83 +++++++++++++++--- .../headers/project-view-issues.tsx | 83 +++++++++++++++--- .../header/filters/filters-selection.tsx | 13 ++- .../roots/all-issue-layout-root.tsx | 6 +- web/constants/event-tracker.ts | 41 +++++++-- web/store/event-tracker.store.ts | 40 ++++++++- 9 files changed, 445 insertions(+), 59 deletions(-) diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 0a36c133b..74306df4c 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -31,6 +31,16 @@ import { renderEmoji } from "helpers/emoji.helper"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { + DP_APPLIED, + DP_REMOVED, + elementFromPath, + FILTER_APPLIED, + FILTER_REMOVED, + FILTER_SEARCHED, + LAYOUT_CHANGED, + LP_UPDATED, +} from "constants/event-tracker"; import { EUserProjectRoles } from "constants/project"; import { cn } from "helpers/common.helper"; import { CycleMobileHeader } from "components/cycles/cycle-mobile-header"; @@ -74,7 +84,8 @@ export const CycleIssuesHeader: React.FC = observer(() => { const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent, captureIssuesFilterEvent, captureIssuesDisplayFilterEvent } = + useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); @@ -97,7 +108,13 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId).then(() => + captureEvent(LAYOUT_CHANGED, { + layout: layout, + element: elementFromPath(router.asPath), + element_id: cycleId, + }) + ); }, [workspaceSlug, projectId, cycleId, updateFilters] ); @@ -106,17 +123,31 @@ export const CycleIssuesHeader: React.FC = observer(() => { (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !projectId) return; const newValues = issueFilters?.filters?.[key] ?? []; - + let isFilterRemoved = false; if (Array.isArray(value)) { value.forEach((val) => { if (!newValues.includes(val)) newValues.push(val); + else isFilterRemoved = true; }); } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (issueFilters?.filters?.[key]?.includes(value)) { + newValues.splice(newValues.indexOf(value), 1); + isFilterRemoved = true; + } else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId).then(() => + captureIssuesFilterEvent({ + eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED, + payload: { + path: router.asPath, + filters: issueFilters, + element_id: cycleId, + filter_property: value, + filter_type: key, + }, + }) + ); }, [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] ); @@ -124,17 +155,39 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId).then( + () => + captureIssuesDisplayFilterEvent({ + eventName: LP_UPDATED, + payload: { + property_type: Object.keys(updatedDisplayFilter).join(","), + property: Object.values(updatedDisplayFilter)?.[0], + path: router.asPath, + filters: issueFilters, + element_id: cycleId, + }, + }) + ); }, - [workspaceSlug, projectId, cycleId, updateFilters] + [workspaceSlug, projectId, cycleId, updateFilters, issueFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId).then(() => + captureIssuesDisplayFilterEvent({ + eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED, + payload: { + display_property: Object.keys(property).join(","), + path: router.asPath, + filters: issueFilters, + element_id: cycleId, + }, + }) + ); }, - [workspaceSlug, projectId, cycleId, updateFilters] + [workspaceSlug, projectId, cycleId, updateFilters, issueFilters] ); // derived values @@ -233,6 +286,17 @@ export const CycleIssuesHeader: React.FC = observer(() => { labels={projectLabels} memberIds={projectMemberIds ?? undefined} states={projectStates} + onSearchCapture={() => + captureIssuesFilterEvent({ + eventName: FILTER_SEARCHED, + payload: { + path: router.asPath, + current_filters: issueFilters?.filters, + layout: issueFilters?.displayFilters?.layout, + element_id: cycleId, + }, + }) + } /> diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index 3c40cbbff..24101a71c 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useLabel, useMember, useUser, useIssues } from "hooks/store"; +import { useLabel, useMember, useUser, useIssues, useEventTracker } from "hooks/store"; // components import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues"; import { CreateUpdateWorkspaceViewModal } from "components/workspace"; @@ -18,6 +18,16 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserWorkspaceRoles } from "constants/workspace"; +import { + DP_APPLIED, + DP_REMOVED, + elementFromPath, + FILTER_APPLIED, + FILTER_REMOVED, + FILTER_SEARCHED, + LAYOUT_CHANGED, + LP_UPDATED, +} from "constants/event-tracker"; const GLOBAL_VIEW_LAYOUTS = [ { key: "list", title: "List", link: "/workspace-views", icon: List }, @@ -46,6 +56,7 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { const { workspace: { workspaceMemberIds }, } = useMember(); + const { captureIssuesFilterEvent, captureEvent, captureIssuesDisplayFilterEvent } = useEventTracker(); const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined; @@ -53,14 +64,18 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !globalViewId) return; const newValues = issueFilters?.filters?.[key] ?? []; + let isFilterRemoved = false; if (Array.isArray(value)) { value.forEach((val) => { if (!newValues.includes(val)) newValues.push(val); + else isFilterRemoved = true; }); } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (issueFilters?.filters?.[key]?.includes(value)) { + isFilterRemoved = true; + newValues.splice(newValues.indexOf(value), 1); + } else newValues.push(value); } updateFilters( @@ -69,7 +84,18 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { EIssueFilterType.FILTERS, { [key]: newValues }, globalViewId.toString() - ); + ).then(() => { + captureIssuesFilterEvent({ + eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED, + payload: { + path: router.asPath, + filters: issueFilters, + element_id: globalViewId, + filter_property: value, + filter_type: key, + }, + }); + }); }, [workspaceSlug, issueFilters, updateFilters, globalViewId] ); @@ -83,9 +109,20 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, globalViewId.toString() + ).then(() => + captureIssuesDisplayFilterEvent({ + eventName: LP_UPDATED, + payload: { + property_type: Object.keys(updatedDisplayFilter).join(","), + property: Object.values(updatedDisplayFilter)?.[0], + path: router.asPath, + filters: issueFilters, + element_id: globalViewId, + }, + }) ); }, - [workspaceSlug, updateFilters, globalViewId] + [workspaceSlug, updateFilters, globalViewId, issueFilters] ); const handleDisplayProperties = useCallback( @@ -97,9 +134,19 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { EIssueFilterType.DISPLAY_PROPERTIES, property, globalViewId.toString() + ).then(() => + captureIssuesDisplayFilterEvent({ + eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED, + payload: { + display_property: Object.keys(property).join(","), + path: router.asPath, + filters: issueFilters, + element_id: globalViewId, + }, + }) ); }, - [workspaceSlug, updateFilters, globalViewId] + [workspaceSlug, updateFilters, globalViewId, issueFilters] ); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; @@ -160,6 +207,17 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { handleFiltersUpdate={handleFiltersUpdate} labels={workspaceLabels ?? undefined} memberIds={workspaceMemberIds ?? undefined} + onSearchCapture={() => + captureIssuesFilterEvent({ + eventName: FILTER_SEARCHED, + payload: { + path: router.asPath, + current_filters: issueFilters?.filters, + layout: issueFilters?.displayFilters?.layout, + element_id: globalViewId, + }, + }) + } /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index f722b506f..d516f708e 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -31,6 +31,16 @@ import { renderEmoji } from "helpers/emoji.helper"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { + DP_APPLIED, + DP_REMOVED, + elementFromPath, + FILTER_APPLIED, + FILTER_REMOVED, + FILTER_SEARCHED, + LAYOUT_CHANGED, + LP_UPDATED, +} from "constants/event-tracker"; import { EUserProjectRoles } from "constants/project"; import { cn } from "helpers/common.helper"; import { ModuleMobileHeader } from "components/modules/module-mobile-header"; @@ -77,7 +87,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent, captureIssuesFilterEvent, captureIssuesDisplayFilterEvent } = + useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); @@ -100,7 +111,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId).then(() => + captureEvent(LAYOUT_CHANGED, { + layout: layout, + element: elementFromPath(router.asPath), + element_id: moduleId, + }) + ); }, [workspaceSlug, projectId, moduleId, updateFilters] ); @@ -109,17 +126,31 @@ export const ModuleIssuesHeader: React.FC = observer(() => { (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !projectId) return; const newValues = issueFilters?.filters?.[key] ?? []; - + let isFilterRemoved = false; if (Array.isArray(value)) { value.forEach((val) => { if (!newValues.includes(val)) newValues.push(val); + else isFilterRemoved = true; }); } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (issueFilters?.filters?.[key]?.includes(value)) { + isFilterRemoved = true; + newValues.splice(newValues.indexOf(value), 1); + } else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId).then(() => { + captureIssuesFilterEvent({ + eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED, + payload: { + path: router.asPath, + filters: issueFilters, + element_id: moduleId, + filter_property: value, + filter_type: key, + }, + }); + }); }, [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] ); @@ -127,17 +158,39 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId).then( + () => + captureIssuesDisplayFilterEvent({ + eventName: LP_UPDATED, + payload: { + property_type: Object.keys(updatedDisplayFilter).join(","), + property: Object.values(updatedDisplayFilter)?.[0], + path: router.asPath, + filters: issueFilters, + element_id: moduleId, + }, + }) + ); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [workspaceSlug, projectId, moduleId, updateFilters, issueFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId).then(() => + captureIssuesDisplayFilterEvent({ + eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED, + payload: { + display_property: Object.keys(property).join(","), + path: router.asPath, + filters: issueFilters, + element_id: moduleId, + }, + }) + ); }, - [workspaceSlug, projectId, moduleId, updateFilters] + [workspaceSlug, projectId, moduleId, updateFilters, issueFilters] ); // derived values @@ -237,6 +290,17 @@ export const ModuleIssuesHeader: React.FC = observer(() => { labels={projectLabels} memberIds={projectMemberIds ?? undefined} states={projectStates} + onSearchCapture={() => + captureIssuesFilterEvent({ + eventName: FILTER_SEARCHED, + payload: { + path: router.asPath, + current_filters: issueFilters?.filters, + layout: issueFilters?.displayFilters?.layout, + element_id: moduleId, + }, + }) + } /> diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 43030c5c2..a54fe023a 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -23,6 +23,16 @@ import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { + DP_APPLIED, + DP_REMOVED, + elementFromPath, + FILTER_APPLIED, + FILTER_REMOVED, + FILTER_SEARCHED, + LAYOUT_CHANGED, + LP_UPDATED, +} from "constants/event-tracker"; // helper import { renderEmoji } from "helpers/emoji.helper"; import { EUserProjectRoles } from "constants/project"; @@ -45,7 +55,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); - const { setTrackElement } = useEventTracker(); + const { captureEvent, setTrackElement, captureIssuesFilterEvent, captureIssuesDisplayFilterEvent } = + useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); @@ -59,17 +70,31 @@ export const ProjectIssuesHeader: React.FC = observer(() => { (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !projectId) return; const newValues = issueFilters?.filters?.[key] ?? []; - + let isFilterRemoved = false; if (Array.isArray(value)) { value.forEach((val) => { if (!newValues.includes(val)) newValues.push(val); + else isFilterRemoved = true; }); } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (issueFilters?.filters?.[key]?.includes(value)) { + newValues.splice(newValues.indexOf(value), 1); + isFilterRemoved = true; + } else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }).then(() => + captureIssuesFilterEvent({ + eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED, + payload: { + path: router.asPath, + filters: issueFilters, + element_id: projectId, + filter_property: value, + filter_type: key, + }, + }) + ); }, [workspaceSlug, projectId, issueFilters, updateFilters] ); @@ -77,7 +102,13 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }).then(() => + captureEvent(LAYOUT_CHANGED, { + layout: layout, + element: elementFromPath(router.asPath), + element_id: projectId, + }) + ); }, [workspaceSlug, projectId, updateFilters] ); @@ -85,17 +116,38 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter).then(() => + captureIssuesDisplayFilterEvent({ + eventName: LP_UPDATED, + payload: { + property_type: Object.keys(updatedDisplayFilter).join(","), + property: Object.values(updatedDisplayFilter)?.[0], + path: router.asPath, + filters: issueFilters, + element_id: projectId, + }, + }) + ); }, - [workspaceSlug, projectId, updateFilters] + [workspaceSlug, projectId, updateFilters, issueFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property).then(() => { + captureIssuesDisplayFilterEvent({ + eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED, + payload: { + display_property: Object.keys(property).join(","), + path: router.asPath, + filters: issueFilters, + element_id: projectId, + }, + }); + }); }, - [workspaceSlug, projectId, updateFilters] + [workspaceSlug, projectId, updateFilters, issueFilters] ); const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL; @@ -183,6 +235,17 @@ export const ProjectIssuesHeader: React.FC = observer(() => { labels={projectLabels} memberIds={projectMemberIds ?? undefined} states={projectStates} + onSearchCapture={() => + captureIssuesFilterEvent({ + eventName: FILTER_SEARCHED, + payload: { + path: router.asPath, + current_filters: issueFilters?.filters, + layout: issueFilters?.displayFilters?.layout, + element_id: projectId, + }, + }) + } /> diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 175534a79..0a9506b7a 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -29,6 +29,16 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // constants import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; +import { + DP_APPLIED, + DP_REMOVED, + elementFromPath, + FILTER_APPLIED, + FILTER_REMOVED, + FILTER_SEARCHED, + LAYOUT_CHANGED, + LP_UPDATED, +} from "constants/event-tracker"; export const ProjectViewIssuesHeader: React.FC = observer(() => { // router @@ -42,7 +52,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { const { issuesFilter: { issueFilters, updateFilters }, } = useIssues(EIssuesStoreType.PROJECT_VIEW); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent, captureIssuesFilterEvent, captureIssuesDisplayFilterEvent } = + useEventTracker(); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); @@ -62,7 +73,13 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, viewId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, viewId).then(() => + captureEvent(LAYOUT_CHANGED, { + layout: layout, + element: elementFromPath(router.asPath), + element_id: viewId, + }) + ); }, [workspaceSlug, projectId, viewId, updateFilters] ); @@ -71,17 +88,31 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !projectId) return; const newValues = issueFilters?.filters?.[key] ?? []; - + let isFilterRemoved = false; if (Array.isArray(value)) { value.forEach((val) => { if (!newValues.includes(val)) newValues.push(val); + else isFilterRemoved = true; }); } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (issueFilters?.filters?.[key]?.includes(value)) { + isFilterRemoved = true; + newValues.splice(newValues.indexOf(value), 1); + } else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, viewId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, viewId).then(() => { + captureIssuesFilterEvent({ + eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED, + payload: { + path: router.asPath, + filters: issueFilters, + element_id: viewId, + filter_property: value, + filter_type: key, + }, + }); + }); }, [workspaceSlug, projectId, viewId, issueFilters, updateFilters] ); @@ -89,17 +120,38 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, viewId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, viewId).then(() => + captureIssuesDisplayFilterEvent({ + eventName: LP_UPDATED, + payload: { + property_type: Object.keys(updatedDisplayFilter).join(","), + property: Object.values(updatedDisplayFilter)?.[0], + path: router.asPath, + filters: issueFilters, + element_id: viewId, + }, + }) + ); }, - [workspaceSlug, projectId, viewId, updateFilters] + [workspaceSlug, projectId, viewId, updateFilters, issueFilters] ); const handleDisplayProperties = useCallback( (property: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, viewId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, viewId).then(() => + captureIssuesDisplayFilterEvent({ + eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED, + payload: { + display_property: Object.keys(property).join(","), + path: router.asPath, + filters: issueFilters, + element_id: viewId, + }, + }) + ); }, - [workspaceSlug, projectId, viewId, updateFilters] + [workspaceSlug, projectId, viewId, updateFilters, issueFilters] ); const viewDetails = viewId ? getViewById(viewId.toString()) : null; @@ -198,6 +250,17 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { labels={projectLabels} memberIds={projectMemberIds ?? undefined} states={projectStates} + onSearchCapture={() => + captureIssuesFilterEvent({ + eventName: FILTER_SEARCHED, + payload: { + path: router.asPath, + current_filters: issueFilters?.filters, + layout: issueFilters?.displayFilters?.layout, + element_id: projectId, + }, + }) + } /> diff --git a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index af8cfc84a..b1090aed2 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { Search, X } from "lucide-react"; // components @@ -14,6 +14,8 @@ import { FilterStateGroup, FilterTargetDate, } from "components/issues"; +// hooks +import useDebounce from "hooks/use-debounce"; // types import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // constants @@ -26,14 +28,21 @@ type Props = { labels?: IIssueLabel[] | undefined; memberIds?: string[] | undefined; states?: IState[] | undefined; + onSearchCapture?: () => void; }; export const FilterSelection: React.FC = observer((props) => { - const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props; + const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states, onSearchCapture } = + props; // states const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions?.filters.includes(filter); + const debouncedValue = useDebounce(filtersSearchQuery, 1500); + + useEffect(() => { + if (debouncedValue && onSearchCapture) onSearchCapture(); + }, [debouncedValue]); return (
diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 3ef5533c6..c7173c30e 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -44,7 +44,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { } = useUser(); const { fetchAllGlobalViews } = useGlobalView(); const { workspaceProjectIds } = useProject(); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureIssuesListOpenedEvent } = useEventTracker(); const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId); const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view"; @@ -100,6 +100,10 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { if (workspaceSlug && globalViewId) { await fetchAllGlobalViews(workspaceSlug.toString()); await fetchFilters(workspaceSlug.toString(), globalViewId.toString()); + captureIssuesListOpenedEvent({ + path: router.asPath, + element_id: globalViewId, + }); await fetchIssues(workspaceSlug.toString(), globalViewId.toString(), issueIds ? "mutation" : "init-loader"); routerFilterParams(); } diff --git a/web/constants/event-tracker.ts b/web/constants/event-tracker.ts index 889093609..a9275e621 100644 --- a/web/constants/event-tracker.ts +++ b/web/constants/event-tracker.ts @@ -1,3 +1,5 @@ +import { ISSUE_ORDER_BY_OPTIONS } from "./issue"; + export type IssueEventProps = { eventName: string; payload: any; @@ -124,23 +126,40 @@ export const getProjectStateEventPayload = (payload: any) => { export const getIssuesListOpenedPayload = (payload: any) => ({ element: elementFromPath(payload.path), element_id: payload.element_id, - type: payload.project_id ? "Project" : "Workspace", layout: payload?.displayFilters?.layout, filters: payload?.filters, display_properties: payload?.displayProperties, }); export const getIssuesFilterEventPayload = (payload: any) => ({ + filter_type: payload?.filter_type, + filter_property: payload?.filter_property, + layout: payload?.filters?.displayFilters?.layout, + current_filters: payload?.filters?.filters, element: elementFromPath(payload.path), - element_id: payload.element_id, - type: payload.project_id ? "Project" : "Workspace", - layout: payload?.displayFilters?.layout, - filters: payload?.filters, - display_properties: payload?.displayProperties, + element_id: payload.element_id, }); -const elementFromPath = (path?: string) => { - if (path?.includes("workspace-views")) return "Workspace view"; +export const getIssuesDisplayFilterPayload = (payload: any) => { + const property = + payload.property_type == "order_by" + ? ISSUE_ORDER_BY_OPTIONS?.filter((option) => option.key === payload.property)?.[0] + .title.toLocaleLowerCase() + .replaceAll(" ", "_") + : payload.property; + return { + layout: payload?.filters?.displayFilters?.layout, + current_display_properties: payload?.filters?.displayProperties, + element: elementFromPath(payload.path), + element_id: payload.element_id, + display_property: payload.display_property, + property: property, + property_type: payload.property_type, + }; +}; + +export const elementFromPath = (path?: string) => { + if (path?.includes("workspace-views")) return "Global view"; if (path?.includes("cycles")) return "Cycle"; if (path?.includes("modules")) return "Module"; if (path?.includes("views")) return "Project view"; @@ -190,6 +209,12 @@ export const ISSUE_OPENED = "Issue opened"; export const FILTER_APPLIED = "Filter applied"; export const FILTER_REMOVED = "Filter removed"; export const FILTER_SEARCHED = "Filter searched"; +// Issues Display Property Events +export const DP_APPLIED = "Display property applied"; +export const DP_REMOVED = "Display property removed"; +// Issues Layout Property Event +export const LP_UPDATED = "Layout property updated"; +export const LAYOUT_CHANGED = "Layout changed"; // Project State Events export const STATE_CREATED = "State created"; export const STATE_UPDATED = "State updated"; diff --git a/web/store/event-tracker.store.ts b/web/store/event-tracker.store.ts index 4697180c7..0239e41a2 100644 --- a/web/store/event-tracker.store.ts +++ b/web/store/event-tracker.store.ts @@ -19,6 +19,9 @@ import { getPageEventPayload, ISSUES_LIST_OPENED, getIssuesListOpenedPayload, + getIssuesFilterEventPayload, + getIssuesDisplayFilterPayload, + LP_UPDATED, } from "constants/event-tracker"; export interface IEventTrackerStore { @@ -40,6 +43,8 @@ export interface IEventTrackerStore { captureIssueEvent: (props: IssueEventProps) => void; captureProjectStateEvent: (props: EventProps) => void; captureIssuesListOpenedEvent: (payload: any) => void; + captureIssuesFilterEvent: (props: EventProps) => void; + captureIssuesDisplayFilterEvent: (props: EventProps) => void; } export class EventTrackerStore implements IEventTrackerStore { @@ -245,15 +250,46 @@ export class EventTrackerStore implements IEventTrackerStore { /** * @description: Captures the event whenever the issues list is opened. - * @param {string} path - * @param {any} filters + * @param {any} payload */ captureIssuesListOpenedEvent = (payload: any) => { const eventPayload = { ...getIssuesListOpenedPayload(payload), ...this.getRequiredProperties, + type: this.getRequiredProperties.project_id ? "Project" : "Workspace", }; posthog?.capture(ISSUES_LIST_OPENED, eventPayload); this.setTrackElement(undefined); }; + + /** + * @description: Captures the event whenever the issues filters are changed. + * @param {IssueEventProps} props + */ + captureIssuesFilterEvent = (props: EventProps) => { + const { eventName, payload } = props; + const eventPayload = { + ...getIssuesFilterEventPayload(payload), + ...this.getRequiredProperties, + type: this.getRequiredProperties.project_id ? "Project" : "Workspace", + }; + posthog?.capture(eventName, eventPayload); + this.setTrackElement(undefined); + }; + + /** + * @description: Captures the event whenever the issues display-filters are changed. + * @param {IssueEventProps} props + */ + captureIssuesDisplayFilterEvent = (props: EventProps) => { + const { eventName, payload } = props; + const eventPayload = { + ...getIssuesDisplayFilterPayload(payload), + ...this.getRequiredProperties, + type: this.getRequiredProperties.project_id ? "Project" : "Workspace", + current_display_filter: eventName === LP_UPDATED ? payload?.filters?.displayFilters : undefined, + }; + posthog?.capture(eventName, eventPayload); + this.setTrackElement(undefined); + }; }