[WEB-4423] refactor: event trackers (#7289)

* feat: event tracker helper

* feat: track click events for `data-ph-element`

* fix: handled click events

* fix: handled name

* chore: tracker element updates

* chore: remove export

* chore: tracker element type

* chore: track element and event helper.

* chore: minor improvements

* chore: minor refactors

* fix: workspace events

* fix: added slug

* fix: changes nomenclature

* fix: nomenclature

* chore: update event tracker helper types

* fix: data id

* refactor: cycle events (#7290)

* chore: update event tracker helper types

* refactor: cycle events

* refactor: cycle events

* refactor: cycle event tracker

* chore: update tracker elements

* chore: check for closest element with data-ph-element attribute

---------

Co-authored-by: Prateek Shourya <prateekshourya@Prateeks-MacBook-Pro.local>

* Refactor module events (#7291)

* chore: update event tracker helper types

* refactor: cycle events

* refactor: cycle events

* refactor: cycle event tracker

* refactor: module tracker event and element

* chore: update tracker element

* chore: revert unnecessary changes

---------

Co-authored-by: Prateek Shourya <prateekshourya@Prateeks-MacBook-Pro.local>

* refactor: global views, product tour, notifications, onboarding, users and sidebar related events

* chore: member tracker events (#7302)

* chore: member-tracker-events

* fix: constants

* refactor: update event tracker constants

* refactor: auth related event trackers (#7306)

* Chore: state events (#7307)

* chore: state events

* fix: refactor

* chore: project events (#7305)

* chore: project-events

* fix: refactor

* fix: removed hardcoded values

* fix: github redirection event

* chore: project page tracker events (#7304)

* added events for most page events

* refactor: simplify lock button event handling in PageLockControl

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
Co-authored-by: M. Palanikannan <73993394+Palanikannan1437@users.noreply.github.com>

* chore: minor cleanup and import fixes

* refactor: added tracker elements for buttons (#7308)

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* fix: event type

* refactor: posthog group event

* chore: removed instances of event tracker (#7309)

* refactor: remove event tracker stores and hooks

* refactor: remove event tracker store

* fix: build errors

* clean up event tracker payloads

* fix: coderabbit suggestions

---------

Co-authored-by: Prateek Shourya <prateekshourya@Prateeks-MacBook-Pro.local>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
Co-authored-by: M. Palanikannan <73993394+Palanikannan1437@users.noreply.github.com>
Co-authored-by: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com>
This commit is contained in:
Akshita Goyal 2025-07-02 15:23:18 +05:30 committed by GitHub
parent fa9c63716c
commit cfe169c6d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 2095 additions and 1888 deletions

View file

@ -1,163 +1,46 @@
export type IssueEventProps = {
eventName: string;
payload: any;
updates?: any;
path?: string;
};
export type EventProps = {
eventName: string;
payload: any;
updates?: any;
path?: string;
};
export const getWorkspaceEventPayload = (payload: any) => ({
workspace_id: payload.id,
created_at: payload.created_at,
updated_at: payload.updated_at,
organization_size: payload.organization_size,
first_time: payload.first_time,
state: payload.state,
element: payload.element,
});
export const getProjectEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id,
project_id: payload.id,
identifier: payload.identifier,
project_visibility: payload.network == 2 ? "Public" : "Private",
changed_properties: payload.changed_properties,
lead_id: payload.project_lead,
created_at: payload.created_at,
updated_at: payload.updated_at,
state: payload.state,
element: payload.element,
});
export const getCycleEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id,
project_id: payload.project,
cycle_id: payload.id,
created_at: payload.created_at,
updated_at: payload.updated_at,
start_date: payload.start_date,
target_date: payload.target_date,
cycle_status: payload.status,
changed_properties: payload.changed_properties,
state: payload.state,
element: payload.element,
});
export const getModuleEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id,
project_id: payload.project,
module_id: payload.id,
created_at: payload.created_at,
updated_at: payload.updated_at,
start_date: payload.start_date,
target_date: payload.target_date,
module_status: payload.status,
lead_id: payload.lead,
changed_properties: payload.changed_properties,
member_ids: payload.members,
state: payload.state,
element: payload.element,
});
export const getPageEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id,
project_id: payload.project,
created_at: payload.created_at,
updated_at: payload.updated_at,
access: payload.access === 0 ? "Public" : "Private",
is_locked: payload.is_locked,
archived_at: payload.archived_at,
created_by: payload.created_by,
state: payload.state,
element: payload.element,
});
export const getIssueEventPayload = (props: IssueEventProps) => {
const { eventName, payload, updates, path } = props;
let eventPayload: any = {
issue_id: payload.id,
estimate_point: payload.estimate_point,
link_count: payload.link_count,
target_date: payload.target_date,
is_draft: payload.is_draft,
label_ids: payload.label_ids,
assignee_ids: payload.assignee_ids,
created_at: payload.created_at,
updated_at: payload.updated_at,
sequence_id: payload.sequence_id,
module_ids: payload.module_ids,
sub_issues_count: payload.sub_issues_count,
parent_id: payload.parent_id,
project_id: payload.project_id,
workspace_id: payload.workspace_id,
priority: payload.priority,
state_id: payload.state_id,
start_date: payload.start_date,
attachment_count: payload.attachment_count,
cycle_id: payload.cycle_id,
module_id: payload.module_id,
archived_at: payload.archived_at,
state: payload.state,
view_id: path?.includes("workspace-views") || path?.includes("views") ? path.split("/").pop() : "",
};
if (eventName === WORK_ITEM_TRACKER_EVENTS.update) {
eventPayload = {
...eventPayload,
...updates,
updated_from: props.path?.includes("workspace-views")
? "All views"
: props.path?.includes("cycles")
? "Cycle"
: props.path?.includes("modules")
? "Module"
: props.path?.includes("views")
? "Project view"
: props.path?.includes("inbox")
? "Inbox"
: props.path?.includes("draft")
? "Draft"
: "Project",
};
}
return eventPayload;
};
export const getProjectStateEventPayload = (payload: any) => ({
workspace_id: payload.workspace_id,
project_id: payload.id,
state_id: payload.id,
created_at: payload.created_at,
updated_at: payload.updated_at,
group: payload.group,
color: payload.color,
default: payload.default,
state: payload.state,
element: payload.element,
});
// Dashboard Events
export const GITHUB_REDIRECTED_TRACKER_EVENT = "github_redirected";
export const HEADER_GITHUB_ICON = "header_github_icon";
// Groups
export const GROUP_WORKSPACE_TRACKER_EVENT = "workspace_metrics";
// Command palette tracker
export const COMMAND_PALETTE_TRACKER_ELEMENTS = {
COMMAND_PALETTE_SHORTCUT_KEY: "command_palette_shortcut_key",
};
export const WORKSPACE_TRACKER_EVENTS = {
create: "workspace_created",
update: "workspace_updated",
delete: "workspace_deleted",
};
export const WORKSPACE_TRACKER_ELEMENTS = {
DELETE_WORKSPACE_BUTTON: "delete_workspace_button",
ONBOARDING_CREATE_WORKSPACE_BUTTON: "onboarding_create_workspace_button",
CREATE_WORKSPACE_BUTTON: "create_workspace_button",
UPDATE_WORKSPACE_BUTTON: "update_workspace_button",
};
export const PROJECT_TRACKER_EVENTS = {
create: "project_created",
update: "project_updated",
delete: "project_deleted",
};
export const PROJECT_TRACKER_ELEMENTS = {
EXTENDED_SIDEBAR_ADD_BUTTON: "extended_sidebar_add_project_button",
SIDEBAR_CREATE_PROJECT_BUTTON: "sidebar_create_project_button",
SIDEBAR_CREATE_PROJECT_TOOLTIP: "sidebar_create_project_tooltip",
COMMAND_PALETTE_CREATE_BUTTON: "command_palette_create_project_button",
COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON: "command_palette_shortcut_create_project_button",
EMPTY_STATE_CREATE_PROJECT_BUTTON: "empty_state_create_project_button",
CREATE_HEADER_BUTTON: "create_project_header_button",
CREATE_FIRST_PROJECT_BUTTON: "create_first_project_button",
DELETE_PROJECT_BUTTON: "delete_project_button",
UPDATE_PROJECT_BUTTON: "update_project_button",
CREATE_PROJECT_JIRA_IMPORT_DETAIL_PAGE: "create_project_jira_import_detail_page",
TOGGLE_FEATURE: "toggle_project_feature",
};
export const CYCLE_TRACKER_EVENTS = {
create: "cycle_created",
@ -165,7 +48,18 @@ export const CYCLE_TRACKER_EVENTS = {
delete: "cycle_deleted",
favorite: "cycle_favorited",
unfavorite: "cycle_unfavorited",
archive: "cycle_archived",
restore: "cycle_restored",
};
export const CYCLE_TRACKER_ELEMENTS = {
RIGHT_HEADER_ADD_BUTTON: "right_header_add_cycle_button",
EMPTY_STATE_ADD_BUTTON: "empty_state_add_cycle_button",
COMMAND_PALETTE_ADD_ITEM: "command_palette_add_cycle_item",
RIGHT_SIDEBAR: "cycle_right_sidebar",
QUICK_ACTIONS: "cycle_quick_actions",
CONTEXT_MENU: "cycle_context_menu",
LIST_ITEM: "cycle_list_item",
} as const;
export const MODULE_TRACKER_EVENTS = {
create: "module_created",
@ -173,32 +67,120 @@ export const MODULE_TRACKER_EVENTS = {
delete: "module_deleted",
favorite: "module_favorited",
unfavorite: "module_unfavorited",
archive: "module_archived",
restore: "module_restored",
link: {
create: "module_link_created",
update: "module_link_updated",
delete: "module_link_deleted",
},
};
export const MODULE_TRACKER_ELEMENTS = {
RIGHT_HEADER_ADD_BUTTON: "right_header_add_module_button",
EMPTY_STATE_ADD_BUTTON: "empty_state_add_module_button",
COMMAND_PALETTE_ADD_ITEM: "command_palette_add_module_item",
RIGHT_SIDEBAR: "module_right_sidebar",
QUICK_ACTIONS: "module_quick_actions",
CONTEXT_MENU: "module_context_menu",
LIST_ITEM: "module_list_item",
CARD_ITEM: "module_card_item",
} as const;
export const WORK_ITEM_TRACKER_EVENTS = {
create: "work_item_created",
add_existing: "work_item_add_existing",
update: "work_item_updated",
delete: "work_item_deleted",
archive: "work_item_archived",
restore: "work_item_restored",
attachment: {
add: "work_item_attachment_added",
remove: "work_item_attachment_removed",
},
sub_issue: {
update: "sub_issue_updated",
remove: "sub_issue_removed",
delete: "sub_issue_deleted",
create: "sub_issue_created",
add_existing: "sub_issue_add_existing",
},
draft: {
create: "draft_work_item_created",
},
};
export const WORK_ITEM_TRACKER_ELEMENTS = {
HEADER_ADD_BUTTON: {
WORK_ITEMS: "work_items_header_add_work_item_button",
PROJECT_VIEW: "project_view_header_add_work_item_button",
CYCLE: "cycle_header_add_work_item_button",
MODULE: "module_header_add_work_item_button",
},
COMMAND_PALETTE_ADD_BUTTON: "command_palette_add_work_item_button",
EMPTY_STATE_ADD_BUTTON: {
WORK_ITEMS: "work_items_empty_state_add_work_item_button",
PROJECT_VIEW: "project_view_empty_state_add_work_item_button",
CYCLE: "cycle_empty_state_add_work_item_button",
MODULE: "module_empty_state_add_work_item_button",
GLOBAL_VIEW: "global_view_empty_state_add_work_item_button",
},
QUICK_ACTIONS: {
WORK_ITEMS: "work_items_quick_actions",
PROJECT_VIEW: "project_view_work_items_quick_actions",
CYCLE: "cycle_work_items_quick_actions",
MODULE: "module_work_items_quick_actions",
GLOBAL_VIEW: "global_view_work_items_quick_actions",
ARCHIVED: "archived_work_items_quick_actions",
DRAFT: "draft_work_items_quick_actions",
},
CONTEXT_MENU: {
WORK_ITEMS: "work_items_context_menu",
PROJECT_VIEW: "project_view_context_menu",
CYCLE: "cycle_context_menu",
MODULE: "module_context_menu",
GLOBAL_VIEW: "global_view_context_menu",
ARCHIVED: "archived_context_menu",
DRAFT: "draft_context_menu",
},
} as const;
export const STATE_TRACKER_EVENTS = {
create: "state_created",
update: "state_updated",
delete: "state_deleted",
};
export const STATE_TRACKER_ELEMENTS = {
STATE_GROUP_ADD_BUTTON: "state_group_add_button",
STATE_LIST_DELETE_BUTTON: "state_list_delete_button",
STATE_LIST_EDIT_BUTTON: "state_list_edit_button",
};
export const PROJECT_PAGE_TRACKER_EVENTS = {
create: "project_page_created",
update: "project_page_updated",
delete: "project_page_deleted",
archive: "project_page_archived",
restore: "project_page_restored",
lock: "project_page_locked",
unlock: "project_page_unlocked",
access_update: "project_page_access_updated",
duplicate: "project_page_duplicated",
favorite: "project_page_favorited",
unfavorite: "project_page_unfavorited",
move: "project_page_moved",
};
export const PROJECT_PAGE_TRACKER_ELEMENTS = {
COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON: "command_palette_shortcut_create_page_button",
EMPTY_STATE_CREATE_BUTTON: "empty_state_create_page_button",
COMMAND_PALETTE_CREATE_BUTTON: "command_palette_create_page_button",
CONTEXT_MENU: "page_context_menu",
QUICK_ACTIONS: "page_quick_actions",
LIST_ITEM: "page_list_item",
FAVORITE_BUTTON: "page_favorite_button",
ARCHIVE_BUTTON: "page_archive_button",
LOCK_BUTTON: "page_lock_button",
ACCESS_TOGGLE: "page_access_toggle",
DUPLICATE_BUTTON: "page_duplicate_button",
} as const;
export const MEMBER_TRACKER_EVENTS = {
invite: "member_invited",
@ -211,48 +193,78 @@ export const MEMBER_TRACKER_EVENTS = {
leave: "workspace_member_left",
},
};
export const MEMBER_TRACKER_ELEMENTS = {
HEADER_ADD_BUTTON: "header_add_member_button",
ACCEPT_INVITATION_BUTTON: "accept_invitation_button",
ONBOARDING_JOIN_WORKSPACE: "workspace_join_continue_to_workspace_button",
ONBOARDING_INVITE_MEMBER: "invite_member_continue_button",
SIDEBAR_PROJECT_QUICK_ACTIONS: "sidebar_project_quick_actions",
PROJECT_MEMBER_TABLE_CONTEXT_MENU: "project_member_table_context_menu",
WORKSPACE_MEMBER_TABLE_CONTEXT_MENU: "workspace_member_table_context_menu",
WORKSPACE_INVITATIONS_LIST_CONTEXT_MENU: "workspace_invitations_list_context_menu",
} as const;
export const AUTH_TRACKER_EVENTS = {
navigate: {
sign_up: "navigate_to_sign_up_page",
sign_in: "navigate_to_sign_in_page",
},
code_verify: "code_verified",
sign_up_with_password: "sign_up_with_password",
sign_in_with_password: "sign_in_with_password",
sign_in_with_code: "sign_in_with_magic_link",
forgot_password: "forgot_password_clicked",
new_code_requested: "new_code_requested",
};
export const AUTH_TRACKER_ELEMENTS = {
NAVIGATE_TO_SIGN_UP: "navigate_to_sign_up",
FORGOT_PASSWORD_FROM_SIGNIN: "forgot_password_from_signin",
SIGNUP_FROM_FORGOT_PASSWORD: "signup_from_forgot_password",
SIGN_IN_FROM_SIGNUP: "sign_in_from_signup",
SIGN_IN_WITH_UNIQUE_CODE: "sign_in_with_unique_code",
REQUEST_NEW_CODE: "request_new_code",
VERIFY_CODE: "verify_code",
};
export const PRODUCT_TOUR_TRACKER_EVENTS = {
start: "product_tour_started",
complete: "product_tour_completed",
skip: "product_tour_skipped",
};
export const GLOBAL_VIEW_TOUR_TRACKER_EVENTS = {
export const GLOBAL_VIEW_TRACKER_EVENTS = {
create: "global_view_created",
update: "global_view_updated",
delete: "global_view_deleted",
open: "global_view_opened",
};
export const GLOBAL_VIEW_TRACKER_ELEMENTS = {
RIGHT_HEADER_ADD_BUTTON: "global_view_right_header_add_button",
HEADER_SAVE_VIEW_BUTTON: "global_view_header_save_view_button",
QUICK_ACTIONS: "global_view_quick_actions",
LIST_ITEM: "global_view_list_item",
};
export const PRODUCT_TOUR_TRACKER_EVENTS = {
complete: "product_tour_completed",
};
export const PRODUCT_TOUR_TRACKER_ELEMENTS = {
START_BUTTON: "product_tour_start_button",
SKIP_BUTTON: "product_tour_skip_button",
CREATE_PROJECT_BUTTON: "product_tour_create_project_button",
};
export const NOTIFICATION_TRACKER_EVENTS = {
archive: "notification_archived",
unarchive: "notification_unarchived",
mark_read: "notification_marked_read",
mark_unread: "notification_marked_unread",
all_marked_read: "all_notifications_marked_read",
};
export const NOTIFICATION_TRACKER_ELEMENTS = {
MARK_ALL_AS_READ_BUTTON: "mark_all_as_read_button",
ARCHIVE_UNARCHIVE_BUTTON: "archive_unarchive_button",
MARK_READ_UNREAD_BUTTON: "mark_read_unread_button",
};
export const USER_TRACKER_EVENTS = {
add_details: "user_details_added",
onboarding_complete: "user_onboarding_completed",
};
export const ONBOARDING_TRACKER_EVENTS = {
root: "onboarding",
step_1: "onboarding_step_1",
step_2: "onboarding_step_2",
export const ONBOARDING_TRACKER_ELEMENTS = {
PROFILE_SETUP_FORM: "onboarding_profile_setup_form",
};
export const SIDEBAR_TRACKER_EVENTS = {
click: "sidenav_clicked",
export const SIDEBAR_TRACKER_ELEMENTS = {
USER_MENU_ITEM: "sidenav_user_menu_item",
CREATE_WORK_ITEM_BUTTON: "sidebar_create_work_item_button",
};

View file

@ -34,8 +34,6 @@ const defaultValues: TUniqueCodeFormValues = {
export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
const { mode, email, nextPath, handleEmailClear, generateEmailUniqueCode } = props;
// hooks
// const { captureEvent } = useEventTracker();
// derived values
const defaultResetTimerValue = 5;
// states

View file

@ -4,7 +4,7 @@ import { useMemo } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
// plane package imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { type TabItem, Tabs } from "@plane/ui";
// components
@ -12,7 +12,8 @@ import AnalyticsFilterActions from "@/components/analytics/analytics-filter-acti
import { PageHead } from "@/components/core";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { getAnalyticsTabs } from "@/plane-web/components/analytics/tabs";
@ -36,7 +37,6 @@ const AnalyticsPage = observer((props: Props) => {
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { workspaceProjectIds, loader } = useProject();
const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
@ -101,8 +101,8 @@ const AnalyticsPage = observer((props: Props) => {
title={t("workspace_analytics.empty_state.general.primary_button.comic.title")}
description={t("workspace_analytics.empty_state.general.primary_button.comic.description")}
onClick={() => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!canPerformEmptyStateActions}
/>

View file

@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { Plus, Search } from "lucide-react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
import { cn, copyUrlToClipboard, orderJoinedProjects } from "@plane/utils";
@ -122,6 +122,7 @@ export const ExtendedProjectSidebar = observer(() => {
<Tooltip tooltipHeading={t("create_project")} tooltipContent="">
<button
type="button"
data-ph-element={PROJECT_TRACKER_ELEMENTS.EXTENDED_SIDEBAR_ADD_BUTTON}
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => {
setIsProjectModalOpen(true);

View file

@ -7,18 +7,17 @@ import { Home } from "lucide-react";
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
// ui
import { GITHUB_REDIRECTED_TRACKER_EVENT } from "@plane/constants";
import { GITHUB_REDIRECTED_TRACKER_EVENT, HEADER_GITHUB_ICON } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
// constants
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
export const WorkspaceDashboardHeader = () => {
// hooks
const { captureEvent } = useEventTracker();
const { resolvedTheme } = useTheme();
const { t } = useTranslation();
@ -39,8 +38,14 @@ export const WorkspaceDashboardHeader = () => {
<Header.RightItem>
<a
onClick={() =>
captureEvent(GITHUB_REDIRECTED_TRACKER_EVENT, {
element: "navbar",
captureElementAndEvent({
element: {
elementName: HEADER_GITHUB_ICON,
},
event: {
eventName: GITHUB_REDIRECTED_TRACKER_EVENT,
state: "SUCCESS",
},
})
}
className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5"

View file

@ -13,6 +13,7 @@ import {
EUserPermissionsLevel,
EProjectFeatureKey,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
@ -34,7 +35,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect
import {
useCommandPalette,
useCycle,
useEventTracker,
useIssues,
useLabel,
useMember,
@ -68,7 +68,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
} = useIssues(EIssuesStoreType.CYCLE);
const { currentProjectCycleIds, getCycleById } = useCycle();
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { currentProjectDetails, loader } = useProject();
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
@ -263,9 +262,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
<Button
className="h-full self-start"
onClick={() => {
setTrackElement("Cycle work items page");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
}}
data-ph-element={WORK_ITEM_TRACKER_ELEMENTS.HEADER_ADD_BUTTON.CYCLE}
size="sm"
>
{t("issue.add.label")}

View file

@ -4,13 +4,13 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// ui
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel, CYCLE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
import { CyclesViewHeader } from "@/components/cycles";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
@ -23,7 +23,6 @@ export const CyclesListHeader: FC = observer(() => {
// store hooks
const { toggleCreateCycleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { currentProjectDetails, loader } = useProject();
const { t } = useTranslation();
@ -51,8 +50,8 @@ export const CyclesListHeader: FC = observer(() => {
<Button
variant="primary"
size="sm"
data-ph-element={CYCLE_TRACKER_ELEMENTS.RIGHT_HEADER_ADD_BUTTON}
onClick={() => {
setTrackElement("Cycles page");
toggleCreateCycleModal(true);
}}
>

View file

@ -4,7 +4,7 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissionsLevel, CYCLE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EUserProjectRoles, TCycleFilters } from "@plane/types";
// components
@ -16,7 +16,7 @@ import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
import { CycleModuleListLayout } from "@/components/ui";
// helpers
// hooks
import { useEventTracker, useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store";
import { useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
@ -24,7 +24,6 @@ const ProjectCyclesPage = observer(() => {
// states
const [createModal, setCreateModal] = useState(false);
// store hooks
const { setTrackElement } = useEventTracker();
const { currentProjectCycleIds, loader } = useCycle();
const { getProjectById, currentProjectDetails } = useProject();
// router
@ -100,8 +99,8 @@ const ProjectCyclesPage = observer(() => {
label={t("project_cycles.empty_state.general.primary_button.text")}
title={t("project_cycles.empty_state.general.primary_button.comic.title")}
description={t("project_cycles.empty_state.general.primary_button.comic.description")}
data-ph-element={CYCLE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON}
onClick={() => {
setTrackElement("Cycle empty state");
setCreateModal(true);
}}
disabled={!hasMemberLevelPermission}

View file

@ -13,6 +13,7 @@ import {
EUserPermissions,
EUserPermissionsLevel,
EProjectFeatureKey,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import {
EIssuesStoreType,
@ -31,7 +32,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect
import { ModuleQuickActions } from "@/components/modules";
// hooks
import {
useEventTracker,
useLabel,
useMember,
useModule,
@ -66,7 +66,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const { updateFilters } = useIssuesActions(EIssuesStoreType.MODULE);
const { projectModuleIds, getModuleById } = useModule();
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { currentProjectDetails, loader } = useProject();
const { projectLabels } = useLabel();
@ -259,9 +258,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<Button
className="hidden sm:flex"
onClick={() => {
setTrackElement("Module work items page");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
}}
data-ph-element={WORK_ITEM_TRACKER_ELEMENTS.HEADER_ADD_BUTTON.MODULE}
size="sm"
>
Add work item

View file

@ -3,14 +3,14 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel, MODULE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
import { ModuleViewHeader } from "@/components/modules";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
@ -22,7 +22,6 @@ export const ModulesListHeader: React.FC = observer(() => {
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
const { toggleCreateModuleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { loader } = useProject();
@ -55,8 +54,8 @@ export const ModulesListHeader: React.FC = observer(() => {
<Button
variant="primary"
size="sm"
data-ph-element={MODULE_TRACKER_ELEMENTS.RIGHT_HEADER_ADD_BUTTON}
onClick={() => {
setTrackElement("Modules page");
toggleCreateModuleModal(true);
}}
>

View file

@ -4,13 +4,13 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
// constants
import { EPageAccess, EProjectFeatureKey } from "@plane/constants";
import { EPageAccess, EProjectFeatureKey, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
// plane types
import { TPage } from "@plane/types";
// plane ui
import { Breadcrumbs, Button, Header, setToast, TOAST_TYPE } from "@plane/ui";
// hooks
import { useEventTracker, useProject } from "@/hooks/store";
import { useProject } from "@/hooks/store";
// plane web
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
// plane web hooks
@ -27,11 +27,9 @@ export const PagesListHeader = observer(() => {
// store hooks
const { currentProjectDetails, loader } = useProject();
const { canCurrentUserCreatePage, createPage } = usePageStore(EPageStoreType.PROJECT);
const { setTrackElement } = useEventTracker();
// handle page create
const handleCreatePage = async () => {
setIsCreatingPage(true);
setTrackElement("Project pages page");
const payload: Partial<TPage> = {
access: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
@ -66,7 +64,13 @@ export const PagesListHeader = observer(() => {
</Header.LeftItem>
{canCurrentUserCreatePage ? (
<Header.RightItem>
<Button variant="primary" size="sm" onClick={handleCreatePage} loading={isCreatingPage}>
<Button
variant="primary"
size="sm"
onClick={handleCreatePage}
loading={isCreatingPage}
data-ph-element={PROJECT_TRACKER_ELEMENTS.CREATE_HEADER_BUTTON}
>
{isCreatingPage ? "Adding" : "Add page"}
</Button>
</Header.RightItem>

View file

@ -12,6 +12,7 @@ import {
EUserPermissions,
EUserPermissionsLevel,
EProjectFeatureKey,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
// types
import {
@ -30,11 +31,9 @@ import { SwitcherIcon, SwitcherLabel } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import { ViewQuickActions } from "@/components/views";
// helpers
// hooks
import {
useCommandPalette,
useEventTracker,
useIssues,
useLabel,
useMember,
@ -57,7 +56,6 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
const { setTrackElement } = useEventTracker();
const { toggleCreateIssueModal } = useCommandPalette();
const { allowPermissions } = useUserPermissions();
@ -258,9 +256,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
{canUserCreateIssue ? (
<Button
onClick={() => {
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
}}
data-ph-element={WORK_ITEM_TRACKER_ELEMENTS.HEADER_ADD_BUTTON.PROJECT_VIEW}
size="sm"
>
Add work item

View file

@ -8,8 +8,9 @@ import { Layers } from "lucide-react";
import {
EIssueFilterType,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
DEFAULT_GLOBAL_VIEWS_LIST,
EIssueLayoutTypes,
GLOBAL_VIEW_TRACKER_ELEMENTS,
DEFAULT_GLOBAL_VIEWS_LIST,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import {
@ -225,7 +226,12 @@ export const GlobalIssuesHeader = observer(() => {
<></>
)}
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
<Button
variant="primary"
size="sm"
data-ph-element={GLOBAL_VIEW_TRACKER_ELEMENTS.RIGHT_HEADER_ADD_BUTTON}
onClick={() => setCreateViewModal(true)}
>
{t("workspace_views.add_view")}
</Button>
<div className="hidden md:block">

View file

@ -5,12 +5,17 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Search } from "lucide-react";
// types
import { EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import {
EUserPermissions,
EUserPermissionsLevel,
MEMBER_TRACKER_ELEMENTS,
MEMBER_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspaceBulkInviteFormData } from "@plane/types";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { cn, getUserRole } from "@plane/utils";
import { cn } from "@plane/utils";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { CountChip } from "@/components/common";
@ -19,7 +24,8 @@ import { SettingsContentWrapper } from "@/components/settings";
import { WorkspaceMembersList } from "@/components/workspace";
// helpers
// hooks
import { useEventTracker, useMember, useUserPermissions, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useMember, useUserPermissions, useWorkspace } from "@/hooks/store";
// plane web components
import { BillingActionsButton } from "@/plane-web/components/workspace/billing";
import { SendWorkspaceInvitationModal } from "@/plane-web/components/workspace/members";
@ -32,7 +38,6 @@ const WorkspaceMembersSettingsPage = observer(() => {
const { workspaceSlug } = useParams();
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { captureEvent } = useEventTracker();
const {
workspace: { workspaceMemberIds, inviteMembersToWorkspace },
} = useMember();
@ -52,16 +57,11 @@ const WorkspaceMembersSettingsPage = observer(() => {
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
.then(() => {
setInviteModal(false);
captureEvent(MEMBER_TRACKER_EVENTS.invite, {
emails: [
...data.emails.map((email) => ({
email: email.email,
role: getUserRole(email.role as unknown as EUserPermissions),
})),
],
project_id: undefined,
state: "SUCCESS",
element: "Workspace settings member page",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
emails: [...data.emails.map((email) => email.email)],
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@ -70,16 +70,12 @@ const WorkspaceMembersSettingsPage = observer(() => {
});
})
.catch((err) => {
captureEvent(MEMBER_TRACKER_EVENTS.invite, {
emails: [
...data.emails.map((email) => ({
email: email.email,
role: getUserRole(email.role as unknown as EUserPermissions),
})),
],
project_id: undefined,
state: "FAILED",
element: "Workspace settings member page",
captureError({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
emails: [...data.emails.map((email) => email.email)],
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
@ -129,7 +125,12 @@ const WorkspaceMembersSettingsPage = observer(() => {
/>
</div>
{canPerformWorkspaceAdminActions && (
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
<Button
variant="primary"
size="sm"
onClick={() => setInviteModal(true)}
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
>
{t("workspace_settings.settings.members.add_member")}
</Button>
)}

View file

@ -2,6 +2,7 @@
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/ui";
import { cn } from "@plane/utils";
import { useCommandPalette } from "@/hooks/store";
@ -27,7 +28,11 @@ const ProjectSettingsPage = () => {
<Link href="https://plane.so/" target="_blank" className={cn(getButtonStyling("neutral-primary", "sm"))}>
Learn more about projects
</Link>
<Button size="sm" onClick={() => toggleCreateProjectModal(true)}>
<Button
size="sm"
onClick={() => toggleCreateProjectModal(true)}
data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON}
>
Start your first project
</Button>
</div>

View file

@ -9,14 +9,15 @@ import { Controller, useForm } from "react-hook-form";
// icons
import { CircleCheck } from "lucide-react";
// plane imports
import { AUTH_TRACKER_EVENTS } from "@plane/constants";
import { AUTH_TRACKER_ELEMENTS, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { cn, checkEmailValidity } from "@plane/utils";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker, useInstance } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useInstance } from "@/hooks/store";
import useTimer from "@/hooks/use-timer";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
@ -45,8 +46,6 @@ const ForgotPasswordPage = observer(() => {
const email = searchParams.get("email");
// plane hooks
const { t } = useTranslation();
// store hooks
const { captureEvent } = useEventTracker();
const { config } = useInstance();
// hooks
const { resolvedTheme } = useTheme();
@ -71,8 +70,11 @@ const ForgotPasswordPage = observer(() => {
email: formData.email,
})
.then(() => {
captureEvent(AUTH_TRACKER_EVENTS.forgot_password, {
state: "SUCCESS",
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@ -82,8 +84,11 @@ const ForgotPasswordPage = observer(() => {
setResendCodeTimer(30);
})
.catch((err) => {
captureEvent(AUTH_TRACKER_EVENTS.forgot_password, {
state: "FAILED",
captureError({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.ERROR,
@ -120,7 +125,7 @@ const ForgotPasswordPage = observer(() => {
{t("auth.common.new_to_plane")}
<Link
href="/"
onClick={() => captureEvent(AUTH_TRACKER_EVENTS.navigate.sign_up, {})}
data-ph-element={AUTH_TRACKER_ELEMENTS.SIGNUP_FROM_FORGOT_PASSWORD}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.create_account")}

View file

@ -9,20 +9,21 @@ import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr";
import { CheckCircle2 } from "lucide-react";
// plane imports
import { ROLE, EUserPermissions, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { ROLE, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS, GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import type { IWorkspaceMemberInvitation } from "@plane/types";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { truncateText, getUserRole } from "@plane/utils";
import { truncateText } from "@plane/utils";
// components
import { EmptyState } from "@/components/common";
import { WorkspaceLogo } from "@/components/workspace/logo";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// helpers
// hooks
import { useEventTracker, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess, joinEventGroup } from "@/helpers/event-tracker.helper";
import { useUser, useUserProfile, useWorkspace } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// services
import { AuthenticationWrapper } from "@/lib/wrappers";
@ -43,7 +44,6 @@ const UserInvitationsPage = observer(() => {
const router = useAppRouter();
// store hooks
const { t } = useTranslation();
const { captureEvent, joinWorkspaceMetricGroup } = useEventTracker();
const { data: currentUser } = useUser();
const { updateUserProfile } = useUserProfile();
@ -85,15 +85,17 @@ const UserInvitationsPage = observer(() => {
const firstInviteId = invitationsRespond[0];
const invitation = invitations?.find((i) => i.id === firstInviteId);
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
joinWorkspaceMetricGroup(redirectWorkspace?.id);
captureEvent(MEMBER_TRACKER_EVENTS.accept, {
if (redirectWorkspace?.id) {
joinEventGroup(GROUP_WORKSPACE_TRACKER_EVENT, redirectWorkspace?.id, {
date: new Date().toDateString(),
workspace_id: redirectWorkspace?.id,
});
}
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitation?.id,
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
role: getUserRole((invitation?.role as unknown as EUserPermissions)!),
project_id: undefined,
accepted_from: "App",
state: "SUCCESS",
element: "Workspace invitations page",
},
});
updateUserProfile({ last_workspace_id: redirectWorkspace?.id })
.then(() => {
@ -111,12 +113,13 @@ const UserInvitationsPage = observer(() => {
setIsJoiningWorkspaces(false);
});
})
.catch(() => {
captureEvent(MEMBER_TRACKER_EVENTS.accept, {
project_id: undefined,
accepted_from: "App",
state: "FAILED",
element: "Workspace invitations page",
.catch((err) => {
captureError({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitationsRespond?.[0],
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
@ -194,6 +197,7 @@ const UserInvitationsPage = observer(() => {
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
data-ph-element={MEMBER_TRACKER_ELEMENTS.ACCEPT_INVITATION_BUTTON}
>
{t("accept_and_join")}
</Button>

View file

@ -16,7 +16,8 @@ import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store";
import { captureSuccess } from "@/helpers/event-tracker.helper";
import { useUser, useWorkspace, useUserProfile } from "@/hooks/store";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
import { WorkspaceService } from "@/plane-web/services";
@ -35,7 +36,6 @@ const OnboardingPage = observer(() => {
const [step, setStep] = useState<EOnboardingSteps | null>(null);
const [totalSteps, setTotalSteps] = useState<number | null>(null);
// store hooks
const { captureEvent } = useEventTracker();
const { isLoading: userLoader, data: user, updateCurrentUser } = useUser();
const { data: profile, updateUserProfile, finishUserOnboarding } = useUserProfile();
const { workspaces, fetchWorkspaces } = useWorkspace();
@ -73,10 +73,12 @@ const OnboardingPage = observer(() => {
await finishUserOnboarding()
.then(() => {
captureEvent(USER_TRACKER_EVENTS.onboarding_complete, {
captureSuccess({
eventName: USER_TRACKER_EVENTS.onboarding_complete,
payload: {
email: user.email,
user_id: user.id,
status: "SUCCESS",
},
});
})
.catch(() => {

View file

@ -6,14 +6,12 @@ import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components
import { AUTH_TRACKER_EVENTS } from "@plane/constants";
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { AuthRoot } from "@/components/account";
// constants
// helpers
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
// assets
import { AuthenticationWrapper } from "@/lib/wrappers";
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
@ -26,8 +24,6 @@ export type AuthType = "sign-in" | "sign-up";
const SignInPage = observer(() => {
// plane hooks
const { t } = useTranslation();
// store hooks
const { captureEvent } = useEventTracker();
// hooks
const { resolvedTheme } = useTheme();
@ -54,7 +50,7 @@ const SignInPage = observer(() => {
{t("auth.common.already_have_an_account")}
<Link
href="/"
onClick={() => captureEvent(AUTH_TRACKER_EVENTS.navigate.sign_in, {})}
data-ph-element={AUTH_TRACKER_ELEMENTS.SIGN_IN_FROM_SIGNUP}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.login")}

View file

@ -6,7 +6,7 @@ import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components
import { AUTH_TRACKER_EVENTS } from "@plane/constants";
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { AuthRoot } from "@/components/account";
import { PageHead } from "@/components/core";
@ -14,7 +14,7 @@ import { PageHead } from "@/components/core";
// helpers
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker, useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks/store";
// layouts
import DefaultLayout from "@/layouts/default-layout";
// wrappers
@ -29,8 +29,6 @@ const HomePage = observer(() => {
const { resolvedTheme } = useTheme();
// plane hooks
const { t } = useTranslation();
// hooks
const { captureEvent } = useEventTracker();
// store
const { config } = useInstance();
// derived values
@ -63,7 +61,7 @@ const HomePage = observer(() => {
{t("auth.common.new_to_plane")}
<Link
href="/sign-up"
onClick={() => captureEvent(AUTH_TRACKER_EVENTS.navigate.sign_up, {})}
data-ph-element={AUTH_TRACKER_ELEMENTS.NAVIGATE_TO_SIGN_UP}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.create_account")}

View file

@ -10,6 +10,7 @@ import {
EUserPermissionsLevel,
SPACE_BASE_PATH,
SPACE_BASE_URL,
WORK_ITEM_TRACKER_ELEMENTS,
EProjectFeatureKey,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
@ -21,7 +22,7 @@ import { CountChip } from "@/components/common";
import HeaderFilters from "@/components/issues/filters";
// helpers
// hooks
import { useEventTracker, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store";
import { useProject, useCommandPalette, useUserPermissions } from "@/hooks/store";
import { useIssues } from "@/hooks/store/use-issues";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -42,7 +43,6 @@ export const IssuesHeader = observer(() => {
const { currentProjectDetails, loader } = useProject();
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { isMobile } = usePlatformOS();
@ -104,9 +104,9 @@ export const IssuesHeader = observer(() => {
{canUserCreateIssue ? (
<Button
onClick={() => {
setTrackElement("Project work items page");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
data-ph-element={WORK_ITEM_TRACKER_ELEMENTS.HEADER_ADD_BUTTON.WORK_ITEMS}
size="sm"
>
<div className="block sm:hidden">{t("issue.label", { count: 1 })}</div>

View file

@ -10,7 +10,6 @@ export interface CopyMenuHelperProps {
shouldRender: boolean;
};
activeLayout: string;
setTrackElement: (element: string) => void;
setCreateUpdateIssueModal: (open: boolean) => void;
setDuplicateWorkItemModal?: (open: boolean) => void;
}

View file

@ -4,7 +4,10 @@ import { useState, useEffect, useRef } from "react";
import { observer } from "mobx-react";
import { LockKeyhole, LockKeyholeOpen } from "lucide-react";
// plane imports
import { PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
import { Tooltip } from "@plane/ui";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { usePageOperations } from "@/hooks/use-page-operations";
// store
@ -78,6 +81,7 @@ export const PageLockControl = observer(({ page }: Props) => {
<button
type="button"
onClick={toggleLock}
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON}
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 transition-colors"
aria-label="Lock"
>
@ -90,6 +94,7 @@ export const PageLockControl = observer(({ page }: Props) => {
<button
type="button"
onClick={toggleLock}
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON}
className="h-6 flex items-center gap-1 px-2 rounded text-custom-primary-100 bg-custom-primary-100/20 hover:bg-custom-primary-100/30 transition-colors"
aria-label="Locked"
>

View file

@ -12,7 +12,8 @@ import ProjectCommonAttributes from "@/components/project/create/common-attribut
import ProjectCreateHeader from "@/components/project/create/header";
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
// hooks
import { useEventTracker, useProject } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web types
import { TProject } from "@/plane-web/types/projects";
@ -32,7 +33,6 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
const { setToFavorite, workspaceSlug, data, onClose, handleNextStep, updateCoverImageStatus } = props;
// store
const { t } = useTranslation();
const { captureProjectEvent } = useEventTracker();
const { addProjectToFavorites, createProject } = useProject();
// states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
@ -70,25 +70,30 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
if (coverImage) {
await updateCoverImageStatus(res.id, coverImage);
}
const newPayload = {
...res,
state: "SUCCESS",
};
captureProjectEvent({
captureSuccess({
eventName: PROJECT_TRACKER_EVENTS.create,
payload: newPayload,
payload: {
identifier: formData.identifier,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("success"),
message: t("project_created_successfully"),
});
if (setToFavorite) {
handleAddToFavorites(res.id);
}
handleNextStep(res.id);
})
.catch((err) => {
captureError({
eventName: PROJECT_TRACKER_EVENTS.create,
payload: {
identifier: formData.identifier,
},
});
if (err?.data.code === "PROJECT_NAME_ALREADY_EXIST") {
setToast({
type: TOAST_TYPE.ERROR,

View file

@ -2,6 +2,7 @@ import { FC, useState } from "react";
import { observer } from "mobx-react";
import { ChevronDown, ChevronUp } from "lucide-react";
// types
import { WORKSPACE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspace } from "@plane/types";
// ui
@ -48,7 +49,11 @@ export const DeleteWorkspaceSection: FC<TDeleteWorkspace> = observer((props) =>
{t("workspace_settings.settings.general.delete_workspace_description")}
</span>
<div>
<Button variant="danger" onClick={() => setDeleteWorkspaceModal(true)}>
<Button
variant="danger"
onClick={() => setDeleteWorkspaceModal(true)}
data-ph-element={WORKSPACE_TRACKER_ELEMENTS.DELETE_WORKSPACE_BUTTON}
>
{t("workspace_settings.settings.general.delete_btn")}
</Button>
</div>

View file

@ -1,6 +1,14 @@
// types
import {
CYCLE_TRACKER_ELEMENTS,
MODULE_TRACKER_ELEMENTS,
PROJECT_PAGE_TRACKER_ELEMENTS,
PROJECT_TRACKER_ELEMENTS,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import { TCommandPaletteActionList, TCommandPaletteShortcut, TCommandPaletteShortcutList } from "@plane/types";
// store
import { captureClick } from "@/helpers/event-tracker.helper";
import { store } from "@/lib/store-context";
export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
@ -10,7 +18,10 @@ export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
c: {
title: "Create a new work item",
description: "Create a new work item in the current project",
action: () => toggleCreateIssueModal(true),
action: () => {
toggleCreateIssueModal(true);
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_BUTTON });
},
},
};
};
@ -22,7 +33,10 @@ export const getWorkspaceShortcutsList: () => TCommandPaletteActionList = () =>
p: {
title: "Create a new project",
description: "Create a new project in the current workspace",
action: () => toggleCreateProjectModal(true),
action: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON });
},
},
};
};
@ -40,17 +54,26 @@ export const getProjectShortcutsList: () => TCommandPaletteActionList = () => {
d: {
title: "Create a new page",
description: "Create a new page in the current project",
action: () => toggleCreatePageModal({ isOpen: true }),
action: () => {
toggleCreatePageModal({ isOpen: true });
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON });
},
},
m: {
title: "Create a new module",
description: "Create a new module in the current project",
action: () => toggleCreateModuleModal(true),
action: () => {
toggleCreateModuleModal(true);
captureClick({ elementName: MODULE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
},
},
q: {
title: "Create a new cycle",
description: "Create a new cycle in the current project",
action: () => toggleCreateCycleModal(true),
action: () => {
toggleCreateCycleModal(true);
captureClick({ elementName: CYCLE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
},
},
v: {
title: "Create a new view",

View file

@ -1,11 +0,0 @@
import { RootStore } from "@/plane-web/store/root.store";
import { CoreEventTrackerStore, ICoreEventTrackerStore } from "@/store/event-tracker.store";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface IEventTrackerStore extends ICoreEventTrackerStore {}
export class EventTrackerStore extends CoreEventTrackerStore implements IEventTrackerStore {
constructor(_rootStore: RootStore) {
super(_rootStore);
}
}

View file

@ -6,7 +6,7 @@ import Link from "next/link";
// icons
import { Eye, EyeOff, Info, X, XCircle } from "lucide-react";
// plane imports
import { API_BASE_URL, E_PASSWORD_STRENGTH, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { API_BASE_URL, E_PASSWORD_STRENGTH, AUTH_TRACKER_EVENTS, AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, Spinner } from "@plane/ui";
import { getPasswordStrength } from "@plane/utils";
@ -16,7 +16,7 @@ import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/accou
// helpers
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// services
import { AuthService } from "@/services/auth.service";
@ -46,8 +46,6 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
const { email, isSMTPConfigured, handleAuthStep, handleEmailClear, mode, nextPath } = props;
// plane imports
const { t } = useTranslation();
// hooks
const { captureEvent } = useEventTracker();
// ref
const formRef = useRef<HTMLFormElement>(null);
// states
@ -77,7 +75,6 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
const redirectToUniqueCodeSignIn = async () => {
handleAuthStep(EAuthSteps.UNIQUE_CODE);
captureEvent(AUTH_TRACKER_EVENTS.sign_in_with_code);
};
const passwordSupport =
@ -85,7 +82,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
<div className="w-full">
{isSMTPConfigured ? (
<Link
onClick={() => captureEvent(AUTH_TRACKER_EVENTS.forgot_password)}
data-ph-element={AUTH_TRACKER_ELEMENTS.FORGOT_PASSWORD_FROM_SIGNIN}
href={`/accounts/forgot-password?email=${encodeURIComponent(email)}`}
className="text-xs font-medium text-custom-primary-100"
>
@ -154,17 +151,32 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
: true;
if (isPasswordValid) {
setIsSubmitting(true);
captureEvent(
captureSuccess({
eventName:
mode === EAuthModes.SIGN_IN
? AUTH_TRACKER_EVENTS.sign_in_with_password
: AUTH_TRACKER_EVENTS.sign_up_with_password
);
: AUTH_TRACKER_EVENTS.sign_up_with_password,
payload: {
email: passwordFormData.email,
},
});
if (formRef.current) formRef.current.submit(); // Manually submit the form if the condition is met
} else {
setBannerMessage(true);
}
}}
onError={() => setIsSubmitting(false)}
onError={() => {
setIsSubmitting(false);
captureError({
eventName:
mode === EAuthModes.SIGN_IN
? AUTH_TRACKER_EVENTS.sign_in_with_password
: AUTH_TRACKER_EVENTS.sign_up_with_password,
payload: {
email: passwordFormData.email,
},
});
}}
>
<input type="hidden" name="csrfmiddlewaretoken" />
<input type="hidden" value={passwordFormData.email} name="email" />
@ -292,6 +304,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
{isSMTPConfigured && (
<Button
type="button"
data-ph-element={AUTH_TRACKER_ELEMENTS.SIGN_IN_WITH_UNIQUE_CODE}
onClick={redirectToUniqueCodeSignIn}
variant="outline-primary"
className="w-full"

View file

@ -2,14 +2,14 @@
import React, { useEffect, useState } from "react";
import { CircleCheck, XCircle } from "lucide-react";
import { API_BASE_URL, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { API_BASE_URL, AUTH_TRACKER_ELEMENTS, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, Spinner } from "@plane/ui";
// constants
// helpers
import { EAuthModes } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import useTimer from "@/hooks/use-timer";
// services
import { AuthService } from "@/services/auth.service";
@ -38,8 +38,6 @@ const defaultValues: TUniqueCodeFormValues = {
export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
const { mode, email, handleEmailClear, generateEmailUniqueCode, isExistingEmail, nextPath } = props;
// hooks
const { captureEvent } = useEventTracker();
// derived values
const defaultResetTimerValue = 5;
// states
@ -62,10 +60,22 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
setResendCodeTimer(defaultResetTimerValue);
handleFormChange("code", uniqueCode?.code || "");
setIsRequestingNewCode(false);
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.new_code_requested,
payload: {
email: email,
},
});
} catch {
setResendCodeTimer(0);
console.error("Error while requesting new code");
setIsRequestingNewCode(false);
captureError({
eventName: AUTH_TRACKER_EVENTS.new_code_requested,
payload: {
email: email,
},
});
}
};
@ -84,12 +94,23 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
onSubmit={() => {
setIsSubmitting(true);
captureEvent(AUTH_TRACKER_EVENTS.code_verify, {
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.code_verify,
payload: {
state: "SUCCESS",
first_time: !isExistingEmail,
},
});
}}
onError={() => {
setIsSubmitting(false);
captureError({
eventName: AUTH_TRACKER_EVENTS.code_verify,
payload: {
state: "FAILED",
},
});
}}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<input type="hidden" value={uniqueCodeFormData.email} name="email" />
@ -145,6 +166,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
</p>
<button
type="button"
data-ph-element={AUTH_TRACKER_ELEMENTS.REQUEST_NEW_CODE}
onClick={() => generateNewCode(uniqueCodeFormData.email)}
className={
isRequestNewCodeDisabled
@ -163,7 +185,14 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
</div>
<div className="space-y-2.5">
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
<Button
type="submit"
variant="primary"
className="w-full"
size="lg"
disabled={isButtonDisabled}
data-ph-element={AUTH_TRACKER_ELEMENTS.VERIFY_CODE}
>
{isRequestingNewCode ? (
t("auth.common.unique_code.sending_code")
) : isSubmitting ? (

View file

@ -3,8 +3,10 @@
import { Command } from "cmdk";
import { ContrastIcon, FileText, Layers } from "lucide-react";
// hooks
import { CYCLE_TRACKER_ELEMENTS, MODULE_TRACKER_ELEMENTS, PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
import { DiceIcon } from "@plane/ui";
import { useCommandPalette, useEventTracker } from "@/hooks/store";
// hooks
import { useCommandPalette } from "@/hooks/store";
// ui
type Props = {
@ -16,15 +18,14 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
// store hooks
const { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal } =
useCommandPalette();
const { setTrackElement } = useEventTracker();
return (
<>
<Command.Group heading="Cycle">
<Command.Item
data-ph-element={CYCLE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM}
onSelect={() => {
closePalette();
setTrackElement("Command palette");
toggleCreateCycleModal(true);
}}
className="focus:outline-none"
@ -38,9 +39,9 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
</Command.Group>
<Command.Group heading="Module">
<Command.Item
data-ph-element={MODULE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM}
onSelect={() => {
closePalette();
setTrackElement("Command palette");
toggleCreateModuleModal(true);
}}
className="focus:outline-none"
@ -56,7 +57,6 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
<Command.Item
onSelect={() => {
closePalette();
setTrackElement("Command palette");
toggleCreateViewModal(true);
}}
className="focus:outline-none"
@ -70,9 +70,9 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
</Command.Group>
<Command.Group heading="Page">
<Command.Item
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON}
onSelect={() => {
closePalette();
setTrackElement("Command palette");
toggleCreatePageModal({ isOpen: true });
}}
className="focus:outline-none"

View file

@ -8,7 +8,13 @@ import useSWR from "swr";
import { CommandIcon, FolderPlus, Search, Settings, X } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_DEFAULT_SEARCH_RESULT } from "@plane/constants";
import {
EUserPermissions,
EUserPermissionsLevel,
PROJECT_TRACKER_ELEMENTS,
WORK_ITEM_TRACKER_ELEMENTS,
WORKSPACE_DEFAULT_SEARCH_RESULT,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspaceSearchResults } from "@plane/types";
import { LayersIcon, Loader, ToggleSwitch } from "@plane/ui";
@ -28,14 +34,8 @@ import {
import { SimpleEmptyState } from "@/components/empty-state";
// helpers
// hooks
import {
useCommandPalette,
useEventTracker,
useIssueDetail,
useProject,
useUser,
useUserPermissions,
} from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import useDebounce from "@/hooks/use-debounce";
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -74,7 +74,6 @@ export const CommandModal: React.FC = observer(() => {
const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } =
useCommandPalette();
const { allowPermissions } = useUserPermissions();
const { setTrackElement } = useEventTracker();
const projectIdentifier = workItem?.toString().split("-")[0];
const sequence_id = workItem?.toString().split("-")[1];
// fetch work item details using identifier
@ -346,7 +345,9 @@ export const CommandModal: React.FC = observer(() => {
<Command.Item
onSelect={() => {
closePalette();
setTrackElement("Command Palette");
captureClick({
elementName: WORK_ITEM_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_BUTTON,
});
toggleCreateIssueModal(true);
}}
className="focus:bg-custom-background-80"
@ -364,7 +365,7 @@ export const CommandModal: React.FC = observer(() => {
<Command.Item
onSelect={() => {
closePalette();
setTrackElement("Command palette");
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON });
toggleCreateProjectModal(true);
}}
className="focus:outline-none"

View file

@ -5,21 +5,15 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { COMMAND_PALETTE_TRACKER_ELEMENTS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { copyTextToClipboard } from "@plane/utils";
import { CommandModal, ShortcutsModal } from "@/components/command-palette";
// helpers
// hooks
import {
useEventTracker,
useUser,
useAppTheme,
useCommandPalette,
useUserPermissions,
useIssueDetail,
} from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useUser, useAppTheme, useCommandPalette, useUserPermissions, useIssueDetail } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import {
@ -42,7 +36,6 @@ export const CommandPalette: FC = observer(() => {
// store hooks
const { fetchIssueWithIdentifier } = useIssueDetail();
const { toggleSidebar } = useAppTheme();
const { setTrackElement } = useEventTracker();
const { platform } = usePlatformOS();
const { data: currentUser, canPerformAnyCreateAction } = useUser();
const { toggleCommandPaletteModal, isShortcutModalOpen, toggleShortcutModal, isAnyModalOpen } = useCommandPalette();
@ -203,7 +196,7 @@ export const CommandPalette: FC = observer(() => {
toggleSidebar();
}
} else if (!isAnyModalOpen) {
setTrackElement("Shortcut key");
captureClick({ elementName: COMMAND_PALETTE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_KEY });
if (
Object.keys(shortcutsList.global).includes(keyPressed) &&
((!projectId && performAnyProjectCreateActions()) || performProjectCreateActions())
@ -242,7 +235,6 @@ export const CommandPalette: FC = observer(() => {
performProjectCreateActions,
performWorkspaceCreateActions,
projectId,
setTrackElement,
shortcutsList,
toggleCommandPaletteModal,
toggleShortcutModal,

View file

@ -1,11 +1,17 @@
"use client";
import React, { FC, useEffect, useState } from "react";
import React, { FC, useEffect } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { ArrowRight, ChevronRight } from "lucide-react";
// Plane Imports
import { CYCLE_TRACKER_EVENTS, CYCLE_STATUS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
CYCLE_TRACKER_EVENTS,
CYCLE_STATUS,
EUserPermissions,
EUserPermissionsLevel,
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ICycle } from "@plane/types";
import { setToast, TOAST_TYPE } from "@plane/ui";
@ -13,13 +19,11 @@ import { getDate, renderFormattedPayloadDate } from "@plane/utils";
// components
import { DateRangeDropdown } from "@/components/dropdowns";
// hooks
import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store";
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
import { useCycle, useUserPermissions } from "@/hooks/store";
import { useTimeZoneConverter } from "@/hooks/use-timezone-converter";
// services
import { CycleService } from "@/services/cycle.service";
// local imports
import { ArchiveCycleModal } from "../archived-cycles";
import { CycleDeleteModal } from "../delete-modal";
type Props = {
workspaceSlug: string;
@ -38,13 +42,9 @@ const cycleService = new CycleService();
export const CycleSidebarHeader: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleDetails, handleClose, isArchived = false } = props;
// states
const [archiveCycleModal, setArchiveCycleModal] = useState(false);
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// hooks
const { allowPermissions } = useUserPermissions();
const { updateCycleDetails } = useCycle();
const { captureCycleEvent } = useEventTracker();
const { t } = useTranslation();
const { renderFormattedDateInUserTimezone, getProjectUTCOffset } = useTimeZoneConverter(projectId);
@ -61,29 +61,36 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const submitChanges = async (data: Partial<ICycle>, changedProperty: string) => {
const submitChanges = async (data: Partial<ICycle>) => {
if (!workspaceSlug || !projectId || !cycleDetails.id) return;
await updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data)
.then((res) => {
captureCycleEvent({
.then(() => {
captureElementAndEvent({
element: {
elementName: CYCLE_TRACKER_ELEMENTS.RIGHT_SIDEBAR,
},
event: {
eventName: CYCLE_TRACKER_EVENTS.update,
payload: {
...res,
changed_properties: [changedProperty],
element: "Right side-peek",
state: "SUCCESS",
payload: {
id: cycleDetails.id,
},
},
});
})
.catch(() => {
captureCycleEvent({
captureElementAndEvent({
element: {
elementName: CYCLE_TRACKER_ELEMENTS.RIGHT_SIDEBAR,
},
event: {
eventName: CYCLE_TRACKER_EVENTS.update,
state: "ERROR",
payload: {
...data,
element: "Right side-peek",
state: "FAILED",
id: cycleDetails.id,
},
},
});
});
@ -122,7 +129,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
isDateValid = true;
}
if (isDateValid) {
submitChanges(payload, "date_range");
submitChanges(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("project_cycles.action.update.success.title"),
@ -145,24 +152,6 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
return (
<>
{cycleDetails && workspaceSlug && projectId && (
<>
<ArchiveCycleModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleDetails.id}
isOpen={archiveCycleModal}
handleClose={() => setArchiveCycleModal(false)}
/>
<CycleDeleteModal
cycle={cycleDetails}
isOpen={cycleDeleteModal}
handleClose={() => setCycleDeleteModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
</>
)}
<div className="sticky z-10 top-0 pt-2 flex items-center justify-between bg-custom-sidebar-background-100">
<div className="flex items-center justify-center size-5">
<button

View file

@ -3,8 +3,10 @@
import { useState, Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
// ui
import { CYCLE_TRACKER_EVENTS } from "@plane/constants";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
@ -42,16 +44,28 @@ export const ArchiveCycleModal: React.FC<Props> = (props) => {
title: "Archive success",
message: "Your archives can be found in project archives.",
});
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.archive,
payload: {
id: cycleId,
},
});
onClose();
router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
})
.catch(() =>
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Cycle could not be archived. Please try again.",
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.archive,
payload: {
id: cycleId,
},
});
})
)
.finally(() => setIsArchiving(false));
};

View file

@ -9,9 +9,10 @@ import { useTranslation } from "@plane/i18n";
import { ICycle } from "@plane/types";
// ui
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useCycle } from "@/hooks/store";
import { useCycle } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
interface ICycleDelete {
@ -27,7 +28,6 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
// states
const [loader, setLoader] = useState(false);
// store hooks
const { captureCycleEvent } = useEventTracker();
const { deleteCycle } = useCycle();
const { t } = useTranslation();
// router
@ -49,9 +49,11 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
title: "Success!",
message: "Cycle deleted successfully.",
});
captureCycleEvent({
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.delete,
payload: { ...cycle, state: "SUCCESS" },
payload: {
id: cycle.id,
},
});
})
.catch((errors) => {
@ -64,13 +66,16 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
type: TOAST_TYPE.ERROR,
message: currentError.i18n_message && t(currentError.i18n_message),
});
captureCycleEvent({
captureError({
eventName: CYCLE_TRACKER_EVENTS.delete,
payload: { ...cycle, state: "FAILED" },
payload: {
id: cycle.id,
},
error: errors,
});
})
.finally(() => handleClose());
} catch (error) {
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Warning!",

View file

@ -16,7 +16,7 @@ import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns";
import { useUser } from "@/hooks/store/user/user-user";
type Props = {
handleFormSubmit: (values: Partial<ICycle>, dirtyFields: any) => Promise<void>;
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
handleClose: () => void;
status: boolean;
projectId: string;
@ -40,7 +40,7 @@ export const CycleForm: React.FC<Props> = (props) => {
const { projectsWithCreatePermissions } = useUser();
// form data
const {
formState: { errors, isSubmitting, dirtyFields },
formState: { errors, isSubmitting },
handleSubmit,
control,
reset,
@ -64,7 +64,7 @@ export const CycleForm: React.FC<Props> = (props) => {
}, [data, reset]);
return (
<form onSubmit={handleSubmit((formData) => handleFormSubmit(formData, dirtyFields))}>
<form onSubmit={handleSubmit((formData) => handleFormSubmit(formData))}>
<div className="space-y-5 p-5">
<div className="flex items-center gap-x-3">
{!status && (

View file

@ -6,7 +6,13 @@ import { useParams, usePathname, useSearchParams } from "next/navigation";
import { useForm } from "react-hook-form";
import { Eye, Users, ArrowRight, CalendarDays } from "lucide-react";
// types
import { CYCLE_TRACKER_EVENTS, EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants";
import {
CYCLE_TRACKER_EVENTS,
EUserPermissions,
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { ICycle, TCycleGroups } from "@plane/types";
@ -19,7 +25,8 @@ import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { MergedDateDisplay } from "@/components/dropdowns/merged-date";
// hooks
import { useCycle, useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle, useMember, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { useTimeZoneConverter } from "@/hooks/use-timezone-converter";
@ -57,7 +64,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
const pathname = usePathname();
// store hooks
const { addCycleToFavorites, removeCycleFromFavorites } = useCycle();
const { captureEvent } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// local storage
@ -98,16 +104,25 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then(
() => {
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
captureEvent(CYCLE_TRACKER_EVENTS.favorite, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.favorite,
payload: {
id: cycleId,
},
});
})
.catch((error) => {
captureError({
eventName: CYCLE_TRACKER_EVENTS.favorite,
payload: {
id: cycleId,
},
error,
});
});
}
);
setPromiseToast(addToFavoritePromise, {
loading: t("project_cycles.action.favorite.loading"),
@ -126,15 +141,22 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const removeFromFavoritePromise = removeCycleFromFavorites(
workspaceSlug?.toString(),
projectId.toString(),
cycleId
).then(() => {
captureEvent(CYCLE_TRACKER_EVENTS.unfavorite, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
const removeFromFavoritePromise = removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.unfavorite,
payload: {
id: cycleId,
},
});
})
.catch((error) => {
captureError({
eventName: CYCLE_TRACKER_EVENTS.unfavorite,
payload: {
id: cycleId,
},
error,
});
});
@ -292,6 +314,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
)}
{isEditingAllowed && !cycleDetails.archived_at && (
<FavoriteStar
data-ph-element={CYCLE_TRACKER_ELEMENTS.LIST_ITEM}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

View file

@ -12,7 +12,8 @@ import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@p
import { CycleForm } from "@/components/cycles";
// constants
// hooks
import { useEventTracker, useCycle, useProject } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle, useProject } from "@/hooks/store";
import useKeypress from "@/hooks/use-keypress";
import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -35,7 +36,6 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
// states
const [activeProject, setActiveProject] = useState<string | null>(null);
// store hooks
const { captureCycleEvent } = useEventTracker();
const { workspaceProjectIds } = useProject();
const { createCycle, updateCycleDetails } = useCycle();
const { isMobile } = usePlatformOS();
@ -63,9 +63,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
title: "Success!",
message: "Cycle created successfully.",
});
captureCycleEvent({
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.create,
payload: { ...res, state: "SUCCESS" },
payload: {
id: res.id,
},
});
})
.catch((err) => {
@ -74,23 +76,24 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
title: "Error!",
message: err?.detail ?? "Error in creating cycle. Please try again.",
});
captureCycleEvent({
captureError({
eventName: CYCLE_TRACKER_EVENTS.create,
payload: { ...payload, state: "FAILED" },
error: err,
});
});
};
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>, dirtyFields: any) => {
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project_id ?? projectId.toString();
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
.then((res) => {
const changed_properties = Object.keys(dirtyFields);
captureCycleEvent({
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.update,
payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" },
payload: {
id: res.id,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@ -99,15 +102,15 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
});
})
.catch((err) => {
captureCycleEvent({
eventName: CYCLE_TRACKER_EVENTS.update,
payload: { ...payload, state: "FAILED" },
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.detail ?? "Error in updating cycle. Please try again.",
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.update,
error: err,
});
});
};
@ -121,7 +124,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
return status;
};
const handleFormSubmit = async (formData: Partial<ICycle>, dirtyFields: any) => {
const handleFormSubmit = async (formData: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return;
const payload: Partial<ICycle> = {
@ -145,7 +148,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
}
if (isDateValid) {
if (data) await handleUpdateCycle(data.id, payload, dirtyFields);
if (data) await handleUpdateCycle(data.id, payload);
else {
await handleCreateCycle(payload).then(() => {
setCycleTab("all");

View file

@ -6,7 +6,12 @@ import { observer } from "mobx-react";
// icons
import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
// ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
CYCLE_TRACKER_EVENTS,
EUserPermissions,
EUserPermissionsLevel,
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils";
@ -14,7 +19,8 @@ import { copyUrlToClipboard, cn } from "@plane/utils";
import { ArchiveCycleModal, CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles";
// helpers
// hooks
import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store";
import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useEndCycle, EndCycleModal } from "@/plane-web/components/cycles";
@ -35,7 +41,6 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
const [archiveCycleModal, setArchiveCycleModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
// store hooks
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { getCycleById, restoreCycle } = useCycle();
const { t } = useTranslation();
@ -69,7 +74,6 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
const handleOpenInNewTab = () => window.open(`/${cycleLink}`, "_blank");
const handleEditCycle = () => {
setTrackElement("Cycles page list layout");
setUpdateModal(true);
};
@ -83,18 +87,29 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
title: t("project_cycles.action.restore.success.title"),
message: t("project_cycles.action.restore.success.description"),
});
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.restore,
payload: {
id: cycleId,
},
});
router.push(`/${workspaceSlug}/projects/${projectId}/archives/cycles`);
})
.catch(() =>
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("project_cycles.action.restore.failed.title"),
message: t("project_cycles.action.restore.failed.description"),
})
);
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.restore,
payload: {
id: cycleId,
},
});
});
const handleDeleteCycle = () => {
setTrackElement("Cycles page list layout");
setDeleteModal(true);
};
@ -149,6 +164,16 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
if (endCycleContextMenu) MENU_ITEMS.splice(3, 0, endCycleContextMenu);
const CONTEXT_MENU_ITEMS = MENU_ITEMS.map((item) => ({
...item,
action: () => {
captureClick({
elementName: CYCLE_TRACKER_ELEMENTS.CONTEXT_MENU,
});
item.action();
},
}));
return (
<>
{cycleDetails && (
@ -187,7 +212,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
)}
</div>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu ellipsis placement="bottom-end" closeOnSelect maxHeight="lg" buttonClassName={customClassName}>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
@ -197,6 +222,9 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({
elementName: CYCLE_TRACKER_ELEMENTS.QUICK_ACTIONS,
});
item.action();
}}
className={cn(

View file

@ -1,18 +1,21 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import useSWR from "swr";
// plane imports
import { PRODUCT_TOUR_TRACKER_EVENTS } from "@plane/constants";
import { ContentWrapper } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { TourRoot } from "@/components/onboarding";
// constants
// helpers
import { captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useUserProfile, useEventTracker, useUser } from "@/hooks/store";
import { useUserProfile, useUser } from "@/hooks/store";
import { useHome } from "@/hooks/store/use-home";
import useSize from "@/hooks/use-window-size";
// plane web components
import { HomePeekOverviewsRoot } from "@/plane-web/components/home";
// local imports
import { DashboardWidgets } from "./home-dashboard-widgets";
import { UserGreetingsView } from "./user-greetings";
@ -21,7 +24,6 @@ export const WorkspaceHomeView = observer(() => {
const { workspaceSlug } = useParams();
const { data: currentUser } = useUser();
const { data: currentUserProfile, updateTourCompleted } = useUserProfile();
const { captureEvent } = useEventTracker();
const { toggleWidgetSettings, fetchWidgets } = useHome();
const [windowWidth] = useSize();
@ -38,9 +40,11 @@ export const WorkspaceHomeView = observer(() => {
const handleTourCompleted = () => {
updateTourCompleted()
.then(() => {
captureEvent(PRODUCT_TOUR_TRACKER_EVENTS.complete, {
captureSuccess({
eventName: PRODUCT_TOUR_TRACKER_EVENTS.complete,
payload: {
user_id: currentUser?.id,
state: "SUCCESS",
},
});
})
.catch((error) => {

View file

@ -5,20 +5,14 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { Briefcase, Check, Hotel, Users, X } from "lucide-react";
// plane ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { cn, getFileURL } from "@plane/utils";
// helpers
// hooks
import {
useCommandPalette,
useEventTracker,
useProject,
useUser,
useUserPermissions,
useWorkspace,
} from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useProject, useUser, useUserPermissions, useWorkspace } from "@/hooks/store";
// plane web constants
export const NoProjectsEmptyState = observer(() => {
@ -27,7 +21,6 @@ export const NoProjectsEmptyState = observer(() => {
// store hooks
const { allowPermissions } = useUserPermissions();
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { data: currentUser } = useUser();
const { joinedProjectIds } = useProject();
const { currentWorkspace: activeWorkspace } = useWorkspace();
@ -59,8 +52,8 @@ export const NoProjectsEmptyState = observer(() => {
if (!canCreateProject) return;
e.preventDefault();
e.stopPropagation();
setTrackElement("Sidebar");
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
},
disabled: !canCreateProject,
},

View file

@ -2,7 +2,6 @@
import { Dispatch, SetStateAction, useEffect, useMemo, useRef } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { EditorRefApi } from "@plane/editor";
@ -22,7 +21,8 @@ import {
} from "@/components/issues";
// helpers
// hooks
import { useEventTracker, useIssueDetail, useMember, useProject, useProjectInbox, useUser } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail, useMember, useProject, useProjectInbox, useUser } from "@/hooks/store";
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
// store types
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
@ -45,8 +45,6 @@ type Props = {
export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, inboxIssue, isEditable, isSubmitting, setIsSubmitting } = props;
// navigation
const pathname = usePathname();
// refs
const editorRef = useRef<EditorRefApi>(null);
// store hooks
@ -57,8 +55,6 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
const { removeIssue, archiveIssue } = useIssueDetail();
// reload confirmation
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
// event tracker
const { captureIssueEvent } = useEventTracker();
useEffect(() => {
if (isSubmitting === "submitted") {
@ -104,10 +100,9 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
type: TOAST_TYPE.SUCCESS,
message: "Work item deleted successfully",
});
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: _issueId, state: "SUCCESS", element: "Work item detail page" },
path: pathname,
payload: { id: _issueId },
});
} catch (error) {
console.log("Error in deleting work item:", error);
@ -116,56 +111,46 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
type: TOAST_TYPE.ERROR,
message: "Work item delete failed",
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: _issueId, state: "FAILED", element: "Work item detail page" },
path: pathname,
payload: { id: _issueId },
error: error as Error,
});
}
},
update: async (_workspaceSlug: string, _projectId: string, _issueId: string, data: Partial<TIssue>) => {
try {
await inboxIssue.updateIssue(data);
captureIssueEvent({
eventName: "Inbox work item updated",
payload: { ...data, state: "SUCCESS", element: "Inbox" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
},
path: pathname,
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: _issueId },
});
} catch {
} catch (error) {
setToast({
title: "Work item update failed",
type: TOAST_TYPE.ERROR,
message: "Work item update failed",
});
captureIssueEvent({
eventName: "Inbox work item updated",
payload: { state: "SUCCESS", element: "Inbox" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
},
path: pathname,
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: _issueId },
error: error as Error,
});
}
},
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await archiveIssue(workspaceSlug, projectId, issueId);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId, state: "SUCCESS", element: "Work item details page" },
path: pathname,
payload: { id: issueId },
});
} catch (error) {
console.log("Error in archiving issue:", error);
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId, state: "FAILED", element: "Work item details page" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},

View file

@ -2,7 +2,6 @@
import { FC, FormEvent, useCallback, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
// plane imports
import { ETabIndices, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { EditorRefApi } from "@plane/editor";
@ -14,9 +13,9 @@ import { renderFormattedPayloadDate, getTabIndex } from "@plane/utils";
// components
import { InboxIssueTitle, InboxIssueDescription, InboxIssueProperties } from "@/components/inbox/modals/create-modal";
// constants
// helpers
// hooks
import { useEventTracker, useProject, useProjectInbox, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProject, useProjectInbox, useWorkspace } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import useKeypress from "@/hooks/use-keypress";
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -53,14 +52,12 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
const [uploadedAssetIds, setUploadedAssetIds] = useState<string[]>([]);
// router
const router = useAppRouter();
const pathname = usePathname();
// refs
const descriptionEditorRef = useRef<EditorRefApi>(null);
const submitBtnRef = useRef<HTMLButtonElement | null>(null);
const formRef = useRef<HTMLFormElement | null>(null);
const modalContainerRef = useRef<HTMLDivElement | null>(null);
// hooks
const { captureIssueEvent } = useEventTracker();
const { createInboxIssue } = useProjectInbox();
const { getWorkspaceBySlug } = useWorkspace();
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
@ -167,14 +164,11 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
descriptionEditorRef?.current?.clearEditor();
setFormData(defaultIssueData);
}
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.create,
payload: {
...formData,
state: "SUCCESS",
element: "Inbox page",
id: res?.issue?.id,
},
path: pathname,
});
setToast({
type: TOAST_TYPE.SUCCESS,
@ -184,14 +178,12 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
})
.catch((error) => {
console.error(error);
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.create,
payload: {
...formData,
state: "FAILED",
element: "Inbox page",
id: formData?.id,
},
path: pathname,
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,

View file

@ -5,19 +5,20 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useFormContext, Controller } from "react-hook-form";
import { Plus } from "lucide-react";
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { IJiraImporterForm } from "@plane/types";
// hooks
// components
import { CustomSelect, Input } from "@plane/ui";
// helpers
import { checkEmailValidity } from "@plane/utils";
import { useCommandPalette, useEventTracker, useProject } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useProject } from "@/hooks/store";
// types
export const JiraGetImportDetail: React.FC = observer(() => {
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { workspaceProjectIds, getProjectById } = useProject();
// form info
const {
@ -201,8 +202,9 @@ export const JiraGetImportDetail: React.FC = observer(() => {
<div>
<button
type="button"
data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON}
onClick={() => {
setTrackElement("Jira import detail page");
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.CREATE_PROJECT_JIRA_IMPORT_DETAIL_PAGE });
toggleCreateProjectModal(true);
}}
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-custom-text-200"

View file

@ -1,10 +1,12 @@
"use client";
import { useMemo } from "react";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { EIssueServiceType, TIssueServiceType } from "@plane/types";
// plane ui
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
// hooks
import { useEventTracker, useIssueDetail } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store";
// types
import { TAttachmentUploadStatus } from "@/store/issue/issue-details/attachment.store";
@ -31,7 +33,6 @@ export const useAttachmentOperations = (
const {
attachment: { createAttachment, removeAttachment, getAttachmentsUploadStatusByIssueId },
} = useIssueDetail(issueServiceType);
const { captureIssueEvent } = useEventTracker();
const attachmentOperations: TAttachmentOperations = useMemo(
() => ({
@ -51,19 +52,16 @@ export const useAttachmentOperations = (
},
});
const res = await attachmentUploadPromise;
captureIssueEvent({
eventName: "Issue attachment added",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "attachment",
change_details: res.id,
},
await attachmentUploadPromise;
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.attachment.add,
payload: { id: issueId },
});
} catch (error) {
captureIssueEvent({
eventName: "Issue attachment added",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.attachment.add,
payload: { id: issueId },
error: error as Error,
});
throw error;
}
@ -77,22 +75,15 @@ export const useAttachmentOperations = (
type: TOAST_TYPE.SUCCESS,
title: "Attachment removed",
});
captureIssueEvent({
eventName: "Issue attachment deleted",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "attachment",
change_details: "",
},
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.attachment.remove,
payload: { id: issueId },
});
} catch (error) {
captureIssueEvent({
eventName: "Issue attachment deleted",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "attachment",
change_details: "",
},
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.attachment.remove,
payload: { id: issueId },
error: error as Error,
});
setToast({
message: "The Attachment could not be removed",
@ -102,7 +93,7 @@ export const useAttachmentOperations = (
}
},
}),
[captureIssueEvent, workspaceSlug, projectId, issueId, createAttachment, removeAttachment]
[workspaceSlug, projectId, issueId, createAttachment, removeAttachment]
);
const attachmentsUploadStatus = getAttachmentsUploadStatusByIssueId(issueId);

View file

@ -1,6 +1,5 @@
"use client";
import { useMemo } from "react";
import { usePathname } from "next/navigation";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
@ -8,7 +7,8 @@ import { EIssueServiceType, TIssue, TIssueServiceType } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
import { copyUrlToClipboard } from "@plane/utils";
// hooks
import { useEventTracker, useIssueDetail } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store";
export type TRelationIssueOperations = {
copyLink: (path: string) => void;
@ -20,8 +20,6 @@ export const useRelationOperations = (
issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
): TRelationIssueOperations => {
const { updateIssue, removeIssue } = useIssueDetail(issueServiceType);
const { captureIssueEvent } = useEventTracker();
const pathname = usePathname();
const { t } = useTranslation();
// derived values
const entityName = issueServiceType === EIssueServiceType.ISSUES ? "Work item" : "Epic";
@ -40,29 +38,20 @@ export const useRelationOperations = (
update: async (workspaceSlug, projectId, issueId, data) => {
try {
await updateIssue(workspaceSlug, projectId, issueId, data);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...data, issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
},
path: pathname,
payload: { id: issueId },
});
setToast({
title: t("toast.success"),
type: TOAST_TYPE.SUCCESS,
message: t("entity.update.success", { entity: entityName }),
});
} catch {
captureIssueEvent({
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
},
path: pathname,
payload: { id: issueId },
error: error as Error,
});
setToast({
title: t("toast.error"),
@ -74,22 +63,21 @@ export const useRelationOperations = (
remove: async (workspaceSlug, projectId, issueId) => {
try {
return removeIssue(workspaceSlug, projectId, issueId).then(() => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
path: pathname,
payload: { id: issueId },
});
});
} catch {
captureIssueEvent({
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
}),
[captureIssueEvent, entityName, pathname, removeIssue, t, updateIssue]
[entityName, removeIssue, t, updateIssue]
);
return issueOperations;

View file

@ -1,21 +1,22 @@
"use client";
import { useMemo } from "react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EIssueServiceType, TIssueServiceType, TSubIssueOperations } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
import { copyUrlToClipboard } from "@plane/utils";
// hooks
import { useEventTracker, useIssueDetail, useProjectState } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail, useProjectState } from "@/hooks/store";
// plane web helpers
import { updateEpicAnalytics } from "@/plane-web/helpers/epic-analytics";
export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSubIssueOperations => {
// router
const { epicId: epicIdParam } = useParams();
const pathname = usePathname();
// translation
const { t } = useTranslation();
// store hooks
@ -32,7 +33,6 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
const { peekIssue: epicPeekIssue } = useIssueDetail(EIssueServiceType.EPICS);
// const { updateEpicAnalytics } = useIssueTypes();
const { updateAnalytics } = updateEpicAnalytics();
const { captureIssueEvent } = useEventTracker();
// derived values
const epicId = epicIdParam || epicPeekIssue?.issueId;
@ -128,14 +128,9 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
}
}
}
captureIssueEvent({
eventName: "Sub-issue updated",
payload: { ...oldIssue, ...issueData, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: Object.keys(issueData).join(","),
change_details: Object.values(issueData).join(","),
},
path: pathname,
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.update,
payload: { id: issueId, parent_id: parentIssueId },
});
setToast({
type: TOAST_TYPE.SUCCESS,
@ -143,15 +138,11 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
message: t("sub_work_item.update.success"),
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
} catch {
captureIssueEvent({
eventName: "Sub-issue updated",
payload: { ...oldIssue, ...issueData, state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: Object.keys(issueData).join(","),
change_details: Object.values(issueData).join(","),
},
path: pathname,
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.update,
payload: { id: issueId, parent_id: parentIssueId },
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,
@ -179,25 +170,16 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
title: t("toast.success"),
message: t("sub_work_item.remove.success"),
});
captureIssueEvent({
eventName: "Sub-issue removed",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "parent_id",
change_details: parentIssueId,
},
path: pathname,
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.remove,
payload: { id: issueId, parent_id: parentIssueId },
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
} catch {
captureIssueEvent({
eventName: "Sub-issue removed",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "parent_id",
change_details: parentIssueId,
},
path: pathname,
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.remove,
payload: { id: issueId, parent_id: parentIssueId },
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,
@ -210,18 +192,17 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
try {
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
return deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId).then(() => {
captureIssueEvent({
eventName: "Sub-issue deleted",
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
path: pathname,
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.delete,
payload: { id: issueId, parent_id: parentIssueId },
});
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
});
} catch {
captureIssueEvent({
eventName: "Sub-issue removed",
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
path: pathname,
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.sub_issue.delete,
payload: { id: issueId, parent_id: parentIssueId },
error: error as Error,
});
setToast({
type: TOAST_TYPE.ERROR,
@ -232,7 +213,6 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
},
}),
[
captureIssueEvent,
createSubIssues,
deleteSubIssue,
epicId,
@ -240,7 +220,6 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
getIssueById,
getStateById,
issueServiceType,
pathname,
removeSubIssue,
setSubIssueHelpers,
t,

View file

@ -3,11 +3,13 @@ import React, { FC } from "react";
import { observer } from "mobx-react";
import { LayersIcon, Plus } from "lucide-react";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TIssue, TIssueServiceType } from "@plane/types";
import { CustomMenu } from "@plane/ui";
// hooks
import { useEventTracker, useIssueDetail } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store";
type Props = {
issueId: string;
@ -28,7 +30,6 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
setIssueCrudOperationState,
issueCrudOperationState,
} = useIssueDetail(issueServiceType);
const { setTrackElement } = useEventTracker();
// derived values
const issue = getIssueById(issueId);
@ -52,13 +53,13 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
};
const handleCreateNew = () => {
setTrackElement("Issue detail nested sub-issue");
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.sub_issue.create });
handleIssueCrudState("create", issueId, null);
toggleCreateIssueModal(true);
};
const handleAddExisting = () => {
setTrackElement("Issue detail nested sub-issue");
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.sub_issue.add_existing });
handleIssueCrudState("existing", issueId, null);
toggleSubIssuesModal(issue.id);
};

View file

@ -2,7 +2,6 @@
import React, { FC, useState } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { ArchiveIcon, ArchiveRestoreIcon, LinkIcon, Trash2 } from "lucide-react";
import {
ARCHIVABLE_STATE_GROUPS,
@ -18,8 +17,8 @@ import { cn, generateWorkItemLink, copyTextToClipboard } from "@plane/utils";
import { ArchiveIssueModal, DeleteIssueModal, IssueSubscription } from "@/components/issues";
// helpers
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import {
useEventTracker,
useIssueDetail,
useIssues,
useProject,
@ -64,8 +63,6 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
const {
issues: { removeIssue: removeArchivedIssue },
} = useIssues(EIssuesStoreType.ARCHIVED);
const { captureIssueEvent } = useEventTracker();
const pathname = usePathname();
// derived values
const issue = getIssueById(issueId);
@ -103,22 +100,21 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
return deleteIssue(workspaceSlug, projectId, issueId).then(() => {
router.push(redirectionPath);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId, state: "SUCCESS", element: "Work item detail page" },
path: pathname,
payload: { id: issueId },
});
});
} catch {
} catch (error) {
setToast({
title: t("toast.error "),
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId, state: "FAILED", element: "Work item detail page" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
};
@ -128,16 +124,15 @@ export const IssueDetailQuickActions: FC<Props> = observer((props) => {
await archiveIssue(workspaceSlug, projectId, issueId).then(() => {
router.push(`/${workspaceSlug}/projects/${projectId}/archives/issues/${issue.id}`);
});
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId, state: "SUCCESS", element: "Issue details page" },
path: pathname,
payload: { id: issueId },
});
} catch {
captureIssueEvent({
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId, state: "FAILED", element: "Issue details page" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
};

View file

@ -2,7 +2,6 @@
import { FC, useMemo } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
// types
import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
@ -14,7 +13,8 @@ import { EmptyState } from "@/components/common";
import { IssueDetailsSidebar, IssuePeekOverview } from "@/components/issues";
// constants
// hooks
import { useAppTheme, useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useAppTheme, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// images
import emptyIssue from "@/public/empty-state/issue.svg";
@ -57,7 +57,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
const { workspaceSlug, projectId, issueId, is_archived = false } = props;
// router
const router = useAppRouter();
const pathname = usePathname();
// hooks
const {
issue: { getIssueById },
@ -74,7 +73,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
const {
issues: { removeIssue: removeArchivedIssue },
} = useIssues(EIssuesStoreType.ARCHIVED);
const { captureIssueEvent } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { issueDetailSidebarCollapsed } = useAppTheme();
@ -90,25 +88,16 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
try {
await updateIssue(workspaceSlug, projectId, issueId, data);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...data, issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
},
path: pathname,
payload: { id: issueId },
});
} catch (error) {
console.log("Error in updating issue:", error);
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
},
path: pathname,
payload: { id: issueId },
error: error as Error,
});
setToast({
title: t("common.error.label"),
@ -126,10 +115,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
type: TOAST_TYPE.SUCCESS,
message: t("entity.delete.success", { entity: t("issue.label") }),
});
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
path: pathname,
payload: { id: issueId },
});
} catch (error) {
console.log("Error in deleting issue:", error);
@ -138,85 +126,66 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label") }),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await archiveIssue(workspaceSlug, projectId, issueId);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId, state: "SUCCESS", element: "Issue details page" },
path: pathname,
payload: { id: issueId },
});
} catch (error) {
console.log("Error in archiving issue:", error);
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId, state: "FAILED", element: "Issue details page" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
addCycleToIssue: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
try {
await addCycleToIssue(workspaceSlug, projectId, cycleId, issueId);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
change_details: cycleId,
},
path: pathname,
payload: { id: issueId },
});
} catch {
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("common.error.label"),
message: t("issue.add.cycle.failed"),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
change_details: cycleId,
},
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
try {
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
change_details: cycleId,
},
path: pathname,
payload: { id: issueId },
});
} catch {
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("common.error.label"),
message: t("issue.add.cycle.failed"),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
change_details: cycleId,
},
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
@ -235,24 +204,15 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
},
});
await removeFromCyclePromise;
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
change_details: "",
},
path: pathname,
payload: { id: issueId },
});
} catch {
captureIssueEvent({
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "cycle_id",
change_details: "",
},
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
@ -271,24 +231,15 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
},
});
await removeFromModulePromise;
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "module_id",
change_details: "",
},
path: pathname,
payload: { id: issueId },
});
} catch {
captureIssueEvent({
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
updates: {
changed_property: "module_id",
change_details: "",
},
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
@ -300,14 +251,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
removeModuleIds: string[]
) => {
const promise = await changeModulesInIssue(workspaceSlug, projectId, issueId, addModuleIds, removeModuleIds);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "module_id",
change_details: { addModuleIds, removeModuleIds },
},
path: pathname,
payload: { id: issueId },
});
return promise;
},
@ -324,9 +270,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
removeIssueFromCycle,
changeModulesInIssue,
removeIssueFromModule,
captureIssueEvent,
pathname,
t,
issueId,
]
);

View file

@ -6,14 +6,15 @@ import size from "lodash/size";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EIssueFilterType, EUserPermissionsLevel } from "@plane/constants";
import { EIssueFilterType, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EIssuesStoreType, EUserProjectRoles, IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { ExistingIssuesListModal } from "@/components/core";
import { DetailedEmptyState } from "@/components/empty-state";
import { useCommandPalette, useCycle, useEventTracker, useIssues, useUserPermissions } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useCycle, useIssues, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const CycleEmptyState: React.FC = observer(() => {
@ -27,7 +28,6 @@ export const CycleEmptyState: React.FC = observer(() => {
const { getCycleById } = useCycle();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// derived values
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
@ -133,7 +133,7 @@ export const CycleEmptyState: React.FC = observer(() => {
primaryButton={{
text: t("project_cycles.empty_state.no_issues.primary_button.text"),
onClick: () => {
setTrackElement("Cycle issue empty state");
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.CYCLE });
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
},
disabled: !canPerformEmptyStateActions,

View file

@ -1,13 +1,14 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EIssuesStoreType, EUserWorkspaceRoles } from "@plane/types";
// components
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const GlobalViewEmptyState: React.FC = observer(() => {
@ -17,7 +18,6 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
// store hooks
const { workspaceProjectIds } = useProject();
const { toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// derived values
const hasMemberLevelPermission = allowPermissions(
@ -46,8 +46,8 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
title={t("workspace_projects.empty_state.no_projects.primary_button.comic.title")}
description={t("workspace_projects.empty_state.no_projects.primary_button.comic.description")}
onClick={() => {
setTrackElement("All issues empty state");
toggleCreateProjectModal(true);
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW });
}}
disabled={!hasMemberLevelPermission}
/>
@ -67,7 +67,7 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
? {
text: t(`workspace_views.empty_state.${resolvedCurrentView}.primary_button.text`),
onClick: () => {
setTrackElement("All issues empty state");
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.GLOBAL_VIEW });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
disabled: !hasMemberLevelPermission,

View file

@ -5,15 +5,16 @@ import size from "lodash/size";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EIssueFilterType, EUserPermissionsLevel } from "@plane/constants";
import { EIssueFilterType, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EIssuesStoreType, EUserProjectRoles, IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { ExistingIssuesListModal } from "@/components/core";
import { DetailedEmptyState } from "@/components/empty-state";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette, useEventTracker, useIssues, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useIssues, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ModuleEmptyState: React.FC = observer(() => {
@ -26,7 +27,6 @@ export const ModuleEmptyState: React.FC = observer(() => {
// store hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// derived values
const userFilters = issuesFilter?.issueFilters?.filters;
@ -119,7 +119,7 @@ export const ModuleEmptyState: React.FC = observer(() => {
primaryButton={{
text: t("project_module.empty_state.no_issues.primary_button.text"),
onClick: () => {
setTrackElement("Module issue empty state");
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.MODULE });
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
},
disabled: !canPerformEmptyStateActions,

View file

@ -2,13 +2,14 @@ import size from "lodash/size";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EIssueFilterType, EUserPermissionsLevel } from "@plane/constants";
import { EIssueFilterType, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EIssuesStoreType, EUserProjectRoles, IIssueFilterOptions } from "@plane/types";
// components
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette, useEventTracker, useIssues, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useIssues, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const ProjectEmptyState: React.FC = observer(() => {
@ -18,7 +19,6 @@ export const ProjectEmptyState: React.FC = observer(() => {
const { t } = useTranslation();
// store hooks
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const { allowPermissions } = useUserPermissions();
// derived values
@ -76,7 +76,7 @@ export const ProjectEmptyState: React.FC = observer(() => {
title={t("project_issues.empty_state.no_issues.primary_button.comic.title")}
description={t("project_issues.empty_state.no_issues.primary_button.comic.description")}
onClick={() => {
setTrackElement("Project issue empty state");
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.WORK_ITEMS });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
disabled={!canPerformEmptyStateActions}

View file

@ -1,19 +1,18 @@
import { observer } from "mobx-react";
import { PlusIcon } from "lucide-react";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EIssuesStoreType } from "@plane/types";
import { EmptyState } from "@/components/common";
// constants
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useUserPermissions } from "@/hooks/store";
// assets
import emptyIssue from "@/public/empty-state/issue.svg";
export const ProjectViewEmptyState: React.FC = observer(() => {
// store hooks
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// auth
@ -34,7 +33,7 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
text: "New work item",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("View work item empty state");
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON.PROJECT_VIEW });
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
},
}

View file

@ -11,7 +11,7 @@ import {
EIssueFilterType,
EUserPermissions,
EUserPermissionsLevel,
GLOBAL_VIEW_TOUR_TRACKER_EVENTS,
GLOBAL_VIEW_TRACKER_EVENTS,
} from "@plane/constants";
import { EIssuesStoreType, EViewAccess, IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
import { Header, EHeaderVariant, Loader } from "@plane/ui";
@ -21,7 +21,8 @@ import { AppliedFiltersList } from "@/components/issues";
import { UpdateViewComponent } from "@/components/views/update-view-component";
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// hooks
import { useEventTracker, useGlobalView, useIssues, useLabel, useUser, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useGlobalView, useIssues, useLabel, useUser, useUserPermissions } from "@/hooks/store";
import { getAreFiltersEqual } from "../../../utils";
type Props = {
@ -39,7 +40,6 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
} = useIssues(EIssuesStoreType.GLOBAL);
const { workspaceLabels } = useLabel();
const { globalViewMap, updateGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker();
const { data } = useUser();
const { allowPermissions } = useUserPermissions();
@ -107,13 +107,23 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
const handleUpdateView = () => {
if (!workspaceSlug || !globalViewId) return;
updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters).then((res) => {
updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters)
.then((res) => {
if (res)
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.update, {
view_id: res.id,
applied_filters: res.filters,
state: "SUCCESS",
element: "Spreadsheet view",
captureSuccess({
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
payload: {
view_id: globalViewId,
},
});
})
.catch((error) => {
captureError({
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
payload: {
view_id: globalViewId,
},
error: error,
});
});
};

View file

@ -5,7 +5,7 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
import {
EIssueLayoutTypes,
EIssueFilterType,
@ -17,7 +17,8 @@ import { EIssueServiceType, EIssuesStoreType } from "@plane/types";
import { DeleteIssueModal } from "@/components/issues";
//constants
//hooks
import { useEventTracker, useIssueDetail, useIssues, useKanbanView, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail, useIssues, useKanbanView, useUserPermissions } from "@/hooks/store";
import { useGroupIssuesDragNDrop } from "@/hooks/use-group-dragndrop";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useIssuesActions } from "@/hooks/use-issues-actions";
@ -62,11 +63,9 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
} = props;
// router
const { workspaceSlug, projectId } = useParams();
const pathname = usePathname();
// store hooks
const storeType = useIssueStoreType() as KanbanStoreType;
const { allowPermissions } = useUserPermissions();
const { captureIssueEvent } = useEventTracker();
const { issueMap, issuesFilter, issues } = useIssues(storeType);
const {
issue: { getIssueById },
@ -205,14 +204,22 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
if (!draggedIssueId || !draggedIssue) return;
await removeIssue(draggedIssue.project_id, draggedIssueId).finally(() => {
await removeIssue(draggedIssue.project_id, draggedIssueId)
.then(() => {
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: draggedIssueId },
});
})
.catch(() => {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: draggedIssueId },
});
})
.finally(() => {
setDeleteIssueModal(false);
setDraggedIssueId(undefined);
captureIssueEvent({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: draggedIssueId, state: "FAILED", element: "Kanban layout drag & drop" },
path: pathname,
});
});
};

View file

@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// lucide icons
import { Minimize2, Maximize2, Circle, Plus } from "lucide-react";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { TIssue, ISearchIssueResponse, TIssueKanbanFilters, TIssueGroupByOptions } from "@plane/types";
// ui
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
@ -12,8 +13,7 @@ import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
import { ExistingIssuesListModal } from "@/components/core";
import { CreateUpdateIssueModal } from "@/components/issues";
// constants
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { CreateUpdateEpicModal } from "@/plane-web/components/epics/epic-modal";
// types
@ -56,7 +56,6 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
// hooks
const storeType = useIssueStoreType();
const { setTrackElement } = useEventTracker();
// router
const { workspaceSlug, projectId, moduleId, cycleId } = useParams();
const pathname = usePathname();
@ -167,7 +166,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
>
<CustomMenu.MenuItem
onClick={() => {
setTrackElement("Kanban layout");
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true);
}}
>
@ -175,7 +174,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
setTrackElement("Kanban layout");
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.add_existing });
setOpenExistingIssueListModal(true);
}}
>
@ -186,7 +185,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
<div
className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
onClick={() => {
setTrackElement("Kanban layout");
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true);
}}
>

View file

@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { CircleDashed, Plus } from "lucide-react";
// types
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { TIssue, ISearchIssueResponse, TIssueGroupByOptions } from "@plane/types";
// ui
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
@ -13,9 +14,7 @@ import { cn } from "@plane/utils";
import { ExistingIssuesListModal, MultipleSelectGroupAction } from "@/components/core";
import { CreateUpdateIssueModal } from "@/components/issues";
// constants
// helpers
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { TSelectionHelper } from "@/hooks/use-multiple-select";
// plane-web
@ -59,8 +58,6 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
// router
const { workspaceSlug, projectId, moduleId, cycleId } = useParams();
const pathname = usePathname();
// hooks
const { setTrackElement } = useEventTracker();
const storeType = useIssueStoreType();
// derived values
const isDraftIssue = pathname.includes("draft-issue");
@ -134,7 +131,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
>
<CustomMenu.MenuItem
onClick={() => {
setTrackElement("List layout");
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true);
}}
>
@ -142,7 +139,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
setTrackElement("List layout");
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.add_existing });
setOpenExistingIssueListModal(true);
}}
>
@ -153,7 +150,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
<div
className="flex h-5 w-5 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
onClick={() => {
setTrackElement("List layout");
captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true);
}}
>

View file

@ -3,7 +3,7 @@
import { useCallback, useMemo, SyntheticEvent } from "react";
import xor from "lodash/xor";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
// icons
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
// types
@ -34,7 +34,8 @@ import {
// constants
// helpers
// hooks
import { useEventTracker, useLabel, useIssues, useProjectState, useProject, useProjectEstimates } from "@/hooks/store";
import { captureSuccess } from "@/helpers/event-tracker.helper";
import { useLabel, useIssues, useProjectState, useProject, useProjectEstimates } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -55,13 +56,12 @@ export interface IIssueProperties {
}
export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const { issue, updateIssue, displayProperties, activeLayout, isReadOnly, className, isEpic = false } = props;
const { issue, updateIssue, displayProperties, isReadOnly, className, isEpic = false } = props;
// i18n
const { t } = useTranslation();
// store hooks
const { getProjectById } = useProject();
const { labelMap } = useLabel();
const { captureIssueEvent } = useEventTracker();
const storeType = useIssueStoreType();
const {
issues: { changeModulesInIssue },
@ -77,9 +77,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams();
const pathname = usePathname();
const currentLayout = `${activeLayout} layout`;
// derived values
const stateDetails = getStateById(issue.state_id);
const subIssueCount = issue?.sub_issues_count ?? 0;
@ -109,14 +107,9 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleState = (stateId: string) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { state_id: stateId }).then(() => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: pathname,
updates: {
changed_property: "state",
change_details: stateId,
},
payload: { id: issue.id },
});
});
};
@ -124,14 +117,9 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handlePriority = (value: TIssuePriorities) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { priority: value }).then(() => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: pathname,
updates: {
changed_property: "priority",
change_details: value,
},
payload: { id: issue.id },
});
});
};
@ -139,14 +127,9 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleLabel = (ids: string[]) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { label_ids: ids }).then(() => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: pathname,
updates: {
changed_property: "labels",
change_details: ids,
},
payload: { id: issue.id },
});
});
};
@ -154,14 +137,9 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleAssignee = (ids: string[]) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { assignee_ids: ids }).then(() => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: pathname,
updates: {
changed_property: "assignees",
change_details: ids,
},
payload: { id: issue.id },
});
});
};
@ -179,14 +157,12 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
if (modulesToAdd.length > 0) issueOperations.addModulesToIssue(modulesToAdd);
if (modulesToRemove.length > 0) issueOperations.removeModulesFromIssue(modulesToRemove);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: pathname,
updates: { changed_property: "module_ids", change_details: { module_ids: moduleIds } },
payload: { id: issue.id },
});
},
[issueOperations, captureIssueEvent, currentLayout, pathname, issue]
[issueOperations, issue]
);
const handleCycle = useCallback(
@ -195,28 +171,21 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
if (cycleId) issueOperations.addIssueToCycle?.(cycleId);
else issueOperations.removeIssueFromCycle?.();
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: pathname,
updates: { changed_property: "cycle", change_details: { cycle_id: cycleId } },
payload: { id: issue.id },
});
},
[issue, issueOperations, captureIssueEvent, currentLayout, pathname]
[issue, issueOperations]
);
const handleStartDate = (date: Date | null) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { start_date: date ? renderFormattedPayloadDate(date) : null }).then(
() => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: pathname,
updates: {
changed_property: "start_date",
change_details: date ? renderFormattedPayloadDate(date) : null,
},
payload: { id: issue.id },
});
}
);
@ -226,14 +195,9 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { target_date: date ? renderFormattedPayloadDate(date) : null }).then(
() => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: pathname,
updates: {
changed_property: "target_date",
change_details: date ? renderFormattedPayloadDate(date) : null,
},
payload: { id: issue.id },
});
}
);
@ -242,14 +206,9 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const handleEstimate = (value: string | undefined) => {
if (updateIssue)
updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issue, state: "SUCCESS", element: currentLayout },
path: pathname,
updates: {
changed_property: "estimate_point",
change_details: value,
},
payload: { id: issue.id },
});
});
};

View file

@ -5,14 +5,15 @@ import omit from "lodash/omit";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { ARCHIVABLE_STATE_GROUPS } from "@plane/constants";
import { ARCHIVABLE_STATE_GROUPS, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EIssuesStoreType, TIssue } from "@plane/types";
import { ContextMenu, CustomMenu } from "@plane/ui";
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// hooks
import { useEventTracker, useProject, useProjectState } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useProject, useProjectState } from "@/hooks/store";
// plane-web components
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
import { IQuickActionProps } from "../list/list-view-types";
@ -39,8 +40,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false);
// router
const { workspaceSlug } = useParams();
// store hooks
const { setTrackElement } = useEventTracker();
const { getStateById } = useProjectState();
const { getProjectIdentifierById } = useProject();
// derived values
@ -70,7 +69,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
isArchivingAllowed,
isDeletingAllowed: isEditingAllowed,
isInArchivableGroup,
setTrackElement,
setIssueToEdit,
setCreateUpdateIssueModal,
setDeleteIssueModal,
@ -84,6 +82,14 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
const MENU_ITEMS = useAllIssueMenuItems(menuItemProps);
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.GLOBAL_VIEW });
item.action();
},
}));
return (
<>
{/* Modals */}
@ -122,7 +128,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
/>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu
ellipsis
customButton={customActionButton}
@ -171,6 +177,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.GLOBAL_VIEW });
nestedItem.action();
}}
className={cn(
@ -208,6 +215,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.GLOBAL_VIEW });
item.action();
}}
className={cn(

View file

@ -4,15 +4,16 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EIssuesStoreType } from "@plane/types";
import { ContextMenu, CustomMenu } from "@plane/ui";
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { DeleteIssueModal } from "@/components/issues";
// helpers
// hooks
import { useEventTracker, useIssues, useUserPermissions } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssues, useUserPermissions } from "@/hooks/store";
// types
import { IQuickActionProps } from "../list/list-view-types";
// helper
@ -36,7 +37,6 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
// store hooks
const { allowPermissions } = useUserPermissions();
const { setTrackElement } = useEventTracker();
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
// derived values
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
@ -54,7 +54,6 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
isEditingAllowed,
isDeletingAllowed: isEditingAllowed,
isRestoringAllowed: !!isRestoringAllowed,
setTrackElement,
setIssueToEdit: () => {},
setCreateUpdateIssueModal: () => {},
setDeleteIssueModal,
@ -64,6 +63,13 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
const MENU_ITEMS = useArchivedIssueMenuItems(menuItemProps);
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.ARCHIVED });
item.action();
},
}));
return (
<>
{/* Modals */}
@ -74,7 +80,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
onSubmit={handleDelete}
/>
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu
ellipsis
customButton={customActionButton}
@ -94,6 +100,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
e.preventDefault();
e.stopPropagation();
item.action();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.ARCHIVED });
}}
className={cn(
"flex items-center gap-2",

View file

@ -5,14 +5,20 @@ import omit from "lodash/omit";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { ARCHIVABLE_STATE_GROUPS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
ARCHIVABLE_STATE_GROUPS,
EUserPermissions,
EUserPermissionsLevel,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import { EIssuesStoreType, TIssue } from "@plane/types";
import { ContextMenu, CustomMenu } from "@plane/ui";
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// hooks
import { useEventTracker, useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store";
// plane-web components
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
// types
@ -41,8 +47,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false);
// router
const { workspaceSlug, cycleId } = useParams();
// store hooks
const { setTrackElement } = useEventTracker();
const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
const { allowPermissions } = useUserPermissions();
const { getStateById } = useProjectState();
@ -78,7 +82,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
isArchivingAllowed,
isDeletingAllowed,
isInArchivableGroup,
setTrackElement,
setIssueToEdit,
setCreateUpdateIssueModal,
setDeleteIssueModal,
@ -94,6 +97,14 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
const MENU_ITEMS = useCycleIssueMenuItems(menuItemProps);
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.CYCLE });
item.action();
},
}));
return (
<>
{/* Modals */}
@ -132,7 +143,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
/>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu
ellipsis
placement={placements}
@ -181,6 +192,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.CYCLE });
nestedItem.action();
}}
className={cn(
@ -218,6 +230,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.CYCLE });
item.action();
}}
className={cn(

View file

@ -5,14 +5,15 @@ import omit from "lodash/omit";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EIssuesStoreType, TIssue } from "@plane/types";
import { ContextMenu, CustomMenu } from "@plane/ui";
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// hooks
import { useEventTracker, useUserPermissions } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useUserPermissions } from "@/hooks/store";
// local imports
import { IQuickActionProps } from "../list/list-view-types";
import { useDraftIssueMenuItems, MenuItemFactoryProps } from "./helper";
@ -37,7 +38,6 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
// store hooks
const { allowPermissions } = useUserPermissions();
const { setTrackElement } = useEventTracker();
// derived values
const activeLayout = "Draft Issues";
// auth
@ -70,7 +70,6 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
isEditingAllowed,
isDeletingAllowed,
isDraftIssue,
setTrackElement,
setIssueToEdit,
setCreateUpdateIssueModal,
setDeleteIssueModal,
@ -81,6 +80,14 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
const MENU_ITEMS = useDraftIssueMenuItems(menuItemProps);
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.DRAFT });
item.action();
},
}));
return (
<>
{/* Modals */}
@ -104,7 +111,7 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
isDraft={isDraftIssue}
/>
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu
ellipsis
placement={placements}
@ -122,6 +129,7 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.DRAFT });
item.action();
}}
className={cn(

View file

@ -56,7 +56,6 @@ export interface MenuItemFactoryProps {
issueTypeDetail?: { is_active?: boolean };
isDraftIssue?: boolean;
// Action handlers
setTrackElement: (element: string) => void;
setIssueToEdit: (issue: TIssue | undefined) => void;
setCreateUpdateIssueModal: (open: boolean) => void;
setDeleteIssueModal: (open: boolean) => void;
@ -144,7 +143,6 @@ export const useMenuItemFactory = (props: MenuItemFactoryProps) => {
isRestoringAllowed = false,
isInArchivableGroup = false,
issueTypeDetail,
setTrackElement,
setIssueToEdit,
setCreateUpdateIssueModal,
setDeleteIssueModal,
@ -160,7 +158,6 @@ export const useMenuItemFactory = (props: MenuItemFactoryProps) => {
action:
customEditAction ||
(() => {
setTrackElement(activeLayout);
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}),
@ -173,7 +170,6 @@ export const useMenuItemFactory = (props: MenuItemFactoryProps) => {
title: t("common.actions.make_a_copy"),
icon: Copy,
action: () => {
setTrackElement(activeLayout);
setCreateUpdateIssueModal(true);
},
shouldRender: isEditingAllowed && (issueTypeDetail?.is_active ?? true),
@ -182,7 +178,6 @@ export const useMenuItemFactory = (props: MenuItemFactoryProps) => {
return createCopyMenuWithDuplication({
baseItem,
activeLayout,
setTrackElement,
setCreateUpdateIssueModal,
setDuplicateWorkItemModal,
});
@ -243,7 +238,6 @@ export const useMenuItemFactory = (props: MenuItemFactoryProps) => {
title: t("common.actions.delete"),
icon: Trash2,
action: () => {
setTrackElement(activeLayout);
setDeleteIssueModal(true);
},
shouldRender: isDeletingAllowed,
@ -304,7 +298,6 @@ export const useCycleIssueMenuItems = (props: MenuItemFactoryProps): TContextMen
...props.issue,
cycle_id: props.cycleId ?? null,
});
props.setTrackElement(props.activeLayout || "");
props.setCreateUpdateIssueModal(true);
};
@ -330,7 +323,6 @@ export const useModuleIssueMenuItems = (props: MenuItemFactoryProps): TContextMe
...props.issue,
module_ids: props.moduleId ? [props.moduleId] : [],
});
props.setTrackElement(props.activeLayout || "");
props.setCreateUpdateIssueModal(true);
};

View file

@ -5,14 +5,20 @@ import omit from "lodash/omit";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { ARCHIVABLE_STATE_GROUPS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
ARCHIVABLE_STATE_GROUPS,
EUserPermissions,
EUserPermissionsLevel,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import { EIssuesStoreType, TIssue } from "@plane/types";
import { ContextMenu, CustomMenu } from "@plane/ui";
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// hooks
import { useIssues, useEventTracker, useProjectState, useUserPermissions, useProject } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssues, useProjectState, useUserPermissions, useProject } from "@/hooks/store";
// plane-web components
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
import { IQuickActionProps } from "../list/list-view-types";
@ -41,7 +47,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
// router
const { workspaceSlug, moduleId } = useParams();
// store hooks
const { setTrackElement } = useEventTracker();
const { issuesFilter } = useIssues(EIssuesStoreType.MODULE);
const { allowPermissions } = useUserPermissions();
const { getStateById } = useProjectState();
@ -77,7 +82,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
isArchivingAllowed,
isDeletingAllowed,
isInArchivableGroup,
setTrackElement,
setIssueToEdit,
setCreateUpdateIssueModal,
setDeleteIssueModal,
@ -93,6 +97,13 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
const MENU_ITEMS = useModuleIssueMenuItems(menuItemProps);
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.MODULE });
item.action();
},
}));
return (
<>
{/* Modals */}
@ -131,7 +142,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
/>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu
ellipsis
placement={placements}
@ -180,6 +191,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.MODULE });
nestedItem.action();
}}
className={cn(
@ -217,6 +229,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.MODULE });
item.action();
}}
className={cn(

View file

@ -5,14 +5,20 @@ import omit from "lodash/omit";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { ARCHIVABLE_STATE_GROUPS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
ARCHIVABLE_STATE_GROUPS,
EUserPermissions,
EUserPermissionsLevel,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import { EIssuesStoreType, TIssue } from "@plane/types";
import { ContextMenu, CustomMenu } from "@plane/ui";
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// hooks
import { useEventTracker, useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store";
// plane-web components
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
import { IQuickActionProps } from "../list/list-view-types";
@ -42,7 +48,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false);
// store hooks
const { allowPermissions } = useUserPermissions();
const { setTrackElement } = useEventTracker();
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const { getStateById } = useProjectState();
const { getProjectIdentifierById } = useProject();
@ -85,7 +90,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
isDeletingAllowed,
isInArchivableGroup,
isDraftIssue,
setTrackElement,
setIssueToEdit,
setCreateUpdateIssueModal,
setDeleteIssueModal,
@ -99,6 +103,14 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
const MENU_ITEMS = useProjectIssueMenuItems(menuItemProps);
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.PROJECT_VIEW });
item.action();
},
}));
return (
<>
{/* Modals */}
@ -137,7 +149,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
/>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu
ellipsis
placement={placements}
@ -185,6 +197,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.PROJECT_VIEW });
nestedItem.action();
}}
className={cn(
@ -222,6 +235,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.PROJECT_VIEW });
item.action();
}}
className={cn(

View file

@ -2,7 +2,7 @@
import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
import { useForm, UseFormRegister } from "react-hook-form";
import { PlusIcon } from "lucide-react";
// plane constants
@ -18,7 +18,7 @@ import { CreateIssueToastActionItems } from "@/components/issues";
// constants
// helpers
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// plane web components
import { QuickAddIssueFormRoot } from "@/plane-web/components/issues";
@ -69,11 +69,8 @@ export const QuickAddIssueRoot: FC<TQuickAddIssueRoot> = observer((props) => {
const { t } = useTranslation();
// router
const { workspaceSlug, projectId } = useParams();
const pathname = usePathname();
// states
const [isOpen, setIsOpen] = useState(isQuickAddOpen ?? false);
// store hooks
const { captureIssueEvent } = useEventTracker();
// form info
const {
reset,
@ -136,17 +133,16 @@ export const QuickAddIssueRoot: FC<TQuickAddIssueRoot> = observer((props) => {
await quickAddPromise
.then((res) => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.create,
payload: { ...res, state: "SUCCESS", element: ` ${layout} quick add` },
path: pathname,
payload: { id: res?.id },
});
})
.catch(() => {
captureIssueEvent({
.catch((error) => {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.create,
payload: { ...payload, state: "FAILED", element: `${layout} quick ad` },
path: pathname,
payload: { id: payload.id },
error: error as Error,
});
});
}

View file

@ -1,12 +1,13 @@
import React, { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
// types
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { TIssue } from "@plane/types";
// components
import { CycleDropdown } from "@/components/dropdowns";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
type Props = {
@ -19,9 +20,7 @@ export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => {
const { issue, disabled, onClose } = props;
// router
const { workspaceSlug } = useParams();
const pathname = usePathname();
// hooks
const { captureIssueEvent } = useEventTracker();
const {
issues: { addCycleToIssue, removeCycleFromIssue },
} = useIssuesStore();
@ -31,18 +30,14 @@ export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => {
if (!workspaceSlug || !issue || !issue.project_id || issue.cycle_id === cycleId) return;
if (cycleId) await addCycleToIssue(workspaceSlug.toString(), issue.project_id, cycleId, issue.id);
else await removeCycleFromIssue(workspaceSlug.toString(), issue.project_id, issue.id);
captureIssueEvent({
eventName: "Work item updated",
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: {
...issue,
cycle_id: cycleId,
element: "Spreadsheet layout",
id: issue.id,
},
updates: { changed_property: "cycle", change_details: { cycle_id: cycleId } },
path: pathname,
});
},
[workspaceSlug, issue, addCycleToIssue, removeCycleFromIssue, captureIssueEvent, pathname]
[workspaceSlug, issue, addCycleToIssue, removeCycleFromIssue]
);
return (

View file

@ -1,14 +1,15 @@
import React, { useCallback } from "react";
import xor from "lodash/xor";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
// types
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { TIssue } from "@plane/types";
// components
import { ModuleDropdown } from "@/components/dropdowns";
// constants
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
type Props = {
@ -21,9 +22,7 @@ export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => {
const { issue, disabled, onClose } = props;
// router
const { workspaceSlug } = useParams();
const pathname = usePathname();
// hooks
const { captureIssueEvent } = useEventTracker();
const {
issues: { changeModulesInIssue },
} = useIssuesStore();
@ -41,18 +40,14 @@ export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => {
}
changeModulesInIssue(workspaceSlug.toString(), issue.project_id, issue.id, modulesToAdd, modulesToRemove);
captureIssueEvent({
eventName: "Work item updated",
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: {
...issue,
module_ids: moduleIds,
element: "Spreadsheet layout",
id: issue.id,
},
updates: { changed_property: "module_ids", change_details: { module_ids: moduleIds } },
path: pathname,
});
},
[workspaceSlug, issue, changeModulesInIssue, captureIssueEvent, pathname]
[workspaceSlug, issue, changeModulesInIssue]
);
return (

View file

@ -1,15 +1,14 @@
import { useRef } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
// types
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { IIssueDisplayProperties, TIssue } from "@plane/types";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureSuccess } from "@/helpers/event-tracker.helper";
// components
import { SPREADSHEET_COLUMNS } from "@/plane-web/components/issues/issue-layouts/utils";
import { shouldRenderColumn } from "@/plane-web/helpers/issue-filter.helper";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
// utils
type Props = {
displayProperties: IIssueDisplayProperties;
@ -23,9 +22,7 @@ type Props = {
export const IssueColumn = observer((props: Props) => {
const { displayProperties, issueDetail, disableUserActions, property, updateIssue } = props;
// router
const pathname = usePathname();
const tableCellRef = useRef<HTMLTableCellElement | null>(null);
const { captureIssueEvent } = useEventTracker();
const shouldRenderProperty = shouldRenderColumn(property);
@ -46,18 +43,14 @@ export const IssueColumn = observer((props: Props) => {
>
<Column
issue={issueDetail}
onChange={(issue: TIssue, data: Partial<TIssue>, updates: any) =>
onChange={(issue: TIssue, data: Partial<TIssue>) =>
updateIssue &&
updateIssue(issue.project_id, issue.id, data).then(() => {
captureIssueEvent({
eventName: "Issue updated",
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: {
...issue,
...data,
element: "Spreadsheet layout",
id: issue.id,
},
updates: updates,
path: pathname,
});
})
}

View file

@ -2,18 +2,18 @@
import React, { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// Plane imports
import { useParams } from "next/navigation";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
// Plane imports
import { useTranslation } from "@plane/i18n";
import { EIssuesStoreType, TBaseIssue, TIssue } from "@plane/types";
import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { CreateIssueToastActionItems, IssuesModalProps } from "@/components/issues";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueModal } from "@/hooks/context/use-issue-modal";
import { useCycle } from "@/hooks/store/use-cycle";
import { useEventTracker } from "@/hooks/store/use-event-tracker";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssues } from "@/hooks/store/use-issues";
import { useModule } from "@/hooks/store/use-module";
@ -61,7 +61,6 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
// store hooks
const { t } = useTranslation();
const { captureIssueEvent } = useEventTracker();
const { workspaceSlug, projectId: routerProjectId, cycleId, moduleId, workItem } = useParams();
const { fetchCycleDetails } = useCycle();
const { fetchModuleDetails } = useModule();
@ -71,8 +70,6 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
const { fetchIssue } = useIssueDetail();
const { allowedProjectIds, handleCreateUpdatePropertyValues } = useIssueModal();
const { getProjectByIdentifier } = useProject();
// pathname
const pathname = usePathname();
// current store details
const { createIssue, updateIssue } = useIssuesActions(storeType);
// derived values
@ -239,10 +236,9 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
/>
),
});
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.create,
payload: { ...response, state: "SUCCESS" },
path: pathname,
payload: { id: response.id },
});
if (!createMore) handleClose();
if (createMore && issueTitleRef) issueTitleRef?.current?.focus();
@ -255,10 +251,10 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
title: t("error"),
message: error?.error ?? t(is_draft_issue ? "draft_creation_failed" : "issue_creation_failed"),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.create,
payload: { ...payload, state: "FAILED" },
path: pathname,
payload: { id: payload.id },
error: error as Error,
});
throw error;
}
@ -301,10 +297,9 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
title: t("success"),
message: t("issue_updated_successfully"),
});
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...payload, issueId: data.id, state: "SUCCESS" },
path: pathname,
payload: { id: data.id },
});
handleClose();
} catch (error: any) {
@ -314,10 +309,10 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
title: t("error"),
message: error?.error ?? t("issue_could_not_be_updated"),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...payload, state: "FAILED" },
path: pathname,
payload: { id: data.id },
error: error as Error,
});
}
};

