[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 // Dashboard Events
export const GITHUB_REDIRECTED_TRACKER_EVENT = "github_redirected"; export const GITHUB_REDIRECTED_TRACKER_EVENT = "github_redirected";
export const HEADER_GITHUB_ICON = "header_github_icon";
// Groups // Groups
export const GROUP_WORKSPACE_TRACKER_EVENT = "workspace_metrics"; 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 = { export const WORKSPACE_TRACKER_EVENTS = {
create: "workspace_created", create: "workspace_created",
update: "workspace_updated", update: "workspace_updated",
delete: "workspace_deleted", 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 = { export const PROJECT_TRACKER_EVENTS = {
create: "project_created", create: "project_created",
update: "project_updated", update: "project_updated",
delete: "project_deleted", 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 = { export const CYCLE_TRACKER_EVENTS = {
create: "cycle_created", create: "cycle_created",
@ -165,7 +48,18 @@ export const CYCLE_TRACKER_EVENTS = {
delete: "cycle_deleted", delete: "cycle_deleted",
favorite: "cycle_favorited", favorite: "cycle_favorited",
unfavorite: "cycle_unfavorited", 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 = { export const MODULE_TRACKER_EVENTS = {
create: "module_created", create: "module_created",
@ -173,32 +67,120 @@ export const MODULE_TRACKER_EVENTS = {
delete: "module_deleted", delete: "module_deleted",
favorite: "module_favorited", favorite: "module_favorited",
unfavorite: "module_unfavorited", unfavorite: "module_unfavorited",
archive: "module_archived",
restore: "module_restored",
link: { link: {
create: "module_link_created", create: "module_link_created",
update: "module_link_updated", update: "module_link_updated",
delete: "module_link_deleted", 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 = { export const WORK_ITEM_TRACKER_EVENTS = {
create: "work_item_created", create: "work_item_created",
add_existing: "work_item_add_existing",
update: "work_item_updated", update: "work_item_updated",
delete: "work_item_deleted", delete: "work_item_deleted",
archive: "work_item_archived", archive: "work_item_archived",
restore: "work_item_restored", 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 = { export const STATE_TRACKER_EVENTS = {
create: "state_created", create: "state_created",
update: "state_updated", update: "state_updated",
delete: "state_deleted", 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 = { export const PROJECT_PAGE_TRACKER_EVENTS = {
create: "project_page_created", create: "project_page_created",
update: "project_page_updated", update: "project_page_updated",
delete: "project_page_deleted", 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 = { export const MEMBER_TRACKER_EVENTS = {
invite: "member_invited", invite: "member_invited",
@ -211,48 +193,78 @@ export const MEMBER_TRACKER_EVENTS = {
leave: "workspace_member_left", 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 = { export const AUTH_TRACKER_EVENTS = {
navigate: {
sign_up: "navigate_to_sign_up_page",
sign_in: "navigate_to_sign_in_page",
},
code_verify: "code_verified", code_verify: "code_verified",
sign_up_with_password: "sign_up_with_password", sign_up_with_password: "sign_up_with_password",
sign_in_with_password: "sign_in_with_password", sign_in_with_password: "sign_in_with_password",
sign_in_with_code: "sign_in_with_magic_link",
forgot_password: "forgot_password_clicked", 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 = { export const GLOBAL_VIEW_TRACKER_EVENTS = {
start: "product_tour_started",
complete: "product_tour_completed",
skip: "product_tour_skipped",
};
export const GLOBAL_VIEW_TOUR_TRACKER_EVENTS = {
create: "global_view_created", create: "global_view_created",
update: "global_view_updated", update: "global_view_updated",
delete: "global_view_deleted", delete: "global_view_deleted",
open: "global_view_opened", 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 = { export const NOTIFICATION_TRACKER_EVENTS = {
archive: "notification_archived", archive: "notification_archived",
unarchive: "notification_unarchived",
mark_read: "notification_marked_read",
mark_unread: "notification_marked_unread",
all_marked_read: "all_notifications_marked_read", 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 = { export const USER_TRACKER_EVENTS = {
add_details: "user_details_added", add_details: "user_details_added",
onboarding_complete: "user_onboarding_completed", onboarding_complete: "user_onboarding_completed",
}; };
export const ONBOARDING_TRACKER_ELEMENTS = {
export const ONBOARDING_TRACKER_EVENTS = { PROFILE_SETUP_FORM: "onboarding_profile_setup_form",
root: "onboarding",
step_1: "onboarding_step_1",
step_2: "onboarding_step_2",
}; };
export const SIDEBAR_TRACKER_EVENTS = { export const SIDEBAR_TRACKER_ELEMENTS = {
click: "sidenav_clicked", 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) => { export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
const { mode, email, nextPath, handleEmailClear, generateEmailUniqueCode } = props; const { mode, email, nextPath, handleEmailClear, generateEmailUniqueCode } = props;
// hooks
// const { captureEvent } = useEventTracker();
// derived values // derived values
const defaultResetTimerValue = 5; const defaultResetTimerValue = 5;
// states // states

View file

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

View file

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

View file

@ -7,18 +7,17 @@ import { Home } from "lucide-react";
import githubBlackImage from "/public/logos/github-black.png"; import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png"; import githubWhiteImage from "/public/logos/github-white.png";
// ui // 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 { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Header } from "@plane/ui"; import { Breadcrumbs, Header } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
// constants // constants
// hooks // hooks
import { useEventTracker } from "@/hooks/store"; import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
export const WorkspaceDashboardHeader = () => { export const WorkspaceDashboardHeader = () => {
// hooks // hooks
const { captureEvent } = useEventTracker();
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@ -39,8 +38,14 @@ export const WorkspaceDashboardHeader = () => {
<Header.RightItem> <Header.RightItem>
<a <a
onClick={() => onClick={() =>
captureEvent(GITHUB_REDIRECTED_TRACKER_EVENT, { captureElementAndEvent({
element: "navbar", 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" 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, EUserPermissionsLevel,
EProjectFeatureKey, EProjectFeatureKey,
ISSUE_DISPLAY_FILTERS_BY_PAGE, ISSUE_DISPLAY_FILTERS_BY_PAGE,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants"; } from "@plane/constants";
import { usePlatformOS } from "@plane/hooks"; import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
@ -34,7 +35,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect
import { import {
useCommandPalette, useCommandPalette,
useCycle, useCycle,
useEventTracker,
useIssues, useIssues,
useLabel, useLabel,
useMember, useMember,
@ -68,7 +68,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
} = useIssues(EIssuesStoreType.CYCLE); } = useIssues(EIssuesStoreType.CYCLE);
const { currentProjectCycleIds, getCycleById } = useCycle(); const { currentProjectCycleIds, getCycleById } = useCycle();
const { toggleCreateIssueModal } = useCommandPalette(); const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { currentProjectDetails, loader } = useProject(); const { currentProjectDetails, loader } = useProject();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { projectLabels } = useLabel(); const { projectLabels } = useLabel();
@ -263,9 +262,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
<Button <Button
className="h-full self-start" className="h-full self-start"
onClick={() => { onClick={() => {
setTrackElement("Cycle work items page");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
}} }}
data-ph-element={WORK_ITEM_TRACKER_ELEMENTS.HEADER_ADD_BUTTON.CYCLE}
size="sm" size="sm"
> >
{t("issue.add.label")} {t("issue.add.label")}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,8 +8,9 @@ import { Layers } from "lucide-react";
import { import {
EIssueFilterType, EIssueFilterType,
ISSUE_DISPLAY_FILTERS_BY_PAGE, ISSUE_DISPLAY_FILTERS_BY_PAGE,
DEFAULT_GLOBAL_VIEWS_LIST,
EIssueLayoutTypes, EIssueLayoutTypes,
GLOBAL_VIEW_TRACKER_ELEMENTS,
DEFAULT_GLOBAL_VIEWS_LIST,
} from "@plane/constants"; } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { 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")} {t("workspace_views.add_view")}
</Button> </Button>
<div className="hidden md:block"> <div className="hidden md:block">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,6 @@ export interface CopyMenuHelperProps {
shouldRender: boolean; shouldRender: boolean;
}; };
activeLayout: string; activeLayout: string;
setTrackElement: (element: string) => void;
setCreateUpdateIssueModal: (open: boolean) => void; setCreateUpdateIssueModal: (open: boolean) => void;
setDuplicateWorkItemModal?: (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 { observer } from "mobx-react";
import { LockKeyhole, LockKeyholeOpen } from "lucide-react"; import { LockKeyhole, LockKeyholeOpen } from "lucide-react";
// plane imports // plane imports
import { PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks // hooks
import { usePageOperations } from "@/hooks/use-page-operations"; import { usePageOperations } from "@/hooks/use-page-operations";
// store // store
@ -78,6 +81,7 @@ export const PageLockControl = observer(({ page }: Props) => {
<button <button
type="button" type="button"
onClick={toggleLock} 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" 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" aria-label="Lock"
> >
@ -90,6 +94,7 @@ export const PageLockControl = observer(({ page }: Props) => {
<button <button
type="button" type="button"
onClick={toggleLock} 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" 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" 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 ProjectCreateHeader from "@/components/project/create/header";
import ProjectCreateButtons from "@/components/project/create/project-create-buttons"; import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
// hooks // 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"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web types // plane web types
import { TProject } from "@/plane-web/types/projects"; 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; const { setToFavorite, workspaceSlug, data, onClose, handleNextStep, updateCoverImageStatus } = props;
// store // store
const { t } = useTranslation(); const { t } = useTranslation();
const { captureProjectEvent } = useEventTracker();
const { addProjectToFavorites, createProject } = useProject(); const { addProjectToFavorites, createProject } = useProject();
// states // states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
@ -70,25 +70,30 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
if (coverImage) { if (coverImage) {
await updateCoverImageStatus(res.id, coverImage); await updateCoverImageStatus(res.id, coverImage);
} }
const newPayload = { captureSuccess({
...res,
state: "SUCCESS",
};
captureProjectEvent({
eventName: PROJECT_TRACKER_EVENTS.create, eventName: PROJECT_TRACKER_EVENTS.create,
payload: newPayload, payload: {
identifier: formData.identifier,
},
}); });
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
title: t("success"), title: t("success"),
message: t("project_created_successfully"), message: t("project_created_successfully"),
}); });
if (setToFavorite) { if (setToFavorite) {
handleAddToFavorites(res.id); handleAddToFavorites(res.id);
} }
handleNextStep(res.id); handleNextStep(res.id);
}) })
.catch((err) => { .catch((err) => {
captureError({
eventName: PROJECT_TRACKER_EVENTS.create,
payload: {
identifier: formData.identifier,
},
});
if (err?.data.code === "PROJECT_NAME_ALREADY_EXIST") { if (err?.data.code === "PROJECT_NAME_ALREADY_EXIST") {
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,

View file

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

View file

@ -1,6 +1,14 @@
// types // 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"; import { TCommandPaletteActionList, TCommandPaletteShortcut, TCommandPaletteShortcutList } from "@plane/types";
// store // store
import { captureClick } from "@/helpers/event-tracker.helper";
import { store } from "@/lib/store-context"; import { store } from "@/lib/store-context";
export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => { export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
@ -10,7 +18,10 @@ export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
c: { c: {
title: "Create a new work item", title: "Create a new work item",
description: "Create a new work item in the current project", 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: { p: {
title: "Create a new project", title: "Create a new project",
description: "Create a new project in the current workspace", 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: { d: {
title: "Create a new page", title: "Create a new page",
description: "Create a new page in the current project", 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: { m: {
title: "Create a new module", title: "Create a new module",
description: "Create a new module in the current project", 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: { q: {
title: "Create a new cycle", title: "Create a new cycle",
description: "Create a new cycle in the current project", 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: { v: {
title: "Create a new view", 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 // icons
import { Eye, EyeOff, Info, X, XCircle } from "lucide-react"; import { Eye, EyeOff, Info, X, XCircle } from "lucide-react";
// plane imports // 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 { useTranslation } from "@plane/i18n";
import { Button, Input, Spinner } from "@plane/ui"; import { Button, Input, Spinner } from "@plane/ui";
import { getPasswordStrength } from "@plane/utils"; import { getPasswordStrength } from "@plane/utils";
@ -16,7 +16,7 @@ import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/accou
// helpers // helpers
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper"; import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
// hooks // hooks
import { useEventTracker } from "@/hooks/store"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// services // services
import { AuthService } from "@/services/auth.service"; 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; const { email, isSMTPConfigured, handleAuthStep, handleEmailClear, mode, nextPath } = props;
// plane imports // plane imports
const { t } = useTranslation(); const { t } = useTranslation();
// hooks
const { captureEvent } = useEventTracker();
// ref // ref
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
// states // states
@ -77,7 +75,6 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
const redirectToUniqueCodeSignIn = async () => { const redirectToUniqueCodeSignIn = async () => {
handleAuthStep(EAuthSteps.UNIQUE_CODE); handleAuthStep(EAuthSteps.UNIQUE_CODE);
captureEvent(AUTH_TRACKER_EVENTS.sign_in_with_code);
}; };
const passwordSupport = const passwordSupport =
@ -85,7 +82,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
<div className="w-full"> <div className="w-full">
{isSMTPConfigured ? ( {isSMTPConfigured ? (
<Link <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)}`} href={`/accounts/forgot-password?email=${encodeURIComponent(email)}`}
className="text-xs font-medium text-custom-primary-100" className="text-xs font-medium text-custom-primary-100"
> >
@ -154,17 +151,32 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
: true; : true;
if (isPasswordValid) { if (isPasswordValid) {
setIsSubmitting(true); setIsSubmitting(true);
captureEvent( captureSuccess({
mode === EAuthModes.SIGN_IN eventName:
? AUTH_TRACKER_EVENTS.sign_in_with_password mode === EAuthModes.SIGN_IN
: AUTH_TRACKER_EVENTS.sign_up_with_password ? AUTH_TRACKER_EVENTS.sign_in_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 if (formRef.current) formRef.current.submit(); // Manually submit the form if the condition is met
} else { } else {
setBannerMessage(true); 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" name="csrfmiddlewaretoken" />
<input type="hidden" value={passwordFormData.email} name="email" /> <input type="hidden" value={passwordFormData.email} name="email" />
@ -292,6 +304,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
{isSMTPConfigured && ( {isSMTPConfigured && (
<Button <Button
type="button" type="button"
data-ph-element={AUTH_TRACKER_ELEMENTS.SIGN_IN_WITH_UNIQUE_CODE}
onClick={redirectToUniqueCodeSignIn} onClick={redirectToUniqueCodeSignIn}
variant="outline-primary" variant="outline-primary"
className="w-full" className="w-full"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,19 +5,20 @@ import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
import { useFormContext, Controller } from "react-hook-form"; import { useFormContext, Controller } from "react-hook-form";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { IJiraImporterForm } from "@plane/types"; import { IJiraImporterForm } from "@plane/types";
// hooks // hooks
// components // components
import { CustomSelect, Input } from "@plane/ui"; import { CustomSelect, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "@plane/utils"; 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 // types
export const JiraGetImportDetail: React.FC = observer(() => { export const JiraGetImportDetail: React.FC = observer(() => {
// store hooks // store hooks
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { workspaceProjectIds, getProjectById } = useProject(); const { workspaceProjectIds, getProjectById } = useProject();
// form info // form info
const { const {
@ -201,8 +202,9 @@ export const JiraGetImportDetail: React.FC = observer(() => {
<div> <div>
<button <button
type="button" type="button"
data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON}
onClick={() => { onClick={() => {
setTrackElement("Jira import detail page"); captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.CREATE_PROJECT_JIRA_IMPORT_DETAIL_PAGE });
toggleCreateProjectModal(true); toggleCreateProjectModal(true);
}} }}
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-custom-text-200" 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"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { EIssueServiceType, TIssueServiceType } from "@plane/types"; import { EIssueServiceType, TIssueServiceType } from "@plane/types";
// plane ui // plane ui
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
// hooks // hooks
import { useEventTracker, useIssueDetail } from "@/hooks/store"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useIssueDetail } from "@/hooks/store";
// types // types
import { TAttachmentUploadStatus } from "@/store/issue/issue-details/attachment.store"; import { TAttachmentUploadStatus } from "@/store/issue/issue-details/attachment.store";
@ -31,7 +33,6 @@ export const useAttachmentOperations = (
const { const {
attachment: { createAttachment, removeAttachment, getAttachmentsUploadStatusByIssueId }, attachment: { createAttachment, removeAttachment, getAttachmentsUploadStatusByIssueId },
} = useIssueDetail(issueServiceType); } = useIssueDetail(issueServiceType);
const { captureIssueEvent } = useEventTracker();
const attachmentOperations: TAttachmentOperations = useMemo( const attachmentOperations: TAttachmentOperations = useMemo(
() => ({ () => ({
@ -51,19 +52,16 @@ export const useAttachmentOperations = (
}, },
}); });
const res = await attachmentUploadPromise; await attachmentUploadPromise;
captureIssueEvent({ captureSuccess({
eventName: "Issue attachment added", eventName: WORK_ITEM_TRACKER_EVENTS.attachment.add,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, payload: { id: issueId },
updates: {
changed_property: "attachment",
change_details: res.id,
},
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureError({
eventName: "Issue attachment added", eventName: WORK_ITEM_TRACKER_EVENTS.attachment.add,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, payload: { id: issueId },
error: error as Error,
}); });
throw error; throw error;
} }
@ -77,22 +75,15 @@ export const useAttachmentOperations = (
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
title: "Attachment removed", title: "Attachment removed",
}); });
captureIssueEvent({ captureSuccess({
eventName: "Issue attachment deleted", eventName: WORK_ITEM_TRACKER_EVENTS.attachment.remove,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, payload: { id: issueId },
updates: {
changed_property: "attachment",
change_details: "",
},
}); });
} catch (error) { } catch (error) {
captureIssueEvent({ captureError({
eventName: "Issue attachment deleted", eventName: WORK_ITEM_TRACKER_EVENTS.attachment.remove,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, payload: { id: issueId },
updates: { error: error as Error,
changed_property: "attachment",
change_details: "",
},
}); });
setToast({ setToast({
message: "The Attachment could not be removed", 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); const attachmentsUploadStatus = getAttachmentsUploadStatusByIssueId(issueId);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ import {
EIssueFilterType, EIssueFilterType,
EUserPermissions, EUserPermissions,
EUserPermissionsLevel, EUserPermissionsLevel,
GLOBAL_VIEW_TOUR_TRACKER_EVENTS, GLOBAL_VIEW_TRACKER_EVENTS,
} from "@plane/constants"; } from "@plane/constants";
import { EIssuesStoreType, EViewAccess, IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; import { EIssuesStoreType, EViewAccess, IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
import { Header, EHeaderVariant, Loader } from "@plane/ui"; 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 { UpdateViewComponent } from "@/components/views/update-view-component";
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// hooks // 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"; import { getAreFiltersEqual } from "../../../utils";
type Props = { type Props = {
@ -39,7 +40,6 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
} = useIssues(EIssuesStoreType.GLOBAL); } = useIssues(EIssuesStoreType.GLOBAL);
const { workspaceLabels } = useLabel(); const { workspaceLabels } = useLabel();
const { globalViewMap, updateGlobalView } = useGlobalView(); const { globalViewMap, updateGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker();
const { data } = useUser(); const { data } = useUser();
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
@ -107,15 +107,25 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
const handleUpdateView = () => { const handleUpdateView = () => {
if (!workspaceSlug || !globalViewId) return; if (!workspaceSlug || !globalViewId) return;
updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters).then((res) => { updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters)
if (res) .then((res) => {
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.update, { if (res)
view_id: res.id, captureSuccess({
applied_filters: res.filters, eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
state: "SUCCESS", payload: {
element: "Spreadsheet view", view_id: globalViewId,
},
});
})
.catch((error) => {
captureError({
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
payload: {
view_id: globalViewId,
},
error: error,
}); });
}); });
}; };
// add a placeholder object instead of appliedFilters if it is undefined // add a placeholder object instead of appliedFilters if it is undefined

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

View file

@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
// lucide icons // lucide icons
import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; import { Minimize2, Maximize2, Circle, Plus } from "lucide-react";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { TIssue, ISearchIssueResponse, TIssueKanbanFilters, TIssueGroupByOptions } from "@plane/types"; import { TIssue, ISearchIssueResponse, TIssueKanbanFilters, TIssueGroupByOptions } from "@plane/types";
// ui // ui
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/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 { ExistingIssuesListModal } from "@/components/core";
import { CreateUpdateIssueModal } from "@/components/issues"; import { CreateUpdateIssueModal } from "@/components/issues";
// constants // constants
// hooks import { captureClick } from "@/helpers/event-tracker.helper";
import { useEventTracker } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { CreateUpdateEpicModal } from "@/plane-web/components/epics/epic-modal"; import { CreateUpdateEpicModal } from "@/plane-web/components/epics/epic-modal";
// types // types
@ -56,7 +56,6 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false); const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
// hooks // hooks
const storeType = useIssueStoreType(); const storeType = useIssueStoreType();
const { setTrackElement } = useEventTracker();
// router // router
const { workspaceSlug, projectId, moduleId, cycleId } = useParams(); const { workspaceSlug, projectId, moduleId, cycleId } = useParams();
const pathname = usePathname(); const pathname = usePathname();
@ -167,7 +166,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
> >
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => { onClick={() => {
setTrackElement("Kanban layout"); captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true); setIsOpen(true);
}} }}
> >
@ -175,7 +174,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => { onClick={() => {
setTrackElement("Kanban layout"); captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.add_existing });
setOpenExistingIssueListModal(true); setOpenExistingIssueListModal(true);
}} }}
> >
@ -186,7 +185,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
<div <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" 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={() => { onClick={() => {
setTrackElement("Kanban layout"); captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true); setIsOpen(true);
}} }}
> >

View file

@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
import { CircleDashed, Plus } from "lucide-react"; import { CircleDashed, Plus } from "lucide-react";
// types // types
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { TIssue, ISearchIssueResponse, TIssueGroupByOptions } from "@plane/types"; import { TIssue, ISearchIssueResponse, TIssueGroupByOptions } from "@plane/types";
// ui // ui
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/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 { ExistingIssuesListModal, MultipleSelectGroupAction } from "@/components/core";
import { CreateUpdateIssueModal } from "@/components/issues"; import { CreateUpdateIssueModal } from "@/components/issues";
// constants // constants
// helpers import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { TSelectionHelper } from "@/hooks/use-multiple-select";
// plane-web // plane-web
@ -59,8 +58,6 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
// router // router
const { workspaceSlug, projectId, moduleId, cycleId } = useParams(); const { workspaceSlug, projectId, moduleId, cycleId } = useParams();
const pathname = usePathname(); const pathname = usePathname();
// hooks
const { setTrackElement } = useEventTracker();
const storeType = useIssueStoreType(); const storeType = useIssueStoreType();
// derived values // derived values
const isDraftIssue = pathname.includes("draft-issue"); const isDraftIssue = pathname.includes("draft-issue");
@ -134,7 +131,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
> >
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => { onClick={() => {
setTrackElement("List layout"); captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true); setIsOpen(true);
}} }}
> >
@ -142,7 +139,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => { onClick={() => {
setTrackElement("List layout"); captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.add_existing });
setOpenExistingIssueListModal(true); setOpenExistingIssueListModal(true);
}} }}
> >
@ -153,7 +150,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
<div <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" 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={() => { onClick={() => {
setTrackElement("List layout"); captureClick({ elementName: WORK_ITEM_TRACKER_EVENTS.create });
setIsOpen(true); setIsOpen(true);
}} }}
> >

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react"; 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 // types
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types"; import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
@ -20,7 +20,8 @@ import { OnboardingHeader, SwitchAccountDropdown } from "@/components/onboarding
// constants // constants
// helpers // helpers
// hooks // hooks
import { useEventTracker, useUser, useUserProfile } from "@/hooks/store"; import { captureError, captureSuccess, captureView } from "@/helpers/event-tracker.helper";
import { useUser, useUserProfile } from "@/hooks/store";
// assets // assets
import ProfileSetupDark from "@/public/onboarding/profile-setup-dark.webp"; import ProfileSetupDark from "@/public/onboarding/profile-setup-dark.webp";
import ProfileSetupLight from "@/public/onboarding/profile-setup-light.webp"; import ProfileSetupLight from "@/public/onboarding/profile-setup-light.webp";
@ -98,7 +99,6 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
// store hooks // store hooks
const { updateCurrentUser } = useUser(); const { updateCurrentUser } = useUser();
const { updateUserProfile } = useUserProfile(); const { updateUserProfile } = useUserProfile();
const { captureEvent } = useEventTracker();
// form info // form info
const { const {
getValues, getValues,
@ -143,11 +143,12 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
updateUserProfile(profileUpdatePayload), updateUserProfile(profileUpdatePayload),
totalSteps > 2 && stepChange({ profile_complete: true }), totalSteps > 2 && stepChange({ profile_complete: true }),
]); ]);
captureEvent(USER_TRACKER_EVENTS.add_details, { captureSuccess({
use_case: formData.use_case, eventName: USER_TRACKER_EVENTS.add_details,
role: formData.role, payload: {
state: "SUCCESS", use_case: formData.use_case,
element: ONBOARDING_TRACKER_EVENTS.step_1, role: formData.role,
},
}); });
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
@ -159,9 +160,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
finishOnboarding(); finishOnboarding();
} }
} catch { } catch {
captureEvent(USER_TRACKER_EVENTS.add_details, { captureError({
state: "FAILED", eventName: USER_TRACKER_EVENTS.add_details,
element: ONBOARDING_TRACKER_EVENTS.step_1,
}); });
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
@ -183,9 +183,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
formData.password && handleSetPassword(formData.password), formData.password && handleSetPassword(formData.password),
]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION)); ]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION));
} catch { } catch {
captureEvent(USER_TRACKER_EVENTS.add_details, { captureError({
state: "FAILED", eventName: USER_TRACKER_EVENTS.add_details,
element: ONBOARDING_TRACKER_EVENTS.step_1,
}); });
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
@ -205,11 +204,12 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
updateUserProfile(profileUpdatePayload), updateUserProfile(profileUpdatePayload),
totalSteps > 2 && stepChange({ profile_complete: true }), totalSteps > 2 && stepChange({ profile_complete: true }),
]); ]);
captureEvent(USER_TRACKER_EVENTS.add_details, { captureSuccess({
use_case: formData.use_case, eventName: USER_TRACKER_EVENTS.add_details,
role: formData.role, payload: {
state: "SUCCESS", use_case: formData.use_case,
element: ONBOARDING_TRACKER_EVENTS.step_2, role: formData.role,
},
}); });
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
@ -221,9 +221,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
finishOnboarding(); finishOnboarding();
} }
} catch { } catch {
captureEvent(USER_TRACKER_EVENTS.add_details, { captureError({
state: "FAILED", eventName: USER_TRACKER_EVENTS.add_details,
element: ONBOARDING_TRACKER_EVENTS.step_2,
}); });
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
@ -235,6 +234,9 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
const onSubmit = async (formData: TProfileSetupFormValues) => { const onSubmit = async (formData: TProfileSetupFormValues) => {
if (!user) return; if (!user) return;
captureView({
elementName: ONBOARDING_TRACKER_ELEMENTS.PROFILE_SETUP_FORM,
});
if (profileSetupStep === EProfileSetupSteps.ALL) await handleSubmitProfileSetup(formData); if (profileSetupStep === EProfileSetupSteps.ALL) await handleSubmitProfileSetup(formData);
if (profileSetupStep === EProfileSetupSteps.USER_DETAILS) await handleSubmitUserDetail(formData); if (profileSetupStep === EProfileSetupSteps.USER_DETAILS) await handleSubmitUserDetail(formData);
if (profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION) await handleSubmitUserPersonalization(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 Image, { StaticImageData } from "next/image";
import { X } from "lucide-react"; import { X } from "lucide-react";
// ui // ui
import { PRODUCT_TOUR_TRACKER_EVENTS } from "@plane/constants"; import { PRODUCT_TOUR_TRACKER_ELEMENTS } from "@plane/constants";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// components // components
import { TourSidebar } from "@/components/onboarding"; import { TourSidebar } from "@/components/onboarding";
// constants // constants
// hooks // hooks
import { useCommandPalette, useEventTracker, useUser } from "@/hooks/store"; import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useUser } from "@/hooks/store";
// assets // assets
import CyclesTour from "@/public/onboarding/cycles.webp"; import CyclesTour from "@/public/onboarding/cycles.webp";
import IssuesTour from "@/public/onboarding/issues.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"); const [step, setStep] = useState<TTourSteps>("welcome");
// store hooks // store hooks
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement, captureEvent } = useEventTracker();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step); const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
@ -112,7 +112,9 @@ export const TourRoot: React.FC<Props> = observer((props) => {
<Button <Button
variant="primary" variant="primary"
onClick={() => { onClick={() => {
captureEvent(PRODUCT_TOUR_TRACKER_EVENTS.start); captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.START_BUTTON,
});
setStep("work-items"); setStep("work-items");
}} }}
> >
@ -122,7 +124,9 @@ export const TourRoot: React.FC<Props> = observer((props) => {
type="button" type="button"
className="bg-transparent text-xs font-medium text-custom-primary-100 outline-custom-text-100" className="bg-transparent text-xs font-medium text-custom-primary-100 outline-custom-text-100"
onClick={() => { onClick={() => {
captureEvent(PRODUCT_TOUR_TRACKER_EVENTS.skip); captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.SKIP_BUTTON,
});
onComplete(); onComplete();
}} }}
> >
@ -171,7 +175,9 @@ export const TourRoot: React.FC<Props> = observer((props) => {
<Button <Button
variant="primary" variant="primary"
onClick={() => { onClick={() => {
setTrackElement("Product tour"); captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.CREATE_PROJECT_BUTTON,
});
onComplete(); onComplete();
toggleCreateProjectModal(true); toggleCreateProjectModal(true);
}} }}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import { SetStateAction } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { GripVertical, Pencil } from "lucide-react"; import { GripVertical, Pencil } from "lucide-react";
// plane imports // plane imports
import { EIconSize } from "@plane/constants"; import { EIconSize, STATE_TRACKER_ELEMENTS } from "@plane/constants";
import { IState, TStateOperationsCallbacks } from "@plane/types"; import { IState, TStateOperationsCallbacks } from "@plane/types";
import { StateGroupIcon } from "@plane/ui"; import { StateGroupIcon } from "@plane/ui";
// local imports // local imports
@ -70,6 +70,7 @@ export const StateItemTitle = observer((props: TStateItemTitleProps) => {
<button <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" 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)} onClick={() => setUpdateStateModal(true)}
data-ph-element={STATE_TRACKER_ELEMENTS.STATE_LIST_EDIT_BUTTON}
> >
<Pencil className="w-3 h-3" /> <Pencil className="w-3 h-3" />
</button> </button>

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