diff --git a/apps/api/plane/app/serializers/user.py b/apps/api/plane/app/serializers/user.py index 670667a853f..59101785734 100644 --- a/apps/api/plane/app/serializers/user.py +++ b/apps/api/plane/app/serializers/user.py @@ -78,6 +78,7 @@ class Meta: "is_password_autoset", "is_email_verified", "last_login_medium", + "last_login_time", ] read_only_fields = fields diff --git a/apps/api/plane/app/views/workspace/invite.py b/apps/api/plane/app/views/workspace/invite.py index 48bcf7eba30..37e46d38a77 100644 --- a/apps/api/plane/app/views/workspace/invite.py +++ b/apps/api/plane/app/views/workspace/invite.py @@ -21,7 +21,7 @@ WorkSpaceMemberSerializer, ) from plane.app.views.base import BaseAPIView -from plane.bgtasks.event_tracking_task import workspace_invite_event +from plane.bgtasks.event_tracking_task import workspace_invite_event, track_event from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.db.models import User, Workspace, WorkspaceMember, WorkspaceMemberInvite from plane.utils.cache import invalidate_cache, invalidate_cache_directly @@ -121,6 +121,19 @@ def create(self, request, slug): current_site, request.user.email, ) + track_event.delay( + user_id=request.user.id, + event_name="user_invited_to_workspace", + slug=slug, + event_properties={ + "user_id": request.user.id, + "workspace_id": workspace.id, + "workspace_slug": workspace.slug, + "invitee_role": invitation.role, + "invited_at": str(timezone.now()), + "invitee_email": invitation.email, + }, + ) return Response({"message": "Emails sent successfully"}, status=status.HTTP_200_OK) @@ -186,20 +199,22 @@ def post(self, request, slug, pk): # Set the user last_workspace_id to the accepted workspace user.last_workspace_id = workspace_invite.workspace.id user.save() + track_event.delay( + user_id=user.id, + event_name="user_joined_workspace", + slug=slug, + event_properties={ + "user_id": user.id, + "workspace_id": workspace_invite.workspace.id, + "workspace_slug": workspace_invite.workspace.slug, + "role": workspace_invite.role, + "joined_at": str(timezone.now()), + }, + ) # Delete the invitation workspace_invite.delete() - # Send event - workspace_invite_event.delay( - user=user.id if user is not None else None, - email=email, - user_agent=request.META.get("HTTP_USER_AGENT"), - ip=get_client_ip(request=request), - event_name="MEMBER_ACCEPTED", - accepted_from="EMAIL", - ) - return Response( {"message": "Workspace Invitation Accepted"}, status=status.HTTP_200_OK, @@ -252,6 +267,20 @@ def create(self, request): is_active=True, role=invitation.role ) + # Track event + track_event.delay( + user_id=request.user.id, + event_name="user_joined_workspace", + slug=invitation.workspace.slug, + event_properties={ + "user_id": request.user.id, + "workspace_id": invitation.workspace.id, + "workspace_slug": invitation.workspace.slug, + "role": invitation.role, + "joined_at": str(timezone.now()), + }, + ) + # Bulk create the user for all the workspaces WorkspaceMember.objects.bulk_create( [ diff --git a/apps/api/plane/authentication/utils/workspace_project_join.py b/apps/api/plane/authentication/utils/workspace_project_join.py index bd5ad8501b2..7acc25df4a8 100644 --- a/apps/api/plane/authentication/utils/workspace_project_join.py +++ b/apps/api/plane/authentication/utils/workspace_project_join.py @@ -1,3 +1,7 @@ +# Django imports +from django.utils import timezone + +# Module imports from plane.db.models import ( ProjectMember, ProjectMemberInvite, @@ -5,6 +9,7 @@ WorkspaceMemberInvite, ) from plane.utils.cache import invalidate_cache_directly +from plane.bgtasks.event_tracking_task import track_event def process_workspace_project_invitations(user): @@ -25,6 +30,22 @@ def process_workspace_project_invitations(user): ignore_conflicts=True, ) + [ + track_event.delay( + user_id=user.id, + event_name="user_joined_workspaces", + slug=workspace_member_invite.workspace.slug, + event_properties={ + "user_id": user.id, + "workspace_id": workspace_member_invite.workspace.id, + "workspace_slug": workspace_member_invite.workspace.slug, + "role": workspace_member_invite.role, + "joined_at": str(timezone.now()), + }, + ) + for workspace_member_invite in workspace_member_invites + ] + [ invalidate_cache_directly( path=f"/api/workspaces/{str(workspace_member_invite.workspace.slug)}/members/", diff --git a/apps/api/plane/bgtasks/event_tracking_task.py b/apps/api/plane/bgtasks/event_tracking_task.py index 0629db93af2..aa27633cef7 100644 --- a/apps/api/plane/bgtasks/event_tracking_task.py +++ b/apps/api/plane/bgtasks/event_tracking_task.py @@ -1,5 +1,7 @@ +import logging import os import uuid +from typing import Dict, Any # third party imports from celery import shared_task @@ -8,45 +10,61 @@ # module imports from plane.license.utils.instance_value import get_configuration_value from plane.utils.exception_logger import log_exception +from plane.db.models import Workspace + + +logger = logging.getLogger("plane.worker") def posthogConfiguration(): - POSTHOG_API_KEY, POSTHOG_HOST = get_configuration_value( - [ - { - "key": "POSTHOG_API_KEY", - "default": os.environ.get("POSTHOG_API_KEY", None), - }, - {"key": "POSTHOG_HOST", "default": os.environ.get("POSTHOG_HOST", None)}, - ] - ) + POSTHOG_API_KEY, POSTHOG_HOST = get_configuration_value([ + { + "key": "POSTHOG_API_KEY", + "default": os.environ.get("POSTHOG_API_KEY", None), + }, + {"key": "POSTHOG_HOST", "default": os.environ.get("POSTHOG_HOST", None)}, + ]) if POSTHOG_API_KEY and POSTHOG_HOST: return POSTHOG_API_KEY, POSTHOG_HOST else: return None, None +def preprocess_data_properties( + user_id: uuid.UUID, event_name: str, slug: str, data_properties: Dict[str, Any] +) -> Dict[str, Any]: + if event_name == "user_invited_to_workspace": + # Check if the current user is the workspace owner + workspace = Workspace.objects.get(slug=slug) + if str(workspace.owner_id) == str(user_id): + data_properties["role"] = "owner" + else: + data_properties["role"] = "admin" + + return data_properties + + @shared_task -def auth_events(user, email, user_agent, ip, event_name, medium, first_time): - try: - POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration() +def track_event(user_id: uuid.UUID, event_name: str, slug: str, event_properties: Dict[str, Any]): + POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration() - if POSTHOG_API_KEY and POSTHOG_HOST: - posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST) - posthog.capture( - email, - event=event_name, - properties={ - "event_id": uuid.uuid4().hex, - "user": {"email": email, "id": str(user)}, - "device_ctx": {"ip": ip, "user_agent": user_agent}, - "medium": medium, - "first_time": first_time, - }, - ) + if not (POSTHOG_API_KEY and POSTHOG_HOST): + logger.warning("Event tracking is not configured") + return + + try: + # preprocess the data properties for massaging the payload + # in the correct format for posthog + data_properties = preprocess_data_properties(user_id, event_name, slug, event_properties) + groups = { + "workspace": slug, + } + # track the event using posthog + posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST) + posthog.capture(distinct_id=user_id, event=event_name, properties=data_properties, groups=groups) except Exception as e: log_exception(e) - return + return False @shared_task diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx index d016913a7aa..4e4037b13cc 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx @@ -1,6 +1,6 @@ -import { useState } from "react"; import { observer } from "mobx-react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; +import { useState } from "react"; // constants import { EPageAccess, PROJECT_PAGE_TRACKER_EVENTS, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; // plane types @@ -12,11 +12,14 @@ import type { TPage } from "@plane/types"; import { Breadcrumbs, Header } from "@plane/ui"; // helpers import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +import { captureError } from "@/helpers/event-tracker.helper"; // hooks import { useProject } from "@/hooks/store/use-project"; // plane web imports +import { useWorkspace } from "@/hooks/store/use-workspace"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common"; +import { trackPageCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store"; export const PagesListHeader = observer(function PagesListHeader() { @@ -28,42 +31,54 @@ export const PagesListHeader = observer(function PagesListHeader() { const searchParams = useSearchParams(); const pageType = searchParams.get("type"); // store hooks + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); + const { data: currentUser } = useUser(); + const { currentWorkspace } = useWorkspace(); const { currentProjectDetails, loader } = useProject(); const { canCurrentUserCreatePage, createPage } = usePageStore(EPageStoreType.PROJECT); // handle page create const handleCreatePage = async () => { - setIsCreatingPage(true); + try { + setIsCreatingPage(true); - const payload: Partial = { - access: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC, - }; + const payload: Partial = { + access: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC, + }; - await createPage(payload) - .then((res) => { - captureSuccess({ - eventName: PROJECT_PAGE_TRACKER_EVENTS.create, - payload: { - id: res?.id, - state: "SUCCESS", - }, - }); - const pageId = `/${workspaceSlug}/projects/${currentProjectDetails?.id}/pages/${res?.id}`; - router.push(pageId); - }) - .catch((err) => { - captureError({ - eventName: PROJECT_PAGE_TRACKER_EVENTS.create, - payload: { - state: "ERROR", + const pageData = await createPage(payload); + if (!pageData?.id) throw new Error("Invalid response"); + if (currentWorkspace && currentUser) { + const role = getWorkspaceRoleByWorkspaceSlug(currentWorkspace.slug); + trackPageCreated( + { + id: pageData.id, + project_id: projectId, + created_at: pageData.created_at ?? "", }, - }); - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: err?.data?.error || "Page could not be created. Please try again.", - }); - }) - .finally(() => setIsCreatingPage(false)); + currentWorkspace, + currentUser, + "project", + role + ); + } + const pageId = `/${workspaceSlug}/projects/${currentProjectDetails?.id}/pages/${pageData.id}`; + router.push(pageId); + } catch (err: any) { + captureError({ + eventName: PROJECT_PAGE_TRACKER_EVENTS.create, + payload: { + state: "ERROR", + error: err?.data?.error, + }, + }); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err?.data?.error || "Page could not be created. Please try again.", + }); + } finally { + setIsCreatingPage(false); + } }; return ( diff --git a/apps/web/app/(all)/invitations/page.tsx b/apps/web/app/(all)/invitations/page.tsx index ccdea5752a5..9efdcbfdfaf 100644 --- a/apps/web/app/(all)/invitations/page.tsx +++ b/apps/web/app/(all)/invitations/page.tsx @@ -29,6 +29,7 @@ import { useAppRouter } from "@/hooks/use-app-router"; import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; // plane web services import { WorkspaceService } from "@/plane-web/services"; +import { joinWorkspaceGroup } from "@/plane-web/helpers/event-tracker-v2.helper"; const workspaceService = new WorkspaceService(); @@ -80,10 +81,9 @@ function UserInvitationsPage() { const invitation = invitations?.find((i) => i.id === firstInviteId); const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace; if (redirectWorkspace?.id) { - joinEventGroup(GROUP_WORKSPACE_TRACKER_EVENT, redirectWorkspace?.id, { - date: new Date().toDateString(), - workspace_id: redirectWorkspace?.id, - }); + if (redirectWorkspace) { + joinWorkspaceGroup(redirectWorkspace); + } } captureSuccess({ eventName: MEMBER_TRACKER_EVENTS.accept, diff --git a/apps/web/ce/components/projects/create/root.tsx b/apps/web/ce/components/projects/create/root.tsx index dce8102940b..970095af502 100644 --- a/apps/web/ce/components/projects/create/root.tsx +++ b/apps/web/ce/components/projects/create/root.tsx @@ -1,22 +1,24 @@ -import { useState } from "react"; +import { PROJECT_TRACKER_EVENTS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { observer } from "mobx-react"; +import { useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; -import { PROJECT_TRACKER_EVENTS, RANDOM_EMOJI_CODES } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; // ui import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { EFileAssetType } from "@plane/types"; -import type { IProject } from "@plane/types"; // constants import ProjectCommonAttributes from "@/components/project/create/common-attributes"; import ProjectCreateHeader from "@/components/project/create/header"; import ProjectCreateButtons from "@/components/project/create/project-create-buttons"; // hooks -import { DEFAULT_COVER_IMAGE_URL, getCoverImageType, uploadCoverImage } from "@/helpers/cover-image.helper"; -import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +import { getCoverImageType, uploadCoverImage } from "@/helpers/cover-image.helper"; +import { captureError } from "@/helpers/event-tracker.helper"; import { useProject } from "@/hooks/store/use-project"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web types +import { useWorkspace } from "@/hooks/store/use-workspace"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { trackProjectCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import type { TProject } from "@/plane-web/types/projects"; import ProjectAttributes from "./attributes"; import { getProjectFormValues } from "./utils"; @@ -36,6 +38,9 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre // store const { t } = useTranslation(); const { addProjectToFavorites, createProject, updateProject } = useProject(); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); + const { data: currentUser } = useUser(); + const { currentWorkspace } = useWorkspace(); // states const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); // form info @@ -98,12 +103,10 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre await updateCoverImageStatus(res.id, coverImage); await updateProject(workspaceSlug.toString(), res.id, { cover_image_url: coverImage }); } - captureSuccess({ - eventName: PROJECT_TRACKER_EVENTS.create, - payload: { - identifier: formData.identifier, - }, - }); + if (currentUser && currentWorkspace && res) { + const role = getWorkspaceRoleByWorkspaceSlug(currentWorkspace.slug); + trackProjectCreated({ id: res.id, created_at: res.created_at ?? "" }, currentWorkspace, currentUser, role); + } setToast({ type: TOAST_TYPE.SUCCESS, title: t("success"), @@ -114,6 +117,8 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre handleAddToFavorites(res.id); } handleNextStep(res.id); + + return res; }) .catch((err) => { try { diff --git a/apps/web/ce/helpers/event-tracker-v2.helper.ts b/apps/web/ce/helpers/event-tracker-v2.helper.ts new file mode 100644 index 00000000000..3fc52c45ad4 --- /dev/null +++ b/apps/web/ce/helpers/event-tracker-v2.helper.ts @@ -0,0 +1,280 @@ +import { posthog } from "posthog-js"; +import { EUserPermissions } from "@plane/types"; +import type { EUserProjectRoles, EUserWorkspaceRoles, IUser, IWorkspace, TUserProfile } from "@plane/types"; + +type TUserRole = "guest" | "member" | "admin" | "unknown"; + +/** + * ============================================================================ + * Utilities + * ============================================================================ + */ + +/** + * Get the user role string from the user role enum + * @param role - The user role enum + * @returns The user role string + */ + +const getUserRoleString = (role: EUserPermissions | EUserWorkspaceRoles | EUserProjectRoles | undefined): TUserRole => { + if (!role) return "unknown"; + switch (role) { + case EUserPermissions.GUEST: + return "guest"; + case EUserPermissions.MEMBER: + return "member"; + case EUserPermissions.ADMIN: + return "admin"; + default: + return "unknown"; + } +}; + +/** + * ============================================================================ + * USER IDENTIFICATION + * ============================================================================ + */ + +/** + * Identify a user in PostHog with all required person properties + * Call this after signup, login, or whenever session becomes authenticated + * + * @param user - User object from the store + * @param profile - Optional user profile object (for onboarding status, role, use_case) + */ + +export const identifyUser = (user: IUser, profile?: TUserProfile) => { + if (!posthog || !user) return; + + posthog.identify(user.id, { + id: user.id, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + display_name: user.display_name, + date_joined: user.date_joined, + last_login_medium: user.last_login_medium || "EMAIL", + timezone: user.user_timezone, + is_email_verified: user.is_email_verified, + is_onboarded: profile?.is_onboarded || false, + role: profile?.role || null, + use_case: profile?.use_case || null, + last_workspace_id: user.last_workspace_id || null, + language: profile?.language || null, + last_login_time: user.last_login_time || null, + }); +}; + +/** + * ============================================================================ + * WORKSPACE GROUP TRACKING + * ============================================================================ + */ + +/** + * Join workspace group properties in PostHog + * Call this whenever a user views a workspace (e.g., on workspace switch) + * + * @param workspace - Workspace object + */ +export const joinWorkspaceGroup = (workspace: Partial) => { + if (!posthog || !workspace.slug) return; + + posthog.group("workspace", workspace.slug, { + workspace_id: workspace.id, + workspace_name: workspace.name, + workspace_slug: workspace.slug, + workspace_size: workspace.organization_size, + created_at: workspace.created_at instanceof Date ? workspace.created_at.toISOString() : workspace.created_at, + owner_user_id: workspace.owner?.id || workspace.created_by, + is_deleted: false, + deleted_at: null, + }); +}; + +/** + * ============================================================================ + * GENERIC EVENT TRACKING + * ============================================================================ + */ + +/** + * Generic event tracking function with workspace context + * All workspace events must include workspace_id, role, and groups + * + * @param eventName - Event name in snake_case (e.g., "workspace_created") + * @param properties - Event-specific properties + * @param workspaceSlug - Workspace slug for group association + * @param role - User's role in the workspace + */ +export const trackEvent = (eventName: string, properties: Record, role: TUserRole) => { + if (!posthog) return; + + const eventProperties = { + ...properties, + role: role || "unknown", + }; + + posthog.capture(eventName, eventProperties); +}; + +/** + * ============================================================================ + * LIFECYCLE EVENTS + * ============================================================================ + */ + +/** + * Track workspace creation + * Call this immediately after a workspace is created + */ +export const trackWorkspaceCreated = ( + workspace: IWorkspace, + user: IUser, + role: EUserPermissions | EUserWorkspaceRoles | undefined, + extraProperties?: Record +) => { + const userRole = getUserRoleString(role); + joinWorkspaceGroup(workspace); + trackEvent( + "workspace_created", + { + id: user.id, + workspace_id: workspace.id, + workspace_slug: workspace.slug, + workspace_name: workspace.name, + created_at: workspace.created_at instanceof Date ? workspace.created_at.toISOString() : workspace.created_at, + ...extraProperties, + }, + userRole + ); +}; + +/** + * Track workspace deletion + */ +export const trackWorkspaceDeleted = ( + workspace: IWorkspace, + user: IUser, + role: EUserPermissions | EUserWorkspaceRoles | undefined +) => { + const userRole = getUserRoleString(role); + trackEvent( + "workspace_deleted", + { + id: user.id, + workspace_id: workspace.id, + workspace_slug: workspace.slug, + deleted_at: new Date().toISOString(), + }, + userRole + ); +}; + +/** + * ============================================================================ + * PRODUCT ACTIVATION EVENTS + * ============================================================================ + */ + +/** + * Track project creation + */ +export const trackProjectCreated = ( + project: { id: string; created_at: string | Date }, + workspace: IWorkspace, + user: IUser, + role: EUserPermissions | EUserWorkspaceRoles | undefined +) => { + const userRole = getUserRoleString(role); + trackEvent( + "project_created", + { + id: user.id, + workspace_id: workspace.id, + workspace_slug: workspace.slug, + project_id: project.id, + created_at: project.created_at instanceof Date ? project.created_at.toISOString() : project.created_at, + }, + userRole + ); +}; + +/** + * Track work item creation + */ +export const trackWorkItemCreated = ( + workItem: { id: string; type?: string; created_at: string | Date }, + project: { id: string }, + workspace: IWorkspace, + user: IUser, + role: EUserPermissions | EUserWorkspaceRoles | undefined +) => { + const userRole = getUserRoleString(role); + trackEvent( + "work_item_created", + { + id: user.id, + workspace_id: workspace.id, + workspace_slug: workspace.slug, + project_id: project.id, + work_item_id: workItem.id, + work_item_type: workItem.type, + created_at: workItem.created_at instanceof Date ? workItem.created_at.toISOString() : workItem.created_at, + }, + userRole + ); +}; + +/** + * Track cycle creation + */ +export const trackCycleCreated = ( + cycle: { id: string; length_days?: number; created_at: string | Date }, + project: { id: string }, + workspace: IWorkspace, + user: IUser, + role: EUserPermissions | EUserWorkspaceRoles | undefined +) => { + const userRole = getUserRoleString(role); + trackEvent( + "cycle_created", + { + id: user.id, + workspace_id: workspace.id, + workspace_slug: workspace.slug, + project_id: project.id, + cycle_id: cycle.id, + cycle_length_days: cycle.length_days || null, + created_at: cycle.created_at instanceof Date ? cycle.created_at.toISOString() : cycle.created_at, + }, + userRole + ); +}; + +/** + * Track page creation + */ +export const trackPageCreated = ( + page: { id: string; created_at: string | Date; project_id?: string | null }, + workspace: IWorkspace, + user: IUser, + location: "project" | "wiki" | "teamspace" | "workitem", + role: EUserPermissions | EUserWorkspaceRoles | undefined +) => { + const userRole = getUserRoleString(role); + trackEvent( + "page_created", + { + id: user.id, + workspace_id: workspace.id, + workspace_slug: workspace.slug, + page_id: page.id, + location, + project_id: page.project_id || null, + created_at: page.created_at instanceof Date ? page.created_at.toISOString() : page.created_at, + }, + userRole + ); +}; diff --git a/apps/web/core/components/cycles/modal.tsx b/apps/web/core/components/cycles/modal.tsx index 1a6855c13c7..db7cdb29be0 100644 --- a/apps/web/core/components/cycles/modal.tsx +++ b/apps/web/core/components/cycles/modal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { mutate } from "swr"; // types import { CYCLE_TRACKER_EVENTS } from "@plane/constants"; @@ -7,16 +7,19 @@ import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types" // ui import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; // hooks -import { renderFormattedPayloadDate } from "@plane/utils"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { useCycle } from "@/hooks/store/use-cycle"; import { useProject } from "@/hooks/store/use-project"; import useKeypress from "@/hooks/use-keypress"; import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { renderFormattedPayloadDate } from "@plane/utils"; // services import { CycleService } from "@/services/cycle.service"; // local imports +import { useWorkspace } from "@/hooks/store/use-workspace"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { trackCycleCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { CycleForm } from "./form"; type CycleModalProps = { @@ -39,6 +42,10 @@ export function CycleCreateUpdateModal(props: CycleModalProps) { const { createCycle, updateCycleDetails } = useCycle(); const { isMobile } = usePlatformOS(); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); + const { data: currentUser } = useUser(); + const { currentWorkspace } = useWorkspace(); + const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); const handleCreateCycle = async (payload: Partial) => { @@ -62,12 +69,16 @@ export function CycleCreateUpdateModal(props: CycleModalProps) { title: "Success!", message: "Cycle created successfully.", }); - captureSuccess({ - eventName: CYCLE_TRACKER_EVENTS.create, - payload: { - id: res.id, - }, - }); + if (currentWorkspace && currentUser) { + const role = getWorkspaceRoleByWorkspaceSlug(currentWorkspace.slug); + trackCycleCreated( + { id: res.id, created_at: res?.created_at ?? "" }, + { id: projectId }, + currentWorkspace, + currentUser, + role + ); + } }) .catch((err) => { setToast({ diff --git a/apps/web/core/components/inbox/modals/create-modal/create-root.tsx b/apps/web/core/components/inbox/modals/create-modal/create-root.tsx index 395ac60a37f..082f66a278a 100644 --- a/apps/web/core/components/inbox/modals/create-modal/create-root.tsx +++ b/apps/web/core/components/inbox/modals/create-modal/create-root.tsx @@ -1,6 +1,6 @@ -import type { FC, FormEvent } from "react"; -import { useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +import type { FormEvent } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; // plane imports import { ETabIndices, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; import type { EditorRefApi } from "@plane/editor"; @@ -9,9 +9,9 @@ import { Button } from "@plane/propel/button"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TIssue } from "@plane/types"; import { ToggleSwitch } from "@plane/ui"; -import { renderFormattedPayloadDate, getTabIndex } from "@plane/utils"; +import { getTabIndex, renderFormattedPayloadDate } from "@plane/utils"; // helpers -import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +import { captureError } from "@/helpers/event-tracker.helper"; // hooks import { useProject } from "@/hooks/store/use-project"; import { useProjectInbox } from "@/hooks/store/use-project-inbox"; @@ -26,6 +26,8 @@ import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-dup // services import { FileService } from "@/services/file.service"; // local imports +import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { trackWorkItemCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { InboxIssueDescription } from "./issue-description"; import { InboxIssueProperties } from "./issue-properties"; import { InboxIssueTitle } from "./issue-title"; @@ -69,6 +71,9 @@ export const InboxIssueCreateRoot = observer(function InboxIssueCreateRoot(props const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id; const { isMobile } = usePlatformOS(); const { getProjectById } = useProject(); + const { currentWorkspace } = useWorkspace(); + const { data: currentUser } = useUser(); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); const { t } = useTranslation(); // states const [createMore, setCreateMore] = useState(false); @@ -157,30 +162,39 @@ export const InboxIssueCreateRoot = observer(function InboxIssueCreateRoot(props await createInboxIssue(workspaceSlug, projectId, payload) .then(async (res) => { + if (!res?.issue) return; if (uploadedAssetIds.length > 0) { - await fileService.updateBulkProjectAssetsUploadStatus(workspaceSlug, projectId, res?.issue.id ?? "", { + await fileService.updateBulkProjectAssetsUploadStatus(workspaceSlug, projectId, res.issue?.id ?? "", { asset_ids: uploadedAssetIds, }); setUploadedAssetIds([]); } if (!createMore) { - router.push(`/${workspaceSlug}/projects/${projectId}/intake/?currentTab=open&inboxIssueId=${res?.issue?.id}`); + router.push(`/${workspaceSlug}/projects/${projectId}/intake/?currentTab=open&inboxIssueId=${res.issue?.id}`); handleModalClose(); } else { descriptionEditorRef?.current?.clearEditor(); setFormData(defaultIssueData); } - captureSuccess({ - eventName: WORK_ITEM_TRACKER_EVENTS.create, - payload: { - id: res?.issue?.id, - }, - }); + + if (currentWorkspace && currentUser) { + const role = getWorkspaceRoleByWorkspaceSlug(currentWorkspace.slug); + trackWorkItemCreated( + { id: res?.issue?.id ?? "", created_at: res.issue?.created_at ?? "" }, + { id: projectId }, + currentWorkspace, + currentUser, + role + ); + } + setToast({ type: TOAST_TYPE.SUCCESS, title: `Success!`, message: "Work item created successfully.", }); + + return res; }) .catch((error) => { console.error(error); diff --git a/apps/web/core/components/issues/issue-layouts/quick-add/root.tsx b/apps/web/core/components/issues/issue-layouts/quick-add/root.tsx index 862b1ee000a..a02d1dd20ad 100644 --- a/apps/web/core/components/issues/issue-layouts/quick-add/root.tsx +++ b/apps/web/core/components/issues/issue-layouts/quick-add/root.tsx @@ -1,21 +1,24 @@ -import type { FC } from "react"; -import { useEffect, useState } from "react"; +import { PlusIcon } from "lucide-react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +import type { FC } from "react"; +import { useEffect, useState } from "react"; import type { UseFormRegister } from "react-hook-form"; import { useForm } from "react-hook-form"; -import { PlusIcon } from "lucide-react"; // plane imports import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { setPromiseToast } from "@plane/propel/toast"; -import type { IProject, TIssue, EIssueLayoutTypes } from "@plane/types"; +import type { EIssueLayoutTypes, IProject, TIssue } from "@plane/types"; import { cn, createIssuePayload } from "@plane/utils"; // helpers -import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +import { captureError } from "@/helpers/event-tracker.helper"; // plane web imports import { QuickAddIssueFormRoot } from "@/plane-web/components/issues/quick-add"; // local imports +import { useWorkspace } from "@/hooks/store/use-workspace"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { trackWorkItemCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { CreateIssueToastActionItems } from "../../create-issue-toast-action-items"; export type TQuickAddIssueForm = { @@ -67,6 +70,12 @@ export const QuickAddIssueRoot = observer(function QuickAddIssueRoot(props: TQui const { workspaceSlug, projectId } = useParams(); // states const [isOpen, setIsOpen] = useState(isQuickAddOpen ?? false); + + // store hooks + const { currentWorkspace } = useWorkspace(); + const { data: currentUser } = useUser(); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); + // form info const { reset, @@ -127,20 +136,26 @@ export const QuickAddIssueRoot = observer(function QuickAddIssueRoot(props: TQui }, }); - await quickAddPromise - .then((res) => { - captureSuccess({ - eventName: WORK_ITEM_TRACKER_EVENTS.create, - payload: { id: res?.id }, - }); - }) - .catch((error) => { - captureError({ - eventName: WORK_ITEM_TRACKER_EVENTS.create, - payload: { id: payload.id }, - error: error as Error, - }); + try { + const quickAddRes = await quickAddPromise; + + if (currentWorkspace && currentUser && quickAddRes) { + const role = getWorkspaceRoleByWorkspaceSlug(currentWorkspace.slug); + trackWorkItemCreated( + { id: quickAddRes.id, created_at: quickAddRes.created_at ?? "" }, + { id: projectId.toString() }, + currentWorkspace, + currentUser, + role + ); + } + } catch (error) { + captureError({ + eventName: WORK_ITEM_TRACKER_EVENTS.create, + payload: { id: payload.id }, + error: error as Error, }); + } } }; diff --git a/apps/web/core/components/issues/issue-modal/base.tsx b/apps/web/core/components/issues/issue-modal/base.tsx index 553f9ff21b5..81b42d60702 100644 --- a/apps/web/core/components/issues/issue-modal/base.tsx +++ b/apps/web/core/components/issues/issue-modal/base.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, useState } from "react"; +import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; import { xor } from "lodash-es"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; +import { useEffect, useRef, useState } from "react"; // Plane imports import { useTranslation } from "@plane/i18n"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; @@ -23,10 +23,13 @@ import { useIssuesActions } from "@/hooks/use-issues-actions"; import { FileService } from "@/services/file.service"; const fileService = new FileService(); // local imports +import { useWorkspace } from "@/hooks/store/use-workspace"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { trackWorkItemCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { CreateIssueToastActionItems } from "../create-issue-toast-action-items"; import { DraftIssueLayout } from "./draft-issue-layout"; -import { IssueFormRoot } from "./form"; import type { IssueFormProps } from "./form"; +import { IssueFormRoot } from "./form"; import type { IssuesModalProps } from "./modal"; export const CreateUpdateIssueModalBase = observer(function CreateUpdateIssueModalBase(props: IssuesModalProps) { @@ -72,6 +75,9 @@ export const CreateUpdateIssueModalBase = observer(function CreateUpdateIssueMod const { fetchIssue } = useIssueDetail(); const { allowedProjectIds, handleCreateUpdatePropertyValues, handleCreateSubWorkItem } = useIssueModal(); const { getProjectByIdentifier } = useProject(); + const { currentWorkspace } = useWorkspace(); + const { data: currentUser } = useUser(); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); // current store details const { createIssue, updateIssue } = useIssuesActions(storeType); // derived values @@ -240,10 +246,16 @@ export const CreateUpdateIssueModalBase = observer(function CreateUpdateIssueMod /> ), }); - captureSuccess({ - eventName: WORK_ITEM_TRACKER_EVENTS.create, - payload: { id: response.id }, - }); + if (currentWorkspace && currentUser && response) { + const role = getWorkspaceRoleByWorkspaceSlug(currentWorkspace.slug); + trackWorkItemCreated( + { id: response.id, created_at: response.created_at }, + { id: payload.project_id }, + currentWorkspace, + currentUser, + role + ); + } if (!createMore) handleClose(); if (createMore && issueTitleRef) issueTitleRef?.current?.focus(); setDescription("