View file

@ -3,7 +3,8 @@
import React, { useState } from "react";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import type { TIssue } from "@plane/types";
@ -14,8 +15,9 @@ import { isEmptyHtmlString } from "@plane/utils";
import { ConfirmIssueDiscard } from "@/components/issues";
// helpers
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueModal } from "@/hooks/context/use-issue-modal";
import { useEventTracker, useWorkspaceDraftIssues } from "@/hooks/store";
import { useWorkspaceDraftIssues } from "@/hooks/store";
// local components
import { IssueFormRoot, type IssueFormProps } from "./form";
@ -30,10 +32,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
const [issueDiscardModal, setIssueDiscardModal] = useState(false);
// router params
const { workspaceSlug } = useParams();
// pathname
const pathname = usePathname();
// store hooks
const { captureIssueEvent } = useEventTracker();
const { handleCreateUpdatePropertyValues } = useIssueModal();
const { createIssue } = useWorkspaceDraftIssues();
const { t } = useTranslation();
@ -92,26 +91,25 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
title: `${t("success")}!`,
message: t("workspace_draft_issues.toasts.created.success"),
});
captureIssueEvent({
eventName: "Draft work item created",
payload: { ...res, state: "SUCCESS" },
path: pathname,
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.draft.create,
payload: { id: res?.id },
});
onChange(null);
setIssueDiscardModal(false);
onClose();
return res;
})
.catch(() => {
.catch((error) => {
setToast({
type: TOAST_TYPE.ERROR,
title: `${t("error")}!`,
message: t("workspace_draft_issues.toasts.created.error"),
});
captureIssueEvent({
eventName: "Draft work item created",
payload: { ...payload, state: "FAILED" },
path: pathname,
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.draft.create,
payload: { id: payload.id },
error,
});
});

View file

@ -11,7 +11,8 @@ import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
// components
import { IssueView, TIssueOperations } from "@/components/issues";
// hooks
import { useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useWorkItemProperties } from "@/plane-web/hooks/use-issue-properties";
@ -40,7 +41,6 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
const issueStoreType = useIssueStoreType();
const storeType = issueStoreFromProps ?? issueStoreType;
const { issues } = useIssues(storeType);
const { captureIssueEvent } = useEventTracker();
useWorkItemProperties(
peekIssue?.projectId,
@ -73,21 +73,16 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
.updateIssue(workspaceSlug, projectId, issueId, data)
.then(async () => {
fetchActivities(workspaceSlug, projectId, issueId);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","),
},
path: pathname,
payload: { id: issueId },
});
})
.catch(() => {
captureIssueEvent({
.catch((error) => {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { state: "FAILED", element: "Issue peek-overview" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
setToast({
title: t("toast.error"),
@ -100,23 +95,22 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
return issues?.removeIssue(workspaceSlug, projectId, issueId).then(() => {
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
path: pathname,
payload: { id: issueId },
});
removeRoutePeekId();
});
} catch {
} catch (error) {
setToast({
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
@ -124,16 +118,15 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
try {
if (!issues?.archiveIssue) return;
await issues.archiveIssue(workspaceSlug, projectId, issueId);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
path: pathname,
payload: { id: issueId },
});
} catch {
captureIssueEvent({
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.archive,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
@ -145,21 +138,20 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
title: t("issue.restore.success.title"),
message: t("issue.restore.success.message"),
});
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.restore,
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
path: pathname,
payload: { id: issueId },
});
} catch {
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("issue.restore.failed.message"),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.restore,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
@ -167,58 +159,40 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
try {
await issues.addCycleToIssue(workspaceSlug, projectId, cycleId, issueId);
fetchActivities(workspaceSlug, projectId, issueId);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { issueId, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
change_details: cycleId,
},
path: pathname,
payload: { id: issueId },
});
} catch {
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("issue.add.cycle.failed"),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { state: "FAILED", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
change_details: cycleId,
},
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
try {
await issues.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
change_details: cycleId,
},
path: pathname,
payload: { id: issueIds },
});
} catch {
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("issue.add.cycle.failed"),
});
captureIssueEvent({
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { state: "FAILED", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
change_details: cycleId,
},
path: pathname,
payload: { id: issueIds },
error: error as Error,
});
}
},
@ -238,24 +212,15 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
});
await removeFromCyclePromise;
fetchActivities(workspaceSlug, projectId, issueId);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { issueId, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
change_details: "",
},
path: pathname,
payload: { id: issueId },
});
} catch {
captureIssueEvent({
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { state: "FAILED", element: "Issue peek-overview" },
updates: {
changed_property: "cycle_id",
change_details: "",
},
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
@ -274,14 +239,9 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
removeModuleIds
);
fetchActivities(workspaceSlug, projectId, issueId);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
updates: {
changed_property: "module_id",
change_details: { addModuleIds, removeModuleIds },
},
path: pathname,
payload: { id: issueId },
});
return promise;
},
@ -301,30 +261,21 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
});
await removeFromModulePromise;
fetchActivities(workspaceSlug, projectId, issueId);
captureIssueEvent({
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
updates: {
changed_property: "module_id",
change_details: "",
},
path: pathname,
payload: { id: issueId },
});
} catch {
captureIssueEvent({
} catch (error) {
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.update,
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
updates: {
changed_property: "module_id",
change_details: "",
},
path: pathname,
payload: { id: issueId },
error: error as Error,
});
}
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[fetchIssue, is_draft, issues, fetchActivities, captureIssueEvent, pathname, removeRoutePeekId, restoreIssue]
[fetchIssue, is_draft, issues, fetchActivities, pathname, removeRoutePeekId, restoreIssue]
);
useEffect(() => {

View file

@ -4,13 +4,14 @@ import { FC, Fragment } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { EDraftIssuePaginationType } from "@plane/constants";
import { EDraftIssuePaginationType, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissionsLevel } from "@plane/constants/src/user";
import { useTranslation } from "@plane/i18n";
import { EUserWorkspaceRoles } from "@plane/types";
// components
import { cn } from "@plane/utils";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
import { captureClick } from "@/helpers/event-tracker.helper";
// constants
// helpers
@ -77,6 +78,7 @@ export const WorkspaceDraftIssuesRoot: FC<TWorkspaceDraftIssuesRoot> = observer(
description={t("workspace_projects.empty_state.no_projects.primary_button.comic.description")}
onClick={() => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!hasMemberLevelPermission}
/>

View file

@ -12,6 +12,7 @@ import {
EUserPermissionsLevel,
EEstimateSystem,
MODULE_TRACKER_EVENTS,
MODULE_TRACKER_ELEMENTS,
} from "@plane/constants";
// plane types
import { useTranslation } from "@plane/i18n";
@ -22,15 +23,10 @@ import { Loader, LayersIcon, CustomSelect, ModuleStatusIcon, TOAST_TYPE, setToas
// helpers
import { getDate, renderFormattedPayloadDate } from "@plane/utils";
import { DateRangeDropdown, MemberDropdown } from "@/components/dropdowns";
import {
ArchiveModuleModal,
DeleteModuleModal,
CreateUpdateModuleLinkModal,
ModuleAnalyticsProgress,
ModuleLinksList,
} from "@/components/modules";
import { CreateUpdateModuleLinkModal, ModuleAnalyticsProgress, ModuleLinksList } from "@/components/modules";
import { captureElementAndEvent, captureSuccess, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useModule, useEventTracker, useProjectEstimates, useUserPermissions } from "@/hooks/store";
import { useModule, useProjectEstimates, useUserPermissions } from "@/hooks/store";
// plane web constants
const defaultValues: Partial<IModule> = {
lead_id: "",
@ -50,8 +46,6 @@ type Props = {
export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
const { moduleId, handleClose, isArchived } = props;
// states
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [archiveModuleModal, setArchiveModuleModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
// router
@ -62,7 +56,6 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
const { allowPermissions } = useUserPermissions();
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
const { captureModuleEvent, captureEvent } = useEventTracker();
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
// derived values
@ -79,15 +72,22 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
if (!workspaceSlug || !projectId || !moduleId) return;
updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data)
.then((res) => {
captureModuleEvent({
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.RIGHT_SIDEBAR,
},
event: {
eventName: MODULE_TRACKER_EVENTS.update,
payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" },
payload: { id: res.id },
state: "SUCCESS",
},
});
})
.catch(() => {
captureModuleEvent({
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.update,
payload: { ...data, state: "FAILED" },
payload: { id: moduleId },
error,
});
});
};
@ -97,12 +97,20 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
const payload = { metadata: {}, ...formData };
await createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload).then(() =>
captureEvent(MODULE_TRACKER_EVENTS.link.create, {
module_id: moduleId,
state: "SUCCESS",
await createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload)
.then(() =>
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.link.create,
payload: { id: moduleId },
})
);
)
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.link.create,
payload: { id: moduleId },
error,
});
});
};
const handleUpdateLink = async (formData: ModuleLink, linkId: string) => {
@ -110,13 +118,20 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
const payload = { metadata: {}, ...formData };
await updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload).then(
() =>
captureEvent(MODULE_TRACKER_EVENTS.link.update, {
module_id: moduleId,
state: "SUCCESS",
await updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload)
.then(() =>
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.link.update,
payload: { id: moduleId },
})
);
)
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.link.update,
payload: { id: moduleId },
error,
});
});
};
const handleDeleteLink = async (linkId: string) => {
@ -124,9 +139,9 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId)
.then(() => {
captureEvent(MODULE_TRACKER_EVENTS.link.delete, {
module_id: moduleId,
state: "SUCCESS",
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.link.delete,
payload: { id: moduleId },
});
setToast({
type: TOAST_TYPE.SUCCESS,
@ -140,6 +155,10 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
title: "Error!",
message: "Some error occurred",
});
captureError({
eventName: MODULE_TRACKER_EVENTS.link.delete,
payload: { id: moduleId },
});
});
};
@ -213,16 +232,6 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
createLink={handleCreateLink}
updateLink={handleUpdateLink}
/>
{workspaceSlug && projectId && (
<ArchiveModuleModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
moduleId={moduleId}
isOpen={archiveModuleModal}
handleClose={() => setArchiveModuleModal(false)}
/>
)}
<DeleteModuleModal isOpen={moduleDeleteModal} onClose={() => setModuleDeleteModal(false)} data={moduleDetails} />
<>
<div
className={`sticky z-10 top-0 flex items-center justify-between bg-custom-sidebar-background-100 pb-5 pt-5`}

