From 4f72ebded9b99f26d4e71ae4de37b5642d45168d Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:22:08 +0530 Subject: [PATCH] chore: added sign-up/in, onboarding, dashboard, all-issues related events (#3595) * chore: added event constants * chore: added workspace events * chore: added workspace group for events * chore: member invitation event added * chore: added project pages related events. * fix: member integer role to string * chore: added sign-up & sign-in events * chore: added global-view related events * chore: added notification related events * chore: project, cycle property change added * chore: cycle favourite, and change-properties added * chore: module davorite, and sidebar property changes added * fix: build errors * chore: all events defined in constants --- apiserver/plane/app/views/auth_extended.py | 12 +- apiserver/plane/app/views/authentication.py | 8 +- apiserver/plane/app/views/oauth.py | 4 +- .../sign-in-forms/optional-set-password.tsx | 27 +++- .../account/sign-in-forms/password.tsx | 16 +- web/components/account/sign-in-forms/root.tsx | 11 +- .../account/sign-in-forms/unique-code.tsx | 18 ++- .../sign-up-forms/optional-set-password.tsx | 27 +++- web/components/account/sign-up-forms/root.tsx | 11 +- .../account/sign-up-forms/unique-code.tsx | 19 ++- web/components/cycles/cycles-board-card.tsx | 47 ++++-- web/components/cycles/cycles-list-item.tsx | 59 ++++--- web/components/cycles/delete-modal.tsx | 6 +- web/components/cycles/form.tsx | 6 +- web/components/cycles/modal.tsx | 23 ++- web/components/cycles/sidebar.tsx | 73 ++++++--- web/components/headers/pages.tsx | 13 +- .../headers/workspace-dashboard.tsx | 21 ++- web/components/inbox/inbox-issue-actions.tsx | 17 +- .../inbox/modals/create-issue-modal.tsx | 17 +- web/components/issues/attachment/root.tsx | 8 +- web/components/issues/issue-detail/root.tsx | 25 +-- .../calendar/quick-add-issue-form.tsx | 6 +- .../roots/global-view-root.tsx | 12 +- .../gantt/quick-add-issue-form.tsx | 6 +- .../issue-layouts/kanban/base-kanban-root.tsx | 3 +- .../kanban/quick-add-issue-form.tsx | 6 +- .../list/quick-add-issue-form.tsx | 6 +- .../properties/all-properties.tsx | 16 +- .../spreadsheet/quick-add-issue-form.tsx | 6 +- web/components/issues/issue-modal/modal.tsx | 30 +--- web/components/issues/peek-overview/root.tsx | 25 +-- .../modules/delete-module-modal.tsx | 6 +- web/components/modules/form.tsx | 6 +- web/components/modules/modal.tsx | 18 ++- web/components/modules/module-card-item.tsx | 47 ++++-- web/components/modules/module-list-item.tsx | 43 +++-- web/components/modules/sidebar.tsx | 33 +++- .../notifications/notification-card.tsx | 29 +++- .../notifications/notification-header.tsx | 21 ++- .../notifications/notification-popover.tsx | 11 +- web/components/onboarding/invitations.tsx | 23 ++- web/components/onboarding/invite-members.tsx | 28 +++- web/components/onboarding/tour/root.tsx | 19 ++- web/components/onboarding/user-details.tsx | 20 ++- web/components/onboarding/workspace.tsx | 35 +++- .../page-views/workspace-dashboard.tsx | 6 +- .../pages/create-update-page-modal.tsx | 34 +++- web/components/pages/delete-page-modal.tsx | 19 ++- .../project/create-project-modal.tsx | 17 +- .../project/delete-project-modal.tsx | 16 +- web/components/project/form.tsx | 26 ++- .../project/leave-project-modal.tsx | 8 +- web/components/project/member-list-item.tsx | 9 +- .../project/send-project-invitation-modal.tsx | 43 +++-- .../project/settings/features-list.tsx | 10 +- .../states/create-update-state-inline.tsx | 42 +++-- web/components/states/delete-state-modal.tsx | 20 ++- .../workspace/create-workspace-form.tsx | 26 +-- .../workspace/delete-workspace-modal.tsx | 23 ++- .../workspace/settings/members-list-item.tsx | 12 +- .../workspace/settings/workspace-details.tsx | 21 ++- web/components/workspace/sidebar-dropdown.tsx | 1 - web/components/workspace/sidebar-menu.tsx | 25 +-- .../workspace/views/delete-view-modal.tsx | 21 ++- web/components/workspace/views/header.tsx | 11 +- web/components/workspace/views/modal.tsx | 38 ++++- .../workspace/views/view-list-item.tsx | 4 +- web/constants/event-tracker.ts | 139 ++++++++++++++-- web/lib/app-provider.tsx | 4 +- web/lib/posthog-provider.tsx | 23 ++- web/package.json | 2 +- .../projects/[projectId]/pages/index.tsx | 8 +- .../[workspaceSlug]/settings/members.tsx | 36 +++-- web/pages/accounts/forgot-password.tsx | 16 +- web/pages/accounts/reset-password.tsx | 21 ++- web/pages/invitations/index.tsx | 21 ++- web/pages/onboarding/index.tsx | 4 +- web/store/event-tracker.store.ts | 152 +++++++++++++----- web/store/user/index.ts | 2 + 80 files changed, 1276 insertions(+), 507 deletions(-) diff --git a/apiserver/plane/app/views/auth_extended.py b/apiserver/plane/app/views/auth_extended.py index 501f47657..29cb43e38 100644 --- a/apiserver/plane/app/views/auth_extended.py +++ b/apiserver/plane/app/views/auth_extended.py @@ -401,8 +401,8 @@ class EmailCheckEndpoint(BaseAPIView): email=email, user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"), - event_name="SIGN_IN", - medium="MAGIC_LINK", + event_name="Sign up", + medium="Magic link", first_time=True, ) key, token, current_attempt = generate_magic_token(email=email) @@ -438,8 +438,8 @@ class EmailCheckEndpoint(BaseAPIView): email=email, user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"), - event_name="SIGN_IN", - medium="MAGIC_LINK", + event_name="Sign in", + medium="Magic link", first_time=False, ) @@ -468,8 +468,8 @@ class EmailCheckEndpoint(BaseAPIView): email=email, user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"), - event_name="SIGN_IN", - medium="EMAIL", + event_name="Sign in", + medium="Email", first_time=False, ) diff --git a/apiserver/plane/app/views/authentication.py b/apiserver/plane/app/views/authentication.py index a41200d61..c2b3e0b7e 100644 --- a/apiserver/plane/app/views/authentication.py +++ b/apiserver/plane/app/views/authentication.py @@ -274,8 +274,8 @@ class SignInEndpoint(BaseAPIView): email=email, user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"), - event_name="SIGN_IN", - medium="EMAIL", + event_name="Sign in", + medium="Email", first_time=False, ) @@ -349,8 +349,8 @@ class MagicSignInEndpoint(BaseAPIView): email=email, user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"), - event_name="SIGN_IN", - medium="MAGIC_LINK", + event_name="Sign in", + medium="Magic link", first_time=False, ) diff --git a/apiserver/plane/app/views/oauth.py b/apiserver/plane/app/views/oauth.py index de90e4337..8152fb0ee 100644 --- a/apiserver/plane/app/views/oauth.py +++ b/apiserver/plane/app/views/oauth.py @@ -296,7 +296,7 @@ class OauthEndpoint(BaseAPIView): email=email, user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"), - event_name="SIGN_IN", + event_name="Sign in", medium=medium.upper(), first_time=False, ) @@ -427,7 +427,7 @@ class OauthEndpoint(BaseAPIView): email=email, user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"), - event_name="SIGN_IN", + event_name="Sign up", medium=medium.upper(), first_time=True, ) diff --git a/web/components/account/sign-in-forms/optional-set-password.tsx b/web/components/account/sign-in-forms/optional-set-password.tsx index d7a595298..1ea5ca792 100644 --- a/web/components/account/sign-in-forms/optional-set-password.tsx +++ b/web/components/account/sign-in-forms/optional-set-password.tsx @@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form"; import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; +import { useEventTracker } from "hooks/store"; // ui import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // icons import { Eye, EyeOff } from "lucide-react"; +import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED } from "constants/event-tracker"; type Props = { email: string; @@ -34,6 +36,8 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => { // states const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false); const [showPassword, setShowPassword] = useState(false); + // store hooks + const { captureEvent } = useEventTracker(); // toast alert const { setToastAlert } = useToast(); // form info @@ -63,21 +67,34 @@ export const SignInOptionalSetPasswordForm: React.FC = (props) => { title: "Success!", message: "Password created successfully.", }); + captureEvent(PASSWORD_CREATE_SELECTED, { + state: "SUCCESS", + first_time: false, + }); await handleSignInRedirection(); }) - .catch((err) => + .catch((err) => { + captureEvent(PASSWORD_CREATE_SELECTED, { + state: "FAILED", + first_time: false, + }); setToastAlert({ type: "error", title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", - }) - ); + }); + }); }; const handleGoToWorkspace = async () => { setIsGoingToWorkspace(true); - - await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false)); + await handleSignInRedirection().finally(() => { + captureEvent(PASSWORD_CREATE_SKIPPED, { + state: "SUCCESS", + first_time: false, + }); + setIsGoingToWorkspace(false); + }); }; return ( diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx index fe20d5b10..98719df63 100644 --- a/web/components/account/sign-in-forms/password.tsx +++ b/web/components/account/sign-in-forms/password.tsx @@ -7,7 +7,7 @@ import { Eye, EyeOff, XCircle } from "lucide-react"; import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; -import { useApplication } from "hooks/store"; +import { useApplication, useEventTracker } from "hooks/store"; // components import { ESignInSteps, ForgotPasswordPopover } from "components/account"; // ui @@ -16,6 +16,8 @@ import { Button, Input } from "@plane/ui"; import { checkEmailValidity } from "helpers/string.helper"; // types import { IPasswordSignInData } from "@plane/types"; +// constants +import { FORGOT_PASSWORD, SIGN_IN_WITH_PASSWORD } from "constants/event-tracker"; type Props = { email: string; @@ -46,6 +48,7 @@ export const SignInPasswordForm: React.FC = observer((props) => { const { config: { envConfig }, } = useApplication(); + const { captureEvent } = useEventTracker(); // derived values const isSmtpConfigured = envConfig?.is_smtp_configured; // form info @@ -72,7 +75,13 @@ export const SignInPasswordForm: React.FC = observer((props) => { await authService .passwordSignIn(payload) - .then(async () => await onSubmit()) + .then(async () => { + captureEvent(SIGN_IN_WITH_PASSWORD, { + state: "SUCCESS", + first_time: false, + }); + await onSubmit(); + }) .catch((err) => setToastAlert({ type: "error", @@ -182,9 +191,10 @@ export const SignInPasswordForm: React.FC = observer((props) => { )} /> -
+
{isSmtpConfigured ? ( captureEvent(FORGOT_PASSWORD)} href={`/accounts/forgot-password?email=${email}`} className="text-xs font-medium text-custom-primary-100" > diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx index c92cd4bd4..62f63caea 100644 --- a/web/components/account/sign-in-forms/root.tsx +++ b/web/components/account/sign-in-forms/root.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Link from "next/link"; import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; +import { useApplication, useEventTracker } from "hooks/store"; import useSignInRedirection from "hooks/use-sign-in-redirection"; // components import { LatestFeatureBlock } from "components/common"; @@ -13,6 +13,8 @@ import { OAuthOptions, SignInOptionalSetPasswordForm, } from "components/account"; +// constants +import { NAVIGATE_TO_SIGNUP } from "constants/event-tracker"; export enum ESignInSteps { EMAIL = "EMAIL", @@ -32,6 +34,7 @@ export const SignInRoot = observer(() => { const { config: { envConfig }, } = useApplication(); + const { captureEvent } = useEventTracker(); // derived values const isSmtpConfigured = envConfig?.is_smtp_configured; @@ -110,7 +113,11 @@ export const SignInRoot = observer(() => {

Don{"'"}t have an account?{" "} - + captureEvent(NAVIGATE_TO_SIGNUP, {})} + className="text-custom-primary-100 font-medium underline" + > Sign up

diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx index 6e0ae3745..55dbe86e2 100644 --- a/web/components/account/sign-in-forms/unique-code.tsx +++ b/web/components/account/sign-in-forms/unique-code.tsx @@ -7,12 +7,15 @@ import { UserService } from "services/user.service"; // hooks import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; +import { useEventTracker } from "hooks/store"; // ui import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData, IMagicSignInData } from "@plane/types"; +// constants +import { CODE_VERIFIED } from "constants/event-tracker"; type Props = { email: string; @@ -41,6 +44,8 @@ export const SignInUniqueCodeForm: React.FC = (props) => { const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); // toast alert const { setToastAlert } = useToast(); + // store hooks + const { captureEvent } = useEventTracker(); // timer const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(30); // form info @@ -69,17 +74,22 @@ export const SignInUniqueCodeForm: React.FC = (props) => { await authService .magicSignIn(payload) .then(async () => { + captureEvent(CODE_VERIFIED, { + state: "SUCCESS", + }); const currentUser = await userService.currentUser(); - await onSubmit(currentUser.is_password_autoset); }) - .catch((err) => + .catch((err) => { + captureEvent(CODE_VERIFIED, { + state: "FAILED", + }); setToastAlert({ type: "error", title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", - }) - ); + }); + }); }; const handleSendNewCode = async (formData: TUniqueCodeFormValues) => { diff --git a/web/components/account/sign-up-forms/optional-set-password.tsx b/web/components/account/sign-up-forms/optional-set-password.tsx index db14f0ccb..b49adabbb 100644 --- a/web/components/account/sign-up-forms/optional-set-password.tsx +++ b/web/components/account/sign-up-forms/optional-set-password.tsx @@ -4,12 +4,14 @@ import { Controller, useForm } from "react-hook-form"; import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; +import { useEventTracker } from "hooks/store"; // ui import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // constants import { ESignUpSteps } from "components/account"; +import { PASSWORD_CREATE_SELECTED, PASSWORD_CREATE_SKIPPED, SETUP_PASSWORD } from "constants/event-tracker"; // icons import { Eye, EyeOff } from "lucide-react"; @@ -37,6 +39,8 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { // states const [isGoingToWorkspace, setIsGoingToWorkspace] = useState(false); const [showPassword, setShowPassword] = useState(false); + // store hooks + const { captureEvent } = useEventTracker(); // toast alert const { setToastAlert } = useToast(); // form info @@ -66,21 +70,34 @@ export const SignUpOptionalSetPasswordForm: React.FC = (props) => { title: "Success!", message: "Password created successfully.", }); + captureEvent(SETUP_PASSWORD, { + state: "SUCCESS", + first_time: true, + }); await handleSignInRedirection(); }) - .catch((err) => + .catch((err) => { + captureEvent(SETUP_PASSWORD, { + state: "FAILED", + first_time: true, + }); setToastAlert({ type: "error", title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", - }) - ); + }); + }); }; const handleGoToWorkspace = async () => { setIsGoingToWorkspace(true); - - await handleSignInRedirection().finally(() => setIsGoingToWorkspace(false)); + await handleSignInRedirection().finally(() => { + captureEvent(PASSWORD_CREATE_SKIPPED, { + state: "SUCCESS", + first_time: true, + }); + setIsGoingToWorkspace(false); + }); }; return ( diff --git a/web/components/account/sign-up-forms/root.tsx b/web/components/account/sign-up-forms/root.tsx index da9d7d79a..8eeb5e99f 100644 --- a/web/components/account/sign-up-forms/root.tsx +++ b/web/components/account/sign-up-forms/root.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; +import { useApplication, useEventTracker } from "hooks/store"; import useSignInRedirection from "hooks/use-sign-in-redirection"; // components import { @@ -12,6 +12,8 @@ import { SignUpUniqueCodeForm, } from "components/account"; import Link from "next/link"; +// constants +import { NAVIGATE_TO_SIGNIN } from "constants/event-tracker"; export enum ESignUpSteps { EMAIL = "EMAIL", @@ -32,6 +34,7 @@ export const SignUpRoot = observer(() => { const { config: { envConfig }, } = useApplication(); + const { captureEvent } = useEventTracker(); // step 1 submit handler- email verification const handleEmailVerification = () => setSignInStep(ESignUpSteps.UNIQUE_CODE); @@ -86,7 +89,11 @@ export const SignUpRoot = observer(() => {

Already using Plane?{" "} - + captureEvent(NAVIGATE_TO_SIGNIN, {})} + className="text-custom-primary-100 font-medium underline" + > Sign in