"); @@ -319,10 +331,13 @@ export const CreateUpdateIssueModalBase = observer(function CreateUpdateIssueMod title: t("success"), message: t("issue_updated_successfully"), }); - captureSuccess({ - eventName: WORK_ITEM_TRACKER_EVENTS.update, - payload: { id: data.id }, - }); + + if (currentWorkspace && currentUser) { + captureSuccess({ + eventName: WORK_ITEM_TRACKER_EVENTS.update, + payload: { id: data.id }, + }); + } handleClose(); } catch (error: any) { console.error(error); diff --git a/apps/web/core/components/onboarding/create-workspace.tsx b/apps/web/core/components/onboarding/create-workspace.tsx index 6ed75a7c68c..f1e3d7fee7d 100644 --- a/apps/web/core/components/onboarding/create-workspace.tsx +++ b/apps/web/core/components/onboarding/create-workspace.tsx @@ -1,12 +1,12 @@ -import { useState } from "react"; import { observer } from "mobx-react"; +import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; // constants import { ORGANIZATION_SIZE, RESTRICTED_URLS, - WORKSPACE_TRACKER_EVENTS, WORKSPACE_TRACKER_ELEMENTS, + WORKSPACE_TRACKER_EVENTS, } from "@plane/constants"; // types import { useTranslation } from "@plane/i18n"; @@ -16,10 +16,11 @@ import type { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; // ui import { CustomSelect, Input, Spinner } from "@plane/ui"; // hooks -import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +import { captureError } from "@/helpers/event-tracker.helper"; import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserProfile, useUserSettings } from "@/hooks/store/user"; +import { useUser, useUserPermissions, useUserProfile, useUserSettings } from "@/hooks/store/user"; // services +import { trackWorkspaceCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { WorkspaceService } from "@/plane-web/services"; type Props = { @@ -42,7 +43,10 @@ export const CreateWorkspace = observer(function CreateWorkspace(props: Props) { // store hooks const { updateUserProfile } = useUserProfile(); const { fetchCurrentUserSettings } = useUserSettings(); - const { createWorkspace, fetchWorkspaces } = useWorkspace(); + const { createWorkspace, fetchWorkspaces, currentWorkspace } = useWorkspace(); + const { data: currentUser } = useUser(); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); + // form info const { handleSubmit, @@ -74,11 +78,14 @@ export const CreateWorkspace = observer(function CreateWorkspace(props: Props) { title: t("workspace_creation.toast.success.title"), message: t("workspace_creation.toast.success.message"), }); - captureSuccess({ - eventName: WORKSPACE_TRACKER_EVENTS.create, - payload: { slug: formData.slug }, - }); + await fetchWorkspaces(); + const role = getWorkspaceRoleByWorkspaceSlug(workspaceResponse.slug); + + if (currentUser) { + trackWorkspaceCreated(workspaceResponse, currentUser, role); + } + await completeStep(workspaceResponse.id); }) .catch(() => { diff --git a/apps/web/core/components/onboarding/steps/workspace/create.tsx b/apps/web/core/components/onboarding/steps/workspace/create.tsx index 5c44923e142..9c837e33653 100644 --- a/apps/web/core/components/onboarding/steps/workspace/create.tsx +++ b/apps/web/core/components/onboarding/steps/workspace/create.tsx @@ -1,7 +1,7 @@ -import { useState } from "react"; +import { CircleCheck } from "lucide-react"; import { observer } from "mobx-react"; +import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; -import { CircleCheck } from "lucide-react"; // plane imports import { ORGANIZATION_SIZE, @@ -16,14 +16,15 @@ import type { IUser, IWorkspace } from "@plane/types"; import { Spinner } from "@plane/ui"; import { cn } from "@plane/utils"; // helpers -import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +import { captureError } from "@/helpers/event-tracker.helper"; // hooks import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserProfile, useUserSettings } from "@/hooks/store/user"; +import { useUser, useUserPermissions, useUserProfile, useUserSettings } from "@/hooks/store/user"; // plane-web imports import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper"; import { WorkspaceService } from "@/plane-web/services"; // local components +import { trackWorkspaceCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { CommonOnboardingHeader } from "../common"; type Props = { @@ -48,9 +49,10 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({ const { t } = useTranslation(); // store hooks const { updateUserProfile } = useUserProfile(); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); const { fetchCurrentUserSettings } = useUserSettings(); const { createWorkspace, fetchWorkspaces } = useWorkspace(); - + const { data: currentUser } = useUser(); const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false; // form info @@ -82,11 +84,14 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({ title: t("workspace_creation.toast.success.title"), message: t("workspace_creation.toast.success.message"), }); - captureSuccess({ - eventName: WORKSPACE_TRACKER_EVENTS.create, - payload: { slug: formData.slug }, - }); + await fetchWorkspaces(); + const role = getWorkspaceRoleByWorkspaceSlug(workspaceResponse.slug); + + if (currentUser) { + trackWorkspaceCreated(workspaceResponse, currentUser, role); + } + await completeStep(workspaceResponse.id); onComplete(formData.organization_size === "Just myself"); } catch { diff --git a/apps/web/core/components/pages/modals/create-page-modal.tsx b/apps/web/core/components/pages/modals/create-page-modal.tsx index f398be341af..f4ac4fe56d6 100644 --- a/apps/web/core/components/pages/modals/create-page-modal.tsx +++ b/apps/web/core/components/pages/modals/create-page-modal.tsx @@ -1,4 +1,3 @@ -import type { FC } from "react"; import { useEffect, useState } from "react"; // constants import type { EPageAccess } from "@plane/constants"; @@ -7,12 +6,16 @@ import type { TPage } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; // hooks -import { captureSuccess, captureError } from "@/helpers/event-tracker.helper"; +import { captureError } from "@/helpers/event-tracker.helper"; import { useAppRouter } from "@/hooks/use-app-router"; +import { setToast, TOAST_TYPE } from "@plane/propel/toast"; // plane web hooks import type { EPageStoreType } from "@/plane-web/hooks/store"; import { usePageStore } from "@/plane-web/hooks/store"; // local imports +import { useWorkspace } from "@/hooks/store/use-workspace"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { trackPageCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { PageForm } from "./page-form"; type Props = { @@ -45,6 +48,10 @@ export function CreatePageModal(props: Props) { const router = useAppRouter(); // store hooks const { createPage } = usePageStore(storeType); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); + const { data: currentUser } = useUser(); + const { currentWorkspace } = useWorkspace(); + const handlePageFormData = (key: T, value: TPage[T]) => setPageFormData((prev) => ({ ...prev, [key]: value })); @@ -64,12 +71,20 @@ export function CreatePageModal(props: Props) { try { const pageData = await createPage(pageFormData); if (pageData) { - captureSuccess({ - eventName: PROJECT_PAGE_TRACKER_EVENTS.create, - payload: { - id: pageData.id, - }, - }); + if (currentWorkspace && currentUser) { + const role = getWorkspaceRoleByWorkspaceSlug(currentWorkspace.slug); + trackPageCreated( + { + id: pageData.id ?? "", + project_id: projectId, + created_at: pageData.created_at ?? "", + }, + currentWorkspace, + currentUser, + "project", + role + ); + } handleStateClear(); if (redirectionEnabled) router.push(`/${workspaceSlug}/projects/${projectId}/pages/${pageData.id}`); } @@ -78,6 +93,11 @@ export function CreatePageModal(props: Props) { eventName: PROJECT_PAGE_TRACKER_EVENTS.create, error, }); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: error?.data?.error || "Page could not be created. Please try again.", + }); } }; diff --git a/apps/web/core/components/pages/pages-list-main-content.tsx b/apps/web/core/components/pages/pages-list-main-content.tsx index 58e239140b9..1bd1e2fb681 100644 --- a/apps/web/core/components/pages/pages-list-main-content.tsx +++ b/apps/web/core/components/pages/pages-list-main-content.tsx @@ -1,10 +1,9 @@ -import { useState } from "react"; import { observer } from "mobx-react"; +import { useState } from "react"; // plane imports -import { useParams, useRouter } from "next/navigation"; import { - EUserPermissionsLevel, EPageAccess, + EUserPermissionsLevel, PROJECT_PAGE_TRACKER_ELEMENTS, PROJECT_PAGE_TRACKER_EVENTS, } from "@plane/constants"; @@ -13,12 +12,15 @@ import { EmptyStateDetailed } from "@plane/propel/empty-state"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TPage, TPageNavigationTabs } from "@plane/types"; import { EUserProjectRoles } from "@plane/types"; +import { useParams, useRouter } from "next/navigation"; // components import { PageLoader } from "@/components/pages/loaders/page-loader"; -import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +import { captureClick, captureError } from "@/helpers/event-tracker.helper"; import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; // plane web hooks +import { useWorkspace } from "@/hooks/store/use-workspace"; +import { trackPageCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store"; type Props = { @@ -35,8 +37,10 @@ export const PagesListMainContent = observer(function PagesListMainContent(props const { currentProjectDetails } = useProject(); const { isAnyPageAvailable, getCurrentProjectFilteredPageIdsByTab, getCurrentProjectPageIdsByTab, loader } = usePageStore(storeType); - const { allowPermissions } = useUserPermissions(); + const { allowPermissions, getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); const { createPage } = usePageStore(EPageStoreType.PROJECT); + const { data: currentUser } = useUser(); + const { currentWorkspace } = useWorkspace(); // states const [isCreatingPage, setIsCreatingPage] = useState(false); // router @@ -60,13 +64,16 @@ export const PagesListMainContent = observer(function PagesListMainContent(props await createPage(payload) .then((res) => { - captureSuccess({ - eventName: PROJECT_PAGE_TRACKER_EVENTS.create, - payload: { - id: res?.id, - state: "SUCCESS", - }, - }); + if (currentWorkspace && currentUser && res?.id) { + const role = getWorkspaceRoleByWorkspaceSlug(currentWorkspace.slug); + trackPageCreated( + { id: res.id, created_at: res.created_at ?? "" }, + currentWorkspace, + currentUser, + "project", + role + ); + } const pageId = `/${workspaceSlug}/projects/${currentProjectDetails?.id}/pages/${res?.id}`; router.push(pageId); }) diff --git a/apps/web/core/components/workspace/create-workspace-form.tsx b/apps/web/core/components/workspace/create-workspace-form.tsx index 028feddf856..fa266209c3b 100644 --- a/apps/web/core/components/workspace/create-workspace-form.tsx +++ b/apps/web/core/components/workspace/create-workspace-form.tsx @@ -1,7 +1,3 @@ -import type { Dispatch, SetStateAction } from "react"; -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import { Controller, useForm } from "react-hook-form"; import { ORGANIZATION_SIZE, RESTRICTED_URLS, @@ -12,13 +8,19 @@ import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { IWorkspace } from "@plane/types"; +import { observer } from "mobx-react"; +import type { Dispatch, SetStateAction } from "react"; +import { useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; // ui import { CustomSelect, Input } from "@plane/ui"; // hooks -import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +import { captureError } from "@/helpers/event-tracker.helper"; import { useWorkspace } from "@/hooks/store/use-workspace"; import { useAppRouter } from "@/hooks/use-app-router"; // services +import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { trackWorkspaceCreated } from "@/plane-web/helpers/event-tracker-v2.helper"; import { WorkspaceService } from "@/plane-web/services"; type Props = { @@ -57,6 +59,9 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props: const router = useAppRouter(); // store hooks const { createWorkspace } = useWorkspace(); + const { data: currentUser } = useUser(); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); + // form info const { handleSubmit, @@ -74,10 +79,10 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props: try { const workspaceResponse = await createWorkspace(formData); - captureSuccess({ - eventName: WORKSPACE_TRACKER_EVENTS.create, - payload: { slug: formData.slug }, - }); + if (currentUser) { + const role = getWorkspaceRoleByWorkspaceSlug(workspaceResponse.slug); + trackWorkspaceCreated(workspaceResponse, currentUser, role); + } setToast({ type: TOAST_TYPE.SUCCESS, title: t("workspace_creation.toast.success.title"), diff --git a/apps/web/core/components/workspace/delete-workspace-form.tsx b/apps/web/core/components/workspace/delete-workspace-form.tsx index 40efa73c98e..4c37e9686dc 100644 --- a/apps/web/core/components/workspace/delete-workspace-form.tsx +++ b/apps/web/core/components/workspace/delete-workspace-form.tsx @@ -1,7 +1,6 @@ -import React from "react"; +import { AlertTriangle } from "lucide-react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; -import { AlertTriangle } from "lucide-react"; // types import { WORKSPACE_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; @@ -11,11 +10,12 @@ import type { IWorkspace } from "@plane/types"; // ui import { Input } from "@plane/ui"; // hooks -import { cn } from "@plane/utils"; -import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +import { captureError } from "@/helpers/event-tracker.helper"; import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserSettings } from "@/hooks/store/user"; +import { useUser, useUserPermissions, useUserSettings } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; +import { trackWorkspaceDeleted } from "@/plane-web/helpers/event-tracker-v2.helper"; +import { cn } from "@plane/utils"; type Props = { data: IWorkspace | null; @@ -36,6 +36,9 @@ export const DeleteWorkspaceForm = observer(function DeleteWorkspaceForm(props: const { t } = useTranslation(); const { getWorkspaceRedirectionUrl } = useWorkspace(); const { fetchCurrentUserSettings } = useUserSettings(); + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); + const { data: currentUser } = useUser(); + // form info const { control, @@ -64,10 +67,13 @@ export const DeleteWorkspaceForm = observer(function DeleteWorkspaceForm(props: await fetchCurrentUserSettings(); handleClose(); router.push(getWorkspaceRedirectionUrl()); - captureSuccess({ - eventName: WORKSPACE_TRACKER_EVENTS.delete, - payload: { slug: data.slug }, - }); + + const role = getWorkspaceRoleByWorkspaceSlug(data.slug); + + if (currentUser) { + trackWorkspaceDeleted(data, currentUser, role); + } + setToast({ type: TOAST_TYPE.SUCCESS, title: t("workspace_settings.settings.general.delete_modal.success_title"), diff --git a/apps/web/core/lib/posthog-provider.tsx b/apps/web/core/lib/posthog-provider.tsx index cb3d691a98d..8565b75aecc 100644 --- a/apps/web/core/lib/posthog-provider.tsx +++ b/apps/web/core/lib/posthog-provider.tsx @@ -1,18 +1,15 @@ -import type { ReactNode } from "react"; -import { lazy, Suspense, useEffect, useCallback, useRef, useState } from "react"; import { PostHogProvider as PHProvider } from "@posthog/react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; import posthog from "posthog-js"; +import type { ReactNode } from "react"; +import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; // constants -import { GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants"; -// helpers -import { getUserRole } from "@plane/utils"; // hooks -import { captureClick, joinEventGroup } from "@/helpers/event-tracker.helper"; +import { captureClick } from "@/helpers/event-tracker.helper"; import { useInstance } from "@/hooks/store/use-instance"; +import { useUser, useUserProfile } from "@/hooks/store/user"; import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { identifyUser, joinWorkspaceGroup } from "@/plane-web/helpers/event-tracker-v2.helper"; // dynamic imports const PostHogPageView = lazy(function PostHogPageView() { return import("@/lib/posthog-view"); @@ -25,42 +22,28 @@ export interface IPosthogWrapper { const PostHogProvider = observer(function PostHogProvider(props: IPosthogWrapper) { const { children } = props; const { data: user } = useUser(); - const { currentWorkspace } = useWorkspace(); + const { data: profile } = useUserProfile(); const { instance } = useInstance(); - const { workspaceSlug, projectId } = useParams(); - const { getWorkspaceRoleByWorkspaceSlug, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions(); + const { currentWorkspace } = useWorkspace(); // refs const isInitializedRef = useRef(false); // states const [hydrated, setHydrated] = useState(false); - // derived values - const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId( - workspaceSlug?.toString(), - projectId?.toString() - ); - const currentWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug?.toString()); + const is_telemetry_enabled = instance?.is_telemetry_enabled || false; const is_posthog_enabled = process.env.VITE_POSTHOG_KEY && process.env.VITE_POSTHOG_HOST && is_telemetry_enabled; useEffect(() => { - if (user && hydrated) { - // Identify sends an event, so you want may want to limit how often you call it - posthog?.identify(user.email, { - id: user.id, - first_name: user.first_name, - last_name: user.last_name, - email: user.email, - workspace_role: currentWorkspaceRole ? getUserRole(currentWorkspaceRole) : undefined, - project_role: currentProjectRole ? getUserRole(currentProjectRole) : undefined, - }); - if (currentWorkspace) { - joinEventGroup(GROUP_WORKSPACE_TRACKER_EVENT, currentWorkspace?.id, { - date: new Date().toDateString(), - workspace_id: currentWorkspace?.id, - }); - } + if (user && profile && hydrated && is_posthog_enabled) { + identifyUser(user, profile); + } + }, [user, profile, hydrated, is_posthog_enabled]); + + useEffect(() => { + if (currentWorkspace && hydrated && is_posthog_enabled) { + joinWorkspaceGroup(currentWorkspace); } - }, [user, currentProjectRole, currentWorkspaceRole, currentWorkspace, hydrated]); + }, [currentWorkspace, hydrated, is_posthog_enabled]); useEffect(() => { if (isInitializedRef.current) return; // prevent multiple initializations diff --git a/apps/web/ee/helpers/event-tracker-v2.helper.ts b/apps/web/ee/helpers/event-tracker-v2.helper.ts new file mode 100644 index 00000000000..04761b8380e --- /dev/null +++ b/apps/web/ee/helpers/event-tracker-v2.helper.ts @@ -0,0 +1 @@ +export * from "ce/helpers/event-tracker-v2.helper"; diff --git a/packages/types/src/users.ts b/packages/types/src/users.ts index 9278996a7ab..e6394e9658d 100644 --- a/packages/types/src/users.ts +++ b/packages/types/src/users.ts @@ -43,6 +43,7 @@ export interface IUser extends IUserLite { user_timezone: string; username: string; last_login_medium: TLoginMediums; + last_login_time: string | null; theme: IUserTheme; }