View file

@ -10,8 +10,10 @@ import type { IModule } from "@plane/types";
// ui
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// helpers
import { captureSuccess, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useModule } from "@/hooks/store";
import { useModule } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
type Props = {
@ -28,7 +30,6 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
const router = useAppRouter();
const { workspaceSlug, projectId, moduleId, peekModule } = useParams();
// store hooks
const { captureModuleEvent } = useEventTracker();
const { deleteModule } = useModule();
const { t } = useTranslation();
@ -51,9 +52,9 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
title: "Success!",
message: "Module deleted successfully.",
});
captureModuleEvent({
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.delete,
payload: { ...data, state: "SUCCESS" },
payload: { id: data.id },
});
})
.catch((errors) => {
@ -66,9 +67,10 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
type: TOAST_TYPE.ERROR,
message: currentError.i18n_message && t(currentError.i18n_message),
});
captureModuleEvent({
captureError({
eventName: MODULE_TRACKER_EVENTS.delete,
payload: { ...data, state: "FAILED" },
payload: { id: data.id },
error: errors,
});
})
.finally(() => handleClose());

View file

@ -1,6 +1,7 @@
import { observer } from "mobx-react";
import { Copy, Pencil, Trash2 } from "lucide-react";
// plane types
import { MODULE_TRACKER_ELEMENTS } from "@plane/constants";
import { ILinkDetails } from "@plane/types";
// plane ui
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
@ -58,6 +59,7 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
<button
type="button"
className="grid place-items-center p-1 hover:bg-custom-background-80"
data-ph-element={MODULE_TRACKER_ELEMENTS.LIST_ITEM}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@ -77,6 +79,7 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
<button
type="button"
className="grid place-items-center p-1 hover:bg-custom-background-80"
data-ph-element={MODULE_TRACKER_ELEMENTS.LIST_ITEM}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