diff --git a/web/components/account/sign-up-forms/unique-code.tsx b/web/components/account/sign-up-forms/unique-code.tsx index 7764b627e..1b54ef9eb 100644 --- a/web/components/account/sign-up-forms/unique-code.tsx +++ b/web/components/account/sign-up-forms/unique-code.tsx @@ -8,12 +8,15 @@ import { UserService } from "services/user.service"; // hooks import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; +import { useEventTracker } from "hooks/store"; // ui import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types import { IEmailCheckData, IMagicSignInData } from "@plane/types"; +// constants +import { CODE_VERIFIED } from "constants/event-tracker"; type Props = { email: string; @@ -39,6 +42,8 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { const { email, handleEmailClear, onSubmit } = props; // states const [isRequestingNewCode, setIsRequestingNewCode] = useState(false); + // store hooks + const { captureEvent } = useEventTracker(); // toast alert const { setToastAlert } = useToast(); // timer @@ -69,17 +74,22 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { await authService .magicSignIn(payload) .then(async () => { + captureEvent(CODE_VERIFIED, { + state: "SUCCESS", + }); const currentUser = await userService.currentUser(); - await onSubmit(currentUser.is_password_autoset); }) - .catch((err) => + .catch((err) => { + captureEvent(CODE_VERIFIED, { + state: "FAILED", + }); setToastAlert({ type: "error", title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", - }) - ); + }); + }); }; const handleSendNewCode = async (formData: TUniqueCodeFormValues) => { @@ -96,7 +106,6 @@ export const SignUpUniqueCodeForm: React.FC = (props) => { title: "Success!", message: "A new unique code has been sent to your email.", }); - reset({ email: formData.email, token: "", diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index bad7df0e5..375c15301 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -16,6 +16,7 @@ import { copyTextToClipboard } from "helpers/string.helper"; // constants import { CYCLE_STATUS } from "constants/cycle"; import { EUserWorkspaceRoles } from "constants/workspace"; +import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; //.types import { TCycleGroups } from "@plane/types"; @@ -33,7 +34,7 @@ export const CyclesBoardCard: FC = (props) => { // router const router = useRouter(); // store - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent } = useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); @@ -90,39 +91,55 @@ export const CyclesBoardCard: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) + .then(() => { + captureEvent(CYCLE_FAVORITED, { + cycle_id: cycleId, + element: "Grid layout", + state: "SUCCESS", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", + }); }); - }); }; const handleRemoveFromFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) + .then(() => { + captureEvent(CYCLE_UNFAVORITED, { + cycle_id: cycleId, + element: "Grid layout", + state: "SUCCESS", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", + }); }); - }); }; const handleEditCycle = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - setTrackElement("Cycles page board layout"); + setTrackElement("Cycles page grid layout"); setUpdateModal(true); }; const handleDeleteCycle = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - setTrackElement("Cycles page board layout"); + setTrackElement("Cycles page grid layout"); setDeleteModal(true); }; diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 725480241..98392cd0e 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -18,6 +18,7 @@ import { CYCLE_STATUS } from "constants/cycle"; import { EUserWorkspaceRoles } from "constants/workspace"; // types import { TCycleGroups } from "@plane/types"; +import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; type TCyclesListItem = { cycleId: string; @@ -37,7 +38,7 @@ export const CyclesListItem: FC = (props) => { // router const router = useRouter(); // store hooks - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent } = useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); @@ -63,26 +64,42 @@ export const CyclesListItem: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) + .then(() => { + captureEvent(CYCLE_FAVORITED, { + cycle_id: cycleId, + element: "List layout", + state: "SUCCESS", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", + }); }); - }); }; const handleRemoveFromFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the cycle to favorites. Please try again.", + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId) + .then(() => { + captureEvent(CYCLE_UNFAVORITED, { + cycle_id: cycleId, + element: "List layout", + state: "SUCCESS", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the cycle to favorites. Please try again.", + }); }); - }); }; const handleEditCycle = (e: MouseEvent) => { @@ -159,9 +176,9 @@ export const CyclesListItem: FC = (props) => { projectId={projectId} /> -
-
-
+
+
+
{isCompleted ? ( @@ -181,20 +198,20 @@ export const CyclesListItem: FC = (props) => {
- + {cycleDetails.name}
-
{currentCycle && (
= (props) => {
)}
-
+
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
-
+
{cycleDetails.assignees.length > 0 ? ( diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 32e067833..5dc0306ab 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -10,6 +10,8 @@ import useToast from "hooks/use-toast"; import { Button } from "@plane/ui"; // types import { ICycle } from "@plane/types"; +// constants +import { CYCLE_DELETED } from "constants/event-tracker"; interface ICycleDelete { cycle: ICycle; @@ -45,13 +47,13 @@ export const CycleDeleteModal: React.FC = observer((props) => { message: "Cycle deleted successfully.", }); captureCycleEvent({ - eventName: "Cycle deleted", + eventName: CYCLE_DELETED, payload: { ...cycle, state: "SUCCESS" }, }); }) .catch(() => { captureCycleEvent({ - eventName: "Cycle deleted", + eventName: CYCLE_DELETED, payload: { ...cycle, state: "FAILED" }, }); }); diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 865cc68a1..dfe2a878e 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -10,7 +10,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { ICycle } from "@plane/types"; type Props = { - handleFormSubmit: (values: Partial) => Promise; + handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; handleClose: () => void; status: boolean; projectId: string; @@ -29,7 +29,7 @@ export const CycleForm: React.FC = (props) => { const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props; // form data const { - formState: { errors, isSubmitting }, + formState: { errors, isSubmitting, dirtyFields }, handleSubmit, control, watch, @@ -61,7 +61,7 @@ export const CycleForm: React.FC = (props) => { maxDate?.setDate(maxDate.getDate() - 1); return ( -
+ handleFormSubmit(formData,dirtyFields))}>
{!status && ( diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index 7e17e55f1..e8f19d6a1 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -10,6 +10,8 @@ import useLocalStorage from "hooks/use-local-storage"; import { CycleForm } from "components/cycles"; // types import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types"; +// constants +import { CYCLE_CREATED, CYCLE_UPDATED } from "constants/event-tracker"; type CycleModalProps = { isOpen: boolean; @@ -47,7 +49,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { message: "Cycle created successfully.", }); captureCycleEvent({ - eventName: "Cycle created", + eventName: CYCLE_CREATED, payload: { ...res, state: "SUCCESS" }, }); }) @@ -58,18 +60,23 @@ export const CycleCreateUpdateModal: React.FC = (props) => { message: err.detail ?? "Error in creating cycle. Please try again.", }); captureCycleEvent({ - eventName: "Cycle created", + eventName: CYCLE_CREATED, payload: { ...payload, state: "FAILED" }, }); }); }; - const handleUpdateCycle = async (cycleId: string, payload: Partial) => { + const handleUpdateCycle = async (cycleId: string, payload: Partial, dirtyFields: any) => { if (!workspaceSlug || !projectId) return; const selectedProjectId = payload.project ?? projectId.toString(); await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload) - .then(() => { + .then((res) => { + const changed_properties = Object.keys(dirtyFields); + captureCycleEvent({ + eventName: CYCLE_UPDATED, + payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" }, + }); setToastAlert({ type: "success", title: "Success!", @@ -77,6 +84,10 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }); }) .catch((err) => { + captureCycleEvent({ + eventName: CYCLE_UPDATED, + payload: { ...payload, state: "FAILED" }, + }); setToastAlert({ type: "error", title: "Error!", @@ -95,7 +106,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { return status; }; - const handleFormSubmit = async (formData: Partial) => { + const handleFormSubmit = async (formData: Partial, dirtyFields: any) => { if (!workspaceSlug || !projectId) return; const payload: Partial = { @@ -119,7 +130,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { } if (isDateValid) { - if (data) await handleUpdateCycle(data.id, payload); + if (data) await handleUpdateCycle(data.id, payload, dirtyFields); else { await handleCreateCycle(payload).then(() => { setCycleTab("all"); diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 6966779b5..27182247b 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -39,6 +39,7 @@ import { import { ICycle } from "@plane/types"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; +import { CYCLE_UPDATED } from "constants/event-tracker"; // fetch-keys import { CYCLE_STATUS } from "constants/cycle"; @@ -67,7 +68,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; // store hooks - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureCycleEvent } = useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); @@ -83,10 +84,32 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { defaultValues, }); - const submitChanges = (data: Partial) => { + const submitChanges = (data: Partial, changedProperty: string) => { if (!workspaceSlug || !projectId || !cycleId) return; - updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); + updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data) + .then((res) => { + captureCycleEvent({ + eventName: CYCLE_UPDATED, + payload: { + ...res, + changed_properties: [changedProperty], + element: "Right side-peek", + state: "SUCCESS", + }, + }); + }) + + .catch((_) => { + captureCycleEvent({ + eventName: CYCLE_UPDATED, + payload: { + ...data, + element: "Right side-peek", + state: "FAILED", + }, + }); + }); }; const handleCopyText = () => { @@ -146,10 +169,13 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }); if (isDateValidForExistingCycle) { - submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }); + submitChanges( + { + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), + end_date: renderFormattedPayloadDate(`${watch("end_date")}`), + }, + "start_date" + ); setToastAlert({ type: "success", title: "Success!", @@ -174,10 +200,13 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }); if (isDateValid) { - submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }); + submitChanges( + { + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), + end_date: renderFormattedPayloadDate(`${watch("end_date")}`), + }, + "start_date" + ); setToastAlert({ type: "success", title: "Success!", @@ -219,10 +248,13 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }); if (isDateValidForExistingCycle) { - submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }); + submitChanges( + { + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), + end_date: renderFormattedPayloadDate(`${watch("end_date")}`), + }, + "end_date" + ); setToastAlert({ type: "success", title: "Success!", @@ -246,10 +278,13 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }); if (isDateValid) { - submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), - end_date: renderFormattedPayloadDate(`${watch("end_date")}`), - }); + submitChanges( + { + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), + end_date: renderFormattedPayloadDate(`${watch("end_date")}`), + }, + "end_date" + ); setToastAlert({ type: "success", title: "Success!", diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index 28116b323..1984971d6 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -2,7 +2,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { FileText, Plus } from "lucide-react"; // hooks -import { useApplication, useProject, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers @@ -25,6 +25,7 @@ export const PagesHeader = observer(() => { membership: { currentProjectRole }, } = useUser(); const { currentProjectDetails } = useProject(); + const { setTrackElement } = useEventTracker(); const canUserCreatePage = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); @@ -64,7 +65,15 @@ export const PagesHeader = observer(() => {
{canUserCreatePage && (
-
diff --git a/web/components/headers/workspace-dashboard.tsx b/web/components/headers/workspace-dashboard.tsx index d8306ab40..6b85577f6 100644 --- a/web/components/headers/workspace-dashboard.tsx +++ b/web/components/headers/workspace-dashboard.tsx @@ -4,13 +4,18 @@ import { useTheme } from "next-themes"; // images import githubBlackImage from "/public/logos/github-black.png"; import githubWhiteImage from "/public/logos/github-white.png"; +// hooks +import { useEventTracker } from "hooks/store"; // components import { BreadcrumbLink } from "components/common"; import { Breadcrumbs } from "@plane/ui"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +// constants +import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "constants/event-tracker"; export const WorkspaceDashboardHeader = () => { // hooks + const { captureEvent } = useEventTracker(); const { resolvedTheme } = useTheme(); return ( @@ -31,16 +36,26 @@ export const WorkspaceDashboardHeader = () => {
diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 82253af88..998ad268c 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -20,6 +20,7 @@ import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle // types import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types"; import { EUserProjectRoles } from "constants/project"; +import { ISSUE_DELETED } from "constants/event-tracker"; type TInboxIssueActionsHeader = { workspaceSlug: string; @@ -86,17 +87,12 @@ export const InboxIssueActionsHeader: FC = observer((p throw new Error("Missing required parameters"); await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId); captureIssueEvent({ - eventName: "Issue deleted", + eventName: ISSUE_DELETED, payload: { id: inboxIssueId, state: "SUCCESS", element: "Inbox page", - }, - group: { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - }, + } }); router.push({ pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, @@ -108,17 +104,12 @@ export const InboxIssueActionsHeader: FC = observer((p message: "Something went wrong while deleting inbox issue. Please try again.", }); captureIssueEvent({ - eventName: "Issue deleted", + eventName: ISSUE_DELETED, payload: { id: inboxIssueId, state: "FAILED", element: "Inbox page", }, - group: { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - }, }); } }, diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx index 066f172ca..84c4bef1e 100644 --- a/web/components/inbox/modals/create-issue-modal.tsx +++ b/web/components/inbox/modals/create-issue-modal.tsx @@ -18,6 +18,8 @@ import { GptAssistantPopover } from "components/core"; import { Button, Input, ToggleSwitch } from "@plane/ui"; // types import { TIssue } from "@plane/types"; +// constants +import { ISSUE_CREATED } from "constants/event-tracker"; type Props = { isOpen: boolean; @@ -65,7 +67,6 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { config: { envConfig }, } = useApplication(); const { captureIssueEvent } = useEventTracker(); - const { currentWorkspace } = useWorkspace(); const { control, @@ -94,34 +95,24 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { handleClose(); } else reset(defaultValues); captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...formData, state: "SUCCESS", element: "Inbox page", }, - group: { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - }, path: router.pathname, }); }) .catch((error) => { console.error(error); captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...formData, state: "FAILED", element: "Inbox page", }, - group: { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - }, path: router.pathname, }); }); diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx index 11d74af0e..ffa17d337 100644 --- a/web/components/issues/attachment/root.tsx +++ b/web/components/issues/attachment/root.tsx @@ -38,7 +38,7 @@ export const IssueAttachmentRoot: FC = (props) => { title: "Attachment uploaded", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: "Issue attachment added", payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "attachment", @@ -47,7 +47,7 @@ export const IssueAttachmentRoot: FC = (props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: "Issue attachment added", payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, }); setToastAlert({ @@ -67,7 +67,7 @@ export const IssueAttachmentRoot: FC = (props) => { title: "Attachment removed", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: "Issue attachment deleted", payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "attachment", @@ -76,7 +76,7 @@ export const IssueAttachmentRoot: FC = (props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: "Issue attachment deleted", payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "attachment", diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index 2e0303a8e..92badf4b2 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -16,6 +16,7 @@ import { TIssue } from "@plane/types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; +import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker"; export type TIssueOperations = { fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -102,7 +103,7 @@ export const IssueDetailRoot: FC = (props) => { }); } captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: Object.keys(data).join(","), @@ -112,7 +113,7 @@ export const IssueDetailRoot: FC = (props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: Object.keys(data).join(","), @@ -138,7 +139,7 @@ export const IssueDetailRoot: FC = (props) => { message: "Issue deleted successfully", }); captureIssueEvent({ - eventName: "Issue deleted", + eventName: ISSUE_DELETED, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, path: router.asPath, }); @@ -149,7 +150,7 @@ export const IssueDetailRoot: FC = (props) => { message: "Issue delete failed", }); captureIssueEvent({ - eventName: "Issue deleted", + eventName: ISSUE_DELETED, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, path: router.asPath, }); @@ -164,7 +165,7 @@ export const IssueDetailRoot: FC = (props) => { message: "Issue added to issue successfully", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -174,7 +175,7 @@ export const IssueDetailRoot: FC = (props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -198,7 +199,7 @@ export const IssueDetailRoot: FC = (props) => { message: "Cycle removed from issue successfully", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -208,7 +209,7 @@ export const IssueDetailRoot: FC = (props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -232,7 +233,7 @@ export const IssueDetailRoot: FC = (props) => { message: "Module added to issue successfully", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "module_id", @@ -242,7 +243,7 @@ export const IssueDetailRoot: FC = (props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "module_id", @@ -266,7 +267,7 @@ export const IssueDetailRoot: FC = (props) => { message: "Module removed from issue successfully", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "module_id", @@ -276,7 +277,7 @@ export const IssueDetailRoot: FC = (props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "module_id", diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 1f62c248c..6db9323fa 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -13,6 +13,8 @@ import { createIssuePayload } from "helpers/issue.helper"; import { PlusIcon } from "lucide-react"; // types import { TIssue } from "@plane/types"; +// constants +import { ISSUE_CREATED } from "constants/event-tracker"; type Props = { formKey: keyof TIssue; @@ -129,7 +131,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { viewId ).then((res) => { captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Calendar quick add" }, path: router.asPath, }); @@ -142,7 +144,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { } catch (err: any) { console.error(err); captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...payload, state: "FAILED", element: "Calendar quick add" }, path: router.asPath, }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index 0dae3c8bd..c03e86504 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -2,7 +2,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import isEqual from "lodash/isEqual"; // hooks -import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store"; +import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "hooks/store"; //ui import { Button } from "@plane/ui"; // components @@ -11,6 +11,8 @@ import { AppliedFiltersList } from "components/issues"; import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace"; +// constants +import { GLOBAL_VIEW_UPDATED } from "constants/event-tracker"; type Props = { globalViewId: string; @@ -27,6 +29,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => { } = useIssues(EIssuesStoreType.GLOBAL); const { workspaceLabels } = useLabel(); const { globalViewMap, updateGlobalView } = useGlobalView(); + const { captureEvent } = useEventTracker(); const { membership: { currentWorkspaceRole }, } = useUser(); @@ -91,6 +94,13 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => { filters: { ...(appliedFilters ?? {}), }, + }).then((res) => { + captureEvent(GLOBAL_VIEW_UPDATED, { + view_id: res.id, + applied_filters: res.filters, + state: "SUCCESS", + element: "Spreadsheet view", + }); }); }; diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index e89f60688..bfecb993b 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -13,6 +13,8 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { createIssuePayload } from "helpers/issue.helper"; // types import { IProject, TIssue } from "@plane/types"; +// constants +import { ISSUE_CREATED } from "constants/event-tracker"; interface IInputProps { formKey: string; @@ -111,7 +113,7 @@ export const GanttQuickAddIssueForm: React.FC = observe quickAddCallback && (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Gantt quick add" }, path: router.asPath, }); @@ -123,7 +125,7 @@ export const GanttQuickAddIssueForm: React.FC = observe }); } catch (err: any) { captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...payload, state: "FAILED", element: "Gantt quick add" }, path: router.asPath, }); diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 36d5e0315..83f72d8ea 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -25,6 +25,7 @@ import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile"; import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue"; +import { ISSUE_DELETED } from "constants/event-tracker"; export interface IBaseKanBanLayout { issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues; @@ -212,7 +213,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas setDeleteIssueModal(false); setDragState({}); captureIssueEvent({ - eventName: "Issue deleted", + eventName: ISSUE_DELETED, payload: { id: dragState.draggedIssueId!, state: "FAILED", element: "Kanban layout drag & drop" }, path: router.asPath, }); diff --git a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx index 8880ca278..513163431 100644 --- a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx @@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { createIssuePayload } from "helpers/issue.helper"; // types import { TIssue } from "@plane/types"; +// constants +import { ISSUE_CREATED } from "constants/event-tracker"; const Inputs = (props: any) => { const { register, setFocus, projectDetail } = props; @@ -106,7 +108,7 @@ export const KanBanQuickAddIssueForm: React.FC = obser viewId ).then((res) => { captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Kanban quick add" }, path: router.asPath, }); @@ -118,7 +120,7 @@ export const KanBanQuickAddIssueForm: React.FC = obser }); } catch (err: any) { captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...payload, state: "FAILED", element: "Kanban quick add" }, path: router.asPath, }); diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx index dd63f09aa..8d1ce6d9c 100644 --- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { TIssue, IProject } from "@plane/types"; // types import { createIssuePayload } from "helpers/issue.helper"; +// constants +import { ISSUE_CREATED } from "constants/event-tracker"; interface IInputProps { formKey: string; @@ -103,7 +105,7 @@ export const ListQuickAddIssueForm: FC = observer((props quickAddCallback && (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId).then((res) => { captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "List quick add" }, path: router.asPath, }); @@ -115,7 +117,7 @@ export const ListQuickAddIssueForm: FC = observer((props }); } catch (err: any) { captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...payload, state: "FAILED", element: "List quick add" }, path: router.asPath, }); diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index e0a0dbd5c..4d851545e 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -18,6 +18,8 @@ import { import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; +// constants +import { ISSUE_UPDATED } from "constants/event-tracker"; export interface IIssueProperties { issue: TIssue; @@ -40,7 +42,7 @@ export const IssueProperties: React.FC = observer((props) => { const handleState = (stateId: string) => { handleIssues({ ...issue, state_id: stateId }).then(() => { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: router.asPath, updates: { @@ -54,7 +56,7 @@ export const IssueProperties: React.FC = observer((props) => { const handlePriority = (value: TIssuePriorities) => { handleIssues({ ...issue, priority: value }).then(() => { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: router.asPath, updates: { @@ -68,7 +70,7 @@ export const IssueProperties: React.FC = observer((props) => { const handleLabel = (ids: string[]) => { handleIssues({ ...issue, label_ids: ids }).then(() => { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: router.asPath, updates: { @@ -82,7 +84,7 @@ export const IssueProperties: React.FC = observer((props) => { const handleAssignee = (ids: string[]) => { handleIssues({ ...issue, assignee_ids: ids }).then(() => { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: router.asPath, updates: { @@ -96,7 +98,7 @@ export const IssueProperties: React.FC = observer((props) => { const handleStartDate = (date: Date | null) => { handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: router.asPath, updates: { @@ -110,7 +112,7 @@ export const IssueProperties: React.FC = observer((props) => { const handleTargetDate = (date: Date | null) => { handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }).then(() => { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: router.asPath, updates: { @@ -124,7 +126,7 @@ export const IssueProperties: React.FC = observer((props) => { const handleEstimate = (value: number | null) => { handleIssues({ ...issue, estimate_point: value }).then(() => { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: router.asPath, updates: { diff --git a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx index b0acd7237..3cba3c6cd 100644 --- a/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx @@ -12,6 +12,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { createIssuePayload } from "helpers/issue.helper"; // types import { TIssue } from "@plane/types"; +// constants +import { ISSUE_CREATED } from "constants/event-tracker"; type Props = { formKey: keyof TIssue; @@ -162,7 +164,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => (await quickAddCallback(currentWorkspace.slug, currentProjectDetails.id, { ...payload } as TIssue, viewId).then( (res) => { captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" }, path: router.asPath, }); @@ -175,7 +177,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => }); } catch (err: any) { captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" }, path: router.asPath, }); diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 02a087314..97d977ace 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -13,6 +13,8 @@ import { IssueFormRoot } from "./form"; import type { TIssue } from "@plane/types"; // constants import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue"; +import { ISSUE_CREATED, ISSUE_UPDATED } from "constants/event-tracker"; + export interface IssuesModalProps { data?: Partial; isOpen: boolean; @@ -157,14 +159,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop message: "Issue created successfully.", }); captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...response, state: "SUCCESS" }, path: router.asPath, - group: { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - }, }); !createMore && handleClose(); return response; @@ -175,14 +172,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop message: "Issue could not be created. Please try again.", }); captureIssueEvent({ - eventName: "Issue created", + eventName: ISSUE_CREATED, payload: { ...payload, state: "FAILED" }, path: router.asPath, - group: { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - }, }); } }; @@ -198,14 +190,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop message: "Issue updated successfully.", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS" }, path: router.asPath, - group: { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - }, }); handleClose(); return response; @@ -216,14 +203,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop message: "Issue could not be created. Please try again.", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...payload, state: "FAILED" }, path: router.asPath, - group: { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - }, }); } }; diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index f14018ed4..b491ebe36 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -11,6 +11,7 @@ import { TIssue } from "@plane/types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; +import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker"; interface IIssuePeekOverview { is_archived?: boolean; @@ -103,7 +104,7 @@ export const IssuePeekOverview: FC = observer((props) => { message: "Issue updated successfully", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: Object.keys(data).join(","), @@ -113,7 +114,7 @@ export const IssuePeekOverview: FC = observer((props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue peek-overview" }, path: router.asPath, }); @@ -135,7 +136,7 @@ export const IssuePeekOverview: FC = observer((props) => { message: "Issue deleted successfully", }); captureIssueEvent({ - eventName: "Issue deleted", + eventName: ISSUE_DELETED, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, path: router.asPath, }); @@ -146,7 +147,7 @@ export const IssuePeekOverview: FC = observer((props) => { message: "Issue delete failed", }); captureIssueEvent({ - eventName: "Issue deleted", + eventName: ISSUE_DELETED, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, path: router.asPath, }); @@ -161,7 +162,7 @@ export const IssuePeekOverview: FC = observer((props) => { message: "Issue added to issue successfully", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -171,7 +172,7 @@ export const IssuePeekOverview: FC = observer((props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -195,7 +196,7 @@ export const IssuePeekOverview: FC = observer((props) => { message: "Cycle removed from issue successfully", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -210,7 +211,7 @@ export const IssuePeekOverview: FC = observer((props) => { message: "Cycle remove from issue failed", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { state: "FAILED", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -229,7 +230,7 @@ export const IssuePeekOverview: FC = observer((props) => { message: "Module added to issue successfully", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { ...response, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "module_id", @@ -239,7 +240,7 @@ export const IssuePeekOverview: FC = observer((props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, updates: { changed_property: "module_id", @@ -263,7 +264,7 @@ export const IssuePeekOverview: FC = observer((props) => { message: "Module removed from issue successfully", }); captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "module_id", @@ -273,7 +274,7 @@ export const IssuePeekOverview: FC = observer((props) => { }); } catch (error) { captureIssueEvent({ - eventName: "Issue updated", + eventName: ISSUE_UPDATED, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, updates: { changed_property: "module_id", diff --git a/web/components/modules/delete-module-modal.tsx b/web/components/modules/delete-module-modal.tsx index 2727b4e3b..636a828ae 100644 --- a/web/components/modules/delete-module-modal.tsx +++ b/web/components/modules/delete-module-modal.tsx @@ -11,6 +11,8 @@ import { Button } from "@plane/ui"; import { AlertTriangle } from "lucide-react"; // types import type { IModule } from "@plane/types"; +// constants +import { MODULE_DELETED } from "constants/event-tracker"; type Props = { data: IModule; @@ -51,7 +53,7 @@ export const DeleteModuleModal: React.FC = observer((props) => { message: "Module deleted successfully.", }); captureModuleEvent({ - eventName: "Module deleted", + eventName: MODULE_DELETED, payload: { ...data, state: "SUCCESS" }, }); }) @@ -62,7 +64,7 @@ export const DeleteModuleModal: React.FC = observer((props) => { message: "Module could not be deleted. Please try again.", }); captureModuleEvent({ - eventName: "Module deleted", + eventName: MODULE_DELETED, payload: { ...data, state: "FAILED" }, }); }) diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index be0792caa..8fa63e826 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -11,7 +11,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { IModule } from "@plane/types"; type Props = { - handleFormSubmit: (values: Partial) => Promise; + handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; handleClose: () => void; status: boolean; projectId: string; @@ -36,7 +36,7 @@ export const ModuleForm: React.FC = ({ data, }) => { const { - formState: { errors, isSubmitting }, + formState: { errors, isSubmitting, dirtyFields }, handleSubmit, watch, control, @@ -53,7 +53,7 @@ export const ModuleForm: React.FC = ({ }); const handleCreateUpdateModule = async (formData: Partial) => { - await handleFormSubmit(formData); + await handleFormSubmit(formData, dirtyFields); reset({ ...defaultValues, diff --git a/web/components/modules/modal.tsx b/web/components/modules/modal.tsx index 0852434c3..7990386df 100644 --- a/web/components/modules/modal.tsx +++ b/web/components/modules/modal.tsx @@ -9,6 +9,8 @@ import useToast from "hooks/use-toast"; import { ModuleForm } from "components/modules"; // types import type { IModule } from "@plane/types"; +// constants +import { MODULE_CREATED, MODULE_UPDATED } from "constants/event-tracker"; type Props = { isOpen: boolean; @@ -59,7 +61,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { message: "Module created successfully.", }); captureModuleEvent({ - eventName: "Module created", + eventName: MODULE_CREATED, payload: { ...res, state: "SUCCESS" }, }); }) @@ -70,13 +72,13 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { message: err.detail ?? "Module could not be created. Please try again.", }); captureModuleEvent({ - eventName: "Module created", + eventName: MODULE_CREATED, payload: { ...data, state: "FAILED" }, }); }); }; - const handleUpdateModule = async (payload: Partial) => { + const handleUpdateModule = async (payload: Partial, dirtyFields: any) => { if (!workspaceSlug || !projectId || !data) return; const selectedProjectId = payload.project ?? projectId.toString(); @@ -90,8 +92,8 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { message: "Module updated successfully.", }); captureModuleEvent({ - eventName: "Module updated", - payload: { ...res, state: "SUCCESS" }, + eventName: MODULE_UPDATED, + payload: { ...res, changed_properties: Object.keys(dirtyFields), state: "SUCCESS" }, }); }) .catch((err) => { @@ -101,20 +103,20 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { message: err.detail ?? "Module could not be updated. Please try again.", }); captureModuleEvent({ - eventName: "Module updated", + eventName: MODULE_UPDATED, payload: { ...data, state: "FAILED" }, }); }); }; - const handleFormSubmit = async (formData: Partial) => { + const handleFormSubmit = async (formData: Partial, dirtyFields: any) => { if (!workspaceSlug || !projectId) return; const payload: Partial = { ...formData, }; if (!data) await handleCreateModule(payload); - else await handleUpdateModule(payload); + else await handleUpdateModule(payload, dirtyFields); }; useEffect(() => { diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 3d83be010..219942550 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -16,6 +16,7 @@ import { renderFormattedDate } from "helpers/date-time.helper"; // constants import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; +import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; type Props = { moduleId: string; @@ -36,7 +37,7 @@ export const ModuleCardItem: React.FC = observer((props) => { membership: { currentProjectRole }, } = useUser(); const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule(); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent } = useEventTracker(); // derived values const moduleDetails = getModuleById(moduleId); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -46,13 +47,21 @@ export const ModuleCardItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the module to favorites. Please try again.", + addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) + .then(() => { + captureEvent(MODULE_FAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the module to favorites. Please try again.", + }); }); - }); }; const handleRemoveFromFavorites = (e: React.MouseEvent) => { @@ -60,13 +69,21 @@ export const ModuleCardItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the module from favorites. Please try again.", + removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) + .then(() => { + captureEvent(MODULE_UNFAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the module from favorites. Please try again.", + }); }); - }); }; const handleCopyText = (e: React.MouseEvent) => { @@ -84,14 +101,14 @@ export const ModuleCardItem: React.FC = observer((props) => { const handleEditModule = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - setTrackElement("Modules page board layout"); + setTrackElement("Modules page grid layout"); setEditModal(true); }; const handleDeleteModule = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - setTrackElement("Modules page board layout"); + setTrackElement("Modules page grid layout"); setDeleteModal(true); }; diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 7232c8815..23e3e5ed4 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -16,6 +16,7 @@ import { renderFormattedDate } from "helpers/date-time.helper"; // constants import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; +import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "constants/event-tracker"; type Props = { moduleId: string; @@ -36,7 +37,7 @@ export const ModuleListItem: React.FC = observer((props) => { membership: { currentProjectRole }, } = useUser(); const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule(); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent } = useEventTracker(); // derived values const moduleDetails = getModuleById(moduleId); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -46,13 +47,21 @@ export const ModuleListItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the module to favorites. Please try again.", + addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) + .then(() => { + captureEvent(MODULE_FAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the module to favorites. Please try again.", + }); }); - }); }; const handleRemoveFromFavorites = (e: React.MouseEvent) => { @@ -60,13 +69,21 @@ export const ModuleListItem: React.FC = observer((props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the module from favorites. Please try again.", + removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), moduleId) + .then(() => { + captureEvent(MODULE_UNFAVORITED, { + module_id: moduleId, + element: "Grid layout", + state: "SUCCESS", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the module from favorites. Please try again.", + }); }); - }); }; const handleCopyText = (e: React.MouseEvent) => { diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index 947885f9a..6b7ac1b3a 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -34,6 +34,7 @@ import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; // constant import { MODULE_STATUS } from "constants/module"; import { EUserProjectRoles } from "constants/project"; +import { MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED } from "constants/event-tracker"; const defaultValues: Partial = { lead: "", @@ -66,7 +67,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { membership: { currentProjectRole }, } = useUser(); const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule(); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); const moduleDetails = getModuleById(moduleId); const { setToastAlert } = useToast(); @@ -77,7 +78,19 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !moduleId) return; - updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data); + updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data) + .then((res) => { + captureModuleEvent({ + eventName: MODULE_UPDATED, + payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" }, + }); + }) + .catch((_) => { + captureModuleEvent({ + eventName: MODULE_UPDATED, + payload: { ...data, state: "FAILED" }, + }); + }); }; const handleCreateLink = async (formData: ModuleLink) => { @@ -87,6 +100,10 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload) .then(() => { + captureEvent(MODULE_LINK_CREATED, { + module_id: moduleId, + state: "SUCCESS", + }); setToastAlert({ type: "success", title: "Module link created", @@ -109,6 +126,10 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload) .then(() => { + captureEvent(MODULE_LINK_UPDATED, { + module_id: moduleId, + state: "SUCCESS", + }); setToastAlert({ type: "success", title: "Module link updated", @@ -129,6 +150,10 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId) .then(() => { + captureEvent(MODULE_LINK_DELETED, { + module_id: moduleId, + state: "SUCCESS", + }); setToastAlert({ type: "success", title: "Module link deleted", @@ -187,8 +212,8 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { submitChanges({ - start_date: renderFormattedPayloadDate(`${watch("start_date")}`), target_date: renderFormattedPayloadDate(`${watch("target_date")}`), + start_date: renderFormattedPayloadDate(`${watch("start_date")}`), }); setToastAlert({ type: "success", @@ -294,7 +319,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { ( + render={({ field: { value, onChange } }) => ( void; @@ -28,6 +32,7 @@ type NotificationCardProps = { export const NotificationCard: React.FC = (props) => { const { + selectedTab, notification, isSnoozedTabOpen, closePopover, @@ -37,6 +42,8 @@ export const NotificationCard: React.FC = (props) => { setSelectedNotificationForSnooze, markSnoozeNotification, } = props; + // store hooks + const { captureEvent } = useEventTracker(); const router = useRouter(); const { workspaceSlug } = router.query; @@ -49,6 +56,10 @@ export const NotificationCard: React.FC = (props) => { { markNotificationReadStatus(notification.id); + captureEvent(ISSUE_OPENED, { + issue_id: notification.data.issue.id, + element: "notification", + }); closePopover(); }} href={`/${workspaceSlug}/projects/${notification.project}/${ @@ -164,6 +175,11 @@ export const NotificationCard: React.FC = (props) => { icon: , onClick: () => { markNotificationReadStatusToggle(notification.id).then(() => { + captureEvent(NOTIFICATIONS_READ, { + issue_id: notification.data.issue.id, + tab: selectedTab, + state: "SUCCESS", + }); setToastAlert({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", type: "success", @@ -181,6 +197,11 @@ export const NotificationCard: React.FC = (props) => { ), onClick: () => { markNotificationArchivedStatus(notification.id).then(() => { + captureEvent(NOTIFICATION_ARCHIVED, { + issue_id: notification.data.issue.id, + tab: selectedTab, + state: "SUCCESS", + }); setToastAlert({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", type: "success", @@ -195,7 +216,6 @@ export const NotificationCard: React.FC = (props) => { onClick={(e) => { e.stopPropagation(); e.preventDefault(); - item.onClick(); }} key={item.id} @@ -228,6 +248,11 @@ export const NotificationCard: React.FC = (props) => { } markSnoozeNotification(notification.id, item.value).then(() => { + captureEvent(NOTIFICATION_SNOOZED, { + issue_id: notification.data.issue.id, + tab: selectedTab, + state: "SUCCESS", + }); setToastAlert({ title: `Notification snoozed till ${renderFormattedDate(item.value)}`, type: "success", diff --git a/web/components/notifications/notification-header.tsx b/web/components/notifications/notification-header.tsx index 39bf0e8fb..cf3a9fd36 100644 --- a/web/components/notifications/notification-header.tsx +++ b/web/components/notifications/notification-header.tsx @@ -2,10 +2,19 @@ import React from "react"; import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X } from "lucide-react"; // ui import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui"; +// hooks +import { useEventTracker } from "hooks/store"; // helpers import { getNumberCount } from "helpers/string.helper"; // type import type { NotificationType, NotificationCount } from "@plane/types"; +// constants +import { + ARCHIVED_NOTIFICATIONS, + NOTIFICATIONS_READ, + SNOOZED_NOTIFICATIONS, + UNREAD_NOTIFICATIONS, +} from "constants/event-tracker"; type NotificationHeaderProps = { notificationCount?: NotificationCount | null; @@ -39,6 +48,8 @@ export const NotificationHeader: React.FC = (props) => setSelectedTab, markAllNotificationsAsRead, } = props; + // store hooks + const { captureEvent } = useEventTracker(); const notificationTabs: Array<{ label: string; @@ -84,6 +95,7 @@ export const NotificationHeader: React.FC = (props) => setSnoozed(false); setArchived(false); setReadNotification((prev) => !prev); + captureEvent(UNREAD_NOTIFICATIONS); }} > @@ -97,7 +109,12 @@ export const NotificationHeader: React.FC = (props) => } closeOnSelect > - + { + markAllNotificationsAsRead(); + captureEvent(NOTIFICATIONS_READ); + }} + >
Mark all as read @@ -108,6 +125,7 @@ export const NotificationHeader: React.FC = (props) => setArchived(false); setReadNotification(false); setSnoozed((prev) => !prev); + captureEvent(SNOOZED_NOTIFICATIONS); }} >
@@ -120,6 +138,7 @@ export const NotificationHeader: React.FC = (props) => setSnoozed(false); setReadNotification(false); setArchived((prev) => !prev); + captureEvent(ARCHIVED_NOTIFICATIONS); }} >
diff --git a/web/components/notifications/notification-popover.tsx b/web/components/notifications/notification-popover.tsx index 4b55ea4cb..2b0689c34 100644 --- a/web/components/notifications/notification-popover.tsx +++ b/web/components/notifications/notification-popover.tsx @@ -3,7 +3,7 @@ import { Popover, Transition } from "@headlessui/react"; import { Bell } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; +import { useApplication, useEventTracker } from "hooks/store"; import useUserNotification from "hooks/use-user-notifications"; // components import { EmptyState } from "components/common"; @@ -13,10 +13,13 @@ import { Loader, Tooltip } from "@plane/ui"; import emptyNotification from "public/empty-state/notification.svg"; // helpers import { getNumberCount } from "helpers/string.helper"; +// constants +import { SIDEBAR_CLICKED } from "constants/event-tracker"; export const NotificationPopover = observer(() => { // store hooks const { theme: themeStore } = useApplication(); + const { captureEvent } = useEventTracker(); const { notifications, @@ -66,6 +69,11 @@ export const NotificationPopover = observer(() => { <> + captureEvent(SIDEBAR_CLICKED, { + destination: "notifications", + }) + } className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${ isActive ? "bg-custom-primary-100/10 text-custom-primary-100" @@ -120,6 +128,7 @@ export const NotificationPopover = observer(() => { key={notification.id} isSnoozedTabOpen={snoozed} closePopover={closePopover} + selectedTab={selectedTab} notification={notification} markNotificationArchivedStatus={markNotificationArchivedStatus} markNotificationReadStatus={markNotificationAsRead} diff --git a/web/components/onboarding/invitations.tsx b/web/components/onboarding/invitations.tsx index 3315ff035..c176ed580 100644 --- a/web/components/onboarding/invitations.tsx +++ b/web/components/onboarding/invitations.tsx @@ -11,11 +11,13 @@ import { WorkspaceService } from "services/workspace.service"; // constants import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys"; import { ROLE } from "constants/workspace"; +import { MEMBER_ACCEPTED } from "constants/event-tracker"; // types import { IWorkspaceMemberInvitation } from "@plane/types"; // icons import { CheckCircle2, Search } from "lucide-react"; import {} from "hooks/store/use-event-tracker"; +import { getUserRole } from "helpers/user.helper"; type Props = { handleNextStep: () => void; @@ -58,11 +60,19 @@ export const Invitations: React.FC = (props) => { if (invitationsRespond.length <= 0) return; setIsJoiningWorkspaces(true); + const invitation = invitations?.find((invitation) => invitation.id === invitationsRespond[0]); await workspaceService .joinWorkspaces({ invitations: invitationsRespond }) - .then(async (res) => { - captureEvent("Member accepted", { ...res, state: "SUCCESS", accepted_from: "App" }); + .then(async () => { + captureEvent(MEMBER_ACCEPTED, { + member_id: invitation?.id, + role: getUserRole(invitation?.role!), + project_id: undefined, + accepted_from: "App", + state: "SUCCESS", + element: "Workspace invitations page", + }); await fetchWorkspaces(); await mutate(USER_WORKSPACES); await updateLastWorkspace(); @@ -71,7 +81,14 @@ export const Invitations: React.FC = (props) => { }) .catch((error) => { console.error(error); - captureEvent("Member accepted", { state: "FAILED", accepted_from: "App" }); + captureEvent(MEMBER_ACCEPTED, { + member_id: invitation?.id, + role: getUserRole(invitation?.role!), + project_id: undefined, + accepted_from: "App", + state: "FAILED", + element: "Workspace invitations page", + }); }) .finally(() => setIsJoiningWorkspaces(false)); }; diff --git a/web/components/onboarding/invite-members.tsx b/web/components/onboarding/invite-members.tsx index dc6e2db96..561a428d6 100644 --- a/web/components/onboarding/invite-members.tsx +++ b/web/components/onboarding/invite-members.tsx @@ -18,6 +18,7 @@ import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; import { WorkspaceService } from "services/workspace.service"; // hooks import useToast from "hooks/use-toast"; +import { useEventTracker } from "hooks/store"; // ui import { Button, Input } from "@plane/ui"; // components @@ -28,6 +29,9 @@ import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; // constants import { EUserWorkspaceRoles, ROLE } from "constants/workspace"; +import { MEMBER_INVITED } from "constants/event-tracker"; +// helpers +import { getUserRole } from "helpers/user.helper"; // assets import user1 from "public/users/user-1.png"; import user2 from "public/users/user-2.png"; @@ -267,6 +271,8 @@ export const InviteMembers: React.FC = (props) => { const { setToastAlert } = useToast(); const { resolvedTheme } = useTheme(); + // store hooks + const { captureEvent } = useEventTracker(); const { control, @@ -305,6 +311,17 @@ export const InviteMembers: React.FC = (props) => { })), }) .then(async () => { + captureEvent(MEMBER_INVITED, { + emails: [ + ...payload.emails.map((email) => ({ + email: email.email, + role: getUserRole(email.role), + })), + ], + project_id: undefined, + state: "SUCCESS", + element: "Onboarding", + }); setToastAlert({ type: "success", title: "Success!", @@ -313,13 +330,18 @@ export const InviteMembers: React.FC = (props) => { await nextStep(); }) - .catch((err) => + .catch((err) => { + captureEvent(MEMBER_INVITED, { + project_id: undefined, + state: "FAILED", + element: "Onboarding", + }); setToastAlert({ type: "error", title: "Error!", message: err?.error, - }) - ); + }); + }); }; const appendField = () => { diff --git a/web/components/onboarding/tour/root.tsx b/web/components/onboarding/tour/root.tsx index 6e1de15dd..c09a2a94c 100644 --- a/web/components/onboarding/tour/root.tsx +++ b/web/components/onboarding/tour/root.tsx @@ -15,6 +15,8 @@ import CyclesTour from "public/onboarding/cycles.webp"; import ModulesTour from "public/onboarding/modules.webp"; import ViewsTour from "public/onboarding/views.webp"; import PagesTour from "public/onboarding/pages.webp"; +// constants +import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "constants/event-tracker"; type Props = { onComplete: () => void; @@ -79,7 +81,7 @@ export const TourRoot: React.FC = observer((props) => { const [step, setStep] = useState("welcome"); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); - const { setTrackElement } = useEventTracker(); + const { setTrackElement, captureEvent } = useEventTracker(); const { currentUser } = useUser(); const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step); @@ -103,13 +105,22 @@ export const TourRoot: React.FC = observer((props) => {

- @@ -156,8 +167,8 @@ export const TourRoot: React.FC = observer((props) => { )} diff --git a/web/pages/accounts/forgot-password.tsx b/web/pages/accounts/forgot-password.tsx index 8d3c4cd28..07fa86045 100644 --- a/web/pages/accounts/forgot-password.tsx +++ b/web/pages/accounts/forgot-password.tsx @@ -7,6 +7,7 @@ import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; +import { useEventTracker } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; // components @@ -19,6 +20,7 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import { checkEmailValidity } from "helpers/string.helper"; // type import { NextPageWithLayout } from "lib/types"; +import { FORGOT_PASS_LINK } from "constants/event-tracker"; type TForgotPasswordFormValues = { email: string; @@ -35,6 +37,8 @@ const ForgotPasswordPage: NextPageWithLayout = () => { // router const router = useRouter(); const { email } = router.query; + // store hooks + const { captureEvent } = useEventTracker(); // toast const { setToastAlert } = useToast(); // timer @@ -57,6 +61,9 @@ const ForgotPasswordPage: NextPageWithLayout = () => { email: formData.email, }) .then(() => { + captureEvent(FORGOT_PASS_LINK, { + state: "SUCCESS", + }); setToastAlert({ type: "success", title: "Email sent", @@ -65,13 +72,16 @@ const ForgotPasswordPage: NextPageWithLayout = () => { }); setResendCodeTimer(30); }) - .catch((err) => + .catch((err) => { + captureEvent(FORGOT_PASS_LINK, { + state: "FAILED", + }); setToastAlert({ type: "error", title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", - }) - ); + }); + }); }; return ( diff --git a/web/pages/accounts/reset-password.tsx b/web/pages/accounts/reset-password.tsx index 9854ec5bb..c4258f39e 100644 --- a/web/pages/accounts/reset-password.tsx +++ b/web/pages/accounts/reset-password.tsx @@ -7,6 +7,7 @@ import { AuthService } from "services/auth.service"; // hooks import useToast from "hooks/use-toast"; import useSignInRedirection from "hooks/use-sign-in-redirection"; +import { useEventTracker } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; // components @@ -21,6 +22,8 @@ import { checkEmailValidity } from "helpers/string.helper"; import { NextPageWithLayout } from "lib/types"; // icons import { Eye, EyeOff } from "lucide-react"; +// constants +import { NEW_PASS_CREATED } from "constants/event-tracker"; type TResetPasswordFormValues = { email: string; @@ -41,6 +44,8 @@ const ResetPasswordPage: NextPageWithLayout = () => { const { uidb64, token, email } = router.query; // states const [showPassword, setShowPassword] = useState(false); + // store hooks + const { captureEvent } = useEventTracker(); // toast const { setToastAlert } = useToast(); // sign in redirection hook @@ -66,14 +71,22 @@ const ResetPasswordPage: NextPageWithLayout = () => { await authService .resetPassword(uidb64.toString(), token.toString(), payload) - .then(() => handleRedirection()) - .catch((err) => + .then(() => { + captureEvent(NEW_PASS_CREATED, { + state: "SUCCESS", + }); + handleRedirection(); + }) + .catch((err) => { + captureEvent(NEW_PASS_CREATED, { + state: "FAILED", + }); setToastAlert({ type: "error", title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", - }) - ); + }); + }); }; return ( diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx index 1d8c3e774..26ced2010 100644 --- a/web/pages/invitations/index.tsx +++ b/web/pages/invitations/index.tsx @@ -23,11 +23,13 @@ import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-l import emptyInvitation from "public/empty-state/invitation.svg"; // helpers import { truncateText } from "helpers/string.helper"; +import { getUserRole } from "helpers/user.helper"; // types import { NextPageWithLayout } from "lib/types"; import type { IWorkspaceMemberInvitation } from "@plane/types"; // constants import { ROLE } from "constants/workspace"; +import { MEMBER_ACCEPTED } from "constants/event-tracker"; // components import { EmptyState } from "components/common"; @@ -40,7 +42,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { const [invitationsRespond, setInvitationsRespond] = useState([]); const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false); // store hooks - const { captureEvent } = useEventTracker(); + const { captureEvent, joinWorkspaceMetricGroup } = useEventTracker(); const { currentUser, currentUserSettings } = useUser(); // router const router = useRouter(); @@ -81,11 +83,16 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { .then((res) => { mutate("USER_WORKSPACES"); const firstInviteId = invitationsRespond[0]; + const invitation = invitations?.find((i) => i.id === firstInviteId); const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace; - captureEvent("Member accepted", { - ...res, - state: "SUCCESS", + joinWorkspaceMetricGroup(redirectWorkspace?.id); + captureEvent(MEMBER_ACCEPTED, { + member_id: invitation?.id, + role: getUserRole(invitation?.role!), + project_id: undefined, accepted_from: "App", + state: "SUCCESS", + element: "Workspace invitations page", }); userService .updateUser({ last_workspace_id: redirectWorkspace?.id }) @@ -103,6 +110,12 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { }); }) .catch(() => { + captureEvent(MEMBER_ACCEPTED, { + project_id: undefined, + accepted_from: "App", + state: "FAILED", + element: "Workspace invitations page", + }); setToastAlert({ type: "error", title: "Error!", diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index 5a5911fca..99886156d 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -24,6 +24,8 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types import { IUser, TOnboardingSteps } from "@plane/types"; import { NextPageWithLayout } from "lib/types"; +// constants +import { USER_ONBOARDING_COMPLETED } from "constants/event-tracker"; // services const workspaceService = new WorkspaceService(); @@ -79,7 +81,7 @@ const OnboardingPage: NextPageWithLayout = observer(() => { await updateUserOnBoard() .then(() => { - captureEvent("User onboarding completed", { + captureEvent(USER_ONBOARDING_COMPLETED, { user_role: user.role, email: user.email, user_id: user.id, diff --git a/web/store/event-tracker.store.ts b/web/store/event-tracker.store.ts index 89e279c40..744ad44fb 100644 --- a/web/store/event-tracker.store.ts +++ b/web/store/event-tracker.store.ts @@ -3,39 +3,49 @@ import posthog from "posthog-js"; // stores import { RootStore } from "./root.store"; import { - EventGroupProps, + GROUP_WORKSPACE, + WORKSPACE_CREATED, EventProps, IssueEventProps, getCycleEventPayload, getIssueEventPayload, getModuleEventPayload, getProjectEventPayload, + getProjectStateEventPayload, + getWorkspaceEventPayload, + getPageEventPayload, } from "constants/event-tracker"; export interface IEventTrackerStore { // properties - trackElement: string; + trackElement: string | undefined; // computed - getRequiredPayload: any; + getRequiredProperties: any; // actions + resetSession: () => void; setTrackElement: (element: string) => void; - captureEvent: (eventName: string, payload: object | [] | null, group?: EventGroupProps) => 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; } export class EventTrackerStore implements IEventTrackerStore { - trackElement: string = ""; + trackElement: string | undefined = undefined; rootStore; constructor(_rootStore: RootStore) { makeObservable(this, { // properties trackElement: observable, // computed - getRequiredPayload: computed, + getRequiredProperties: computed, // actions + resetSession: action, setTrackElement: action, captureEvent: action, captureProjectEvent: action, @@ -48,12 +58,12 @@ export class EventTrackerStore implements IEventTrackerStore { /** * @description: Returns the necessary property for the event tracking */ - get getRequiredPayload() { + get getRequiredProperties() { const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace; const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails; return { - workspace_id: currentWorkspaceDetails?.id ?? "", - project_id: currentProjectDetails?.id ?? "", + workspace_id: currentWorkspaceDetails?.id, + project_id: currentProjectDetails?.id, }; } @@ -61,42 +71,74 @@ export class EventTrackerStore implements IEventTrackerStore { * @description: Set the trigger point of event. * @param {string} element */ - setTrackElement = (element: string) => { + setTrackElement = (element?: string) => { this.trackElement = element; }; - postHogGroup = (group: EventGroupProps) => { - if (group && group!.isGrouping === true) { - posthog?.group(group!.groupType!, group!.groupId!, { - date: new Date(), - workspace_id: group!.groupId, - }); - } + /** + * @description: Reset the session. + */ + resetSession = () => { + posthog?.reset(); }; - captureEvent = (eventName: string, payload: object | [] | null) => { - posthog?.capture(eventName, { - ...payload, - element: this.trackElement ?? "", + /** + * @description: Creates the workspace metric group. + * @param {string} userEmail + * @param {string} workspaceId + */ + joinWorkspaceMetricGroup = (workspaceId?: string) => { + if (!workspaceId) return; + posthog?.group(GROUP_WORKSPACE, workspaceId, { + date: new Date().toDateString(), + workspace_id: workspaceId, }); }; + /** + * @description: Captures the event. + * @param {string} eventName + * @param {any} payload + */ + captureEvent = (eventName: string, payload?: any) => { + posthog?.capture(eventName, { + ...this.getRequiredProperties, + ...payload, + element: payload?.element ?? this.trackElement, + }); + this.setTrackElement(undefined); + }; + + /** + * @description: Captures the workspace crud related events. + * @param {EventProps} props + */ + captureWorkspaceEvent = (props: EventProps) => { + const { eventName, payload } = props; + if (eventName === WORKSPACE_CREATED && 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, group } = props; - if (group) { - this.postHogGroup(group); - } + const { eventName, payload } = props; const eventPayload: any = getProjectEventPayload({ - ...this.getRequiredPayload, + ...this.getRequiredProperties, ...payload, element: payload.element ?? this.trackElement, }); posthog?.capture(eventName, eventPayload); - this.setTrackElement(""); + this.setTrackElement(undefined); }; /** @@ -104,17 +146,14 @@ export class EventTrackerStore implements IEventTrackerStore { * @param {EventProps} props */ captureCycleEvent = (props: EventProps) => { - const { eventName, payload, group } = props; - if (group) { - this.postHogGroup(group); - } + const { eventName, payload } = props; const eventPayload: any = getCycleEventPayload({ - ...this.getRequiredPayload, + ...this.getRequiredProperties, ...payload, element: payload.element ?? this.trackElement, }); posthog?.capture(eventName, eventPayload); - this.setTrackElement(""); + this.setTrackElement(undefined); }; /** @@ -122,17 +161,29 @@ export class EventTrackerStore implements IEventTrackerStore { * @param {EventProps} props */ captureModuleEvent = (props: EventProps) => { - const { eventName, payload, group } = props; - if (group) { - this.postHogGroup(group); - } + const { eventName, payload } = props; const eventPayload: any = getModuleEventPayload({ - ...this.getRequiredPayload, + ...this.getRequiredProperties, ...payload, element: payload.element ?? this.trackElement, }); posthog?.capture(eventName, eventPayload); - this.setTrackElement(""); + this.setTrackElement(undefined); + }; + + /** + * @description: Captures the project pages related events. + * @param {EventProps} props + */ + capturePageEvent = (props: EventProps) => { + const { eventName, payload } = props; + const eventPayload: any = getPageEventPayload({ + ...this.getRequiredProperties, + ...payload, + element: payload.element ?? this.trackElement, + }); + posthog?.capture(eventName, eventPayload); + this.setTrackElement(undefined); }; /** @@ -140,16 +191,29 @@ export class EventTrackerStore implements IEventTrackerStore { * @param {IssueEventProps} props */ captureIssueEvent = (props: IssueEventProps) => { - const { eventName, payload, group } = props; - if (group) { - this.postHogGroup(group); - } + const { eventName, payload } = props; const eventPayload: any = { ...getIssueEventPayload(props), - ...this.getRequiredPayload, + ...this.getRequiredProperties, state_group: this.rootStore.state.getStateById(payload.state_id)?.group ?? "", element: payload.element ?? this.trackElement, }; posthog?.capture(eventName, eventPayload); + this.setTrackElement(undefined); + }; + + /** + * @description: Captures the issue related events. + * @param {IssueEventProps} props + */ + captureProjectStateEvent = (props: EventProps) => { + const { eventName, payload } = props; + const eventPayload: any = getProjectStateEventPayload({ + ...this.getRequiredProperties, + ...payload, + element: payload.element ?? this.trackElement, + }); + posthog?.capture(eventName, eventPayload); + this.setTrackElement(undefined); }; } diff --git a/web/store/user/index.ts b/web/store/user/index.ts index b07764a05..15f9e5772 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -250,6 +250,7 @@ export class UserRootStore implements IUserRootStore { this.isUserLoggedIn = false; }); this.membership = new UserMembershipStore(this.rootStore); + this.rootStore.eventTracker.resetSession(); this.rootStore.resetOnSignout(); }); @@ -264,6 +265,7 @@ export class UserRootStore implements IUserRootStore { this.isUserLoggedIn = false; }); this.membership = new UserMembershipStore(this.rootStore); + this.rootStore.eventTracker.resetSession(); this.rootStore.resetOnSignout(); }); }