Compare commits

...
Author SHA1 Message Date
Palanikannan M 60fe33d3f6 fix: project tracker event import 2025-07-01 17:28:24 +05:30
Palanikannan M 0a50a61644 refactor: simplify lock button event handling in PageLockControl 2025-07-01 17:20:30 +05:30
M. PalanikannanandGitHub 7c633f67c6 Merge branch 'refactor-event-trackers' into chore-project-page-tracker-events 2025-07-01 17:15:38 +05:30
Akshita GoyalandGitHub b17ee7282f chore: project events (#7305)
* chore: project-events

* fix: refactor
2025-07-01 17:09:15 +05:30
Palanikannan M e9f58a9b26 added events for most page events 2025-07-01 17:06:25 +05:30
Akshita GoyalandGitHub d4d49f5abc Chore: state events (#7307)
* chore: state events

* fix: refactor
2025-07-01 17:04:14 +05:30
Prateek ShouryaandGitHub 72d9318994 refactor: auth related event trackers (#7306) 2025-07-01 16:52:33 +05:30
Prateek Shourya 37fd368833 refactor: update event tracker constants 2025-07-01 16:10:51 +05:30
Akshita GoyalandGitHub 47f3dd4f35 chore: member tracker events (#7302)
* chore: member-tracker-events

* fix: constants
2025-07-01 15:17:44 +05:30
Akshita GoyalandGitHub 477d5f74d9 Merge pull request #7303 from makeplane/refactor-addition-events
refactor: global views, product tour, notifications, onboarding, users and sidebar related events
2025-07-01 15:13:23 +05:30
Prateek Shourya 59140aab74 refactor: global views, product tour, notifications, onboarding, users and sidebar related events 2025-07-01 15:07:19 +05:30
Prateek Shourya 9cd75e62c2 Merge branch 'preview' of github.com:makeplane/plane into refactor-event-trackers 2025-07-01 15:04:09 +05:30
Prateek Shourya 785f85754e Merge branch 'refactor-event-trackers' of github.com:makeplane/plane into refactor-event-trackers 2025-07-01 15:03:54 +05:30
90991dfeb3 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>
2025-07-01 15:03:27 +05:30
Prateek Shourya be77cf89c8 Merge branch 'refactor-event-trackers' of github.com:makeplane/plane into refactor-event-trackers 2025-07-01 14:58:45 +05:30
db1caa79eb 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>
2025-07-01 14:31:36 +05:30
gakshita 3545008a1e fix: data id 2025-06-30 19:57:09 +05:30
Prateek Shourya f5cc076ca6 chore: update event tracker helper types 2025-06-30 19:56:47 +05:30
gakshita bc390536c6 fix: nomenclature 2025-06-30 19:51:56 +05:30
Akshita GoyalandGitHub 7ec3673ce3 Merge pull request #7287 from makeplane/fix-workspace-events
fix: workspace events
2025-06-30 19:45:13 +05:30
gakshita 06dc8020c4 Merge branch 'refactor-event-trackers' of https://github.com/makeplane/plane into fix-workspace-events 2025-06-30 19:42:28 +05:30
gakshita 0d58c18fdd fix: changes nomenclature 2025-06-30 19:33:48 +05:30
gakshita 9f8872e431 fix: added slug 2025-06-30 19:29:44 +05:30
gakshita 4b510b8dfd fix: workspace events 2025-06-30 19:25:09 +05:30
Prateek Shourya ef93781ce6 chore: minor refactors 2025-06-30 19:23:34 +05:30
Prateek Shourya 5c30d0d911 chore: minor improvements 2025-06-30 19:09:15 +05:30
Prateek Shourya ff0112fc91 chore: track element and event helper. 2025-06-30 19:00:39 +05:30
Prateek Shourya 8011b76211 chore: tracker element type 2025-06-30 18:30:31 +05:30
Prateek Shourya 5301f63ddb chore: remove export 2025-06-30 18:27:32 +05:30
Prateek Shourya f366f9531c chore: tracker element updates 2025-06-30 18:23:45 +05:30
gakshita f19befd109 fix: handled name 2025-06-30 18:09:16 +05:30
gakshita bb56fc36bf fix: handled click events 2025-06-30 18:07:26 +05:30
Prateek Shourya edc3c817dc feat: track click events for data-ph-element 2025-06-30 17:52:59 +05:30
Prateek Shourya f65c1f4510 feat: event tracker helper 2025-06-30 17:19:04 +05:30
93 changed files with 1503 additions and 910 deletions
+112 -72
View File
@@ -12,59 +12,6 @@ export type EventProps = {
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,
@@ -159,13 +106,37 @@ export const PROJECT_TRACKER_EVENTS = {
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",
};
export const CYCLE_TRACKER_EVENTS = {
create: "cycle_created",
update: "cycle_updated",
delete: "cycle_deleted",
favorite: "cycle_favorited",
unfavorite: "cycle_unfavorited",
archive: "cycle_archived",
restore: "cycle_restored",
};
export const CYCLE_TRACKER_ELEMENTS = {
RIGHT_HEADER_ADD_BUTTON: "right_header_add_cycle_button",
EMPTY_STATE_ADD_BUTTON: "empty_state_add_cycle_button",
COMMAND_PALETTE_ADD_ITEM: "command_palette_add_cycle_item",
RIGHT_SIDEBAR: "cycle_right_sidebar",
QUICK_ACTIONS: "cycle_quick_actions",
CONTEXT_MENU: "cycle_context_menu",
LIST_ITEM: "cycle_list_item",
} as const;
export const MODULE_TRACKER_EVENTS = {
create: "module_created",
@@ -173,12 +144,24 @@ export const MODULE_TRACKER_EVENTS = {
delete: "module_deleted",
favorite: "module_favorited",
unfavorite: "module_unfavorited",
archive: "module_archived",
restore: "module_restored",
link: {
create: "module_link_created",
update: "module_link_updated",
delete: "module_link_deleted",
},
};
export const MODULE_TRACKER_ELEMENTS = {
RIGHT_HEADER_ADD_BUTTON: "right_header_add_module_button",
EMPTY_STATE_ADD_BUTTON: "empty_state_add_module_button",
COMMAND_PALETTE_ADD_ITEM: "command_palette_add_module_item",
RIGHT_SIDEBAR: "module_right_sidebar",
QUICK_ACTIONS: "module_quick_actions",
CONTEXT_MENU: "module_context_menu",
LIST_ITEM: "module_list_item",
CARD_ITEM: "module_card_item",
} as const;
export const WORK_ITEM_TRACKER_EVENTS = {
create: "work_item_created",
@@ -193,13 +176,41 @@ export const STATE_TRACKER_EVENTS = {
update: "state_updated",
delete: "state_deleted",
};
export const STATE_TRACKER_ELEMENTS = {
STATE_GROUP_ADD_BUTTON: "state_group_add_button",
STATE_LIST_DELETE_BUTTON: "state_list_delete_button",
STATE_LIST_EDIT_BUTTON: "state_list_edit_button",
};
export const PROJECT_PAGE_TRACKER_EVENTS = {
create: "project_page_created",
update: "project_page_updated",
delete: "project_page_deleted",
archive: "project_page_archived",
restore: "project_page_restored",
lock: "project_page_locked",
unlock: "project_page_unlocked",
access_update: "project_page_access_updated",
duplicate: "project_page_duplicated",
favorite: "project_page_favorited",
unfavorite: "project_page_unfavorited",
move: "project_page_moved",
};
export const PROJECT_PAGE_TRACKER_ELEMENTS = {
COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON: "command_palette_shortcut_create_page_button",
EMPTY_STATE_CREATE_BUTTON: "empty_state_create_page_button",
COMMAND_PALETTE_CREATE_BUTTON: "command_palette_create_page_button",
CONTEXT_MENU: "page_context_menu",
QUICK_ACTIONS: "page_quick_actions",
LIST_ITEM: "page_list_item",
FAVORITE_BUTTON: "page_favorite_button",
ARCHIVE_BUTTON: "page_archive_button",
LOCK_BUTTON: "page_lock_button",
ACCESS_TOGGLE: "page_access_toggle",
DUPLICATE_BUTTON: "page_duplicate_button",
} as const;
export const MEMBER_TRACKER_EVENTS = {
invite: "member_invited",
accept: "member_accepted",
@@ -211,48 +222,77 @@ export const MEMBER_TRACKER_EVENTS = {
leave: "workspace_member_left",
},
};
export const MEMBER_TRACKER_ELEMENTS = {
HEADER_ADD_BUTTON: "header_add_member_button",
ACCEPT_INVITATION_BUTTON: "accept_invitation_button",
ONBOARDING_JOIN_WORKSPACE: "workspace_join_continue_to_workspace_button",
ONBOARDING_INVITE_MEMBER: "invite_member_continue_button",
SIDEBAR_PROJECT_QUICK_ACTIONS: "sidebar_project_quick_actions",
PROJECT_MEMBER_TABLE_CONTEXT_MENU: "project_member_table_context_menu",
WORKSPACE_MEMBER_TABLE_CONTEXT_MENU: "workspace_member_table_context_menu",
WORKSPACE_INVITATIONS_LIST_CONTEXT_MENU: "workspace_invitations_list_context_menu",
} as const;
export const AUTH_TRACKER_EVENTS = {
navigate: {
sign_up: "navigate_to_sign_up_page",
sign_in: "navigate_to_sign_in_page",
},
code_verify: "code_verified",
sign_up_with_password: "sign_up_with_password",
sign_in_with_password: "sign_in_with_password",
sign_in_with_code: "sign_in_with_magic_link",
forgot_password: "forgot_password_clicked",
new_code_requested: "new_code_requested",
};
export const AUTH_TRACKER_ELEMENTS = {
NAVIGATE_TO_SIGN_UP: "navigate_to_sign_up",
FORGOT_PASSWORD_FROM_SIGNIN: "forgot_password_from_signin",
SIGNUP_FROM_FORGOT_PASSWORD: "signup_from_forgot_password",
SIGN_IN_FROM_SIGNUP: "sign_in_from_signup",
SIGN_IN_WITH_UNIQUE_CODE: "sign_in_with_unique_code",
REQUEST_NEW_CODE: "request_new_code",
VERIFY_CODE: "verify_code",
};
export const PRODUCT_TOUR_TRACKER_EVENTS = {
start: "product_tour_started",
complete: "product_tour_completed",
skip: "product_tour_skipped",
};
export const GLOBAL_VIEW_TOUR_TRACKER_EVENTS = {
export const GLOBAL_VIEW_TRACKER_EVENTS = {
create: "global_view_created",
update: "global_view_updated",
delete: "global_view_deleted",
open: "global_view_opened",
};
export const GLOBAL_VIEW_TRACKER_ELEMENTS = {
RIGHT_HEADER_ADD_BUTTON: "global_view_right_header_add_button",
HEADER_SAVE_VIEW_BUTTON: "global_view_header_save_view_button",
QUICK_ACTIONS: "global_view_quick_actions",
LIST_ITEM: "global_view_list_item",
};
export const PRODUCT_TOUR_TRACKER_EVENTS = {
complete: "product_tour_completed",
};
export const PRODUCT_TOUR_TRACKER_ELEMENTS = {
START_BUTTON: "product_tour_start_button",
SKIP_BUTTON: "product_tour_skip_button",
CREATE_PROJECT_BUTTON: "product_tour_create_project_button",
};
export const NOTIFICATION_TRACKER_EVENTS = {
archive: "notification_archived",
unarchive: "notification_unarchived",
mark_read: "notification_marked_read",
mark_unread: "notification_marked_unread",
all_marked_read: "all_notifications_marked_read",
};
export const NOTIFICATION_TRACKER_ELEMENTS = {
MARK_ALL_AS_READ_BUTTON: "mark_all_as_read_button",
ARCHIVE_UNARCHIVE_BUTTON: "archive_unarchive_button",
MARK_READ_UNREAD_BUTTON: "mark_read_unread_button",
};
export const USER_TRACKER_EVENTS = {
add_details: "user_details_added",
onboarding_complete: "user_onboarding_completed",
};
export const ONBOARDING_TRACKER_EVENTS = {
root: "onboarding",
step_1: "onboarding_step_1",
step_2: "onboarding_step_2",
export const ONBOARDING_TRACKER_ELEMENTS = {
PROFILE_SETUP_FORM: "onboarding_profile_setup_form",
};
export const SIDEBAR_TRACKER_EVENTS = {
click: "sidenav_clicked",
export const SIDEBAR_TRACKER_ELEMENTS = {
USER_MENU_ITEM: "sidenav_user_menu_item",
};
@@ -4,7 +4,7 @@ import { useMemo } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
// plane package imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { type TabItem, Tabs } from "@plane/ui";
// components
@@ -12,6 +12,7 @@ import AnalyticsFilterActions from "@/components/analytics/analytics-filter-acti
import { PageHead } from "@/components/core";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { getAnalyticsTabs } from "@/plane-web/components/analytics/tabs";
@@ -103,6 +104,7 @@ const AnalyticsPage = observer((props: Props) => {
onClick={() => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!canPerformEmptyStateActions}
/>
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { Plus, Search } from "lucide-react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
import { cn, copyUrlToClipboard, orderJoinedProjects } from "@plane/utils";
@@ -122,6 +122,7 @@ export const ExtendedProjectSidebar = observer(() => {
<Tooltip tooltipHeading={t("create_project")} tooltipContent="">
<button
type="button"
data-ph-element={PROJECT_TRACKER_ELEMENTS.EXTENDED_SIDEBAR_ADD_BUTTON}
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => {
setIsProjectModalOpen(true);
@@ -4,13 +4,13 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// ui
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel, CYCLE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
import { CyclesViewHeader } from "@/components/cycles";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
@@ -23,7 +23,6 @@ export const CyclesListHeader: FC = observer(() => {
// store hooks
const { toggleCreateCycleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { currentProjectDetails, loader } = useProject();
const { t } = useTranslation();
@@ -51,8 +50,8 @@ export const CyclesListHeader: FC = observer(() => {
<Button
variant="primary"
size="sm"
data-ph-element={CYCLE_TRACKER_ELEMENTS.RIGHT_HEADER_ADD_BUTTON}
onClick={() => {
setTrackElement("Cycles page");
toggleCreateCycleModal(true);
}}
>
@@ -4,7 +4,7 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { EUserPermissionsLevel, EUserProjectRoles, CYCLE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TCycleFilters } from "@plane/types";
// components
@@ -16,7 +16,7 @@ import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
import { CycleModuleListLayout } from "@/components/ui";
// helpers
// hooks
import { useEventTracker, useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store";
import { useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
@@ -24,7 +24,6 @@ const ProjectCyclesPage = observer(() => {
// states
const [createModal, setCreateModal] = useState(false);
// store hooks
const { setTrackElement } = useEventTracker();
const { currentProjectCycleIds, loader } = useCycle();
const { getProjectById, currentProjectDetails } = useProject();
// router
@@ -100,8 +99,8 @@ const ProjectCyclesPage = observer(() => {
label={t("project_cycles.empty_state.general.primary_button.text")}
title={t("project_cycles.empty_state.general.primary_button.comic.title")}
description={t("project_cycles.empty_state.general.primary_button.comic.description")}
data-ph-element={CYCLE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON}
onClick={() => {
setTrackElement("Cycle empty state");
setCreateModal(true);
}}
disabled={!hasMemberLevelPermission}
@@ -3,14 +3,14 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel, MODULE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
import { ModuleViewHeader } from "@/components/modules";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
@@ -22,7 +22,6 @@ export const ModulesListHeader: React.FC = observer(() => {
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
const { toggleCreateModuleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { loader } = useProject();
@@ -55,8 +54,8 @@ export const ModulesListHeader: React.FC = observer(() => {
<Button
variant="primary"
size="sm"
data-ph-element={MODULE_TRACKER_ELEMENTS.RIGHT_HEADER_ADD_BUTTON}
onClick={() => {
setTrackElement("Modules page");
toggleCreateModuleModal(true);
}}
>
@@ -10,7 +10,8 @@ import {
EIssueFilterType,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_PAGE,
EIssueLayoutTypes
EIssueLayoutTypes,
GLOBAL_VIEW_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
@@ -228,7 +229,12 @@ export const GlobalIssuesHeader = observer(() => {
<></>
)}
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
<Button
variant="primary"
size="sm"
data-ph-element={GLOBAL_VIEW_TRACKER_ELEMENTS.RIGHT_HEADER_ADD_BUTTON}
onClick={() => setCreateViewModal(true)}
>
{t("workspace_views.add_view")}
</Button>
<div className="hidden md:block">
@@ -5,12 +5,17 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Search } from "lucide-react";
// types
import { EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import {
EUserPermissions,
EUserPermissionsLevel,
MEMBER_TRACKER_ELEMENTS,
MEMBER_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspaceBulkInviteFormData } from "@plane/types";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { cn, getUserRole } from "@plane/utils";
import { cn } from "@plane/utils";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { CountChip } from "@/components/common";
@@ -19,7 +24,8 @@ import { SettingsContentWrapper } from "@/components/settings";
import { WorkspaceMembersList } from "@/components/workspace";
// helpers
// hooks
import { useEventTracker, useMember, useUserPermissions, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useMember, useUserPermissions, useWorkspace } from "@/hooks/store";
// plane web components
import { BillingActionsButton } from "@/plane-web/components/workspace/billing";
import { SendWorkspaceInvitationModal } from "@/plane-web/components/workspace/members";
@@ -32,7 +38,6 @@ const WorkspaceMembersSettingsPage = observer(() => {
const { workspaceSlug } = useParams();
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { captureEvent } = useEventTracker();
const {
workspace: { workspaceMemberIds, inviteMembersToWorkspace },
} = useMember();
@@ -52,16 +57,11 @@ const WorkspaceMembersSettingsPage = observer(() => {
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
.then(() => {
setInviteModal(false);
captureEvent(MEMBER_TRACKER_EVENTS.invite, {
emails: [
...data.emails.map((email) => ({
email: email.email,
role: getUserRole(email.role as unknown as EUserPermissions),
})),
],
project_id: undefined,
state: "SUCCESS",
element: "Workspace settings member page",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
emails: [...data.emails.map((email) => email.email)],
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -70,16 +70,12 @@ const WorkspaceMembersSettingsPage = observer(() => {
});
})
.catch((err) => {
captureEvent(MEMBER_TRACKER_EVENTS.invite, {
emails: [
...data.emails.map((email) => ({
email: email.email,
role: getUserRole(email.role as unknown as EUserPermissions),
})),
],
project_id: undefined,
state: "FAILED",
element: "Workspace settings member page",
captureError({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
emails: [...data.emails.map((email) => email.email)],
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -129,7 +125,12 @@ const WorkspaceMembersSettingsPage = observer(() => {
/>
</div>
{canPerformWorkspaceAdminActions && (
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
<Button
variant="primary"
size="sm"
onClick={() => setInviteModal(true)}
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
>
{t("workspace_settings.settings.members.add_member")}
</Button>
)}
@@ -2,6 +2,7 @@
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/ui";
import { cn } from "@plane/utils";
import { useCommandPalette } from "@/hooks/store";
@@ -27,7 +28,11 @@ const ProjectSettingsPage = () => {
<Link href="https://plane.so/" target="_blank" className={cn(getButtonStyling("neutral-primary", "sm"))}>
Learn more about projects
</Link>
<Button size="sm" onClick={() => toggleCreateProjectModal(true)}>
<Button
size="sm"
onClick={() => toggleCreateProjectModal(true)}
data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON}
>
Start your first project
</Button>
</div>
@@ -9,14 +9,15 @@ import { Controller, useForm } from "react-hook-form";
// icons
import { CircleCheck } from "lucide-react";
// plane imports
import { AUTH_TRACKER_EVENTS } from "@plane/constants";
import { AUTH_TRACKER_ELEMENTS, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { cn, checkEmailValidity } from "@plane/utils";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker, useInstance } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useInstance } from "@/hooks/store";
import useTimer from "@/hooks/use-timer";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
@@ -45,8 +46,6 @@ const ForgotPasswordPage = observer(() => {
const email = searchParams.get("email");
// plane hooks
const { t } = useTranslation();
// store hooks
const { captureEvent } = useEventTracker();
const { config } = useInstance();
// hooks
const { resolvedTheme } = useTheme();
@@ -71,8 +70,11 @@ const ForgotPasswordPage = observer(() => {
email: formData.email,
})
.then(() => {
captureEvent(AUTH_TRACKER_EVENTS.forgot_password, {
state: "SUCCESS",
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -82,8 +84,11 @@ const ForgotPasswordPage = observer(() => {
setResendCodeTimer(30);
})
.catch((err) => {
captureEvent(AUTH_TRACKER_EVENTS.forgot_password, {
state: "FAILED",
captureError({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -120,7 +125,7 @@ const ForgotPasswordPage = observer(() => {
{t("auth.common.new_to_plane")}
<Link
href="/"
onClick={() => captureEvent(AUTH_TRACKER_EVENTS.navigate.sign_up, {})}
data-ph-element={AUTH_TRACKER_ELEMENTS.SIGNUP_FROM_FORGOT_PASSWORD}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.create_account")}
+17 -17
View File
@@ -9,19 +9,20 @@ import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr";
import { CheckCircle2 } from "lucide-react";
// plane imports
import { ROLE, EUserPermissions, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { ROLE, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import type { IWorkspaceMemberInvitation } from "@plane/types";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { truncateText, getUserRole } from "@plane/utils";
import { truncateText } from "@plane/utils";
// components
import { EmptyState } from "@/components/common";
import { WorkspaceLogo } from "@/components/workspace/logo";
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// helpers
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useEventTracker, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// services
@@ -43,7 +44,7 @@ const UserInvitationsPage = observer(() => {
const router = useAppRouter();
// store hooks
const { t } = useTranslation();
const { captureEvent, joinWorkspaceMetricGroup } = useEventTracker();
const { joinWorkspaceMetricGroup } = useEventTracker();
const { data: currentUser } = useUser();
const { updateUserProfile } = useUserProfile();
@@ -86,14 +87,11 @@ const UserInvitationsPage = observer(() => {
const invitation = invitations?.find((i) => i.id === firstInviteId);
const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace;
joinWorkspaceMetricGroup(redirectWorkspace?.id);
captureEvent(MEMBER_TRACKER_EVENTS.accept, {
member_id: invitation?.id,
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
role: getUserRole((invitation?.role as unknown as EUserPermissions)!),
project_id: undefined,
accepted_from: "App",
state: "SUCCESS",
element: "Workspace invitations page",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitation?.id,
},
});
updateUserProfile({ last_workspace_id: redirectWorkspace?.id })
.then(() => {
@@ -111,12 +109,13 @@ const UserInvitationsPage = observer(() => {
setIsJoiningWorkspaces(false);
});
})
.catch(() => {
captureEvent(MEMBER_TRACKER_EVENTS.accept, {
project_id: undefined,
accepted_from: "App",
state: "FAILED",
element: "Workspace invitations page",
.catch((err) => {
captureError({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitationsRespond?.[0],
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -194,6 +193,7 @@ const UserInvitationsPage = observer(() => {
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
loading={isJoiningWorkspaces}
data-ph-element={MEMBER_TRACKER_ELEMENTS.ACCEPT_INVITATION_BUTTON}
>
{t("accept_and_join")}
</Button>
+8 -6
View File
@@ -16,7 +16,8 @@ import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store";
import { captureSuccess } from "@/helpers/event-tracker.helper";
import { useUser, useWorkspace, useUserProfile } from "@/hooks/store";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers";
import { WorkspaceService } from "@/plane-web/services";
@@ -35,7 +36,6 @@ const OnboardingPage = observer(() => {
const [step, setStep] = useState<EOnboardingSteps | null>(null);
const [totalSteps, setTotalSteps] = useState<number | null>(null);
// store hooks
const { captureEvent } = useEventTracker();
const { isLoading: userLoader, data: user, updateCurrentUser } = useUser();
const { data: profile, updateUserProfile, finishUserOnboarding } = useUserProfile();
const { workspaces, fetchWorkspaces } = useWorkspace();
@@ -73,10 +73,12 @@ const OnboardingPage = observer(() => {
await finishUserOnboarding()
.then(() => {
captureEvent(USER_TRACKER_EVENTS.onboarding_complete, {
email: user.email,
user_id: user.id,
status: "SUCCESS",
captureSuccess({
eventName: USER_TRACKER_EVENTS.onboarding_complete,
payload: {
email: user.email,
user_id: user.id,
},
});
})
.catch(() => {
+2 -6
View File
@@ -6,14 +6,12 @@ import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components
import { AUTH_TRACKER_EVENTS } from "@plane/constants";
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { AuthRoot } from "@/components/account";
// constants
// helpers
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
// assets
import { AuthenticationWrapper } from "@/lib/wrappers";
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
@@ -26,8 +24,6 @@ export type AuthType = "sign-in" | "sign-up";
const SignInPage = observer(() => {
// plane hooks
const { t } = useTranslation();
// store hooks
const { captureEvent } = useEventTracker();
// hooks
const { resolvedTheme } = useTheme();
@@ -54,7 +50,7 @@ const SignInPage = observer(() => {
{t("auth.common.already_have_an_account")}
<Link
href="/"
onClick={() => captureEvent(AUTH_TRACKER_EVENTS.navigate.sign_in, {})}
data-ph-element={AUTH_TRACKER_ELEMENTS.SIGN_IN_FROM_SIGNUP}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.login")}
+3 -5
View File
@@ -6,7 +6,7 @@ import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components
import { AUTH_TRACKER_EVENTS } from "@plane/constants";
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { AuthRoot } from "@/components/account";
import { PageHead } from "@/components/core";
@@ -14,7 +14,7 @@ import { PageHead } from "@/components/core";
// helpers
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker, useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks/store";
// layouts
import DefaultLayout from "@/layouts/default-layout";
// wrappers
@@ -29,8 +29,6 @@ const HomePage = observer(() => {
const { resolvedTheme } = useTheme();
// plane hooks
const { t } = useTranslation();
// hooks
const { captureEvent } = useEventTracker();
// store
const { config } = useInstance();
// derived values
@@ -63,7 +61,7 @@ const HomePage = observer(() => {
{t("auth.common.new_to_plane")}
<Link
href="/sign-up"
onClick={() => captureEvent(AUTH_TRACKER_EVENTS.navigate.sign_up, {})}
data-ph-element={AUTH_TRACKER_ELEMENTS.NAVIGATE_TO_SIGN_UP}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.create_account")}
@@ -4,7 +4,10 @@ import { useState, useEffect, useRef } from "react";
import { observer } from "mobx-react";
import { LockKeyhole, LockKeyholeOpen } from "lucide-react";
// plane imports
import { PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
import { Tooltip } from "@plane/ui";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { usePageOperations } from "@/hooks/use-page-operations";
// store
@@ -78,6 +81,7 @@ export const PageLockControl = observer(({ page }: Props) => {
<button
type="button"
onClick={toggleLock}
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON}
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 transition-colors"
aria-label="Lock"
>
@@ -90,6 +94,7 @@ export const PageLockControl = observer(({ page }: Props) => {
<button
type="button"
onClick={toggleLock}
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON}
className="h-6 flex items-center gap-1 px-2 rounded text-custom-primary-100 bg-custom-primary-100/20 hover:bg-custom-primary-100/30 transition-colors"
aria-label="Locked"
>
+13 -8
View File
@@ -12,7 +12,8 @@ import ProjectCommonAttributes from "@/components/project/create/common-attribut
import ProjectCreateHeader from "@/components/project/create/header";
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
// hooks
import { useEventTracker, useProject } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web types
import { TProject } from "@/plane-web/types/projects";
@@ -32,7 +33,6 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
const { setToFavorite, workspaceSlug, data, onClose, handleNextStep, updateCoverImageStatus } = props;
// store
const { t } = useTranslation();
const { captureProjectEvent } = useEventTracker();
const { addProjectToFavorites, createProject } = useProject();
// states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
@@ -70,25 +70,30 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
if (coverImage) {
await updateCoverImageStatus(res.id, coverImage);
}
const newPayload = {
...res,
state: "SUCCESS",
};
captureProjectEvent({
captureSuccess({
eventName: PROJECT_TRACKER_EVENTS.create,
payload: newPayload,
payload: {
identifier: formData.identifier,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("success"),
message: t("project_created_successfully"),
});
if (setToFavorite) {
handleAddToFavorites(res.id);
}
handleNextStep(res.id);
})
.catch((err) => {
captureError({
eventName: PROJECT_TRACKER_EVENTS.create,
payload: {
identifier: formData.identifier,
},
});
if (err?.data.code === "PROJECT_NAME_ALREADY_EXIST") {
setToast({
type: TOAST_TYPE.ERROR,
@@ -48,7 +48,11 @@ export const DeleteWorkspaceSection: FC<TDeleteWorkspace> = observer((props) =>
{t("workspace_settings.settings.general.delete_workspace_description")}
</span>
<div>
<Button variant="danger" onClick={() => setDeleteWorkspaceModal(true)}>
<Button
variant="danger"
onClick={() => setDeleteWorkspaceModal(true)}
data-ph-element="WORKSPACE_SETTINGS_DELETE_WORKSPACE_BUTTON"
>
{t("workspace_settings.settings.general.delete_btn")}
</Button>
</div>
+23 -4
View File
@@ -1,6 +1,13 @@
// types
import {
CYCLE_TRACKER_ELEMENTS,
MODULE_TRACKER_ELEMENTS,
PROJECT_PAGE_TRACKER_ELEMENTS,
PROJECT_TRACKER_ELEMENTS,
} from "@plane/constants";
import { TCommandPaletteActionList, TCommandPaletteShortcut, TCommandPaletteShortcutList } from "@plane/types";
// store
import { captureClick } from "@/helpers/event-tracker.helper";
import { store } from "@/lib/store-context";
export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
@@ -22,7 +29,10 @@ export const getWorkspaceShortcutsList: () => TCommandPaletteActionList = () =>
p: {
title: "Create a new project",
description: "Create a new project in the current workspace",
action: () => toggleCreateProjectModal(true),
action: () => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON });
},
},
};
};
@@ -40,17 +50,26 @@ export const getProjectShortcutsList: () => TCommandPaletteActionList = () => {
d: {
title: "Create a new page",
description: "Create a new page in the current project",
action: () => toggleCreatePageModal({ isOpen: true }),
action: () => {
toggleCreatePageModal({ isOpen: true });
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON });
},
},
m: {
title: "Create a new module",
description: "Create a new module in the current project",
action: () => toggleCreateModuleModal(true),
action: () => {
toggleCreateModuleModal(true);
captureClick({ elementName: MODULE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
},
},
q: {
title: "Create a new cycle",
description: "Create a new cycle in the current project",
action: () => toggleCreateCycleModal(true),
action: () => {
toggleCreateCycleModal(true);
captureClick({ elementName: CYCLE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
},
},
v: {
title: "Create a new view",
@@ -6,7 +6,7 @@ import Link from "next/link";
// icons
import { Eye, EyeOff, Info, X, XCircle } from "lucide-react";
// plane imports
import { API_BASE_URL, E_PASSWORD_STRENGTH, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { API_BASE_URL, E_PASSWORD_STRENGTH, AUTH_TRACKER_EVENTS, AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, Spinner } from "@plane/ui";
import { getPasswordStrength } from "@plane/utils";
@@ -16,7 +16,7 @@ import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/accou
// helpers
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// services
import { AuthService } from "@/services/auth.service";
@@ -46,8 +46,6 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
const { email, isSMTPConfigured, handleAuthStep, handleEmailClear, mode, nextPath } = props;
// plane imports
const { t } = useTranslation();
// hooks
const { captureEvent } = useEventTracker();
// ref
const formRef = useRef<HTMLFormElement>(null);
// states
@@ -77,7 +75,6 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
const redirectToUniqueCodeSignIn = async () => {
handleAuthStep(EAuthSteps.UNIQUE_CODE);
captureEvent(AUTH_TRACKER_EVENTS.sign_in_with_code);
};
const passwordSupport =
@@ -85,7 +82,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
<div className="w-full">
{isSMTPConfigured ? (
<Link
onClick={() => captureEvent(AUTH_TRACKER_EVENTS.forgot_password)}
data-ph-element={AUTH_TRACKER_ELEMENTS.FORGOT_PASSWORD_FROM_SIGNIN}
href={`/accounts/forgot-password?email=${encodeURIComponent(email)}`}
className="text-xs font-medium text-custom-primary-100"
>
@@ -154,17 +151,32 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
: true;
if (isPasswordValid) {
setIsSubmitting(true);
captureEvent(
mode === EAuthModes.SIGN_IN
? AUTH_TRACKER_EVENTS.sign_in_with_password
: AUTH_TRACKER_EVENTS.sign_up_with_password
);
captureSuccess({
eventName:
mode === EAuthModes.SIGN_IN
? 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
} else {
setBannerMessage(true);
}
}}
onError={() => setIsSubmitting(false)}
onError={() => {
setIsSubmitting(false);
captureError({
eventName:
mode === EAuthModes.SIGN_IN
? AUTH_TRACKER_EVENTS.sign_in_with_password
: AUTH_TRACKER_EVENTS.sign_up_with_password,
payload: {
email: passwordFormData.email,
},
});
}}
>
<input type="hidden" name="csrfmiddlewaretoken" />
<input type="hidden" value={passwordFormData.email} name="email" />
@@ -292,6 +304,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
{isSMTPConfigured && (
<Button
type="button"
data-ph-element={AUTH_TRACKER_ELEMENTS.SIGN_IN_WITH_UNIQUE_CODE}
onClick={redirectToUniqueCodeSignIn}
variant="outline-primary"
className="w-full"
@@ -2,14 +2,14 @@
import React, { useEffect, useState } from "react";
import { CircleCheck, XCircle } from "lucide-react";
import { API_BASE_URL, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { API_BASE_URL, AUTH_TRACKER_ELEMENTS, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, Spinner } from "@plane/ui";
// constants
// helpers
import { EAuthModes } from "@/helpers/authentication.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import useTimer from "@/hooks/use-timer";
// services
import { AuthService } from "@/services/auth.service";
@@ -38,8 +38,6 @@ const defaultValues: TUniqueCodeFormValues = {
export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
const { mode, email, handleEmailClear, generateEmailUniqueCode, isExistingEmail, nextPath } = props;
// hooks
const { captureEvent } = useEventTracker();
// derived values
const defaultResetTimerValue = 5;
// states
@@ -62,10 +60,22 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
setResendCodeTimer(defaultResetTimerValue);
handleFormChange("code", uniqueCode?.code || "");
setIsRequestingNewCode(false);
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.new_code_requested,
payload: {
email: email,
},
});
} catch {
setResendCodeTimer(0);
console.error("Error while requesting new code");
setIsRequestingNewCode(false);
captureError({
eventName: AUTH_TRACKER_EVENTS.new_code_requested,
payload: {
email: email,
},
});
}
};
@@ -84,12 +94,23 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
onSubmit={() => {
setIsSubmitting(true);
captureEvent(AUTH_TRACKER_EVENTS.code_verify, {
state: "SUCCESS",
first_time: !isExistingEmail,
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.code_verify,
payload: {
state: "SUCCESS",
first_time: !isExistingEmail,
},
});
}}
onError={() => {
setIsSubmitting(false);
captureError({
eventName: AUTH_TRACKER_EVENTS.code_verify,
payload: {
state: "FAILED",
},
});
}}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<input type="hidden" value={uniqueCodeFormData.email} name="email" />
@@ -145,6 +166,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
</p>
<button
type="button"
data-ph-element={AUTH_TRACKER_ELEMENTS.REQUEST_NEW_CODE}
onClick={() => generateNewCode(uniqueCodeFormData.email)}
className={
isRequestNewCodeDisabled
@@ -163,7 +185,14 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
</div>
<div className="space-y-2.5">
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
<Button
type="submit"
variant="primary"
className="w-full"
size="lg"
disabled={isButtonDisabled}
data-ph-element={AUTH_TRACKER_ELEMENTS.VERIFY_CODE}
>
{isRequestingNewCode ? (
t("auth.common.unique_code.sending_code")
) : isSubmitting ? (
@@ -3,8 +3,10 @@
import { Command } from "cmdk";
import { ContrastIcon, FileText, Layers } from "lucide-react";
// hooks
import { CYCLE_TRACKER_ELEMENTS, MODULE_TRACKER_ELEMENTS, PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
import { DiceIcon } from "@plane/ui";
import { useCommandPalette, useEventTracker } from "@/hooks/store";
// hooks
import { useCommandPalette } from "@/hooks/store";
// ui
type Props = {
@@ -16,15 +18,14 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
// store hooks
const { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal } =
useCommandPalette();
const { setTrackElement } = useEventTracker();
return (
<>
<Command.Group heading="Cycle">
<Command.Item
data-ph-element={CYCLE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM}
onSelect={() => {
closePalette();
setTrackElement("Command palette");
toggleCreateCycleModal(true);
}}
className="focus:outline-none"
@@ -38,9 +39,9 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
</Command.Group>
<Command.Group heading="Module">
<Command.Item
data-ph-element={MODULE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM}
onSelect={() => {
closePalette();
setTrackElement("Command palette");
toggleCreateModuleModal(true);
}}
className="focus:outline-none"
@@ -56,7 +57,6 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
<Command.Item
onSelect={() => {
closePalette();
setTrackElement("Command palette");
toggleCreateViewModal(true);
}}
className="focus:outline-none"
@@ -70,9 +70,9 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
</Command.Group>
<Command.Group heading="Page">
<Command.Item
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON}
onSelect={() => {
closePalette();
setTrackElement("Command palette");
toggleCreatePageModal({ isOpen: true });
}}
className="focus:outline-none"
@@ -8,7 +8,12 @@ import useSWR from "swr";
import { CommandIcon, FolderPlus, Search, Settings, X } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_DEFAULT_SEARCH_RESULT } from "@plane/constants";
import {
EUserPermissions,
EUserPermissionsLevel,
PROJECT_TRACKER_ELEMENTS,
WORKSPACE_DEFAULT_SEARCH_RESULT,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspaceSearchResults } from "@plane/types";
import { LayersIcon, Loader, ToggleSwitch } from "@plane/ui";
@@ -28,6 +33,7 @@ import {
import { SimpleEmptyState } from "@/components/empty-state";
// helpers
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import {
useCommandPalette,
useEventTracker,
@@ -364,7 +370,7 @@ export const CommandModal: React.FC = observer(() => {
<Command.Item
onSelect={() => {
closePalette();
setTrackElement("Command palette");
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON });
toggleCreateProjectModal(true);
}}
className="focus:outline-none"
@@ -1,11 +1,17 @@
"use client";
import React, { FC, useEffect, useState } from "react";
import React, { FC, useEffect } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { ArrowRight, ChevronRight } from "lucide-react";
// Plane Imports
import { CYCLE_TRACKER_EVENTS, CYCLE_STATUS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
CYCLE_TRACKER_EVENTS,
CYCLE_STATUS,
EUserPermissions,
EUserPermissionsLevel,
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ICycle } from "@plane/types";
import { setToast, TOAST_TYPE } from "@plane/ui";
@@ -13,13 +19,11 @@ import { getDate, renderFormattedPayloadDate } from "@plane/utils";
// components
import { DateRangeDropdown } from "@/components/dropdowns";
// hooks
import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store";
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
import { useCycle, useUserPermissions } from "@/hooks/store";
import { useTimeZoneConverter } from "@/hooks/use-timezone-converter";
// services
import { CycleService } from "@/services/cycle.service";
// local imports
import { ArchiveCycleModal } from "../archived-cycles";
import { CycleDeleteModal } from "../delete-modal";
type Props = {
workspaceSlug: string;
@@ -38,13 +42,9 @@ const cycleService = new CycleService();
export const CycleSidebarHeader: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleDetails, handleClose, isArchived = false } = props;
// states
const [archiveCycleModal, setArchiveCycleModal] = useState(false);
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// hooks
const { allowPermissions } = useUserPermissions();
const { updateCycleDetails } = useCycle();
const { captureCycleEvent } = useEventTracker();
const { t } = useTranslation();
const { renderFormattedDateInUserTimezone, getProjectUTCOffset } = useTimeZoneConverter(projectId);
@@ -61,29 +61,36 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const submitChanges = async (data: Partial<ICycle>, changedProperty: string) => {
const submitChanges = async (data: Partial<ICycle>) => {
if (!workspaceSlug || !projectId || !cycleDetails.id) return;
await updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data)
.then((res) => {
captureCycleEvent({
eventName: CYCLE_TRACKER_EVENTS.update,
payload: {
...res,
changed_properties: [changedProperty],
element: "Right side-peek",
.then(() => {
captureElementAndEvent({
element: {
elementName: CYCLE_TRACKER_ELEMENTS.RIGHT_SIDEBAR,
},
event: {
eventName: CYCLE_TRACKER_EVENTS.update,
state: "SUCCESS",
payload: {
id: cycleDetails.id,
},
},
});
})
.catch(() => {
captureCycleEvent({
eventName: CYCLE_TRACKER_EVENTS.update,
payload: {
...data,
element: "Right side-peek",
state: "FAILED",
captureElementAndEvent({
element: {
elementName: CYCLE_TRACKER_ELEMENTS.RIGHT_SIDEBAR,
},
event: {
eventName: CYCLE_TRACKER_EVENTS.update,
state: "ERROR",
payload: {
id: cycleDetails.id,
},
},
});
});
@@ -122,7 +129,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
isDateValid = true;
}
if (isDateValid) {
submitChanges(payload, "date_range");
submitChanges(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("project_cycles.action.update.success.title"),
@@ -145,24 +152,6 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
return (
<>
{cycleDetails && workspaceSlug && projectId && (
<>
<ArchiveCycleModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleDetails.id}
isOpen={archiveCycleModal}
handleClose={() => setArchiveCycleModal(false)}
/>
<CycleDeleteModal
cycle={cycleDetails}
isOpen={cycleDeleteModal}
handleClose={() => setCycleDeleteModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
</>
)}
<div className="sticky z-10 top-0 pt-2 flex items-center justify-between bg-custom-sidebar-background-100">
<div className="flex items-center justify-center size-5">
<button
@@ -3,8 +3,10 @@
import { useState, Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
// ui
import { CYCLE_TRACKER_EVENTS } from "@plane/constants";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -42,16 +44,28 @@ export const ArchiveCycleModal: React.FC<Props> = (props) => {
title: "Archive success",
message: "Your archives can be found in project archives.",
});
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.archive,
payload: {
id: cycleId,
},
});
onClose();
router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
})
.catch(() =>
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Cycle could not be archived. Please try again.",
})
)
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.archive,
payload: {
id: cycleId,
},
});
})
.finally(() => setIsArchiving(false));
};
+13 -8
View File
@@ -9,9 +9,10 @@ import { useTranslation } from "@plane/i18n";
import { ICycle } from "@plane/types";
// ui
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useCycle } from "@/hooks/store";
import { useCycle } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
interface ICycleDelete {
@@ -27,7 +28,6 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
// states
const [loader, setLoader] = useState(false);
// store hooks
const { captureCycleEvent } = useEventTracker();
const { deleteCycle } = useCycle();
const { t } = useTranslation();
// router
@@ -49,9 +49,11 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
title: "Success!",
message: "Cycle deleted successfully.",
});
captureCycleEvent({
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.delete,
payload: { ...cycle, state: "SUCCESS" },
payload: {
id: cycle.id,
},
});
})
.catch((errors) => {
@@ -64,13 +66,16 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
type: TOAST_TYPE.ERROR,
message: currentError.i18n_message && t(currentError.i18n_message),
});
captureCycleEvent({
captureError({
eventName: CYCLE_TRACKER_EVENTS.delete,
payload: { ...cycle, state: "FAILED" },
payload: {
id: cycle.id,
},
error: errors,
});
})
.finally(() => handleClose());
} catch (error) {
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Warning!",
+3 -3
View File
@@ -16,7 +16,7 @@ import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns";
import { useUser } from "@/hooks/store/user/user-user";
type Props = {
handleFormSubmit: (values: Partial<ICycle>, dirtyFields: any) => Promise<void>;
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
handleClose: () => void;
status: boolean;
projectId: string;
@@ -40,7 +40,7 @@ export const CycleForm: React.FC<Props> = (props) => {
const { projectsWithCreatePermissions } = useUser();
// form data
const {
formState: { errors, isSubmitting, dirtyFields },
formState: { errors, isSubmitting },
handleSubmit,
control,
reset,
@@ -64,7 +64,7 @@ export const CycleForm: React.FC<Props> = (props) => {
}, [data, reset]);
return (
<form onSubmit={handleSubmit((formData) => handleFormSubmit(formData, dirtyFields))}>
<form onSubmit={handleSubmit((formData) => handleFormSubmit(formData))}>
<div className="space-y-5 p-5">
<div className="flex items-center gap-x-3">
{!status && (
@@ -6,7 +6,13 @@ import { useParams, usePathname, useSearchParams } from "next/navigation";
import { useForm } from "react-hook-form";
import { Eye, Users, ArrowRight, CalendarDays } from "lucide-react";
// types
import { CYCLE_TRACKER_EVENTS, EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants";
import {
CYCLE_TRACKER_EVENTS,
EUserPermissions,
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { ICycle, TCycleGroups } from "@plane/types";
@@ -19,7 +25,8 @@ import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { MergedDateDisplay } from "@/components/dropdowns/merged-date";
// hooks
import { useCycle, useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle, useMember, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { useTimeZoneConverter } from "@/hooks/use-timezone-converter";
@@ -57,7 +64,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
const pathname = usePathname();
// store hooks
const { addCycleToFavorites, removeCycleFromFavorites } = useCycle();
const { captureEvent } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// local storage
@@ -98,16 +104,25 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then(
() => {
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
captureEvent(CYCLE_TRACKER_EVENTS.favorite, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.favorite,
payload: {
id: cycleId,
},
});
}
);
})
.catch((error) => {
captureError({
eventName: CYCLE_TRACKER_EVENTS.favorite,
payload: {
id: cycleId,
},
error,
});
});
setPromiseToast(addToFavoritePromise, {
loading: t("project_cycles.action.favorite.loading"),
@@ -126,17 +141,24 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const removeFromFavoritePromise = removeCycleFromFavorites(
workspaceSlug?.toString(),
projectId.toString(),
cycleId
).then(() => {
captureEvent(CYCLE_TRACKER_EVENTS.unfavorite, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
const removeFromFavoritePromise = removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId)
.then(() => {
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.unfavorite,
payload: {
id: cycleId,
},
});
})
.catch((error) => {
captureError({
eventName: CYCLE_TRACKER_EVENTS.unfavorite,
payload: {
id: cycleId,
},
error,
});
});
});
setPromiseToast(removeFromFavoritePromise, {
loading: t("project_cycles.action.unfavorite.loading"),
@@ -292,6 +314,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
)}
{isEditingAllowed && !cycleDetails.archived_at && (
<FavoriteStar
data-ph-element={CYCLE_TRACKER_ELEMENTS.LIST_ITEM}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
+19 -16
View File
@@ -12,7 +12,8 @@ import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@p
import { CycleForm } from "@/components/cycles";
// constants
// hooks
import { useEventTracker, useCycle, useProject } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle, useProject } from "@/hooks/store";
import useKeypress from "@/hooks/use-keypress";
import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -35,7 +36,6 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
// states
const [activeProject, setActiveProject] = useState<string | null>(null);
// store hooks
const { captureCycleEvent } = useEventTracker();
const { workspaceProjectIds } = useProject();
const { createCycle, updateCycleDetails } = useCycle();
const { isMobile } = usePlatformOS();
@@ -63,9 +63,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
title: "Success!",
message: "Cycle created successfully.",
});
captureCycleEvent({
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.create,
payload: { ...res, state: "SUCCESS" },
payload: {
id: res.id,
},
});
})
.catch((err) => {
@@ -74,23 +76,24 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
title: "Error!",
message: err?.detail ?? "Error in creating cycle. Please try again.",
});
captureCycleEvent({
captureError({
eventName: CYCLE_TRACKER_EVENTS.create,
payload: { ...payload, state: "FAILED" },
error: err,
});
});
};
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>, dirtyFields: any) => {
const handleUpdateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project_id ?? projectId.toString();
await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload)
.then((res) => {
const changed_properties = Object.keys(dirtyFields);
captureCycleEvent({
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.update,
payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" },
payload: {
id: res.id,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -99,15 +102,15 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
});
})
.catch((err) => {
captureCycleEvent({
eventName: CYCLE_TRACKER_EVENTS.update,
payload: { ...payload, state: "FAILED" },
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.detail ?? "Error in updating cycle. Please try again.",
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.update,
error: err,
});
});
};
@@ -121,7 +124,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
return status;
};
const handleFormSubmit = async (formData: Partial<ICycle>, dirtyFields: any) => {
const handleFormSubmit = async (formData: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return;
const payload: Partial<ICycle> = {
@@ -145,7 +148,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
}
if (isDateValid) {
if (data) await handleUpdateCycle(data.id, payload, dirtyFields);
if (data) await handleUpdateCycle(data.id, payload);
else {
await handleCreateCycle(payload).then(() => {
setCycleTab("all");
+37 -9
View File
@@ -6,7 +6,12 @@ import { observer } from "mobx-react";
// icons
import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
// ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
CYCLE_TRACKER_EVENTS,
EUserPermissions,
EUserPermissionsLevel,
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils";
@@ -14,7 +19,8 @@ import { copyUrlToClipboard, cn } from "@plane/utils";
import { ArchiveCycleModal, CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles";
// helpers
// hooks
import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store";
import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useCycle, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useEndCycle, EndCycleModal } from "@/plane-web/components/cycles";
@@ -35,7 +41,6 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
const [archiveCycleModal, setArchiveCycleModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
// store hooks
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { getCycleById, restoreCycle } = useCycle();
const { t } = useTranslation();
@@ -69,7 +74,6 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
const handleOpenInNewTab = () => window.open(`/${cycleLink}`, "_blank");
const handleEditCycle = () => {
setTrackElement("Cycles page list layout");
setUpdateModal(true);
};
@@ -83,18 +87,29 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
title: t("project_cycles.action.restore.success.title"),
message: t("project_cycles.action.restore.success.description"),
});
captureSuccess({
eventName: CYCLE_TRACKER_EVENTS.restore,
payload: {
id: cycleId,
},
});
router.push(`/${workspaceSlug}/projects/${projectId}/archives/cycles`);
})
.catch(() =>
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("project_cycles.action.restore.failed.title"),
message: t("project_cycles.action.restore.failed.description"),
})
);
});
captureError({
eventName: CYCLE_TRACKER_EVENTS.restore,
payload: {
id: cycleId,
},
});
});
const handleDeleteCycle = () => {
setTrackElement("Cycles page list layout");
setDeleteModal(true);
};
@@ -149,6 +164,16 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
if (endCycleContextMenu) MENU_ITEMS.splice(3, 0, endCycleContextMenu);
const CONTEXT_MENU_ITEMS = MENU_ITEMS.map((item) => ({
...item,
action: () => {
captureClick({
elementName: CYCLE_TRACKER_ELEMENTS.CONTEXT_MENU,
});
item.action();
},
}));
return (
<>
{cycleDetails && (
@@ -187,7 +212,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
)}
</div>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu ellipsis placement="bottom-end" closeOnSelect maxHeight="lg" buttonClassName={customClassName}>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
@@ -197,6 +222,9 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({
elementName: CYCLE_TRACKER_ELEMENTS.QUICK_ACTIONS,
});
item.action();
}}
className={cn(
+11 -7
View File
@@ -1,18 +1,21 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import useSWR from "swr";
// plane imports
import { PRODUCT_TOUR_TRACKER_EVENTS } from "@plane/constants";
import { ContentWrapper } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { TourRoot } from "@/components/onboarding";
// constants
// helpers
import { captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useUserProfile, useEventTracker, useUser } from "@/hooks/store";
import { useUserProfile, useUser } from "@/hooks/store";
import { useHome } from "@/hooks/store/use-home";
import useSize from "@/hooks/use-window-size";
// plane web components
import { HomePeekOverviewsRoot } from "@/plane-web/components/home";
// local imports
import { DashboardWidgets } from "./home-dashboard-widgets";
import { UserGreetingsView } from "./user-greetings";
@@ -21,7 +24,6 @@ export const WorkspaceHomeView = observer(() => {
const { workspaceSlug } = useParams();
const { data: currentUser } = useUser();
const { data: currentUserProfile, updateTourCompleted } = useUserProfile();
const { captureEvent } = useEventTracker();
const { toggleWidgetSettings, fetchWidgets } = useHome();
const [windowWidth] = useSize();
@@ -38,9 +40,11 @@ export const WorkspaceHomeView = observer(() => {
const handleTourCompleted = () => {
updateTourCompleted()
.then(() => {
captureEvent(PRODUCT_TOUR_TRACKER_EVENTS.complete, {
user_id: currentUser?.id,
state: "SUCCESS",
captureSuccess({
eventName: PRODUCT_TOUR_TRACKER_EVENTS.complete,
payload: {
user_id: currentUser?.id,
},
});
})
.catch((error) => {
@@ -5,12 +5,13 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { Briefcase, Check, Hotel, Users, X } from "lucide-react";
// plane ui
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { cn, getFileURL } from "@plane/utils";
// helpers
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import {
useCommandPalette,
useEventTracker,
@@ -61,6 +62,7 @@ export const NoProjectsEmptyState = observer(() => {
e.stopPropagation();
setTrackElement("Sidebar");
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
},
disabled: !canCreateProject,
},
@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useFormContext, Controller } from "react-hook-form";
import { Plus } from "lucide-react";
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { IJiraImporterForm } from "@plane/types";
// hooks
// components
@@ -201,6 +202,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
<div>
<button
type="button"
data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON}
onClick={() => {
setTrackElement("Jira import detail page");
toggleCreateProjectModal(true);
@@ -1,11 +1,17 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EIssuesStoreType, EUserPermissionsLevel, EUserWorkspaceRoles } from "@plane/constants";
import {
EIssuesStoreType,
EUserPermissionsLevel,
EUserWorkspaceRoles,
PROJECT_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
@@ -47,6 +53,7 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
onClick={() => {
setTrackElement("All issues empty state");
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!hasMemberLevelPermission}
/>
@@ -13,7 +13,7 @@ import {
EViewAccess,
EUserPermissions,
EUserPermissionsLevel,
GLOBAL_VIEW_TOUR_TRACKER_EVENTS,
GLOBAL_VIEW_TRACKER_EVENTS,
} from "@plane/constants";
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
//ui
@@ -26,7 +26,8 @@ import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// constants
// helpers
// hooks
import { useEventTracker, useGlobalView, useIssues, useLabel, useUser, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useGlobalView, useIssues, useLabel, useUser, useUserPermissions } from "@/hooks/store";
import { getAreFiltersEqual } from "../../../utils";
type Props = {
@@ -44,7 +45,6 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
} = useIssues(EIssuesStoreType.GLOBAL);
const { workspaceLabels } = useLabel();
const { globalViewMap, updateGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker();
const { data } = useUser();
const { allowPermissions } = useUserPermissions();
@@ -112,15 +112,25 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
const handleUpdateView = () => {
if (!workspaceSlug || !globalViewId) return;
updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters).then((res) => {
if (res)
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.update, {
view_id: res.id,
applied_filters: res.filters,
state: "SUCCESS",
element: "Spreadsheet view",
updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters)
.then((res) => {
if (res)
captureSuccess({
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
payload: {
view_id: globalViewId,
},
});
})
.catch((error) => {
captureError({
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
payload: {
view_id: globalViewId,
},
error: error,
});
});
});
};
// add a placeholder object instead of appliedFilters if it is undefined
@@ -4,12 +4,13 @@ import { FC, Fragment } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { EDraftIssuePaginationType } from "@plane/constants";
import { EDraftIssuePaginationType, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { EUserPermissionsLevel, EUserWorkspaceRoles } from "@plane/constants/src/user";
import { useTranslation } from "@plane/i18n";
// components
import { cn } from "@plane/utils";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
import { captureClick } from "@/helpers/event-tracker.helper";
// constants
// helpers
@@ -76,6 +77,7 @@ export const WorkspaceDraftIssuesRoot: FC<TWorkspaceDraftIssuesRoot> = observer(
description={t("workspace_projects.empty_state.no_projects.primary_button.comic.description")}
onClick={() => {
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!hasMemberLevelPermission}
/>
@@ -12,6 +12,7 @@ import {
EUserPermissionsLevel,
EEstimateSystem,
MODULE_TRACKER_EVENTS,
MODULE_TRACKER_ELEMENTS,
} from "@plane/constants";
// plane types
import { useTranslation } from "@plane/i18n";
@@ -22,15 +23,10 @@ import { Loader, LayersIcon, CustomSelect, ModuleStatusIcon, TOAST_TYPE, setToas
// helpers
import { getDate, renderFormattedPayloadDate } from "@plane/utils";
import { DateRangeDropdown, MemberDropdown } from "@/components/dropdowns";
import {
ArchiveModuleModal,
DeleteModuleModal,
CreateUpdateModuleLinkModal,
ModuleAnalyticsProgress,
ModuleLinksList,
} from "@/components/modules";
import { CreateUpdateModuleLinkModal, ModuleAnalyticsProgress, ModuleLinksList } from "@/components/modules";
import { captureElementAndEvent, captureSuccess, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useModule, useEventTracker, useProjectEstimates, useUserPermissions } from "@/hooks/store";
import { useModule, useProjectEstimates, useUserPermissions } from "@/hooks/store";
// plane web constants
const defaultValues: Partial<IModule> = {
lead_id: "",
@@ -50,8 +46,6 @@ type Props = {
export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
const { moduleId, handleClose, isArchived } = props;
// states
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [archiveModuleModal, setArchiveModuleModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
// router
@@ -62,7 +56,6 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
const { allowPermissions } = useUserPermissions();
const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
const { captureModuleEvent, captureEvent } = useEventTracker();
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
// derived values
@@ -79,15 +72,22 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
if (!workspaceSlug || !projectId || !moduleId) return;
updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data)
.then((res) => {
captureModuleEvent({
eventName: MODULE_TRACKER_EVENTS.update,
payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" },
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.RIGHT_SIDEBAR,
},
event: {
eventName: MODULE_TRACKER_EVENTS.update,
payload: { id: res.id },
state: "SUCCESS",
},
});
})
.catch(() => {
captureModuleEvent({
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.update,
payload: { ...data, state: "FAILED" },
payload: { id: moduleId },
error,
});
});
};
@@ -97,12 +97,20 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
const payload = { metadata: {}, ...formData };
await createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload).then(() =>
captureEvent(MODULE_TRACKER_EVENTS.link.create, {
module_id: moduleId,
state: "SUCCESS",
})
);
await createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload)
.then(() =>
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.link.create,
payload: { id: moduleId },
})
)
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.link.create,
payload: { id: moduleId },
error,
});
});
};
const handleUpdateLink = async (formData: ModuleLink, linkId: string) => {
@@ -110,13 +118,20 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
const payload = { metadata: {}, ...formData };
await updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload).then(
() =>
captureEvent(MODULE_TRACKER_EVENTS.link.update, {
module_id: moduleId,
state: "SUCCESS",
await updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload)
.then(() =>
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.link.update,
payload: { id: moduleId },
})
);
)
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.link.update,
payload: { id: moduleId },
error,
});
});
};
const handleDeleteLink = async (linkId: string) => {
@@ -124,9 +139,9 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId)
.then(() => {
captureEvent(MODULE_TRACKER_EVENTS.link.delete, {
module_id: moduleId,
state: "SUCCESS",
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.link.delete,
payload: { id: moduleId },
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -140,6 +155,10 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
title: "Error!",
message: "Some error occurred",
});
captureError({
eventName: MODULE_TRACKER_EVENTS.link.delete,
payload: { id: moduleId },
});
});
};
@@ -213,16 +232,6 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
createLink={handleCreateLink}
updateLink={handleUpdateLink}
/>
{workspaceSlug && projectId && (
<ArchiveModuleModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
moduleId={moduleId}
isOpen={archiveModuleModal}
handleClose={() => setArchiveModuleModal(false)}
/>
)}
<DeleteModuleModal isOpen={moduleDeleteModal} onClose={() => setModuleDeleteModal(false)} data={moduleDetails} />
<>
<div
className={`sticky z-10 top-0 flex items-center justify-between bg-custom-sidebar-background-100 pb-5 pt-5`}
@@ -10,8 +10,10 @@ import type { IModule } from "@plane/types";
// ui
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// helpers
import { captureSuccess, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useModule } from "@/hooks/store";
import { useModule } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
type Props = {
@@ -28,7 +30,6 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
const router = useAppRouter();
const { workspaceSlug, projectId, moduleId, peekModule } = useParams();
// store hooks
const { captureModuleEvent } = useEventTracker();
const { deleteModule } = useModule();
const { t } = useTranslation();
@@ -51,9 +52,9 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
title: "Success!",
message: "Module deleted successfully.",
});
captureModuleEvent({
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.delete,
payload: { ...data, state: "SUCCESS" },
payload: { id: data.id },
});
})
.catch((errors) => {
@@ -66,9 +67,10 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
type: TOAST_TYPE.ERROR,
message: currentError.i18n_message && t(currentError.i18n_message),
});
captureModuleEvent({
captureError({
eventName: MODULE_TRACKER_EVENTS.delete,
payload: { ...data, state: "FAILED" },
payload: { id: data.id },
error: errors,
});
})
.finally(() => handleClose());
@@ -1,6 +1,7 @@
import { observer } from "mobx-react";
import { Copy, Pencil, Trash2 } from "lucide-react";
// plane types
import { MODULE_TRACKER_ELEMENTS } from "@plane/constants";
import { ILinkDetails } from "@plane/types";
// plane ui
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
@@ -58,6 +59,7 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
<button
type="button"
className="grid place-items-center p-1 hover:bg-custom-background-80"
data-ph-element={MODULE_TRACKER_ELEMENTS.LIST_ITEM}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -77,6 +79,7 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
<button
type="button"
className="grid place-items-center p-1 hover:bg-custom-background-80"
data-ph-element={MODULE_TRACKER_ELEMENTS.LIST_ITEM}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
+16 -13
View File
@@ -11,8 +11,10 @@ import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@p
// components
import { ModuleForm } from "@/components/modules";
// constants
// helpers
import { captureSuccess, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useModule, useProject } from "@/hooks/store";
import { useModule, useProject } from "@/hooks/store";
import useKeypress from "@/hooks/use-keypress";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -37,7 +39,6 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
// states
const [activeProject, setActiveProject] = useState<string | null>(null);
// store hooks
const { captureModuleEvent } = useEventTracker();
const { workspaceProjectIds } = useProject();
const { createModule, updateModuleDetails } = useModule();
const { isMobile } = usePlatformOS();
@@ -63,9 +64,9 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
title: "Success!",
message: "Module created successfully.",
});
captureModuleEvent({
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.create,
payload: { ...res, state: "SUCCESS" },
payload: { id: res.id },
});
})
.catch((err) => {
@@ -74,14 +75,15 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
title: "Error!",
message: err?.detail ?? err?.error ?? "Module could not be created. Please try again.",
});
captureModuleEvent({
captureError({
eventName: MODULE_TRACKER_EVENTS.create,
payload: { ...data, state: "FAILED" },
payload: { id: data?.id },
error: err,
});
});
};
const handleUpdateModule = async (payload: Partial<IModule>, dirtyFields: any) => {
const handleUpdateModule = async (payload: Partial<IModule>) => {
if (!workspaceSlug || !projectId || !data) return;
const selectedProjectId = payload.project_id ?? projectId.toString();
@@ -94,9 +96,9 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
title: "Success!",
message: "Module updated successfully.",
});
captureModuleEvent({
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.update,
payload: { ...res, changed_properties: Object.keys(dirtyFields || {}), state: "SUCCESS" },
payload: { id: res.id },
});
})
.catch((err) => {
@@ -105,21 +107,22 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
title: "Error!",
message: err?.detail ?? err?.error ?? "Module could not be updated. Please try again.",
});
captureModuleEvent({
captureError({
eventName: MODULE_TRACKER_EVENTS.update,
payload: { ...data, state: "FAILED" },
payload: { id: data.id },
error: err,
});
});
};
const handleFormSubmit = async (formData: Partial<IModule>, dirtyFields: unknown) => {
const handleFormSubmit = async (formData: Partial<IModule>) => {
if (!workspaceSlug || !projectId) return;
const payload: Partial<IModule> = {
...formData,
};
if (!data) await handleCreateModule(payload);
else await handleUpdateModule(payload, dirtyFields);
else await handleUpdateModule(payload);
};
useEffect(() => {
@@ -13,6 +13,7 @@ import {
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
MODULE_TRACKER_EVENTS,
MODULE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { IModule } from "@plane/types";
@@ -33,8 +34,9 @@ import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { ModuleQuickActions } from "@/components/modules";
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
// helpers
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useMember, useModule, useUserPermissions } from "@/hooks/store";
import { useMember, useModule, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web constants
@@ -56,7 +58,6 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const { allowPermissions } = useUserPermissions();
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule();
const { getUserDetails } = useMember();
const { captureEvent } = useEventTracker();
// local storage
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
@@ -79,10 +80,15 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then(
() => {
if (!storedValue) toggleFavoriteMenu(true);
captureEvent(MODULE_TRACKER_EVENTS.favorite, {
module_id: moduleId,
element: "Grid layout",
state: "SUCCESS",
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.CARD_ITEM,
},
event: {
eventName: MODULE_TRACKER_EVENTS.favorite,
payload: { id: moduleId },
state: "SUCCESS",
},
});
}
);
@@ -110,10 +116,15 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
projectId.toString(),
moduleId
).then(() => {
captureEvent(MODULE_TRACKER_EVENTS.unfavorite, {
module_id: moduleId,
element: "Grid layout",
state: "SUCCESS",
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.CARD_ITEM,
},
event: {
eventName: MODULE_TRACKER_EVENTS.unfavorite,
payload: { id: moduleId },
state: "SUCCESS",
},
});
});
@@ -12,6 +12,7 @@ import {
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
MODULE_TRACKER_EVENTS,
MODULE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
@@ -24,8 +25,10 @@ import { DateRangeDropdown } from "@/components/dropdowns";
import { ModuleQuickActions } from "@/components/modules";
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
// constants
// helpers
import { captureElementAndEvent, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useEventTracker, useMember, useModule, useUserPermissions } from "@/hooks/store";
import { useMember, useModule, useUserPermissions } from "@/hooks/store";
import { ButtonAvatars } from "../dropdowns/member/avatar";
type Props = {
@@ -42,7 +45,6 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
const { allowPermissions } = useUserPermissions();
const { addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule();
const { getUserDetails } = useMember();
const { captureEvent } = useEventTracker();
const { t } = useTranslation();
@@ -64,17 +66,28 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then(
() => {
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId)
.then(() => {
// open favorites menu if closed
if (!storedValue) toggleFavoriteMenu(true);
captureEvent(MODULE_TRACKER_EVENTS.favorite, {
module_id: moduleId,
element: "Grid layout",
state: "SUCCESS",
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.LIST_ITEM,
},
event: {
eventName: MODULE_TRACKER_EVENTS.favorite,
payload: { id: moduleId },
state: "SUCCESS",
},
});
}
);
})
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.favorite,
payload: { id: moduleId },
error,
});
});
setPromiseToast(addToFavoritePromise, {
loading: "Adding module to favorites...",
@@ -98,13 +111,26 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
workspaceSlug.toString(),
projectId.toString(),
moduleId
).then(() => {
captureEvent(MODULE_TRACKER_EVENTS.unfavorite, {
module_id: moduleId,
element: "Grid layout",
state: "SUCCESS",
)
.then(() => {
captureElementAndEvent({
element: {
elementName: MODULE_TRACKER_ELEMENTS.LIST_ITEM,
},
event: {
eventName: MODULE_TRACKER_EVENTS.unfavorite,
payload: { id: moduleId },
state: "SUCCESS",
},
});
})
.catch((error) => {
captureError({
eventName: MODULE_TRACKER_EVENTS.unfavorite,
payload: { id: moduleId },
error,
});
});
});
setPromiseToast(removeFromFavoritePromise, {
loading: "Removing module from favorites...",
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import Image from "next/image";
import { useParams, useSearchParams } from "next/navigation";
// components
import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
import { EUserPermissionsLevel, EUserProjectRoles, MODULE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ContentWrapper, Row, ERowVariant } from "@plane/ui";
import { ListLayout } from "@/components/core/list";
@@ -10,7 +10,7 @@ import { DetailedEmptyState, ComicBoxButton } from "@/components/empty-state";
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules";
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
// hooks
import { useCommandPalette, useEventTracker, useModule, useModuleFilter, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useModule, useModuleFilter, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import AllFiltersImage from "@/public/empty-state/module/all-filters.svg";
import NameFilterImage from "@/public/empty-state/module/name-filter.svg";
@@ -24,7 +24,6 @@ export const ModulesListView: React.FC = observer(() => {
const { t } = useTranslation();
// store hooks
const { toggleCreateModuleModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { getProjectModuleIds, getFilteredModuleIds, loader } = useModule();
const { currentProjectDisplayFilters: displayFilters, searchQuery } = useModuleFilter();
const { allowPermissions } = useUserPermissions();
@@ -59,8 +58,8 @@ export const ModulesListView: React.FC = observer(() => {
label={t("project_module.empty_state.general.primary_button.text")}
title={t("project_module.empty_state.general.primary_button.comic.title")}
description={t("project_module.empty_state.general.primary_button.comic.description")}
data-ph-element={MODULE_TRACKER_ELEMENTS.EMPTY_STATE_ADD_BUTTON}
onClick={() => {
setTrackElement("Module empty state");
toggleCreateModuleModal(true);
}}
disabled={!canPerformEmptyStateActions}
+34 -9
View File
@@ -6,7 +6,12 @@ import { observer } from "mobx-react";
// icons
import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
EUserPermissions,
EUserPermissionsLevel,
MODULE_TRACKER_ELEMENTS,
MODULE_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
@@ -14,8 +19,9 @@ import { copyUrlToClipboard, cn } from "@plane/utils";
// components
import { ArchiveModuleModal, CreateUpdateModuleModal, DeleteModuleModal } from "@/components/modules";
// helpers
import { captureClick, captureSuccess, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useModule, useEventTracker, useUserPermissions } from "@/hooks/store";
import { useModule, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
type Props = {
@@ -35,7 +41,6 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
const [archiveModuleModal, setArchiveModuleModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
// store hooks
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { getModuleById, restoreModule } = useModule();
@@ -67,7 +72,6 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
const handleOpenInNewTab = () => window.open(`/${moduleLink}`, "_blank");
const handleEditModule = () => {
setTrackElement("Modules page list layout");
setEditModal(true);
};
@@ -81,18 +85,26 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
title: "Restore success",
message: "Your module can be found in project modules.",
});
captureSuccess({
eventName: MODULE_TRACKER_EVENTS.restore,
payload: { id: moduleId },
});
router.push(`/${workspaceSlug}/projects/${projectId}/archives/modules`);
})
.catch(() =>
.catch((error) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Module could not be restored. Please try again.",
})
);
});
captureError({
eventName: MODULE_TRACKER_EVENTS.restore,
payload: { id: moduleId },
error,
});
});
const handleDeleteModule = () => {
setTrackElement("Modules page list layout");
setDeleteModal(true);
};
@@ -145,6 +157,16 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
},
];
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({
elementName: MODULE_TRACKER_ELEMENTS.CONTEXT_MENU,
});
item.action();
},
}));
return (
<>
{moduleDetails && (
@@ -166,7 +188,7 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
<DeleteModuleModal data={moduleDetails} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
</div>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
@@ -176,6 +198,9 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({
elementName: MODULE_TRACKER_ELEMENTS.QUICK_ACTIONS,
});
item.action();
}}
className={cn(
@@ -4,19 +4,15 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
// constants
import {
ONBOARDING_TRACKER_EVENTS,
ORGANIZATION_SIZE,
RESTRICTED_URLS,
WORKSPACE_TRACKER_EVENTS,
} from "@plane/constants";
import { ORGANIZATION_SIZE, RESTRICTED_URLS, WORKSPACE_TRACKER_EVENTS } from "@plane/constants";
// types
import { useTranslation } from "@plane/i18n";
import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
// ui
import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useEventTracker, useUserProfile, useUserSettings, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUserProfile, useUserSettings, useWorkspace } from "@/hooks/store";
// services
import { WorkspaceService } from "@/plane-web/services";
@@ -41,7 +37,6 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
const { updateUserProfile } = useUserProfile();
const { fetchCurrentUserSettings } = useUserSettings();
const { createWorkspace, fetchWorkspaces } = useWorkspace();
const { captureWorkspaceEvent } = useEventTracker();
// form info
const {
handleSubmit,
@@ -73,26 +68,18 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
title: t("workspace_creation.toast.success.title"),
message: t("workspace_creation.toast.success.message"),
});
captureWorkspaceEvent({
captureSuccess({
eventName: WORKSPACE_TRACKER_EVENTS.create,
payload: {
...workspaceResponse,
state: "SUCCESS",
first_time: true,
element: ONBOARDING_TRACKER_EVENTS.root,
},
payload: { slug: formData.slug },
});
await fetchWorkspaces();
await completeStep(workspaceResponse.id);
})
.catch(() => {
captureWorkspaceEvent({
captureError({
eventName: WORKSPACE_TRACKER_EVENTS.create,
payload: {
state: "FAILED",
first_time: true,
element: ONBOARDING_TRACKER_EVENTS.root,
},
payload: { slug: formData.slug },
error: new Error("Error creating workspace"),
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -290,7 +277,14 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
)}
</div>
</div>
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
<Button
data-ph-element="ONBOARDING_CREATE_WORKSPACE_BUTTON"
variant="primary"
type="submit"
size="lg"
className="w-full"
disabled={isButtonDisabled}
>
{isSubmitting ? <Spinner height="20px" width="20px" /> : t("workspace_creation.button.default")}
</Button>
</form>
+17 -19
View File
@@ -2,17 +2,18 @@
import React, { useState } from "react";
// plane imports
import { ROLE, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { ROLE, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
// types
import { IWorkspaceMemberInvitation } from "@plane/types";
// ui
import { Button, Checkbox, Spinner } from "@plane/ui";
import { truncateText, getUserRole } from "@plane/utils";
import { truncateText } from "@plane/utils";
// constants
// helpers
import { WorkspaceLogo } from "@/components/workspace/logo";
// hooks
import { useEventTracker, useUserSettings, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUserSettings, useWorkspace } from "@/hooks/store";
// services
import { WorkspaceService } from "@/plane-web/services";
@@ -29,7 +30,6 @@ export const Invitations: React.FC<Props> = (props) => {
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
// store hooks
const { captureEvent } = useEventTracker();
const { fetchWorkspaces } = useWorkspace();
const { fetchCurrentUserSettings } = useUserSettings();
@@ -50,26 +50,23 @@ export const Invitations: React.FC<Props> = (props) => {
try {
await workspaceService.joinWorkspaces({ invitations: invitationsRespond });
captureEvent(MEMBER_TRACKER_EVENTS.accept, {
member_id: invitation?.id,
role: getUserRole(invitation?.role as any),
project_id: undefined,
accepted_from: "App",
state: "SUCCESS",
element: "Workspace invitations page",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitation?.id,
},
});
await fetchWorkspaces();
await fetchCurrentUserSettings();
await handleNextStep();
} catch (error) {
} catch (error: any) {
console.error(error);
captureEvent(MEMBER_TRACKER_EVENTS.accept, {
member_id: invitation?.id,
role: getUserRole(invitation?.role as any),
project_id: undefined,
accepted_from: "App",
state: "FAILED",
element: "Workspace invitations page",
captureError({
eventName: MEMBER_TRACKER_EVENTS.accept,
payload: {
member_id: invitation?.id,
},
error: error,
});
setIsJoiningWorkspaces(false);
}
@@ -117,6 +114,7 @@ export const Invitations: React.FC<Props> = (props) => {
className="w-full"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || !invitationsRespond.length}
data-ph-element={MEMBER_TRACKER_ELEMENTS.ONBOARDING_JOIN_WORKSPACE}
>
{isJoiningWorkspaces ? <Spinner height="20px" width="20px" /> : "Continue to workspace"}
</Button>
@@ -20,7 +20,7 @@ import { usePopper } from "react-popper";
import { Check, ChevronDown, Plus, XCircle } from "lucide-react";
import { Listbox } from "@headlessui/react";
// plane imports
import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS } from "@plane/constants";
import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { IUser, IWorkspace } from "@plane/types";
@@ -28,9 +28,8 @@ import { IUser, IWorkspace } from "@plane/types";
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// helpers
import { getUserRole } from "@plane/utils";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// services
import { WorkspaceService } from "@/plane-web/services";
// assets
@@ -276,8 +275,6 @@ export const InviteMembers: React.FC<Props> = (props) => {
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
const { resolvedTheme } = useTheme();
// store hooks
const { captureEvent } = useEventTracker();
const {
control,
@@ -311,16 +308,11 @@ export const InviteMembers: React.FC<Props> = (props) => {
})),
})
.then(async () => {
captureEvent(MEMBER_TRACKER_EVENTS.invite, {
emails: [
...payload.emails.map((email) => ({
email: email.email,
role: getUserRole(email.role),
})),
],
project_id: undefined,
state: "SUCCESS",
element: "Onboarding",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
workspace: workspace.slug,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -331,10 +323,12 @@ export const InviteMembers: React.FC<Props> = (props) => {
await nextStep();
})
.catch((err) => {
captureEvent(MEMBER_TRACKER_EVENTS.invite, {
project_id: undefined,
state: "FAILED",
element: "Onboarding",
captureError({
eventName: MEMBER_TRACKER_EVENTS.invite,
payload: {
workspace: workspace.slug,
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -426,6 +420,7 @@ export const InviteMembers: React.FC<Props> = (props) => {
size="lg"
className="w-full"
disabled={isInvitationDisabled || !isValid || isSubmitting}
data-ph-element={MEMBER_TRACKER_ELEMENTS.ONBOARDING_INVITE_MEMBER}
>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
@@ -6,7 +6,7 @@ import Image from "next/image";
import { useTheme } from "next-themes";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react";
import { E_PASSWORD_STRENGTH, ONBOARDING_TRACKER_EVENTS, USER_TRACKER_EVENTS } from "@plane/constants";
import { E_PASSWORD_STRENGTH, ONBOARDING_TRACKER_ELEMENTS, USER_TRACKER_EVENTS } from "@plane/constants";
// types
import { useTranslation } from "@plane/i18n";
import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
@@ -20,7 +20,8 @@ import { OnboardingHeader, SwitchAccountDropdown } from "@/components/onboarding
// constants
// helpers
// hooks
import { useEventTracker, useUser, useUserProfile } from "@/hooks/store";
import { captureError, captureSuccess, captureView } from "@/helpers/event-tracker.helper";
import { useUser, useUserProfile } from "@/hooks/store";
// assets
import ProfileSetupDark from "@/public/onboarding/profile-setup-dark.webp";
import ProfileSetupLight from "@/public/onboarding/profile-setup-light.webp";
@@ -98,7 +99,6 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
// store hooks
const { updateCurrentUser } = useUser();
const { updateUserProfile } = useUserProfile();
const { captureEvent } = useEventTracker();
// form info
const {
getValues,
@@ -143,11 +143,12 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
updateUserProfile(profileUpdatePayload),
totalSteps > 2 && stepChange({ profile_complete: true }),
]);
captureEvent(USER_TRACKER_EVENTS.add_details, {
use_case: formData.use_case,
role: formData.role,
state: "SUCCESS",
element: ONBOARDING_TRACKER_EVENTS.step_1,
captureSuccess({
eventName: USER_TRACKER_EVENTS.add_details,
payload: {
use_case: formData.use_case,
role: formData.role,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -159,9 +160,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
finishOnboarding();
}
} catch {
captureEvent(USER_TRACKER_EVENTS.add_details, {
state: "FAILED",
element: ONBOARDING_TRACKER_EVENTS.step_1,
captureError({
eventName: USER_TRACKER_EVENTS.add_details,
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -183,9 +183,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
formData.password && handleSetPassword(formData.password),
]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION));
} catch {
captureEvent(USER_TRACKER_EVENTS.add_details, {
state: "FAILED",
element: ONBOARDING_TRACKER_EVENTS.step_1,
captureError({
eventName: USER_TRACKER_EVENTS.add_details,
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -205,11 +204,12 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
updateUserProfile(profileUpdatePayload),
totalSteps > 2 && stepChange({ profile_complete: true }),
]);
captureEvent(USER_TRACKER_EVENTS.add_details, {
use_case: formData.use_case,
role: formData.role,
state: "SUCCESS",
element: ONBOARDING_TRACKER_EVENTS.step_2,
captureSuccess({
eventName: USER_TRACKER_EVENTS.add_details,
payload: {
use_case: formData.use_case,
role: formData.role,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -221,9 +221,8 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
finishOnboarding();
}
} catch {
captureEvent(USER_TRACKER_EVENTS.add_details, {
state: "FAILED",
element: ONBOARDING_TRACKER_EVENTS.step_2,
captureError({
eventName: USER_TRACKER_EVENTS.add_details,
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -235,6 +234,9 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
const onSubmit = async (formData: TProfileSetupFormValues) => {
if (!user) return;
captureView({
elementName: ONBOARDING_TRACKER_ELEMENTS.PROFILE_SETUP_FORM,
});
if (profileSetupStep === EProfileSetupSteps.ALL) await handleSubmitProfileSetup(formData);
if (profileSetupStep === EProfileSetupSteps.USER_DETAILS) await handleSubmitUserDetail(formData);
if (profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION) await handleSubmitUserPersonalization(formData);
+12 -6
View File
@@ -5,13 +5,14 @@ import { observer } from "mobx-react";
import Image, { StaticImageData } from "next/image";
import { X } from "lucide-react";
// ui
import { PRODUCT_TOUR_TRACKER_EVENTS } from "@plane/constants";
import { PRODUCT_TOUR_TRACKER_ELEMENTS } from "@plane/constants";
import { Button } from "@plane/ui";
// components
import { TourSidebar } from "@/components/onboarding";
// constants
// hooks
import { useCommandPalette, useEventTracker, useUser } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useUser } from "@/hooks/store";
// assets
import CyclesTour from "@/public/onboarding/cycles.webp";
import IssuesTour from "@/public/onboarding/issues.webp";
@@ -85,7 +86,6 @@ export const TourRoot: React.FC<Props> = observer((props) => {
const [step, setStep] = useState<TTourSteps>("welcome");
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement, captureEvent } = useEventTracker();
const { data: currentUser } = useUser();
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
@@ -112,7 +112,9 @@ export const TourRoot: React.FC<Props> = observer((props) => {
<Button
variant="primary"
onClick={() => {
captureEvent(PRODUCT_TOUR_TRACKER_EVENTS.start);
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.START_BUTTON,
});
setStep("work-items");
}}
>
@@ -122,7 +124,9 @@ export const TourRoot: React.FC<Props> = observer((props) => {
type="button"
className="bg-transparent text-xs font-medium text-custom-primary-100 outline-custom-text-100"
onClick={() => {
captureEvent(PRODUCT_TOUR_TRACKER_EVENTS.skip);
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.SKIP_BUTTON,
});
onComplete();
}}
>
@@ -171,7 +175,9 @@ export const TourRoot: React.FC<Props> = observer((props) => {
<Button
variant="primary"
onClick={() => {
setTrackElement("Product tour");
captureClick({
elementName: PRODUCT_TOUR_TRACKER_ELEMENTS.CREATE_PROJECT_BUTTON,
});
onComplete();
toggleCreateProjectModal(true);
}}
@@ -16,7 +16,7 @@ import {
Trash2,
} from "lucide-react";
// constants
import { EPageAccess } from "@plane/constants";
import { EPageAccess, PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
// plane editor
import { EditorRefApi } from "@plane/editor";
// plane ui
@@ -26,6 +26,7 @@ import { cn } from "@plane/utils";
import { DeletePageModal } from "@/components/pages";
// helpers
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { usePageOperations } from "@/hooks/use-page-operations";
// plane web components
import { MovePageModal } from "@/plane-web/components/pages";
@@ -92,14 +93,24 @@ export const PageActions: React.FC<Props> = observer((props) => {
const menuItems: (TContextMenuItem & { key: TPageActions })[] = [
{
key: "toggle-lock",
action: pageOperations.toggleLock,
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.LOCK_BUTTON,
});
pageOperations.toggleLock();
},
title: is_locked ? "Unlock" : "Lock",
icon: is_locked ? LockKeyholeOpen : LockKeyhole,
shouldRender: canCurrentUserLockPage,
},
{
key: "toggle-access",
action: pageOperations.toggleAccess,
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.ACCESS_TOGGLE,
});
pageOperations.toggleAccess();
},
title: access === EPageAccess.PUBLIC ? "Make private" : "Make public",
icon: access === EPageAccess.PUBLIC ? Lock : Globe2,
shouldRender: canCurrentUserChangeAccess && !archived_at,
@@ -120,21 +131,36 @@ export const PageActions: React.FC<Props> = observer((props) => {
},
{
key: "make-a-copy",
action: pageOperations.duplicate,
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.DUPLICATE_BUTTON,
});
pageOperations.duplicate();
},
title: "Make a copy",
icon: Copy,
shouldRender: canCurrentUserDuplicatePage,
},
{
key: "archive-restore",
action: pageOperations.toggleArchive,
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.ARCHIVE_BUTTON,
});
pageOperations.toggleArchive();
},
title: archived_at ? "Restore" : "Archive",
icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon,
shouldRender: canCurrentUserArchivePage,
},
{
key: "delete",
action: () => setDeletePageModal(true),
action: () => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.CONTEXT_MENU,
});
setDeletePageModal(true);
},
title: "Delete",
icon: Trash2,
shouldRender: canCurrentUserDeletePage && !!archived_at,
@@ -1,6 +1,10 @@
import { observer } from "mobx-react";
// plane imports
// constants
import { PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
// ui
import { FavoriteStar } from "@plane/ui";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { usePageOperations } from "@/hooks/use-page-operations";
// store
@@ -23,7 +27,12 @@ export const PageFavoriteControl = observer(({ page }: Props) => {
return (
<FavoriteStar
selected={is_favorite}
onClick={pageOperations.toggleFavorite}
onClick={() => {
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.FAVORITE_BUTTON,
});
pageOperations.toggleFavorite();
}}
buttonClassName="flex-shrink-0 size-6 group rounded hover:bg-custom-background-80 transition-colors"
iconClassName="size-3.5 text-custom-text-200 group-hover:text-custom-text-10"
/>
@@ -3,12 +3,15 @@
import React, { FC } from "react";
import { observer } from "mobx-react";
import { Earth, Info, Lock, Minus } from "lucide-react";
// constants
import { PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
// ui
import { Avatar, FavoriteStar, Tooltip } from "@plane/ui";
import { renderFormattedDate, getFileURL } from "@plane/utils";
// components
import { PageActions } from "@/components/pages";
// helpers
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useMember } from "@/hooks/store";
import { usePageOperations } from "@/hooks/use-page-operations";
@@ -64,6 +67,9 @@ export const BlockItemAction: FC<Props> = observer((props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({
elementName: PROJECT_PAGE_TRACKER_ELEMENTS.FAVORITE_BUTTON,
});
pageOperations.toggleFavorite();
}}
selected={is_favorite}
@@ -7,7 +7,7 @@ import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// components
import { PageForm } from "@/components/pages";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureSuccess, captureError } from "@/helpers/event-tracker.helper";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web hooks
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
@@ -42,7 +42,6 @@ export const CreatePageModal: FC<Props> = (props) => {
const router = useAppRouter();
// store hooks
const { createPage } = usePageStore(storeType);
const { capturePageEvent } = useEventTracker();
const handlePageFormData = <T extends keyof TPage>(key: T, value: TPage[T]) =>
setPageFormData((prev) => ({ ...prev, [key]: value }));
@@ -62,22 +61,19 @@ export const CreatePageModal: FC<Props> = (props) => {
try {
const pageData = await createPage(pageFormData);
if (pageData) {
capturePageEvent({
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
payload: {
...pageData,
state: "SUCCESS",
id: pageData.id,
},
});
handleStateClear();
if (redirectionEnabled) router.push(`/${workspaceSlug}/projects/${projectId}/pages/${pageData.id}`);
}
} catch {
capturePageEvent({
} catch (error: any) {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
payload: {
state: "FAILED",
},
error,
});
}
};
@@ -8,7 +8,7 @@ import { PROJECT_PAGE_TRACKER_EVENTS } from "@plane/constants";
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// plane web hooks
import { useAppRouter } from "@/hooks/use-app-router";
import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store";
@@ -28,7 +28,6 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
const [isDeleting, setIsDeleting] = useState(false);
// store hooks
const { removePage } = usePageStore(storeType);
const { capturePageEvent } = useEventTracker();
if (!page || !page.id) return null;
// derived values
const { id: pageId, name } = page;
@@ -45,11 +44,10 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
setIsDeleting(true);
await removePage(pageId)
.then(() => {
capturePageEvent({
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.delete,
payload: {
...page,
state: "SUCCESS",
id: pageId,
},
});
handleClose();
@@ -64,11 +62,10 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
}
})
.catch(() => {
capturePageEvent({
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.delete,
payload: {
...page,
state: "FAILED",
id: pageId,
},
});
setToast({
@@ -1,12 +1,13 @@
import { observer } from "mobx-react";
import Image from "next/image";
// plane imports
import { EUserPermissionsLevel, EUserProjectRoles, EPageAccess } from "@plane/constants";
import { EUserPermissionsLevel, EUserProjectRoles, EPageAccess, PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TPageNavigationTabs } from "@plane/types";
// components
import { DetailedEmptyState } from "@/components/empty-state";
import { PageLoader } from "@/components/pages";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// plane web hooks
@@ -63,6 +64,7 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
text: t("project_page.empty_state.general.primary_button.text"),
onClick: () => {
toggleCreatePageModal({ isOpen: true });
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
},
disabled: !canPerformEmptyStateActions,
}}
@@ -79,6 +81,7 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
text: t("project_page.empty_state.public.primary_button.text"),
onClick: () => {
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC });
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
},
disabled: !canPerformEmptyStateActions,
}}
@@ -94,6 +97,7 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
text: t("project_page.empty_state.private.primary_button.text"),
onClick: () => {
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE });
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
},
disabled: !canPerformEmptyStateActions,
}}
@@ -2,13 +2,13 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { EventProps, STATE_TRACKER_EVENTS, STATE_GROUPS } from "@plane/constants";
import { STATE_TRACKER_EVENTS, STATE_GROUPS } from "@plane/constants";
import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { StateForm } from "@/components/project-states";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
type TStateCreate = {
groupKey: TStateGroups;
@@ -19,17 +19,10 @@ type TStateCreate = {
export const StateCreate: FC<TStateCreate> = observer((props) => {
const { groupKey, shouldTrackEvents, createStateCallback, handleClose } = props;
// hooks
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
// states
const [loader, setLoader] = useState(false);
const captureEventIfEnabled = (props: EventProps) => {
if (shouldTrackEvents) {
captureProjectStateEvent(props);
}
};
const onCancel = () => {
setLoader(false);
handleClose();
@@ -38,19 +31,16 @@ export const StateCreate: FC<TStateCreate> = observer((props) => {
const onSubmit = async (formData: Partial<IState>) => {
if (!groupKey) return { status: "error" };
if (shouldTrackEvents) {
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
}
try {
const stateResponse = await createStateCallback({ ...formData, group: groupKey });
captureEventIfEnabled({
eventName: STATE_TRACKER_EVENTS.create,
payload: {
...stateResponse,
state: "SUCCESS",
element: "Project settings states page",
},
});
const response = await createStateCallback({ ...formData, group: groupKey });
if (shouldTrackEvents)
captureSuccess({
eventName: STATE_TRACKER_EVENTS.create,
payload: {
state_group: groupKey,
id: response.id,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
@@ -60,14 +50,13 @@ export const StateCreate: FC<TStateCreate> = observer((props) => {
return { status: "success" };
} catch (error) {
const errorStatus = error as unknown as { status: number; data: { error: string } };
captureEventIfEnabled({
eventName: STATE_TRACKER_EVENTS.create,
payload: {
...formData,
state: "FAILED",
element: "Project settings states page",
},
});
if (shouldTrackEvents)
captureError({
eventName: STATE_TRACKER_EVENTS.create,
payload: {
state_group: groupKey,
},
});
if (errorStatus?.status === 400) {
setToast({
type: TOAST_TYPE.ERROR,
@@ -2,13 +2,13 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { EventProps, STATE_TRACKER_EVENTS } from "@plane/constants";
import { STATE_TRACKER_EVENTS } from "@plane/constants";
import { IState, TStateOperationsCallbacks } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { StateForm } from "@/components/project-states";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
type TStateUpdate = {
state: IState;
@@ -19,8 +19,6 @@ type TStateUpdate = {
export const StateUpdate: FC<TStateUpdate> = observer((props) => {
const { state, updateStateCallback, shouldTrackEvents, handleClose } = props;
// hooks
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
// states
const [loader, setLoader] = useState(false);
@@ -29,28 +27,20 @@ export const StateUpdate: FC<TStateUpdate> = observer((props) => {
handleClose();
};
const captureEventIfEnabled = (props: EventProps) => {
if (shouldTrackEvents) {
captureProjectStateEvent(props);
}
};
const onSubmit = async (formData: Partial<IState>) => {
if (!state.id) return { status: "error" };
if (shouldTrackEvents) {
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
}
try {
const stateResponse = await updateStateCallback(state.id, formData);
captureEventIfEnabled({
eventName: STATE_TRACKER_EVENTS.update,
payload: {
...stateResponse,
state: "SUCCESS",
element: "Project settings states page",
},
});
await updateStateCallback(state.id, formData);
if (shouldTrackEvents) {
captureSuccess({
eventName: STATE_TRACKER_EVENTS.update,
payload: {
state_group: state.group,
id: state.id,
},
});
}
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
@@ -73,14 +63,15 @@ export const StateUpdate: FC<TStateUpdate> = observer((props) => {
title: "Error!",
message: "State could not be updated. Please try again.",
});
captureEventIfEnabled({
eventName: STATE_TRACKER_EVENTS.update,
payload: {
...formData,
state: "FAILED",
element: "Project settings states page",
},
});
if (shouldTrackEvents) {
captureError({
eventName: STATE_TRACKER_EVENTS.update,
payload: {
state_group: state.group,
id: state.id,
},
});
}
return { status: "error" };
}
}
@@ -4,7 +4,7 @@ import { FC, useState, useRef } from "react";
import { observer } from "mobx-react";
import { ChevronDown, Plus } from "lucide-react";
// plane imports
import { EIconSize } from "@plane/constants";
import { EIconSize, STATE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
import { StateGroupIcon } from "@plane/ui";
@@ -81,6 +81,7 @@ export const GroupItem: FC<TGroupItem> = observer((props) => {
</div>
<button
type="button"
data-ph-element={STATE_TRACKER_ELEMENTS.STATE_GROUP_ADD_BUTTON}
className={cn(
"flex-shrink-0 w-6 h-6 rounded flex justify-center items-center overflow-hidden transition-colors hover:bg-custom-background-80 cursor-pointer text-custom-primary-100/80 hover:text-custom-primary-100",
(!isEditable || createState) && "cursor-not-allowed text-custom-text-400 hover:text-custom-text-400"
@@ -4,12 +4,12 @@ import { FC, useState } from "react";
import { observer } from "mobx-react";
import { Loader, X } from "lucide-react";
// plane imports
import { EventProps, STATE_TRACKER_EVENTS } from "@plane/constants";
import { STATE_TRACKER_EVENTS, STATE_TRACKER_ELEMENTS } from "@plane/constants";
import { IState, TStateOperationsCallbacks } from "@plane/types";
import { AlertModalCore, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TStateDelete = {
@@ -23,45 +23,39 @@ export const StateDelete: FC<TStateDelete> = observer((props) => {
const { totalStates, state, deleteStateCallback, shouldTrackEvents } = props;
// hooks
const { isMobile } = usePlatformOS();
const { captureProjectStateEvent, setTrackElement } = useEventTracker();
// states
const [isDeleteModal, setIsDeleteModal] = useState(false);
const [isDelete, setIsDelete] = useState(false);
// derived values
const isDeleteDisabled = state.default ? true : totalStates === 1 ? true : false;
const captureEventIfEnabled = (props: EventProps) => {
if (shouldTrackEvents) {
captureProjectStateEvent(props);
}
};
const handleDeleteState = async () => {
if (isDeleteDisabled) return;
if (shouldTrackEvents) {
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
}
setIsDelete(true);
try {
await deleteStateCallback(state.id);
captureEventIfEnabled({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
...state,
state: "SUCCESS",
},
});
if (shouldTrackEvents) {
captureSuccess({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
id: state.id,
},
});
}
setIsDelete(false);
} catch (error) {
const errorStatus = error as unknown as { status: number; data: { error: string } };
captureEventIfEnabled({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
...state,
state: "FAILED",
},
});
if (shouldTrackEvents) {
captureError({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
id: state.id,
},
});
}
if (errorStatus.status === 400) {
setToast({
type: TOAST_TYPE.ERROR,
@@ -107,6 +101,7 @@ export const StateDelete: FC<TStateDelete> = observer((props) => {
)}
disabled={isDeleteDisabled}
onClick={() => setIsDeleteModal(true)}
data-ph-element={STATE_TRACKER_ELEMENTS.STATE_LIST_DELETE_BUTTON}
>
<Tooltip
tooltipContent={
@@ -10,7 +10,8 @@ import type { IState } from "@plane/types";
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// hooks
import { useEventTracker, useProjectState } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProjectState } from "@/hooks/store";
type TStateDeleteModal = {
isOpen: boolean;
@@ -24,8 +25,6 @@ export const StateDeleteModal: React.FC<TStateDeleteModal> = observer((props) =>
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// router
const { workspaceSlug } = useParams();
// store hooks
const { captureProjectStateEvent } = useEventTracker();
const { deleteState } = useProjectState();
const handleClose = () => {
@@ -40,11 +39,10 @@ export const StateDeleteModal: React.FC<TStateDeleteModal> = observer((props) =>
await deleteState(workspaceSlug.toString(), data.project_id, data.id)
.then(() => {
captureProjectStateEvent({
captureSuccess({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
...data,
state: "SUCCESS",
id: data.id,
},
});
handleClose();
@@ -63,11 +61,10 @@ export const StateDeleteModal: React.FC<TStateDeleteModal> = observer((props) =>
title: "Error!",
message: "State could not be deleted. Please try again.",
});
captureProjectStateEvent({
captureError({
eventName: STATE_TRACKER_EVENTS.delete,
payload: {
...data,
state: "FAILED",
id: data.id,
},
});
})
@@ -2,7 +2,7 @@ import { SetStateAction } from "react";
import { observer } from "mobx-react";
import { GripVertical, Pencil } from "lucide-react";
// plane imports
import { EIconSize } from "@plane/constants";
import { EIconSize, STATE_TRACKER_ELEMENTS } from "@plane/constants";
import { IState, TStateOperationsCallbacks } from "@plane/types";
import { StateGroupIcon } from "@plane/ui";
// local imports
@@ -70,6 +70,7 @@ export const StateItemTitle = observer((props: TStateItemTitleProps) => {
<button
className="flex-shrink-0 w-5 h-5 rounded flex justify-center items-center overflow-hidden transition-colors hover:bg-custom-background-80 cursor-pointer text-custom-text-200 hover:text-custom-text-100"
onClick={() => setUpdateStateModal(true)}
data-ph-element={STATE_TRACKER_ELEMENTS.STATE_LIST_EDIT_BUTTON}
>
<Pencil className="w-3 h-3" />
</button>
+4 -4
View File
@@ -1,15 +1,16 @@
import { observer } from "mobx-react";
import Image from "next/image";
// plane imports
import { EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
import { EUserPermissionsLevel, EUserPermissions, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ContentWrapper } from "@plane/ui";
// components
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
import { ProjectCard } from "@/components/project";
import { ProjectsLoader } from "@/components/ui";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette, useEventTracker, useProject, useProjectFilter, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useProject, useProjectFilter, useUserPermissions } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
// assets
import AllFiltersImage from "@/public/empty-state/project/all-filters.svg";
@@ -26,7 +27,6 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => {
const { t } = useTranslation();
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const {
loader,
fetchStatus,
@@ -65,8 +65,8 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => {
title={t("workspace_projects.empty_state.general.primary_button.comic.title")}
description={t("workspace_projects.empty_state.general.primary_button.comic.description")}
onClick={() => {
setTrackElement("Project empty state");
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!canPerformEmptyStateActions}
/>
@@ -12,7 +12,8 @@ import type { IProject } from "@plane/types";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// hooks
import { useEventTracker, useProject } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProject } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
type DeleteProjectModal = {
@@ -29,7 +30,6 @@ const defaultValues = {
export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
const { isOpen, project, onClose } = props;
// store hooks
const { captureProjectEvent } = useEventTracker();
const { deleteProject } = useProject();
// router
const router = useAppRouter();
@@ -62,9 +62,11 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
if (projectId && projectId.toString() === project.id) router.push(`/${workspaceSlug}/projects`);
handleClose();
captureProjectEvent({
captureSuccess({
eventName: PROJECT_TRACKER_EVENTS.delete,
payload: { ...project, state: "SUCCESS", element: "Project general settings" },
payload: {
id: project.id,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -73,9 +75,11 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
});
})
.catch(() => {
captureProjectEvent({
captureError({
eventName: PROJECT_TRACKER_EVENTS.delete,
payload: { ...project, state: "FAILED", element: "Project general settings" },
payload: {
id: project.id,
},
});
setToast({
type: TOAST_TYPE.ERROR,
+17 -14
View File
@@ -3,7 +3,7 @@
import { FC, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Info, Lock } from "lucide-react";
import { NETWORK_CHOICES, PROJECT_TRACKER_EVENTS } from "@plane/constants";
import { NETWORK_CHOICES, PROJECT_TRACKER_ELEMENTS, PROJECT_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// plane types
import { IProject, IWorkspace } from "@plane/types";
@@ -27,7 +27,8 @@ import { TimezoneSelect } from "@/components/global";
import { ProjectNetworkIcon } from "@/components/project";
// helpers
// 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";
// services
import { ProjectService } from "@/services/project";
@@ -46,7 +47,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// store hooks
const { captureProjectEvent } = useEventTracker();
const { updateProject } = useProject();
const { isMobile } = usePlatformOS();
@@ -58,7 +58,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
setValue,
setError,
reset,
formState: { errors, dirtyFields },
formState: { errors },
getValues,
} = useForm<IProject>({
defaultValues: {
@@ -92,15 +92,10 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
if (!workspaceSlug || !project) return;
return updateProject(workspaceSlug.toString(), project.id, payload)
.then((res) => {
const changed_properties = Object.keys(dirtyFields);
captureProjectEvent({
captureSuccess({
eventName: PROJECT_TRACKER_EVENTS.update,
payload: {
...res,
changed_properties: changed_properties,
state: "SUCCESS",
element: "Project general settings",
id: projectId,
},
});
setToast({
@@ -110,9 +105,11 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
});
})
.catch((error) => {
captureProjectEvent({
captureError({
eventName: PROJECT_TRACKER_EVENTS.update,
payload: { ...payload, state: "FAILED", element: "Project general settings" },
payload: {
id: projectId,
},
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -397,7 +394,13 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
</div>
<div className="flex items-center justify-between py-2">
<>
<Button variant="primary" type="submit" loading={isLoading} disabled={!isAdmin}>
<Button
data-ph-element={PROJECT_TRACKER_ELEMENTS.UPDATE_PROJECT_BUTTON}
variant="primary"
type="submit"
loading={isLoading}
disabled={!isAdmin}
>
{isLoading ? `${t("updating")}...` : t("common.update_project")}
</Button>
<span className="text-sm italic text-custom-sidebar-text-400">
+4 -4
View File
@@ -4,14 +4,15 @@ import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Briefcase } from "lucide-react";
// i18n
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useUserPermissions } from "@/hooks/store";
// plane web constants
// components
import HeaderFilters from "./filters";
@@ -22,7 +23,6 @@ export const ProjectsBaseHeader = observer(() => {
const { t } = useTranslation();
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const pathname = usePathname();
@@ -57,9 +57,9 @@ export const ProjectsBaseHeader = observer(() => {
<Button
size="sm"
onClick={() => {
setTrackElement("Projects page");
toggleCreateProjectModal(true);
}}
data-ph-element={PROJECT_TRACKER_ELEMENTS.CREATE_HEADER_BUTTON}
className="items-center gap-1"
>
<span className="hidden sm:inline-block">{t("workspace_projects.create.label")}</span>
@@ -14,7 +14,8 @@ import { IProject } from "@plane/types";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// hooks
import { useEventTracker, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
type FormData = {
@@ -39,7 +40,6 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
const router = useAppRouter();
const { workspaceSlug } = useParams();
// store hooks
const { captureEvent } = useEventTracker();
const { leaveProject } = useUserPermissions();
const {
@@ -64,21 +64,26 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
return leaveProject(workspaceSlug.toString(), project.id)
.then(() => {
handleClose();
captureEvent(MEMBER_TRACKER_EVENTS.project.leave, {
state: "SUCCESS",
element: "Project settings members page",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.project.leave,
payload: {
project: project.id,
},
});
})
.catch(() => {
.catch((err) => {
captureError({
eventName: MEMBER_TRACKER_EVENTS.project.leave,
payload: {
project: project.id,
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Something went wrong please try again later.",
});
captureEvent(MEMBER_TRACKER_EVENTS.project.leave, {
state: "FAILED",
element: "Project settings members page",
});
});
} else {
setToast({
@@ -7,7 +7,8 @@ import { TOAST_TYPE, Table, setToast } from "@plane/ui";
// components
import { ConfirmProjectMemberRemove } from "@/components/project";
// hooks
import { useEventTracker, useMember, useUser, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useMember, useUser, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// plane web imports
import { useProjectColumns } from "@/plane-web/components/projects/settings/useProjectColumns";
@@ -30,7 +31,6 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
const {
project: { removeMemberFromProject },
} = useMember();
const { captureEvent } = useEventTracker();
// helper hooks
const { columns, removeMemberModal, setRemoveMemberModal } = useProjectColumns({
projectId,
@@ -44,18 +44,27 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
await leaveProject(workspaceSlug.toString(), projectId.toString())
.then(async () => {
router.push(`/${workspaceSlug}/projects`);
captureEvent(MEMBER_TRACKER_EVENTS.project.leave, {
state: "SUCCESS",
element: "Project settings members page",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.project.leave,
payload: {
project: projectId,
},
});
})
.catch((err) =>
.catch((err) => {
captureError({
eventName: MEMBER_TRACKER_EVENTS.project.leave,
payload: {
project: projectId,
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "You cant leave this project yet.",
message: err?.error || "Something went wrong. Please try again.",
})
);
});
});
} else
await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), memberId).catch((err) =>
setToast({
+3 -5
View File
@@ -4,14 +4,14 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { Search } from "lucide-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/ui";
// components
import { ProjectMemberListItem, SendProjectInvitationModal } from "@/components/project";
import { MembersSettingsLoader } from "@/components/ui";
// hooks
import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
import { useMember, useUserPermissions } from "@/hooks/store";
type TProjectMemberListProps = {
projectId: string;
@@ -23,8 +23,6 @@ export const ProjectMemberList: React.FC<TProjectMemberListProps> = observer((pr
// states
const [inviteModal, setInviteModal] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
// store hooks
const { setTrackElement } = useEventTracker();
const {
project: { projectMemberIds, getProjectMemberDetails },
} = useMember();
@@ -73,9 +71,9 @@ export const ProjectMemberList: React.FC<TProjectMemberListProps> = observer((pr
variant="primary"
size="sm"
onClick={() => {
setTrackElement("PROJECT_SETTINGS_MEMBERS_PAGE_HEADER");
setInviteModal(true);
}}
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
>
{t("add_member")}
</Button>
@@ -12,7 +12,8 @@ import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast
// helpers
import { getFileURL } from "@plane/utils";
// hooks
import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useMember, useUserPermissions } from "@/hooks/store";
type Props = {
isOpen: boolean;
@@ -45,7 +46,6 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
// plane hooks
const { t } = useTranslation();
// store hooks
const { captureEvent } = useEventTracker();
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
const {
project: { getProjectMemberDetails, bulkAddMembersToProject },
@@ -86,22 +86,22 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
type: TOAST_TYPE.SUCCESS,
message: "Members added successfully.",
});
captureEvent(MEMBER_TRACKER_EVENTS.project.add, {
members: [
...payload.members.map((member) => ({
member_id: member.member_id,
role: ROLE[member.role],
})),
],
state: "SUCCESS",
element: "Project settings members page",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.project.add,
payload: {
members: [...payload.members.map((member) => member.member_id)],
},
});
})
.catch((error) => {
console.error(error);
captureEvent(MEMBER_TRACKER_EVENTS.project.add, {
state: "FAILED",
element: "Project settings members page",
captureError({
eventName: MEMBER_TRACKER_EVENTS.project.add,
payload: {
members: [...payload.members.map((member) => member.member_id)],
},
error: error,
});
})
.finally(() => {
@@ -4,6 +4,7 @@ import React from "react";
import { ChevronRight, ChevronUp } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// types
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { IProject } from "@plane/types";
// ui
import { Button, Loader } from "@plane/ui";
@@ -43,7 +44,11 @@ export const DeleteProjectSection: React.FC<IDeleteProjectSection> = (props) =>
<div>
{projectDetails ? (
<div>
<Button variant="danger" onClick={handleDelete}>
<Button
variant="danger"
onClick={handleDelete}
data-ph-element={PROJECT_TRACKER_ELEMENTS.DELETE_PROJECT_BUTTON}
>
Delete my project
</Button>
</div>
@@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
import { CircleMinus } from "lucide-react";
import { Disclosure } from "@headlessui/react";
// plane imports
import { ROLE, EUserPermissions, EUserProjectRoles } from "@plane/constants";
import { ROLE, EUserPermissions, EUserProjectRoles, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { IUser, IWorkspaceMember, TProjectMembership } from "@plane/types";
import { CustomMenu, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
import { getFileURL } from "@plane/utils";
@@ -70,6 +70,7 @@ export const NameColumn: React.FC<NameProps> = (props) => {
<CustomMenu.MenuItem>
<div
className="flex items-center gap-x-1 cursor-pointer text-red-600 font-medium"
data-ph-element={MEMBER_TRACKER_ELEMENTS.PROJECT_MEMBER_TABLE_CONTEXT_MENU}
onClick={() => setRemoveMemberModal(rowData)}
>
<CircleMinus className="flex-shrink-0 size-3.5" />
@@ -1,4 +1,5 @@
import { SetStateAction, useEffect, useState } from "react";
import { GLOBAL_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
import { Button } from "@plane/ui";
import { LockedComponent } from "../icons/locked-component";
@@ -58,7 +59,13 @@ export const UpdateViewComponent = (props: Props) => {
!areFiltersEqual &&
isAuthorizedUser && (
<>
<Button variant="outline-primary" size="md" className="flex-shrink-0" onClick={() => setIsModalOpen(true)}>
<Button
variant="outline-primary"
size="md"
className="flex-shrink-0"
data-ph-element={GLOBAL_VIEW_TRACKER_ELEMENTS.HEADER_SAVE_VIEW_BUTTON}
onClick={() => setIsModalOpen(true)}
>
Save as
</Button>
{isOwner && <>{updateButton}</>}
@@ -2,14 +2,20 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { CheckCheck, RefreshCw } from "lucide-react";
// plane imports
import { ENotificationLoader, ENotificationQueryParamType, NOTIFICATION_TRACKER_EVENTS } from "@plane/constants";
import {
ENotificationLoader,
ENotificationQueryParamType,
NOTIFICATION_TRACKER_ELEMENTS,
NOTIFICATION_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Spinner, Tooltip } from "@plane/ui";
// components
import { NotificationFilter, NotificationHeaderMenuOption } from "@/components/workspace-notifications";
// constants
// hooks
import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
import { captureSuccess } from "@/helpers/event-tracker.helper";
import { useWorkspaceNotifications } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TNotificationSidebarHeaderOptions = {
@@ -21,7 +27,6 @@ export const NotificationSidebarHeaderOptions: FC<TNotificationSidebarHeaderOpti
// hooks
const { isMobile } = usePlatformOS();
const { loader, getNotifications, markAllNotificationsAsRead } = useWorkspaceNotifications();
const { captureEvent } = useEventTracker();
const { t } = useTranslation();
const refreshNotifications = async () => {
@@ -49,8 +54,11 @@ export const NotificationSidebarHeaderOptions: FC<TNotificationSidebarHeaderOpti
<Tooltip tooltipContent={t("notification.options.mark_all_as_read")} isMobile={isMobile} position="bottom">
<div
className="flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm"
data-ph-element={NOTIFICATION_TRACKER_ELEMENTS.MARK_ALL_AS_READ_BUTTON}
onClick={() => {
captureEvent(NOTIFICATION_TRACKER_EVENTS.all_marked_read);
captureSuccess({
eventName: NOTIFICATION_TRACKER_EVENTS.all_marked_read,
});
handleMarkAllNotificationsAsRead();
}}
>
@@ -3,14 +3,15 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { ArchiveRestore } from "lucide-react";
import { NOTIFICATION_TRACKER_EVENTS } from "@plane/constants";
import { NOTIFICATION_TRACKER_ELEMENTS, NOTIFICATION_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ArchiveIcon, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { NotificationItemOptionButton } from "@/components/workspace-notifications";
// constants
// hooks
import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useWorkspaceNotifications } from "@/hooks/store";
// store
import { INotification } from "@/store/notifications/notification";
@@ -22,7 +23,6 @@ type TNotificationItemArchiveOption = {
export const NotificationItemArchiveOption: FC<TNotificationItemArchiveOption> = observer((props) => {
const { workspaceSlug, notification } = props;
// hooks
const { captureEvent } = useEventTracker();
const { currentNotificationTab } = useWorkspaceNotifications();
const { asJson: data, archiveNotification, unArchiveNotification } = notification;
const { t } = useTranslation();
@@ -31,10 +31,12 @@ export const NotificationItemArchiveOption: FC<TNotificationItemArchiveOption> =
try {
const request = data.archived_at ? unArchiveNotification : archiveNotification;
await request(workspaceSlug);
captureEvent(NOTIFICATION_TRACKER_EVENTS.archive, {
issue_id: data?.data?.issue?.id,
tab: currentNotificationTab,
state: "SUCCESS",
captureSuccess({
eventName: data.archived_at ? NOTIFICATION_TRACKER_EVENTS.unarchive : NOTIFICATION_TRACKER_EVENTS.archive,
payload: {
id: data?.data?.issue?.id,
tab: currentNotificationTab,
},
});
setToast({
title: data.archived_at ? t("notification.toasts.unarchived") : t("notification.toasts.archived"),
@@ -42,11 +44,19 @@ export const NotificationItemArchiveOption: FC<TNotificationItemArchiveOption> =
});
} catch (e) {
console.error(e);
captureError({
eventName: data.archived_at ? NOTIFICATION_TRACKER_EVENTS.unarchive : NOTIFICATION_TRACKER_EVENTS.archive,
payload: {
id: data?.data?.issue?.id,
tab: currentNotificationTab,
},
});
}
};
return (
<NotificationItemOptionButton
data-ph-element={NOTIFICATION_TRACKER_ELEMENTS.ARCHIVE_UNARCHIVE_BUTTON}
tooltipContent={
data.archived_at ? t("notification.options.mark_unarchive") : t("notification.options.mark_archive")
}
@@ -3,14 +3,15 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { MessageSquare } from "lucide-react";
import { NOTIFICATION_TRACKER_EVENTS } from "@plane/constants";
import { NOTIFICATION_TRACKER_ELEMENTS, NOTIFICATION_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { NotificationItemOptionButton } from "@/components/workspace-notifications";
// constants
// hooks
import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useWorkspaceNotifications } from "@/hooks/store";
// store
import { INotification } from "@/store/notifications/notification";
@@ -22,7 +23,6 @@ type TNotificationItemReadOption = {
export const NotificationItemReadOption: FC<TNotificationItemReadOption> = observer((props) => {
const { workspaceSlug, notification } = props;
// hooks
const { captureEvent } = useEventTracker();
const { currentNotificationTab } = useWorkspaceNotifications();
const { asJson: data, markNotificationAsRead, markNotificationAsUnRead } = notification;
const { t } = useTranslation();
@@ -31,10 +31,12 @@ export const NotificationItemReadOption: FC<TNotificationItemReadOption> = obser
try {
const request = data.read_at ? markNotificationAsUnRead : markNotificationAsRead;
await request(workspaceSlug);
captureEvent(NOTIFICATION_TRACKER_EVENTS.all_marked_read, {
issue_id: data?.data?.issue?.id,
tab: currentNotificationTab,
state: "SUCCESS",
captureSuccess({
eventName: data.read_at ? NOTIFICATION_TRACKER_EVENTS.mark_unread : NOTIFICATION_TRACKER_EVENTS.mark_read,
payload: {
id: data?.data?.issue?.id,
tab: currentNotificationTab,
},
});
setToast({
title: data.read_at ? t("notification.toasts.unread") : t("notification.toasts.read"),
@@ -42,11 +44,19 @@ export const NotificationItemReadOption: FC<TNotificationItemReadOption> = obser
});
} catch (e) {
console.error(e);
captureError({
eventName: data.read_at ? NOTIFICATION_TRACKER_EVENTS.mark_unread : NOTIFICATION_TRACKER_EVENTS.mark_read,
payload: {
id: data?.data?.issue?.id,
tab: currentNotificationTab,
},
});
}
};
return (
<NotificationItemOptionButton
data-ph-element={NOTIFICATION_TRACKER_ELEMENTS.MARK_READ_UNREAD_BUTTON}
tooltipContent={data.read_at ? t("notification.options.mark_unread") : t("notification.options.mark_read")}
callBack={handleNotificationUpdate}
>
@@ -11,7 +11,8 @@ import { IWorkspace } from "@plane/types";
// ui
import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useEventTracker, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useWorkspace } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// services
import { WorkspaceService } from "@/plane-web/services";
@@ -51,7 +52,6 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
// router
const router = useAppRouter();
// store hooks
const { captureWorkspaceEvent } = useEventTracker();
const { createWorkspace } = useWorkspace();
// form info
const {
@@ -71,13 +71,9 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
await createWorkspace(formData)
.then(async (res) => {
captureWorkspaceEvent({
captureSuccess({
eventName: WORKSPACE_TRACKER_EVENTS.create,
payload: {
...res,
state: "SUCCESS",
element: "Create workspace page",
},
payload: { slug: formData.slug },
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -88,12 +84,10 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
if (onSubmit) await onSubmit(res);
})
.catch(() => {
captureWorkspaceEvent({
captureError({
eventName: WORKSPACE_TRACKER_EVENTS.create,
payload: {
state: "FAILED",
element: "Create workspace page",
},
payload: { slug: formData.slug },
error: new Error("Error creating workspace"),
});
setToast({
type: TOAST_TYPE.ERROR,
@@ -248,7 +242,14 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
<div className="flex items-center gap-4">
{secondaryButton}
<Button variant="primary" type="submit" size="md" disabled={!isValid} loading={isSubmitting}>
<Button
data-ph-element="CREATE_WORKSPACE_BUTTON"
variant="primary"
type="submit"
size="md"
disabled={!isValid}
loading={isSubmitting}
>
{isSubmitting ? t(primaryButtonText.loading) : t(primaryButtonText.default)}
</Button>
{!secondaryButton && (
@@ -13,7 +13,8 @@ import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// hooks
import { cn } from "@plane/utils";
import { useEventTracker, useUserSettings, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUserSettings, useWorkspace } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
type Props = {
@@ -31,7 +32,6 @@ export const DeleteWorkspaceForm: React.FC<Props> = observer((props) => {
// router
const router = useAppRouter();
// store hooks
const { captureWorkspaceEvent } = useEventTracker();
const { deleteWorkspace } = useWorkspace();
const { t } = useTranslation();
const { getWorkspaceRedirectionUrl } = useWorkspace();
@@ -64,13 +64,9 @@ export const DeleteWorkspaceForm: React.FC<Props> = observer((props) => {
await fetchCurrentUserSettings();
handleClose();
router.push(getWorkspaceRedirectionUrl());
captureWorkspaceEvent({
captureSuccess({
eventName: WORKSPACE_TRACKER_EVENTS.delete,
payload: {
...data,
state: "SUCCESS",
element: "Workspace general settings page",
},
payload: { slug: data.slug },
});
setToast({
type: TOAST_TYPE.SUCCESS,
@@ -84,13 +80,10 @@ export const DeleteWorkspaceForm: React.FC<Props> = observer((props) => {
title: t("workspace_settings.settings.general.delete_modal.error_title"),
message: t("workspace_settings.settings.general.delete_modal.error_message"),
});
captureWorkspaceEvent({
captureError({
eventName: WORKSPACE_TRACKER_EVENTS.delete,
payload: {
...data,
state: "FAILED",
element: "Workspace general settings page",
},
payload: { slug: data.slug },
error: new Error("Error deleting workspace"),
});
});
};
@@ -5,13 +5,14 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ChevronDown, LinkIcon, Trash2 } from "lucide-react";
// plane imports
import { ROLE, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { ROLE, EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { CustomSelect, TOAST_TYPE, setToast, TContextMenuItem, CustomMenu } from "@plane/ui";
import { cn, copyTextToClipboard } from "@plane/utils";
// components
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useMember, useUserPermissions } from "@/hooks/store";
type Props = {
@@ -93,7 +94,12 @@ export const WorkspaceInvitationsListItem: FC<Props> = observer((props) => {
},
{
key: "remove",
action: () => setRemoveMemberModal(true),
action: () => {
captureClick({
elementName: MEMBER_TRACKER_ELEMENTS.WORKSPACE_INVITATIONS_LIST_CONTEXT_MENU,
});
setRemoveMemberModal(true);
},
title: t("common.remove"),
icon: Trash2,
shouldRender: isAdmin,
@@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
import { Trash2 } from "lucide-react";
import { Disclosure } from "@headlessui/react";
// plane imports
import { ROLE, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { ROLE, EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { IUser, IWorkspaceMember } from "@plane/types";
// plane ui
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
@@ -74,6 +74,7 @@ export const NameColumn: React.FC<NameProps> = (props) => {
<div
className="flex items-center gap-x-3 cursor-pointer"
onClick={() => setRemoveMemberModal(rowData)}
data-ph-element={MEMBER_TRACKER_ELEMENTS.WORKSPACE_MEMBER_TABLE_CONTEXT_MENU}
>
<Trash2 className="size-3.5 align-middle" /> {id === currentUser?.id ? "Leave " : "Remove "}
</div>
@@ -13,7 +13,8 @@ import { MembersLayoutLoader } from "@/components/ui/loader/layouts/members-layo
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace";
// constants
// hooks
import { useEventTracker, useMember, useUser, useUserPermissions, useUserSettings, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useMember, useUser, useUserPermissions, useUserSettings, useWorkspace } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useMemberColumns } from "@/plane-web/components/workspace/settings/useMemberColumns";
@@ -32,7 +33,6 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
workspace: { removeMemberFromWorkspace },
} = useMember();
const { leaveWorkspace } = useUserPermissions();
const { captureEvent } = useEventTracker();
const { getWorkspaceRedirectionUrl } = useWorkspace();
const { fetchCurrentUserSettings } = useUserSettings();
const { t } = useTranslation();
@@ -45,18 +45,27 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
.then(async () => {
await fetchCurrentUserSettings();
router.push(getWorkspaceRedirectionUrl());
captureEvent(MEMBER_TRACKER_EVENTS.workspace.leave, {
state: "SUCCESS",
element: "Workspace settings members page",
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.workspace.leave,
payload: {
workspace: workspaceSlug,
},
});
})
.catch((err: any) =>
.catch((err: any) => {
captureError({
eventName: MEMBER_TRACKER_EVENTS.workspace.leave,
payload: {
workspace: workspaceSlug,
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error || t("something_went_wrong_please_try_again"),
})
);
});
});
};
const handleRemoveMember = async (memberId: string) => {
@@ -15,7 +15,8 @@ import { LogoSpinner } from "@/components/common";
import { WorkspaceImageUploadModal } from "@/components/core";
// helpers
// hooks
import { useEventTracker, useUserPermissions, useWorkspace } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useUserPermissions, useWorkspace } from "@/hooks/store";
// plane web components
import { DeleteWorkspaceSection } from "@/plane-web/components/workspace";
@@ -31,7 +32,6 @@ export const WorkspaceDetails: FC = observer(() => {
const [isLoading, setIsLoading] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
// store hooks
const { captureWorkspaceEvent } = useEventTracker();
const { currentWorkspace, updateWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
@@ -61,13 +61,9 @@ export const WorkspaceDetails: FC = observer(() => {
await updateWorkspace(currentWorkspace.slug, payload)
.then((res) => {
captureWorkspaceEvent({
captureSuccess({
eventName: WORKSPACE_TRACKER_EVENTS.update,
payload: {
...res,
state: "SUCCESS",
element: "Workspace general settings page",
},
payload: { slug: currentWorkspace.slug },
});
setToast({
title: "Success!",
@@ -76,12 +72,10 @@ export const WorkspaceDetails: FC = observer(() => {
});
})
.catch((err) => {
captureWorkspaceEvent({
captureError({
eventName: WORKSPACE_TRACKER_EVENTS.update,
payload: {
state: "FAILED",
element: "Workspace general settings page",
},
payload: { slug: currentWorkspace.slug },
error: err,
});
console.error(err);
});
@@ -282,7 +276,12 @@ export const WorkspaceDetails: FC = observer(() => {
{isAdmin && (
<div className="flex items-center justify-between py-2">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isLoading}>
<Button
data-ph-element="WORKSPACE_SETTINGS_UPDATE_WORKSPACE_BUTTON"
variant="primary"
onClick={handleSubmit(onSubmit)}
loading={isLoading}
>
{isLoading ? t("updating") : t("workspace_settings.settings.general.update_workspace")}
</Button>
</div>
@@ -12,7 +12,7 @@ import { createRoot } from "react-dom/client";
import { LinkIcon, Settings, Share2, LogOut, MoreHorizontal, ChevronRight } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// plane helpers
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
// ui
@@ -23,7 +23,7 @@ import { Logo } from "@/components/common/logo";
import { LeaveProjectModal, PublishProjectModal } from "@/components/project";
// helpers
// hooks
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useAppTheme, useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane-web components
import { ProjectNavigationRoot } from "@/plane-web/components/sidebar";
@@ -59,7 +59,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
// store hooks
const { sidebarCollapsed } = useAppTheme();
const { t } = useTranslation();
const { setTrackElement } = useEventTracker();
const { getPartialProjectById } = useProject();
const { isMobile } = usePlatformOS();
const { allowPermissions } = useUserPermissions();
@@ -97,7 +96,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
);
const handleLeaveProject = () => {
setTrackElement("APP_SIDEBAR_PROJECT_DROPDOWN");
setLeaveProjectModal(true);
};
@@ -376,7 +374,10 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
</CustomMenu.MenuItem>
{/* leave project */}
{!isAuthorized && (
<CustomMenu.MenuItem onClick={handleLeaveProject}>
<CustomMenu.MenuItem
onClick={handleLeaveProject}
data-ph-element={MEMBER_TRACKER_ELEMENTS.SIDEBAR_PROJECT_QUICK_ACTIONS}
>
<div className="flex items-center justify-start gap-2">
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("leave_project")}</span>
@@ -7,7 +7,7 @@ import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { Briefcase, ChevronRight, Plus } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { Loader, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
@@ -17,7 +17,7 @@ import { CreateProjectModal } from "@/components/project";
import { SidebarProjectsListItem } from "@/components/workspace";
// helpers
// hooks
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
import { useAppTheme, useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
// plane web types
import { TProject } from "@/plane-web/types";
@@ -33,7 +33,6 @@ export const SidebarProjectsList: FC = observer(() => {
const { t } = useTranslation();
const { toggleCreateProjectModal } = useCommandPalette();
const { sidebarCollapsed } = useAppTheme();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { loader, getPartialProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject();
@@ -193,9 +192,9 @@ export const SidebarProjectsList: FC = observer(() => {
<Tooltip tooltipHeading={t("create_project")} tooltipContent="">
<button
type="button"
data-ph-element={PROJECT_TRACKER_ELEMENTS.SIDEBAR_CREATE_PROJECT_TOOLTIP}
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => {
setTrackElement(`APP_SIDEBAR_JOINED_BLOCK`);
setIsProjectModalOpen(true);
}}
aria-label={t("aria_labels.projects_sidebar.create_new_project")}
@@ -277,8 +276,8 @@ export const SidebarProjectsList: FC = observer(() => {
"p-0 size-8 aspect-square justify-center mx-auto": sidebarCollapsed,
}
)}
data-ph-element={PROJECT_TRACKER_ELEMENTS.SIDEBAR_CREATE_PROJECT_BUTTON}
onClick={() => {
setTrackElement("Sidebar");
toggleCreateProjectModal(true);
}}
>
@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, EUserWorkspaceRoles, SIDEBAR_TRACKER_EVENTS } from "@plane/constants";
import { EUserPermissionsLevel, EUserWorkspaceRoles, SIDEBAR_TRACKER_ELEMENTS } from "@plane/constants";
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/ui";
@@ -11,7 +11,8 @@ import { Tooltip } from "@plane/ui";
import { SidebarNavItem } from "@/components/sidebar";
import { NotificationAppSidebarOption } from "@/components/workspace-notifications";
// hooks
import { useAppTheme, useEventTracker, useUserPermissions } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useAppTheme, useUserPermissions } from "@/hooks/store";
export interface SidebarUserMenuItemProps {
item: {
@@ -33,7 +34,6 @@ export const SidebarUserMenuItem: FC<SidebarUserMenuItemProps> = observer((props
// package hooks
const { t } = useTranslation();
// store hooks
const { captureEvent } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { isMobile } = usePlatformOS();
@@ -49,8 +49,11 @@ export const SidebarUserMenuItem: FC<SidebarUserMenuItemProps> = observer((props
if (window.innerWidth < 768) {
toggleSidebar();
}
captureEvent(SIDEBAR_TRACKER_EVENTS.click, {
destination: itemKey,
captureClick({
elementName: SIDEBAR_TRACKER_ELEMENTS.USER_MENU_ITEM,
context: {
destination: itemKey,
},
});
};
@@ -4,13 +4,14 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { GLOBAL_VIEW_TOUR_TRACKER_EVENTS } from "@plane/constants";
import { GLOBAL_VIEW_TRACKER_EVENTS } from "@plane/constants";
import { IWorkspaceView } from "@plane/types";
// ui
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
// constants
// hooks
import { useGlobalView, useEventTracker } from "@/hooks/store";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useGlobalView } from "@/hooks/store";
type Props = {
data: IWorkspaceView;
@@ -26,7 +27,6 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
const { workspaceSlug } = useParams();
// store hooks
const { deleteGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker();
const handleClose = () => onClose();
@@ -37,15 +37,20 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
await deleteGlobalView(workspaceSlug.toString(), data.id)
.then(() => {
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.delete, {
view_id: data.id,
state: "SUCCESS",
captureSuccess({
eventName: GLOBAL_VIEW_TRACKER_EVENTS.delete,
payload: {
view_id: data.id,
},
});
})
.catch((error: any) => {
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.delete, {
view_id: data.id,
state: "FAILED",
captureError({
eventName: GLOBAL_VIEW_TRACKER_EVENTS.delete,
payload: {
view_id: data.id,
},
error: error,
});
setToast({
type: TOAST_TYPE.ERROR,
+12 -6
View File
@@ -8,7 +8,8 @@ import {
DEFAULT_GLOBAL_VIEWS_LIST,
EUserPermissions,
EUserPermissionsLevel,
GLOBAL_VIEW_TOUR_TRACKER_EVENTS,
GLOBAL_VIEW_TRACKER_ELEMENTS,
GLOBAL_VIEW_TRACKER_EVENTS,
} from "@plane/constants";
import { TStaticViewTypes } from "@plane/types";
// components
@@ -20,6 +21,7 @@ import {
} from "@/components/workspace";
// constants
// store hooks
import { captureSuccess } from "@/helpers/event-tracker.helper";
import { useEventTracker, useGlobalView, useUserPermissions } from "@/hooks/store";
const ViewTab = observer((props: { viewId: string }) => {
@@ -77,11 +79,14 @@ export const GlobalViewsHeader: React.FC = observer(() => {
// bring the active view to the centre of the header
useEffect(() => {
if (globalViewId && currentWorkspaceViews) {
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.open, {
view_id: globalViewId,
view_type: ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString())
? "Default"
: "Custom",
captureSuccess({
eventName: GLOBAL_VIEW_TRACKER_EVENTS.open,
payload: {
view_id: globalViewId,
view_type: ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString())
? "Default"
: "Custom",
},
});
const activeTabElement = document.querySelector(`#global-view-${globalViewId.toString()}`);
if (activeTabElement && containerRef.current) {
@@ -115,6 +120,7 @@ export const GlobalViewsHeader: React.FC = observer(() => {
{isAuthorizedUser ? (
<button
type="button"
data-ph-element={GLOBAL_VIEW_TRACKER_ELEMENTS.RIGHT_HEADER_ADD_BUTTON}
className="sticky -right-4 flex flex-shrink-0 items-center justify-center border-transparent bg-custom-background-100 py-3 hover:border-custom-border-200 hover:text-custom-text-400"
onClick={() => setCreateViewModal(true)}
>
@@ -4,7 +4,7 @@ import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { GLOBAL_VIEW_TOUR_TRACKER_EVENTS } from "@plane/constants";
import { GLOBAL_VIEW_TRACKER_EVENTS } from "@plane/constants";
import { IWorkspaceView } from "@plane/types";
// ui
import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui";
@@ -47,7 +47,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
await createGlobalView(workspaceSlug.toString(), payloadData)
.then((res) => {
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.create, {
captureEvent(GLOBAL_VIEW_TRACKER_EVENTS.create, {
view_id: res.id,
applied_filters: res.filters,
state: "SUCCESS",
@@ -62,7 +62,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
handleClose();
})
.catch(() => {
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.create, {
captureEvent(GLOBAL_VIEW_TRACKER_EVENTS.create, {
applied_filters: payload?.filters,
state: "FAILED",
});
@@ -87,7 +87,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
await updateGlobalView(workspaceSlug.toString(), data.id, payloadData)
.then((res) => {
if (res) {
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.update, {
captureEvent(GLOBAL_VIEW_TRACKER_EVENTS.update, {
view_id: res.id,
applied_filters: res.filters,
state: "SUCCESS",
@@ -101,7 +101,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
}
})
.catch(() => {
captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.update, {
captureEvent(GLOBAL_VIEW_TRACKER_EVENTS.update, {
view_id: data.id,
applied_filters: data.filters,
state: "FAILED",
@@ -4,7 +4,7 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
// types
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, GLOBAL_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspaceView } from "@plane/types";
import { CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
@@ -14,6 +14,7 @@ import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/compone
// constants
// helpers
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useUser, useUserPermissions } from "@/hooks/store";
type Props = {
@@ -93,6 +94,9 @@ export const WorkspaceViewQuickActions: React.FC<Props> = observer((props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({
elementName: GLOBAL_VIEW_TRACKER_ELEMENTS.QUICK_ACTIONS,
});
item.action();
}}
className={cn(
@@ -7,13 +7,15 @@ import { useParams } from "next/navigation";
// icons
import { Pencil, Trash2 } from "lucide-react";
// ui
import { GLOBAL_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
import { CustomMenu } from "@plane/ui";
import { calculateTotalFilters, truncateText } from "@plane/utils";
// components
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace";
// helpers
// store hooks
import { useEventTracker, useGlobalView } from "@/hooks/store";
import { captureClick } from "@/helpers/event-tracker.helper";
import { useGlobalView } from "@/hooks/store";
type Props = { viewId: string };
@@ -26,7 +28,6 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
const { workspaceSlug } = useParams();
// store hooks
const { getViewDetailsById } = useGlobalView();
const { setTrackElement } = useEventTracker();
// derived data
const view = getViewDetailsById(viewId);
@@ -58,7 +59,9 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setTrackElement("List view");
captureClick({
elementName: GLOBAL_VIEW_TRACKER_ELEMENTS.LIST_ITEM,
});
setUpdateViewModal(true);
}}
>
@@ -71,6 +74,9 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({
elementName: GLOBAL_VIEW_TRACKER_ELEMENTS.LIST_ITEM,
});
setDeleteViewModal(true);
}}
>
+124 -21
View File
@@ -1,10 +1,13 @@
import { useMemo } from "react";
// plane imports
import { IS_FAVORITE_MENU_OPEN } from "@plane/constants";
// constants
import { IS_FAVORITE_MENU_OPEN, PROJECT_PAGE_TRACKER_EVENTS } from "@plane/constants";
import { EditorRefApi } from "@plane/editor";
import { EPageAccess } from "@plane/types/src/enums";
// ui
import { setToast, TOAST_TYPE } from "@plane/ui";
import { copyUrlToClipboard } from "@plane/utils";
// helpers
import { captureSuccess, captureError } from "@/helpers/event-tracker.helper";
// hooks
import { useCollaborativePageActions } from "@/hooks/use-collaborative-page-actions";
// store types
@@ -68,12 +71,23 @@ export const usePageOperations = (
duplicate: async () => {
try {
await duplicate();
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.duplicate,
payload: {
id: page.id,
state: "SUCCESS",
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page duplicated successfully.",
});
} catch {
} catch (error: any) {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.duplicate,
error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -85,17 +99,33 @@ export const usePageOperations = (
openInNewTab: () => window.open(pageLink, "_blank"),
toggleAccess: async () => {
const changedPageType = access === EPageAccess.PUBLIC ? "private" : "public";
const eventName = PROJECT_PAGE_TRACKER_EVENTS.access_update;
try {
if (access === EPageAccess.PUBLIC)
await executeCollaborativeAction({ type: "sendMessageToServer", message: "make-private" });
else await executeCollaborativeAction({ type: "sendMessageToServer", message: "make-public" });
captureSuccess({
eventName,
payload: {
id: page.id,
from_access: access === EPageAccess.PUBLIC ? "Public" : "Private",
to_access: changedPageType === EPageAccess.PUBLIC ? "Public" : "Private",
state: "SUCCESS",
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: `The page has been marked ${changedPageType} and moved to the ${changedPageType} section.`,
});
} catch {
} catch (error: any) {
captureError({
eventName,
error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -107,12 +137,23 @@ export const usePageOperations = (
if (archived_at) {
try {
await executeCollaborativeAction({ type: "sendMessageToServer", message: "unarchive" });
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.restore,
payload: {
id: page.id,
state: "SUCCESS",
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page restored successfully.",
});
} catch {
} catch (error: any) {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.restore,
error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -122,12 +163,23 @@ export const usePageOperations = (
} else {
try {
await executeCollaborativeAction({ type: "sendMessageToServer", message: "archive" });
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.archive,
payload: {
id: page.id,
state: "SUCCESS",
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page archived successfully.",
});
} catch {
} catch (error: any) {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.archive,
error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -138,34 +190,73 @@ export const usePageOperations = (
},
toggleFavorite: () => {
if (is_favorite) {
removePageFromFavorites().then(() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page removed from favorites.",
removePageFromFavorites()
.then(() => {
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.unfavorite,
payload: {
id: page.id,
state: "SUCCESS",
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page removed from favorites.",
});
})
);
.catch((error) => {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.unfavorite,
error,
});
});
} else {
addToFavorites().then(() => {
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page added to favorites.",
addToFavorites()
.then(() => {
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.favorite,
payload: {
id: page.id,
state: "SUCCESS",
},
});
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page added to favorites.",
});
})
.catch((error) => {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.favorite,
error,
});
});
});
}
},
toggleLock: async () => {
if (is_locked) {
try {
await executeCollaborativeAction({ type: "sendMessageToServer", message: "unlock" });
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.unlock,
payload: {
id: page.id,
state: "SUCCESS",
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page unlocked successfully.",
});
} catch {
} catch (error: any) {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.unlock,
error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -175,12 +266,23 @@ export const usePageOperations = (
} else {
try {
await executeCollaborativeAction({ type: "sendMessageToServer", message: "lock" });
captureSuccess({
eventName: PROJECT_PAGE_TRACKER_EVENTS.lock,
payload: {
id: page.id,
state: "SUCCESS",
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page locked successfully.",
});
} catch {
} catch (error: any) {
captureError({
eventName: PROJECT_PAGE_TRACKER_EVENTS.lock,
error,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
@@ -200,6 +302,7 @@ export const usePageOperations = (
is_favorite,
is_locked,
isFavoriteMenuOpen,
page.id,
removePageFromFavorites,
toggleFavoriteMenu,
]);
@@ -4,7 +4,7 @@ import { FC, ReactNode, useEffect } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { EProjectNetwork } from "@plane/types/src/enums";
@@ -12,6 +12,7 @@ import { JoinProject } from "@/components/auth-screens";
import { LogoSpinner } from "@/components/common";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
import { ETimeLineTypeType } from "@/components/gantt-chart/contexts";
import { captureClick } from "@/helpers/event-tracker.helper";
// hooks
import {
useCommandPalette,
@@ -195,8 +196,8 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
title={t("workspace_projects.empty_state.general.primary_button.comic.title")}
description={t("workspace_projects.empty_state.general.primary_button.comic.description")}
onClick={() => {
setTrackElement("Project empty state");
toggleCreateProjectModal(true);
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON });
}}
disabled={!canPerformEmptyStateActions}
/>
+26 -1
View File
@@ -11,6 +11,7 @@ import { GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants";
// helpers
import { getUserRole } from "@plane/utils";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useWorkspace, useUser, useInstance, useUserPermissions } from "@/hooks/store";
// dynamic imports
const PostHogPageView = dynamic(() => import("@/lib/posthog-view"), { ssr: false });
@@ -33,6 +34,8 @@ const PostHogProvider: FC<IPosthogWrapper> = observer((props) => {
);
const currentWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug?.toString());
const is_telemetry_enabled = instance?.is_telemetry_enabled || false;
const is_posthog_enabled =
process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST && is_telemetry_enabled;
useEffect(() => {
if (user) {
@@ -65,7 +68,29 @@ const PostHogProvider: FC<IPosthogWrapper> = observer((props) => {
}
}, []);
if (process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST && is_telemetry_enabled)
useEffect(() => {
const clickHandler = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Use closest to find the nearest parent element with data-ph-element attribute
const elementWithAttribute = target.closest("[data-ph-element]") as HTMLElement;
if (elementWithAttribute) {
const element = elementWithAttribute.getAttribute("data-ph-element");
if (element) {
captureClick({ elementName: element });
}
}
};
if (is_posthog_enabled) {
document.addEventListener("click", clickHandler);
}
return () => {
document.removeEventListener("click", clickHandler);
};
}, [is_posthog_enabled]);
if (is_posthog_enabled)
return (
<PHProvider client={posthog}>
<PostHogPageView />
-73
View File
@@ -5,14 +5,9 @@ import {
GROUP_WORKSPACE_TRACKER_EVENT,
EventProps,
IssueEventProps,
getCycleEventPayload,
getIssueEventPayload,
getModuleEventPayload,
getProjectEventPayload,
getProjectStateEventPayload,
getWorkspaceEventPayload,
getPageEventPayload,
WORKSPACE_TRACKER_EVENTS,
} from "@plane/constants";
import { CoreRootStore } from "./root.store";
@@ -26,10 +21,6 @@ export interface ICoreEventTrackerStore {
setTrackElement: (element: string) => void;
captureEvent: (eventName: string, payload?: any) => void;
joinWorkspaceMetricGroup: (workspaceId?: string) => void;
captureWorkspaceEvent: (props: EventProps) => void;
captureProjectEvent: (props: EventProps) => void;
captureCycleEvent: (props: EventProps) => void;
captureModuleEvent: (props: EventProps) => void;
capturePageEvent: (props: EventProps) => void;
captureIssueEvent: (props: IssueEventProps) => void;
captureProjectStateEvent: (props: EventProps) => void;
@@ -48,8 +39,6 @@ export abstract class CoreEventTrackerStore implements ICoreEventTrackerStore {
resetSession: action,
setTrackElement: action,
captureEvent: action,
captureProjectEvent: action,
captureCycleEvent: action,
});
// store
this.rootStore = _rootStore;
@@ -109,68 +98,6 @@ export abstract class CoreEventTrackerStore implements ICoreEventTrackerStore {
this.setTrackElement(undefined);
};
/**
* @description: Captures the workspace crud related events.
* @param {EventProps} props
*/
captureWorkspaceEvent = (props: EventProps) => {
const { eventName, payload } = props;
if (eventName === WORKSPACE_TRACKER_EVENTS.create && payload.state == "SUCCESS") {
this.joinWorkspaceMetricGroup(payload.id);
}
const eventPayload: any = getWorkspaceEventPayload({
...payload,
element: payload.element ?? this.trackElement,
});
posthog?.capture(eventName, eventPayload);
this.setTrackElement(undefined);
};
/**
* @description: Captures the project related events.
* @param {EventProps} props
*/
captureProjectEvent = (props: EventProps) => {
const { eventName, payload } = props;
const eventPayload: any = getProjectEventPayload({
...this.getRequiredProperties,
...payload,
element: payload.element ?? this.trackElement,
});
posthog?.capture(eventName, eventPayload);
this.setTrackElement(undefined);
};
/**
* @description: Captures the cycle related events.
* @param {EventProps} props
*/
captureCycleEvent = (props: EventProps) => {
const { eventName, payload } = props;
const eventPayload: any = getCycleEventPayload({
...this.getRequiredProperties,
...payload,
element: payload.element ?? this.trackElement,
});
posthog?.capture(eventName, eventPayload);
this.setTrackElement(undefined);
};
/**
* @description: Captures the module related events.
* @param {EventProps} props
*/
captureModuleEvent = (props: EventProps) => {
const { eventName, payload } = props;
const eventPayload: any = getModuleEventPayload({
...this.getRequiredProperties,
...payload,
element: payload.element ?? this.trackElement,
});
posthog?.capture(eventName, eventPayload);
this.setTrackElement(undefined);
};
/**
* @description: Captures the project pages related events.
* @param {EventProps} props
+136
View File
@@ -0,0 +1,136 @@
import posthog from "posthog-js";
export type TEventState = "SUCCESS" | "ERROR";
export type TElementContext = Record<string, any>;
export type TEventContext = Record<string, any>;
export type TInteractionType = "clicked" | "viewed" | "hovered";
type TCaptureElementParams = {
elementName: string;
interaction_type: TInteractionType;
context?: TElementContext;
};
/**
* Capture UI element interactions (clicks, hovers, views, etc.)
* This helps understand user behavior and interaction patterns
*
* @param element - Generic UI element type
* @param context - Context about where and why the interaction happened
*/
const captureElement = (params: TCaptureElementParams) => {
const { elementName, interaction_type, context } = params;
if (!posthog) return;
const elementEvent = `${elementName}_${interaction_type}`;
const payload = {
element_type: elementName,
timestamp: new Date().toISOString(),
...context,
};
posthog.capture(elementEvent, payload);
};
type TCaptureClickParams = Omit<TCaptureElementParams, "interaction_type">;
/**
* Capture click events
* @param element - The element that was clicked
* @param context - Additional context
*/
export const captureClick = (params: TCaptureClickParams) => {
captureElement({ ...params, interaction_type: "clicked" });
};
type TCaptureViewParams = Omit<TCaptureElementParams, "interaction_type">;
/**
* Capture view events
* @param element - The element that was viewed
* @param context - Additional context
*/
export const captureView = (params: TCaptureViewParams) => {
captureElement({ ...params, interaction_type: "viewed" });
};
type TCaptureHoverParams = Omit<TCaptureElementParams, "interaction_type">;
/**
* Capture hover events
* @param element - The element that was hovered
* @param context - Additional context
*/
export const captureHover = (params: TCaptureHoverParams) => {
captureElement({ ...params, interaction_type: "hovered" });
};
type TCaptureEventParams = {
eventName: string;
payload?: Record<string, any>;
context?: TEventContext;
state: TEventState;
};
/**
* Capture business events (outcomes, state changes, etc.)
* This helps understand business metrics and conversion rates
*
* @param eventName - Business event name (e.g., "cycle_created", "project_updated")
* @param state - Success or error state
* @param payload - Event-specific data
* @param context - Additional context
*/
const captureEvent = (params: TCaptureEventParams) => {
const { eventName, payload, context, state } = params;
if (!posthog) return;
const finalPayload = {
...context,
...payload,
state,
timestamp: new Date().toISOString(),
};
posthog.capture(eventName, finalPayload);
};
type TCaptureSuccessParams = Omit<TCaptureEventParams, "state">;
/**
* Capture success events
* @param eventName - The name of the event
* @param payload - Additional payload
* @param context - Additional context
*/
export const captureSuccess = (params: TCaptureSuccessParams) => {
captureEvent({ ...params, state: "SUCCESS" });
};
type TCaptureErrorParams = Omit<TCaptureEventParams, "state"> & {
error?: Error | string;
};
/**
* Capture error events
* @param eventName - The name of the event
* @param error - The error object
* @param payload - Additional payload
* @param context - Additional context
*/
export const captureError = (params: TCaptureErrorParams) => {
captureEvent({ ...params, state: "ERROR", payload: { ...params.payload, error: params.error } });
};
type TCaptureElementAndEventParams = {
element: Omit<TCaptureElementParams, "interaction_type">;
event: TCaptureEventParams;
};
/**
* Capture both element interaction and business event together
* @param element - The element that was interacted with
* @param event - The business event that was triggered
*/
export const captureElementAndEvent = (params: TCaptureElementAndEventParams) => {
const { element, event } = params;
// Capture the element interaction first
captureElement({ ...element, interaction_type: "clicked" });
// Then capture the business event
captureEvent(event);
};