View file

@ -11,8 +11,10 @@ import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@p
// components
import { ModuleForm } from "@/components/modules";
// constants
// helpers
import { captureSuccess, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useModule, useProject } from "@/hooks/store";
import { useModule, useProject } from "@/hooks/store";
import useKeypress from "@/hooks/use-keypress";
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -37,7 +39,6 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
// states
const [activeProject, setActiveProject] = useState<string | null>(null);
// store hooks
const { captureModuleEvent } = useEventTracker();
const { workspaceProjectIds } = useProject();
const { createModule, updateModuleDetails } = useModule();
const { isMobile } = usePlatformOS();
@ -63,9 +64,9 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
title: "Success!",
message: "Module created successfully.",
});
captureModuleEvent({
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.create,
payload: { ...res, state: "SUCCESS" },
payload: { id: res.id },
});
})
.catch((err) => {
@ -74,14 +75,15 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
title: "Error!",
message: err?.detail ?? err?.error ?? "Module could not be created. Please try again.",
});
captureModuleEvent({
captureError({
eventName: MODULE_TRACKER_EVENTS.create,
payload: { ...data, state: "FAILED" },
payload: { id: data?.id },
error: err,
});
});
};
const handleUpdateModule = async (payload: Partial<IModule>, dirtyFields: any) => {
const handleUpdateModule = async (payload: Partial<IModule>) => {
if (!workspaceSlug || !projectId || !data) return;
const selectedProjectId = payload.project_id ?? projectId.toString();
@ -94,9 +96,9 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
title: "Success!",
message: "Module updated successfully.",
});
captureModuleEvent({
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.update,
payload: { ...res, changed_properties: Object.keys(dirtyFields || {}), state: "SUCCESS" },
payload: { id: res.id },
});
})
.catch((err) => {
@ -105,21 +107,22 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
title: "Error!",
message: err?.detail ?? err?.error ?? "Module could not be updated. Please try again.",
});
captureModuleEvent({
captureError({
eventName: MODULE_TRACKER_EVENTS.update,
payload: { ...data, state: "FAILED" },
payload: { id: data.id },
error: err,
});
});
};
const handleFormSubmit = async (formData: Partial<IModule>, dirtyFields: unknown) => {
const handleFormSubmit = async (formData: Partial<IModule>) => {
if (!workspaceSlug || !projectId) return;
const payload: Partial<IModule> = {
...formData,
};
if (!data) await handleCreateModule(payload);
else await handleUpdateModule(payload, dirtyFields);
else await handleUpdateModule(payload);
};
useEffect(() => {

View file

@ -13,6 +13,7 @@ import {
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
MODULE_TRACKER_EVENTS,
MODULE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { IModule } from "@plane/types";
@ -33,8 +34,9 @@ import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { ModuleQuickActions } from "@/components/modules";
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
// helpers
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useMember, useModule, useUserPermissions } from "@/hooks/store";
import { useMember, useModule, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web constants
@ -56,7 +58,6 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const { allowPermissions } = useUserPermissions();
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule();
const { getUserDetails } = useMember();
const { captureEvent } = useEventTracker();
// local storage
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
@ -79,10 +80,15 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then(
() => {
if (!storedValue) toggleFavoriteMenu(true);
captureEvent(MODULE_TRACKER_EVENTS.favorite, {
module_id: moduleId,
element: "Grid layout",
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.CARD_ITEM,
},
event: {
eventName: MODULE_TRACKER_EVENTS.favorite,
payload: { id: moduleId },
state: "SUCCESS",
},
});
}
);
@ -110,10 +116,15 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
projectId.toString(),
moduleId
).then(() => {
captureEvent(MODULE_TRACKER_EVENTS.unfavorite, {
module_id: moduleId,
element: "Grid layout",
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.CARD_ITEM,
},
event: {
eventName: MODULE_TRACKER_EVENTS.unfavorite,
payload: { id: moduleId },
state: "SUCCESS",
},
});
});

View file

@ -12,6 +12,7 @@ import {
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
MODULE_TRACKER_EVENTS,
MODULE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
@ -24,8 +25,10 @@ import { DateRangeDropdown } from "@/components/dropdowns";
import { ModuleQuickActions } from "@/components/modules";
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
// constants
// helpers
import { captureElementAndEvent, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useMember, useModule, useUserPermissions } from "@/hooks/store";
import { useMember, useModule, useUserPermissions } from "@/hooks/store";
import { ButtonAvatars } from "../dropdowns/member/avatar";
type Props = {
@ -42,7 +45,6 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
const { allowPermissions } = useUserPermissions();
const { addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule();
const { getUserDetails } = useMember();
const { captureEvent } = useEventTracker();
const { t } = useTranslation();
@ -64,17 +66,28 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then(
() => {
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
.then(() => {
// open favorites menu if closed
if (!storedValue) toggleFavoriteMenu(true);
captureEvent(MODULE_TRACKER_EVENTS.favorite, {
module_id: moduleId,
element: "Grid layout",
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.LIST_ITEM,
},
event: {
eventName: MODULE_TRACKER_EVENTS.favorite,
payload: { id: moduleId },
state: "SUCCESS",
},
});
})
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.favorite,
payload: { id: moduleId },
error,
});
});
}
);
setPromiseToast(addToFavoritePromise, {
loading: "Adding module to favorites...",
@ -98,11 +111,24 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
workspaceSlug.toString(),
projectId.toString(),
moduleId
).then(() => {
captureEvent(MODULE_TRACKER_EVENTS.unfavorite, {
module_id: moduleId,
element: "Grid layout",
)
.then(() => {
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.LIST_ITEM,
},
event: {
eventName: MODULE_TRACKER_EVENTS.unfavorite,
payload: { id: moduleId },
state: "SUCCESS",
},
});
})
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.unfavorite,
payload: { id: moduleId },
error,
});
});

View file

@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import Image from "next/image";
import { useParams, useSearchParams } from "next/navigation";
// components
import { EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissionsLevel, MODULE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EUserProjectRoles } from "@plane/types";
import { ContentWrapper, Row, ERowVariant } from "@plane/ui";
@ -11,7 +11,7 @@ import { DetailedEmptyState, ComicBoxButton } from "@/components/empty-state";
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules";
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
// hooks
import { useCommandPalette, useEventTracker, useModule, useModuleFilter, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useModule, useModuleFilter, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import AllFiltersImage from "@/public/empty-state/module/all-filters.svg";
import NameFilterImage from "@/public/empty-state/module/name-filter.svg";
@ -25,7 +25,6 @@ export const ModulesListView: React.FC = observer(() => {
const { t } = useTranslation();
// store hooks
const { toggleCreateModuleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { getProjectModuleIds, getFilteredModuleIds, loader } = useModule();
const { currentProjectDisplayFilters: displayFilters, searchQuery } = useModuleFilter();
const { allowPermissions } = useUserPermissions();
@ -60,8 +59,8 @@ export const ModulesListView: React.FC = observer(() => {
label={t("project_module.empty_state.general.primary_button.text")}
title={t("project_module.empty_state.general.primary_button.comic.title")}
description={t("project_module.empty_state.general.primary_button.comic.description")}
data-ph-element={MODULE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON}
onClick={() => {
setTrackElement("Module empty state");
toggleCreateModuleModal(true);
}}
disabled={!canPerformEmptyStateActions}

View file

@ -6,7 +6,12 @@ import { observer } from "mobx-react";
// icons
import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
EUserPermissions,
EUserPermissionsLevel,
MODULE_TRACKER_ELEMENTS,
MODULE_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
@ -14,8 +19,9 @@ import { copyUrlToClipboard, cn } from "@plane/utils";
// components
import { ArchiveModuleModal, CreateUpdateModuleModal, DeleteModuleModal } from "@/components/modules";
// helpers
import { captureClick, captureSuccess, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useModule, useEventTracker, useUserPermissions } from "@/hooks/store";
import { useModule, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
type Props = {
@ -35,7 +41,6 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
const [archiveModuleModal, setArchiveModuleModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
// store hooks
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { getModuleById, restoreModule } = useModule();
@ -67,7 +72,6 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
const handleOpenInNewTab = () => window.open(`/${moduleLink}`, "_blank");
const handleEditModule = () => {
setTrackElement("Modules page list layout");
setEditModal(true);
};
@ -81,18 +85,26 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
title: "Restore success",
message: "Your module can be found in project modules.",
});
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.restore,
payload: { id: moduleId },
});
router.push(`/${workspaceSlug}/projects/${projectId}/archives/modules`);
})
.catch(() =>
.catch((error) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Module could not be restored. Please try again.",
})
);
});
captureError({
eventName: MODULE_TRACKER_EVENTS.restore,
payload: { id: moduleId },
error,
});
});
const handleDeleteModule = () => {
setTrackElement("Modules page list layout");
setDeleteModal(true);
};
@ -145,6 +157,16 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
},
];
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({
elementName: MODULE_TRACKER_ELEMENTS.CONTEXT_MENU,
});
item.action();
},
}));
return (
<>
{moduleDetails && (
@ -166,7 +188,7 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
<DeleteModuleModal data={moduleDetails} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
</div>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
@ -176,6 +198,9 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({
elementName: MODULE_TRACKER_ELEMENTS.QUICK_ACTIONS,
});
item.action();
}}
className={cn(

View file

@ -5,10 +5,10 @@ import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
// constants
import {
ONBOARDING_TRACKER_EVENTS,
ORGANIZATION_SIZE,
RESTRICTED_URLS,
WORKSPACE_TRACKER_EVENTS,
WORKSPACE_TRACKER_ELEMENTS,
} from "@plane/constants";
// types
import { useTranslation } from "@plane/i18n";
@ -16,7 +16,8 @@ import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
// ui
import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useEventTracker, useUserProfile, useUserSettings, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUserProfile, useUserSettings, useWorkspace } from "@/hooks/store";
// services
import { WorkspaceService } from "@/plane-web/services";
@ -41,7 +42,6 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
const { updateUserProfile } = useUserProfile();
const { fetchCurrentUserSettings } = useUserSettings();
const { createWorkspace, fetchWorkspaces } = useWorkspace();
const { captureWorkspaceEvent } = useEventTracker();
// form info
const {
handleSubmit,
@ -73,26 +73,18 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
title: t("workspace_creation.toast.success.title"),
message: t("workspace_creation.toast.success.message"),
});
captureWorkspaceEvent({
captureSuccess({
eventName: WORKSPACE_TRACKER_EVENTS.create,
payload: {
...workspaceResponse,
state: "SUCCESS",
first_time: true,
element: ONBOARDING_TRACKER_EVENTS.root,
},
payload: { slug: formData.slug },
});
await fetchWorkspaces();
await completeStep(workspaceResponse.id);
})
.catch(() => {
captureWorkspaceEvent({
captureError({
eventName: WORKSPACE_TRACKER_EVENTS.create,
payload: {
state: "FAILED",
first_time: true,
element: ONBOARDING_TRACKER_EVENTS.root,
},
payload: { slug: formData.slug },
error: new Error("Error creating workspace"),
});
setToast({
type: TOAST_TYPE.ERROR,
@ -290,7 +282,14 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
)}
</div>
</div>
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
<Button
data-ph-element={WORKSPACE_TRACKER_ELEMENTS.ONBOARDING_CREATE_WORKSPACE_BUTTON}
variant="primary"
type="submit"
size="lg"
className="w-full"
disabled={isButtonDisabled}
>
{isSubmitting ? <Spinner height="20px" width="20px" /> : t("workspace_creation.button.default")}
</Button>
</form>

View file

@ -2,17 +2,18 @@
import React, { useState } from "react";
// plane imports
import { ROLE, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { ROLE, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
// types
import { IWorkspaceMemberInvitation } from "@plane/types";
// ui
import { Button, Checkbox, Spinner } from "@plane/ui";
import { truncateText, getUserRole } from "@plane/utils";
import { truncateText } from "@plane/utils";
// constants
// helpers
import { WorkspaceLogo } from "@/components/workspace/logo";
// hooks
import { useEventTracker, useUserSettings, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUserSettings, useWorkspace } from "@/hooks/store";
// services
import { WorkspaceService } from "@/plane-web/services";
@ -29,7 +30,6 @@ export const Invitations: React.FC<Props> = (props) => {
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
// store hooks
const { captureEvent } = useEventTracker();
const { fetchWorkspaces } = useWorkspace();
const { fetchCurrentUserSettings } = useUserSettings();
@ -50,26 +50,23 @@ export const Invitations: React.FC<Props> = (props) => {
try {
await workspaceService.joinWorkspaces({ invitations: invitationsRespond });
captureEvent(MEMBER_TRACKER_EVENTS.accept, {
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitation?.id,
role: getUserRole(invitation?.role as any),
project_id: undefined,
accepted_from: "App",
state: "SUCCESS",
element: "Workspace invitations page",
},
});
await fetchWorkspaces();
await fetchCurrentUserSettings();
await handleNextStep();
} catch (error) {
} catch (error: any) {
console.error(error);
captureEvent(MEMBER_TRACKER_EVENTS.accept, {
captureError({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitation?.id,
role: getUserRole(invitation?.role as any),
project_id: undefined,
accepted_from: "App",
state: "FAILED",
element: "Workspace invitations page",
},
error: error,
});
setIsJoiningWorkspaces(false);
}
@ -117,6 +114,7 @@ export const Invitations: React.FC<Props> = (props) => {
className="w-full"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || !invitationsRespond.length}
data-ph-element={MEMBER_TRACKER_ELEMENTS.ONBOARDING_JOIN_WORKSPACE}
>
{isJoiningWorkspaces ? <Spinner height="20px" width="20px" /> : "Continue to workspace"}
</Button>

View file

@ -20,7 +20,7 @@ import { usePopper } from "react-popper";
import { Check, ChevronDown, Plus, XCircle } from "lucide-react";
import { Listbox } from "@headlessui/react";
// plane imports
import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { IUser, IWorkspace } from "@plane/types";
@ -28,9 +28,8 @@ import { IUser, IWorkspace } from "@plane/types";
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// helpers
import { getUserRole } from "@plane/utils";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// services
import { WorkspaceService } from "@/plane-web/services";
// assets
@ -276,8 +275,6 @@ export const InviteMembers: React.FC<Props> = (props) => {
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
const { resolvedTheme } = useTheme();
// store hooks
const { captureEvent } = useEventTracker();
const {
control,
@ -311,16 +308,11 @@ export const InviteMembers: React.FC<Props> = (props) => {
})),
})
.then(async () => {
captureEvent(MEMBER_TRACKER_EVENTS.invite, {
emails: [
...payload.emails.map((email) => ({
email: email.email,
role: getUserRole(email.role),
})),
],
project_id: undefined,
state: "SUCCESS",
element: "Onboarding",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
workspace: workspace.slug,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@ -331,10 +323,12 @@ export const InviteMembers: React.FC<Props> = (props) => {
await nextStep();
})
.catch((err) => {
captureEvent(MEMBER_TRACKER_EVENTS.invite, {
project_id: undefined,
state: "FAILED",
element: "Onboarding",
captureError({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
workspace: workspace.slug,
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
@ -426,6 +420,7 @@ export const InviteMembers: React.FC<Props> = (props) => {
size="lg"
className="w-full"
disabled={isInvitationDisabled || !isValid || isSubmitting}
data-ph-element={MEMBER_TRACKER_ELEMENTS.ONBOARDING_INVITE_MEMBER}
>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>

View file

@ -6,7 +6,7 @@ import Image from "next/image";
import { useTheme } from "next-themes";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react";
import { E_PASSWORD_STRENGTH, ONBOARDING_TRACKER_EVENTS, USER_TRACKER_EVENTS } from "@plane/constants";
import { E_PASSWORD_STRENGTH, ONBOARDING_TRACKER_ELEMENTS, USER_TRACKER_EVENTS } from "@plane/constants";
// types
import { useTranslation } from "@plane/i18n";
import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
@ -20,7 +20,8 @@ import { OnboardingHeader, SwitchAccountDropdown } from "@/components/onboarding
// constants
// helpers
// hooks
import { useEventTracker, useUser, useUserProfile } from "@/hooks/store";
import { captureError, captureSuccess, captureView } from "@/helpers/event-tracker.helper";
import { useUser, useUserProfile } from "@/hooks/store";
// assets
import ProfileSetupDark from "@/public/onboarding/profile-setup-dark.webp";
import ProfileSetupLight from "@/public/onboarding/profile-setup-light.webp";
@ -98,7 +99,6 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
// store hooks
const { updateCurrentUser } = useUser();
const { updateUserProfile } = useUserProfile();
const { captureEvent } = useEventTracker();
// form info
const {
getValues,
@ -143,11 +143,12 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
updateUserProfile(profileUpdatePayload),
totalSteps > 2 && stepChange({ profile_complete: true }),
]);
captureEvent(USER_TRACKER_EVENTS.add_details, {
captureSuccess({
eventName: USER_TRACKER_EVENTS.add_details,
payload: {
use_case: formData.use_case,
role: formData.role,
state: "SUCCESS",
element: ONBOARDING_TRACKER_EVENTS.step_1,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@ -159,9 +160,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
finishOnboarding();
}
} catch {
captureEvent(USER_TRACKER_EVENTS.add_details, {
state: "FAILED",
element: ONBOARDING_TRACKER_EVENTS.step_1,
captureError({
eventName: USER_TRACKER_EVENTS.add_details,
});
setToast({
type: TOAST_TYPE.ERROR,
@ -183,9 +183,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
formData.password && handleSetPassword(formData.password),
]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION));
} catch {
captureEvent(USER_TRACKER_EVENTS.add_details, {
state: "FAILED",
element: ONBOARDING_TRACKER_EVENTS.step_1,
captureError({
eventName: USER_TRACKER_EVENTS.add_details,
});
setToast({
type: TOAST_TYPE.ERROR,
@ -205,11 +204,12 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
updateUserProfile(profileUpdatePayload),
totalSteps > 2 && stepChange({ profile_complete: true }),
]);
captureEvent(USER_TRACKER_EVENTS.add_details, {
captureSuccess({
eventName: USER_TRACKER_EVENTS.add_details,
payload: {
use_case: formData.use_case,
role: formData.role,
state: "SUCCESS",
element: ONBOARDING_TRACKER_EVENTS.step_2,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@ -221,9 +221,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
finishOnboarding();
}
} catch {
captureEvent(USER_TRACKER_EVENTS.add_details, {
state: "FAILED",
element: ONBOARDING_TRACKER_EVENTS.step_2,
captureError({
eventName: USER_TRACKER_EVENTS.add_details,
});
setToast({
type: TOAST_TYPE.ERROR,
@ -235,6 +234,9 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
const onSubmit = async (formData: TProfileSetupFormValues) => {
if (!user) return;
captureView({
elementName: ONBOARDING_TRACKER_ELEMENTS.PROFILE_SETUP_FORM,
});
if (profileSetupStep === EProfileSetupSteps.ALL) await handleSubmitProfileSetup(formData);
if (profileSetupStep === EProfileSetupSteps.USER_DETAILS) await handleSubmitUserDetail(formData);
if (profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION) await handleSubmitUserPersonalization(formData);

View file

@ -5,13 +5,14 @@ import { observer } from "mobx-react";
import Image, { StaticImageData } from "next/image";
import { X } from "lucide-react";
// ui
import { PRODUCT_TOUR_TRACKER_EVENTS } from "@plane/constants";
import { PRODUCT_TOUR_TRACKER_ELEMENTS } from "@plane/constants";
import { Button } from "@plane/ui";
// components
import { TourSidebar } from "@/components/onboarding";
// constants
// hooks
import { useCommandPalette, useEventTracker, useUser } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useUser } from "@/hooks/store";
// assets
import CyclesTour from "@/public/onboarding/cycles.webp";
import IssuesTour from "@/public/onboarding/issues.webp";
@ -85,7 +86,6 @@ export const TourRoot: React.FC<Props> = observer((props) => {
const [step, setStep] = useState<TTourSteps>("welcome");
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement, captureEvent } = useEventTracker();
const { data: currentUser } = useUser();
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
@ -112,7 +112,9 @@ export const TourRoot: React.FC<Props> = observer((props) => {
<Button
variant="primary"
onClick={() => {
captureEvent(PRODUCT_TOUR_TRACKER_EVENTS.start);
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.START_BUTTON,
});
setStep("work-items");
}}
>
@ -122,7 +124,9 @@ export const TourRoot: React.FC<Props> = observer((props) => {
type="button"
className="bg-transparent text-xs font-medium text-custom-primary-100 outline-custom-text-100"
onClick={() => {
captureEvent(PRODUCT_TOUR_TRACKER_EVENTS.skip);
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.SKIP_BUTTON,
});
onComplete();
}}
>
@ -171,7 +175,9 @@ export const TourRoot: React.FC<Props> = observer((props) => {
<Button
variant="primary"
onClick={() => {
setTrackElement("Product tour");
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.CREATE_PROJECT_BUTTON,
});
onComplete();
toggleCreateProjectModal(true);
}}

View file

@ -16,7 +16,7 @@ import {
Trash2,
} from "lucide-react";
// constants
import { EPageAccess } from "@plane/constants";
import { EPageAccess, PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
// plane editor
import { EditorRefApi } from "@plane/editor";
// plane ui
@ -26,6 +26,7 @@ import { cn } from "@plane/utils";
import { DeletePageModal } from "@/components/pages";
// helpers
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { usePageOperations } from "@/hooks/use-page-operations";
// plane web components
import { MovePageModal } from "@/plane-web/components/pages";
@ -92,14 +93,24 @@ export const PageActions: React.FC<Props> = observer((props) => {
const menuItems: (TContextMenuItem & { key: TPageActions })[] = [
{
key: "toggle-lock",
action: pageOperations.toggleLock,
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON,
});
pageOperations.toggleLock();
},
title: is_locked ? "Unlock" : "Lock",
icon: is_locked ? LockKeyholeOpen : LockKeyhole,
shouldRender: canCurrentUserLockPage,
},
{
key: "toggle-access",
action: pageOperations.toggleAccess,
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.ACCESS_TOGGLE,
});
pageOperations.toggleAccess();
},
title: access === EPageAccess.PUBLIC ? "Make private" : "Make public",
icon: access === EPageAccess.PUBLIC ? Lock : Globe2,
shouldRender: canCurrentUserChangeAccess && !archived_at,
@ -120,21 +131,36 @@ export const PageActions: React.FC<Props> = observer((props) => {
},
{
key: "make-a-copy",
action: pageOperations.duplicate,
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.DUPLICATE_BUTTON,
});
pageOperations.duplicate();
},
title: "Make a copy",
icon: Copy,
shouldRender: canCurrentUserDuplicatePage,
},
{
key: "archive-restore",
action: pageOperations.toggleArchive,
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.ARCHIVE_BUTTON,
});
pageOperations.toggleArchive();
},
title: archived_at ? "Restore" : "Archive",
icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,
shouldRender: canCurrentUserArchivePage,
},
{
key: "delete",
action: () => setDeletePageModal(true),
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.CONTEXT_MENU,
});
setDeletePageModal(true);
},
title: "Delete",
icon: Trash2,
shouldRender: canCurrentUserDeletePage && !!archived_at,

View file

@ -1,6 +1,10 @@
import { observer } from "mobx-react";
// plane imports
// constants
import { PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
// ui
import { FavoriteStar } from "@plane/ui";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { usePageOperations } from "@/hooks/use-page-operations";
// store
@ -23,7 +27,12 @@ export const PageFavoriteControl = observer(({ page }: Props) => {
return (
<FavoriteStar
selected={is_favorite}
onClick={pageOperations.toggleFavorite}
onClick={() => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.FAVORITE_BUTTON,
});
pageOperations.toggleFavorite();
}}
buttonClassName="flex-shrink-0 size-6 group rounded hover:bg-custom-background-80 transition-colors"
iconClassName="size-3.5 text-custom-text-200 group-hover:text-custom-text-10"
/>

View file

@ -3,12 +3,15 @@
import React, { FC } from "react";
import { observer } from "mobx-react";
import { Earth, Info, Lock, Minus } from "lucide-react";
// constants
import { PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
// ui
import { Avatar, FavoriteStar, Tooltip } from "@plane/ui";
import { renderFormattedDate, getFileURL } from "@plane/utils";
// components
import { PageActions } from "@/components/pages";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useMember } from "@/hooks/store";
import { usePageOperations } from "@/hooks/use-page-operations";
@ -64,6 +67,9 @@ export const BlockItemAction: FC<Props> = observer((props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.FAVORITE_BUTTON,
});
pageOperations.toggleFavorite();
}}
selected={is_favorite}

View file

@ -7,7 +7,7 @@ import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// components
import { PageForm } from "@/components/pages";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureSuccess, captureError } from "@/helpers/event-tracker.helper";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web hooks
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
@ -42,7 +42,6 @@ export const CreatePageModal: FC<Props> = (props) => {
const router = useAppRouter();
// store hooks
const { createPage } = usePageStore(storeType);
const { capturePageEvent } = useEventTracker();
const handlePageFormData = <T extends keyof TPage>(key: T, value: TPage[T]) =>
setPageFormData((prev) => ({ ...prev, [key]: value }));
@ -62,22 +61,19 @@ export const CreatePageModal: FC<Props> = (props) => {
try {
const pageData = await createPage(pageFormData);
if (pageData) {
capturePageEvent({
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
payload: {
...pageData,
state: "SUCCESS",
id: pageData.id,
},
});
handleStateClear();
if (redirectionEnabled) router.push(`/${workspaceSlug}/projects/${projectId}/pages/${pageData.id}`);
}
} catch {
capturePageEvent({
} catch (error: any) {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
payload: {
state: "FAILED",
},
error,
});
}
};

View file

@ -8,7 +8,7 @@ import { PROJECT_PAGE_TRACKER_EVENTS } from "@plane/constants";
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// plane web hooks
import { useAppRouter } from "@/hooks/use-app-router";
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
@ -28,7 +28,6 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
const [isDeleting, setIsDeleting] = useState(false);
// store hooks
const { removePage } = usePageStore(storeType);
const { capturePageEvent } = useEventTracker();
if (!page || !page.id) return null;
// derived values
const { id: pageId, name } = page;
@ -45,11 +44,10 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
setIsDeleting(true);
await removePage(pageId)
.then(() => {
capturePageEvent({
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.delete,
payload: {
...page,
state: "SUCCESS",
id: pageId,
},
});
handleClose();
@ -64,11 +62,10 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
}
})
.catch(() => {
capturePageEvent({
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.delete,
payload: {
...page,
state: "FAILED",
id: pageId,
},
});
setToast({

View file

@ -1,12 +1,13 @@
import { observer } from "mobx-react";
import Image from "next/image";
// plane imports
import { EUserPermissionsLevel, EPageAccess } from "@plane/constants";
import { EUserPermissionsLevel, EPageAccess, PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EUserProjectRoles, TPageNavigationTabs } from "@plane/types";
// components
import { DetailedEmptyState } from "@/components/empty-state";
import { PageLoader } from "@/components/pages";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// plane web hooks
@ -63,6 +64,7 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
text: t("project_page.empty_state.general.primary_button.text"),
onClick: () => {
toggleCreatePageModal({ isOpen: true });
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
},
disabled: !canPerformEmptyStateActions,
}}
@ -79,6 +81,7 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
text: t("project_page.empty_state.public.primary_button.text"),
onClick: () => {
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC });
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
},
disabled: !canPerformEmptyStateActions,
}}
@ -94,6 +97,7 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
text: t("project_page.empty_state.private.primary_button.text"),
onClick: () => {
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE });
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
},
disabled: !canPerformEmptyStateActions,
}}

View file

@ -2,13 +2,13 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { EventProps, STATE_TRACKER_EVENTS, STATE_GROUPS } from "@plane/constants";
import { STATE_TRACKER_EVENTS, STATE_GROUPS } from "@plane/constants";
import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { StateForm } from "@/components/project-states";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
type TStateCreate = {
groupKey: TStateGroups;
@ -19,17 +19,10 @@ type TStateCreate = {
export const StateCreate: FC<TStateCreate> = observer((props) => {
const { groupKey, shouldTrackEvents, createStateCallback, handleClose } = props;
// hooks
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
// states
const [loader, setLoader] = useState(false);
const captureEventIfEnabled = (props: EventProps) => {
if (shouldTrackEvents) {
captureProjectStateEvent(props);
}
};
const onCancel = () => {
setLoader(false);
handleClose();
@ -38,17 +31,14 @@ export const StateCreate: FC<TStateCreate> = observer((props) => {
const onSubmit = async (formData: Partial<IState>) => {
if (!groupKey) return { status: "error" };
if (shouldTrackEvents) {
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
}
try {
const stateResponse = await createStateCallback({ ...formData, group: groupKey });
captureEventIfEnabled({
const response = await createStateCallback({ ...formData, group: groupKey });
if (shouldTrackEvents)
captureSuccess({
eventName: STATE_TRACKER_EVENTS.create,
payload: {
...stateResponse,
state: "SUCCESS",
element: "Project settings states page",
state_group: groupKey,
id: response.id,
},
});
setToast({
@ -60,12 +50,11 @@ export const StateCreate: FC<TStateCreate> = observer((props) => {
return { status: "success" };
} catch (error) {
const errorStatus = error as unknown as { status: number; data: { error: string } };
captureEventIfEnabled({
if (shouldTrackEvents)
captureError({
eventName: STATE_TRACKER_EVENTS.create,
payload: {
...formData,
state: "FAILED",
element: "Project settings states page",
state_group: groupKey,
},
});
if (errorStatus?.status === 400) {

View file

@ -2,13 +2,13 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { EventProps, STATE_TRACKER_EVENTS } from "@plane/constants";
import { STATE_TRACKER_EVENTS } from "@plane/constants";
import { IState, TStateOperationsCallbacks } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { StateForm } from "@/components/project-states";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
type TStateUpdate = {
state: IState;
@ -19,8 +19,6 @@ type TStateUpdate = {
export const StateUpdate: FC<TStateUpdate> = observer((props) => {
const { state, updateStateCallback, shouldTrackEvents, handleClose } = props;
// hooks
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
// states
const [loader, setLoader] = useState(false);
@ -29,28 +27,20 @@ export const StateUpdate: FC<TStateUpdate> = observer((props) => {
handleClose();
};
const captureEventIfEnabled = (props: EventProps) => {
if (shouldTrackEvents) {
captureProjectStateEvent(props);
}
};
const onSubmit = async (formData: Partial<IState>) => {
if (!state.id) return { status: "error" };
if (shouldTrackEvents) {
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
}
try {
const stateResponse = await updateStateCallback(state.id, formData);
captureEventIfEnabled({
await updateStateCallback(state.id, formData);
if (shouldTrackEvents) {
captureSuccess({
eventName: STATE_TRACKER_EVENTS.update,
payload: {
...stateResponse,
state: "SUCCESS",
element: "Project settings states page",
state_group: state.group,
id: state.id,
},
});
}
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
@ -73,14 +63,15 @@ export const StateUpdate: FC<TStateUpdate> = observer((props) => {
title: "Error!",
message: "State could not be updated. Please try again.",
});
captureEventIfEnabled({
if (shouldTrackEvents) {
captureError({
eventName: STATE_TRACKER_EVENTS.update,
payload: {
...formData,
state: "FAILED",
element: "Project settings states page",
state_group: state.group,
id: state.id,
},
});
}
return { status: "error" };
}
}

View file

@ -4,7 +4,7 @@ import { FC, useState, useRef } from "react";
import { observer } from "mobx-react";
import { ChevronDown, Plus } from "lucide-react";
// plane imports
import { EIconSize } from "@plane/constants";
import { EIconSize, STATE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
import { StateGroupIcon } from "@plane/ui";
@ -81,6 +81,7 @@ export const GroupItem: FC<TGroupItem> = observer((props) => {
</div>
<button
type="button"
data-ph-element={STATE_TRACKER_ELEMENTS.STATE_GROUP_ADD_BUTTON}
className={cn(
"flex-shrink-0 w-6 h-6 rounded flex justify-center items-center overflow-hidden transition-colors hover:bg-custom-background-80 cursor-pointer text-custom-primary-100/80 hover:text-custom-primary-100",
(!isEditable || createState) && "cursor-not-allowed text-custom-text-400 hover:text-custom-text-400"

View file

@ -4,12 +4,12 @@ import { FC, useState } from "react";
import { observer } from "mobx-react";
import { Loader, X } from "lucide-react";
// plane imports
import { EventProps, STATE_TRACKER_EVENTS } from "@plane/constants";
import { STATE_TRACKER_EVENTS, STATE_TRACKER_ELEMENTS } from "@plane/constants";
import { IState, TStateOperationsCallbacks } from "@plane/types";
import { AlertModalCore, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TStateDelete = {
@ -23,45 +23,39 @@ export const StateDelete: FC<TStateDelete> = observer((props) => {
const { totalStates, state, deleteStateCallback, shouldTrackEvents } = props;
// hooks
const { isMobile } = usePlatformOS();
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
// states
const [isDeleteModal, setIsDeleteModal] = useState(false);
const [isDelete, setIsDelete] = useState(false);
// derived values
const isDeleteDisabled = state.default ? true : totalStates === 1 ? true : false;
const captureEventIfEnabled = (props: EventProps) => {
if (shouldTrackEvents) {
captureProjectStateEvent(props);
}
};
const handleDeleteState = async () => {
if (isDeleteDisabled) return;
if (shouldTrackEvents) {
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
}
setIsDelete(true);
try {
await deleteStateCallback(state.id);
captureEventIfEnabled({
if (shouldTrackEvents) {
captureSuccess({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
...state,
state: "SUCCESS",
id: state.id,
},
});
}
setIsDelete(false);
} catch (error) {
const errorStatus = error as unknown as { status: number; data: { error: string } };
captureEventIfEnabled({
if (shouldTrackEvents) {
captureError({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
...state,
state: "FAILED",
id: state.id,
},
});
}
if (errorStatus.status === 400) {
setToast({
type: TOAST_TYPE.ERROR,
@ -107,6 +101,7 @@ export const StateDelete: FC<TStateDelete> = observer((props) => {
)}
disabled={isDeleteDisabled}
onClick={() => setIsDeleteModal(true)}
data-ph-element={STATE_TRACKER_ELEMENTS.STATE_LIST_DELETE_BUTTON}
>
<Tooltip
tooltipContent={

View file

@ -10,7 +10,8 @@ import type { IState } from "@plane/types";
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// hooks
import { useEventTracker, useProjectState } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProjectState } from "@/hooks/store";
type TStateDeleteModal = {
isOpen: boolean;
@ -24,8 +25,6 @@ export const StateDeleteModal: React.FC<TStateDeleteModal> = observer((props) =>
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// router
const { workspaceSlug } = useParams();
// store hooks
const { captureProjectStateEvent } = useEventTracker();
const { deleteState } = useProjectState();
const handleClose = () => {
@ -40,11 +39,10 @@ export const StateDeleteModal: React.FC<TStateDeleteModal> = observer((props) =>
await deleteState(workspaceSlug.toString(), data.project_id, data.id)
.then(() => {
captureProjectStateEvent({
captureSuccess({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
...data,
state: "SUCCESS",
id: data.id,
},
});
handleClose();
@ -63,11 +61,10 @@ export const StateDeleteModal: React.FC<TStateDeleteModal> = observer((props) =>
title: "Error!",
message: "State could not be deleted. Please try again.",
});
captureProjectStateEvent({
captureError({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
...data,
state: "FAILED",
id: data.id,
},
});
})

View file

@ -2,7 +2,7 @@ import { SetStateAction } from "react";
import { observer } from "mobx-react";
import { GripVertical, Pencil } from "lucide-react";
// plane imports
import { EIconSize } from "@plane/constants";
import { EIconSize, STATE_TRACKER_ELEMENTS } from "@plane/constants";
import { IState, TStateOperationsCallbacks } from "@plane/types";
import { StateGroupIcon } from "@plane/ui";
// local imports
@ -70,6 +70,7 @@ export const StateItemTitle = observer((props: TStateItemTitleProps) => {
<button
className="flex-shrink-0 w-5 h-5 rounded flex justify-center items-center overflow-hidden transition-colors hover:bg-custom-background-80 cursor-pointer text-custom-text-200 hover:text-custom-text-100"
onClick={() => setUpdateStateModal(true)}
data-ph-element={STATE_TRACKER_ELEMENTS.STATE_LIST_EDIT_BUTTON}
>
<Pencil className="w-3 h-3" />
</button>

Some files were not shown because too many files have changed in this diff Show more