fix: track events updated, extra parameters added, added events for issues, pages, states, cycles (#2875)

* fix: event tracking method updated to store, chore: updated and added events for workspace, projects and create issue

* fix: posthog auth event tracking

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Ramesh Kumar Chandra 2023-11-25 21:26:26 +05:30 committed by GitHub
parent 2980c7b00d
commit 11d57a5bf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 662 additions and 305 deletions

View File

@ -35,7 +35,7 @@ from plane.settings.redis import redis_instance
from plane.bgtasks.magic_link_code_task import magic_link from plane.bgtasks.magic_link_code_task import magic_link
from plane.license.models import InstanceConfiguration from plane.license.models import InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.event_tracking_task import auth_events
def get_tokens_for_user(user): def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user) refresh = RefreshToken.for_user(user)
@ -162,30 +162,17 @@ class SignUpEndpoint(BaseAPIView):
workspace_member_invites.delete() workspace_member_invites.delete()
project_member_invites.delete() project_member_invites.delete()
try: # Send event
# Send Analytics if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
if settings.ANALYTICS_BASE_API: auth_events.delay(
_ = requests.post( user=user.id,
settings.ANALYTICS_BASE_API, email=email,
headers={ user_agent=request.META.get("HTTP_USER_AGENT"),
"Content-Type": "application/json", ip=request.META.get("REMOTE_ADDR"),
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY, event_name="SIGN_IN",
}, medium="EMAIL",
json={ first_time=True
"event_id": uuid.uuid4().hex, )
"event_data": {
"medium": "email",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_UP",
},
)
except RequestException as e:
capture_exception(e)
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
@ -305,30 +292,17 @@ class SignInEndpoint(BaseAPIView):
# Delete all the invites # Delete all the invites
workspace_member_invites.delete() workspace_member_invites.delete()
project_member_invites.delete() project_member_invites.delete()
try: # Send event
# Send Analytics if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
if settings.ANALYTICS_BASE_API: auth_events.delay(
_ = requests.post( user=user.id,
settings.ANALYTICS_BASE_API, email=email,
headers={ user_agent=request.META.get("HTTP_USER_AGENT"),
"Content-Type": "application/json", ip=request.META.get("REMOTE_ADDR"),
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY, event_name="SIGN_IN",
}, medium="EMAIL",
json={ first_time=False
"event_id": uuid.uuid4().hex, )
"event_data": {
"medium": "email",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_IN",
},
)
except RequestException as e:
capture_exception(e)
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
data = { data = {
@ -476,32 +450,25 @@ class MagicSignInEndpoint(BaseAPIView):
if str(token) == str(user_token): if str(token) == str(user_token):
if User.objects.filter(email=email).exists(): if User.objects.filter(email=email).exists():
user = User.objects.get(email=email) user = User.objects.get(email=email)
try: if not user.is_active:
# Send event to Jitsu for tracking return Response(
if settings.ANALYTICS_BASE_API: {
_ = requests.post( "error": "Your account has been deactivated. Please contact your site administrator."
settings.ANALYTICS_BASE_API, },
headers={ status=status.HTTP_403_FORBIDDEN,
"Content-Type": "application/json", )
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY, # Send event
}, if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
json={ auth_events.delay(
"event_id": uuid.uuid4().hex, user=user.id,
"event_data": { email=email,
"medium": "code", user_agent=request.META.get("HTTP_USER_AGENT"),
}, ip=request.META.get("REMOTE_ADDR"),
"user": {"email": email, "id": str(user.id)}, event_name="SIGN_IN",
"device_ctx": { medium="MAGIC_LINK",
"ip": request.META.get("REMOTE_ADDR"), first_time=False
"user_agent": request.META.get( )
"HTTP_USER_AGENT"
),
},
"event_type": "SIGN_IN",
},
)
except RequestException as e:
capture_exception(e)
else: else:
user = User.objects.create( user = User.objects.create(
email=email, email=email,
@ -509,32 +476,18 @@ class MagicSignInEndpoint(BaseAPIView):
password=make_password(uuid.uuid4().hex), password=make_password(uuid.uuid4().hex),
is_password_autoset=True, is_password_autoset=True,
) )
try:
# Send event to Jitsu for tracking # Send event
if settings.ANALYTICS_BASE_API: if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
_ = requests.post( auth_events.delay(
settings.ANALYTICS_BASE_API, user=user.id,
headers={ email=email,
"Content-Type": "application/json", user_agent=request.META.get("HTTP_USER_AGENT"),
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY, ip=request.META.get("REMOTE_ADDR"),
}, event_name="SIGN_IN",
json={ medium="MAGIC_LINK",
"event_id": uuid.uuid4().hex, first_time=True
"event_data": { )
"medium": "code",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get(
"HTTP_USER_AGENT"
),
},
"event_type": "SIGN_UP",
},
)
except RequestException as e:
capture_exception(e)
user.is_active = True user.is_active = True
user.last_active = timezone.now() user.last_active = timezone.now()

View File

@ -29,6 +29,7 @@ from plane.db.models import (
ProjectMemberInvite, ProjectMemberInvite,
ProjectMember, ProjectMember,
) )
from plane.bgtasks.event_tracking_task import auth_events
from .base import BaseAPIView from .base import BaseAPIView
from plane.license.models import InstanceConfiguration from plane.license.models import InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
@ -285,29 +286,18 @@ class OauthEndpoint(BaseAPIView):
"last_login_at": timezone.now(), "last_login_at": timezone.now(),
}, },
) )
try:
if settings.ANALYTICS_BASE_API: # Send event
_ = requests.post( if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
settings.ANALYTICS_BASE_API, auth_events.delay(
headers={ user=user.id,
"Content-Type": "application/json", email=email,
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY, user_agent=request.META.get("HTTP_USER_AGENT"),
}, ip=request.META.get("REMOTE_ADDR"),
json={ event_name="SIGN_IN",
"event_id": uuid.uuid4().hex, medium=medium.upper(),
"event_data": { first_time=False
"medium": f"oauth-{medium}", )
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_IN",
},
)
except RequestException as e:
capture_exception(e)
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
@ -428,29 +418,17 @@ class OauthEndpoint(BaseAPIView):
workspace_member_invites.delete() workspace_member_invites.delete()
project_member_invites.delete() project_member_invites.delete()
try: # Send event
if settings.ANALYTICS_BASE_API: if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST:
_ = requests.post( auth_events.delay(
settings.ANALYTICS_BASE_API, user=user.id,
headers={ email=email,
"Content-Type": "application/json", user_agent=request.META.get("HTTP_USER_AGENT"),
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY, ip=request.META.get("REMOTE_ADDR"),
}, event_name="SIGN_IN",
json={ medium=medium.upper(),
"event_id": uuid.uuid4().hex, first_time=True
"event_data": { )
"medium": f"oauth-{medium}",
},
"user": {"email": email, "id": str(user.id)},
"device_ctx": {
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
},
"event_type": "SIGN_UP",
},
)
except RequestException as e:
capture_exception(e)
SocialLoginConnection.objects.update_or_create( SocialLoginConnection.objects.update_or_create(
medium=medium, medium=medium,

View File

@ -0,0 +1,31 @@
import uuid
from posthog import Posthog
from django.conf import settings
#third party imports
from celery import shared_task
from sentry_sdk import capture_exception
@shared_task
def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
print(user, email, user_agent, ip, event_name, medium, first_time)
try:
posthog = Posthog(settings.POSTHOG_API_KEY, host=settings.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
}
)
except Exception as e:
capture_exception(e)

View File

@ -322,4 +322,8 @@ ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False) ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
# Use Minio settings # Use Minio settings
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
# Posthog settings
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)

View File

@ -36,3 +36,4 @@ scout-apm==2.26.1
openpyxl==3.1.2 openpyxl==3.1.2
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
dj-database-url==2.1.0 dj-database-url==2.1.0
posthog==3.0.2

View File

@ -14,6 +14,7 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
const { const {
commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal }, commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal },
trackEvent: { setTrackElement }
} = useMobxStore(); } = useMobxStore();
return ( return (
@ -22,6 +23,7 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
<Command.Item <Command.Item
onSelect={() => { onSelect={() => {
closePalette(); closePalette();
setTrackElement("COMMAND_PALETTE")
toggleCreateCycleModal(true); toggleCreateCycleModal(true);
}} }}
className="focus:outline-none" className="focus:outline-none"

View File

@ -62,6 +62,7 @@ export const CommandModal: React.FC = observer(() => {
toggleCreateIssueModal, toggleCreateIssueModal,
toggleCreateProjectModal, toggleCreateProjectModal,
}, },
trackEvent: { setTrackElement }
} = useMobxStore(); } = useMobxStore();
// router // router
@ -273,6 +274,7 @@ export const CommandModal: React.FC = observer(() => {
<Command.Item <Command.Item
onSelect={() => { onSelect={() => {
closePalette(); closePalette();
setTrackElement("COMMAND_PALETTE");
toggleCreateIssueModal(true); toggleCreateIssueModal(true);
}} }}
className="focus:bg-custom-background-80" className="focus:bg-custom-background-80"
@ -290,6 +292,7 @@ export const CommandModal: React.FC = observer(() => {
<Command.Item <Command.Item
onSelect={() => { onSelect={() => {
closePalette(); closePalette();
setTrackElement("COMMAND_PALETTE");
toggleCreateProjectModal(true); toggleCreateProjectModal(true);
}} }}
className="focus:outline-none" className="focus:outline-none"

View File

@ -33,6 +33,7 @@ export const CommandPalette: FC = observer(() => {
commandPalette, commandPalette,
theme: { toggleSidebar }, theme: { toggleSidebar },
user: { currentUser }, user: { currentUser },
trackEvent: { setTrackElement }
} = useMobxStore(); } = useMobxStore();
const { const {
toggleCommandPaletteModal, toggleCommandPaletteModal,
@ -112,8 +113,10 @@ export const CommandPalette: FC = observer(() => {
} }
} else { } else {
if (keyPressed === "c") { if (keyPressed === "c") {
setTrackElement("SHORTCUT_KEY");
toggleCreateIssueModal(true); toggleCreateIssueModal(true);
} else if (keyPressed === "p") { } else if (keyPressed === "p") {
setTrackElement("SHORTCUT_KEY");
toggleCreateProjectModal(true); toggleCreateProjectModal(true);
} else if (keyPressed === "h") { } else if (keyPressed === "h") {
toggleShortcutModal(true); toggleShortcutModal(true);

View File

@ -33,7 +33,7 @@ export interface ICyclesBoardCard {
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => { export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
const { cycle, workspaceSlug, projectId } = props; const { cycle, workspaceSlug, projectId } = props;
// store // store
const { cycle: cycleStore } = useMobxStore(); const { cycle: cycleStore, trackEvent: { setTrackElement } } = useMobxStore();
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// states // states
@ -119,6 +119,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setDeleteModal(true); setDeleteModal(true);
setTrackElement("CYCLE_PAGE_BOARD_LAYOUT");
}; };
const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => { const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => {
@ -252,7 +253,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
<CustomMenu.MenuItem onClick={handleDeleteCycle}> <CustomMenu.MenuItem onClick={handleDeleteCycle}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
<span>Delete module</span> <span>Delete cycle</span>
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</> </>

View File

@ -38,7 +38,7 @@ type TCyclesListItem = {
export const CyclesListItem: FC<TCyclesListItem> = (props) => { export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const { cycle, workspaceSlug, projectId } = props; const { cycle, workspaceSlug, projectId } = props;
// store // store
const { cycle: cycleStore } = useMobxStore(); const { cycle: cycleStore, trackEvent: { setTrackElement } } = useMobxStore();
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// states // states
@ -119,6 +119,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setDeleteModal(true); setDeleteModal(true);
setTrackElement("CYCLE_PAGE_LIST_LAYOUT");
}; };
const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => { const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => {

View File

@ -19,7 +19,7 @@ export interface ICyclesList {
export const CyclesList: FC<ICyclesList> = observer((props) => { export const CyclesList: FC<ICyclesList> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId } = props; const { cycles, filter, workspaceSlug, projectId } = props;
const { commandPalette: commandPaletteStore } = useMobxStore(); const { commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
return ( return (
<> <>
@ -57,7 +57,11 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
<button <button
type="button" type="button"
className="text-custom-primary-100 text-sm outline-none" className="text-custom-primary-100 text-sm outline-none"
onClick={() => commandPaletteStore.toggleCreateCycleModal(true)} onClick={() => {
setTrackElement("CYCLES_PAGE_EMPTY-STATE");
commandPaletteStore.toggleCreateCycleModal(true)
}
}
> >
Create a new cycle Create a new cycle
</button> </button>

View File

@ -24,7 +24,7 @@ interface ICycleDelete {
export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => { export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props; const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
// store // store
const { cycle: cycleStore } = useMobxStore(); const { cycle: cycleStore, trackEvent: { postHogEventTracker } } = useMobxStore();
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// states // states
@ -36,11 +36,25 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
setLoader(true); setLoader(true);
if (cycle?.id) if (cycle?.id)
try { try {
await cycleStore.removeCycle(workspaceSlug, projectId, cycle?.id); await cycleStore.removeCycle(workspaceSlug, projectId, cycle?.id).then((res) => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Cycle deleted successfully.", message: "Cycle deleted successfully.",
});
postHogEventTracker(
"CYCLE_DELETE",
{
state: "SUCCESS"
}
);
}).catch((error) => {
postHogEventTracker(
"CYCLE_DELETE",
{
state: "FAILED"
}
);
}); });
if (cycleId) router.replace(`/${workspaceSlug}/projects/${projectId}/cycles`); if (cycleId) router.replace(`/${workspaceSlug}/projects/${projectId}/cycles`);

View File

@ -24,7 +24,7 @@ const cycleService = new CycleService();
export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => { export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const { isOpen, handleClose, data, workspaceSlug, projectId } = props; const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
// store // store
const { cycle: cycleStore } = useMobxStore(); const { cycle: cycleStore, trackEvent: { postHogEventTracker } } = useMobxStore();
// states // states
const [activeProject, setActiveProject] = useState<string>(projectId); const [activeProject, setActiveProject] = useState<string>(projectId);
// toast // toast
@ -35,12 +35,19 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const selectedProjectId = payload.project ?? projectId.toString(); const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore await cycleStore
.createCycle(workspaceSlug, selectedProjectId, payload) .createCycle(workspaceSlug, selectedProjectId, payload)
.then(() => { .then((res) => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Cycle created successfully.", message: "Cycle created successfully.",
}); });
postHogEventTracker(
"CYCLE_CREATE",
{
...res,
state: "SUCCESS",
}
);
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
@ -48,6 +55,12 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
title: "Error!", title: "Error!",
message: "Error in creating cycle. Please try again.", message: "Error in creating cycle. Please try again.",
}); });
postHogEventTracker(
"CYCLE_CREATE",
{
state: "FAILED",
}
);
}); });
}; };

View File

@ -52,7 +52,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query; const { workspaceSlug, projectId, peekCycle } = router.query;
const { cycle: cycleDetailsStore } = useMobxStore(); const { cycle: cycleDetailsStore, trackEvent: { setTrackElement, postHogEventTracker } } = useMobxStore();
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined; const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
@ -74,8 +74,27 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
cycleService cycleService
.patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data) .patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data)
.then(() => mutate(CYCLE_DETAILS(cycleId as string))) .then((res) => {
.catch((e) => console.log(e)); mutate(CYCLE_DETAILS(cycleId as string));
postHogEventTracker(
"CYCLE_UPDATE",
{
...res,
state: "SUCCESS"
}
);
}
)
.catch((e) => {
console.log(e);
postHogEventTracker(
"CYCLE_UPDATE",
{
state: "FAILED"
}
);
}
);
}; };
const handleCopyText = () => { const handleCopyText = () => {
@ -284,10 +303,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
cycleDetails.total_issues === 0 cycleDetails.total_issues === 0
? "0 Issue" ? "0 Issue"
: cycleDetails.total_issues === cycleDetails.completed_issues : cycleDetails.total_issues === cycleDetails.completed_issues
? cycleDetails.total_issues > 1 ? cycleDetails.total_issues > 1
? `${cycleDetails.total_issues}` ? `${cycleDetails.total_issues}`
: `${cycleDetails.total_issues}` : `${cycleDetails.total_issues}`
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
return ( return (
<> <>
@ -317,7 +336,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</button> </button>
{!isCompleted && ( {!isCompleted && (
<CustomMenu width="lg" placement="bottom-end" ellipsis> <CustomMenu width="lg" placement="bottom-end" ellipsis>
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}> <CustomMenu.MenuItem onClick={() => {
setTrackElement("CYCLE_PAGE_SIDEBAR");
setCycleDeleteModal(true)
}
}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
<span>Delete cycle</span> <span>Delete cycle</span>
@ -367,6 +390,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date} value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
onChange={(val) => { onChange={(val) => {
if (val) { if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_START_DATE_BUTTON");
handleStartDateChange(val); handleStartDateChange(val);
} }
}} }}
@ -402,6 +426,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date} value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
onChange={(val) => { onChange={(val) => {
if (val) { if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_END_DATE_BUTTON");
handleEndDateChange(val); handleEndDateChange(val);
} }
}} }}

View File

@ -40,7 +40,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
projectLabel: { projectLabels }, projectLabel: { projectLabels },
projectState: projectStateStore, projectState: projectStateStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
cycleIssuesFilter: { issueFilters, updateFilters }, cycleIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
@ -190,7 +190,11 @@ export const CycleIssuesHeader: React.FC = observer(() => {
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics Analytics
</Button> </Button>
<Button onClick={() => commandPaletteStore.toggleCreateIssueModal(true)} size="sm" prependIcon={<Plus />}> <Button onClick={() => {
setTrackElement("CYCLE_PAGE_HEADER")
commandPaletteStore.toggleCreateIssueModal(true)
}
} size="sm" prependIcon={<Plus />}>
Add Issue Add Issue
</Button> </Button>
<button <button

View File

@ -14,7 +14,7 @@ export const CyclesHeader: FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const { currentProjectDetails } = projectStore; const { currentProjectDetails } = projectStore;
return ( return (
@ -51,7 +51,11 @@ export const CyclesHeader: FC = observer(() => {
variant="primary" variant="primary"
size="sm" size="sm"
prependIcon={<Plus />} prependIcon={<Plus />}
onClick={() => commandPaletteStore.toggleCreateCycleModal(true)} onClick={() => {
setTrackElement("CYCLES_PAGE_HEADER");
commandPaletteStore.toggleCreateCycleModal(true);
}
}
> >
Add Cycle Add Cycle
</Button> </Button>

View File

@ -38,6 +38,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
projectLabel: { projectLabels }, projectLabel: { projectLabels },
moduleIssuesFilter: { issueFilters, updateFilters }, moduleIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
@ -190,7 +191,11 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics Analytics
</Button> </Button>
<Button onClick={() => commandPaletteStore.toggleCreateIssueModal(true)} size="sm" prependIcon={<Plus />}> <Button onClick={() => {
setTrackElement("MODULE_PAGE_HEADER")
commandPaletteStore.toggleCreateIssueModal(true)
}
} size="sm" prependIcon={<Plus />}>
Add Issue Add Issue
</Button> </Button>
<button <button

View File

@ -31,6 +31,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
projectState: projectStateStore, projectState: projectStateStore,
inbox: inboxStore, inbox: inboxStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
// issue filters // issue filters
projectIssuesFilter: { issueFilters, updateFilters }, projectIssuesFilter: { issueFilters, updateFilters },
projectIssues: {}, projectIssues: {},
@ -198,7 +199,11 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics Analytics
</Button> </Button>
<Button onClick={() => commandPaletteStore.toggleCreateIssueModal(true)} size="sm" prependIcon={<Plus />}> <Button onClick={() => {
setTrackElement("PROJECT_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true)
}
} size="sm" prependIcon={<Plus />}>
Add Issue Add Issue
</Button> </Button>
</div> </div>

View File

@ -11,7 +11,7 @@ export const ProjectsHeader = observer(() => {
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: {setTrackElement} } = useMobxStore();
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : []; const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : [];
@ -41,7 +41,10 @@ export const ProjectsHeader = observer(() => {
</div> </div>
)} )}
<Button prependIcon={<Plus />} size="sm" onClick={() => commandPaletteStore.toggleCreateProjectModal(true)}> <Button prependIcon={<Plus />} size="sm" onClick={() => {
setTrackElement("PROJECTS_PAGE_HEADER");
commandPaletteStore.toggleCreateProjectModal(true)
}}>
Add Project Add Project
</Button> </Button>
</div> </div>

View File

@ -8,9 +8,11 @@ import githubWhiteImage from "/public/logos/github-white.png";
// components // components
import { ProductUpdatesModal } from "components/common"; import { ProductUpdatesModal } from "components/common";
import { Breadcrumbs } from "@plane/ui"; import { Breadcrumbs } from "@plane/ui";
import { useMobxStore } from "lib/mobx/store-provider";
export const WorkspaceDashboardHeader = () => { export const WorkspaceDashboardHeader = () => {
const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false); const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false);
const { trackEvent: { postHogEventTracker } } = useMobxStore();
// theme // theme
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();

View File

@ -46,7 +46,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, inboxId } = router.query; const { workspaceSlug, projectId, inboxId } = router.query;
const { inboxIssueDetails: inboxIssueDetailsStore } = useMobxStore(); const { inboxIssueDetails: inboxIssueDetailsStore, trackEvent: { postHogEventTracker } } = useMobxStore();
const { const {
control, control,
@ -70,6 +70,21 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
router.push(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}`); router.push(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}`);
handleClose(); handleClose();
} else reset(defaultValues); } else reset(defaultValues);
postHogEventTracker(
"ISSUE_CREATE",
{
...res,
state: "SUCCESS"
}
);
}).catch((error) => {
console.log(error);
postHogEventTracker(
"ISSUE_CREATE",
{
state: "FAILED"
}
);
}); });
}; };
@ -172,7 +187,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
onClick={() => setCreateMore((prevData) => !prevData)} onClick={() => setCreateMore((prevData) => !prevData)}
> >
<span className="text-xs">Create more</span> <span className="text-xs">Create more</span>
<ToggleSwitch value={createMore} onChange={() => {}} size="md" /> <ToggleSwitch value={createMore} onChange={() => { }} size="md" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="neutral-primary" size="sm" onClick={() => handleClose()}> <Button variant="neutral-primary" size="sm" onClick={() => handleClose()}>

View File

@ -16,7 +16,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
const { const {
@ -190,7 +190,10 @@ export const JiraGetImportDetail: React.FC = observer(() => {
<div> <div>
<button <button
type="button" type="button"
onClick={() => commandPaletteStore.toggleCreateProjectModal(true)} onClick={() => {
setTrackElement("JIRA_IMPORT_DETAIL");
commandPaletteStore.toggleCreateProjectModal(true)
}}
className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-custom-text-200" className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-custom-text-200"
> >
<Plus className="h-4 w-4 text-custom-text-200" /> <Plus className="h-4 w-4 text-custom-text-200" />

View File

@ -26,7 +26,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
// states // states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const { cycleIssue: cycleIssueStore, commandPalette: commandPaletteStore } = useMobxStore(); const { cycleIssue: cycleIssueStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -62,7 +62,10 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
primaryButton={{ primaryButton={{
text: "New issue", text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />, icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => commandPaletteStore.toggleCreateIssueModal(true), onClick: () => {
setTrackElement("CYCLE_EMPTY_STATE")
commandPaletteStore.toggleCreateIssueModal(true)
}
}} }}
secondaryButton={ secondaryButton={
<Button <Button

View File

@ -15,7 +15,7 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { commandPalette: commandPaletteStore, project: projectStore } = useMobxStore(); const { commandPalette: commandPaletteStore, project: projectStore, trackEvent: { setTrackElement } } = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
@ -29,7 +29,10 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
primaryButton={{ primaryButton={{
icon: <Plus className="h-4 w-4" />, icon: <Plus className="h-4 w-4" />,
text: "New Project", text: "New Project",
onClick: () => commandPaletteStore.toggleCreateProjectModal(true), onClick: () => {
setTrackElement("ALL_ISSUES_EMPTY_STATE")
commandPaletteStore.toggleCreateProjectModal(true)
},
}} }}
/> />
) : ( ) : (
@ -40,7 +43,10 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
primaryButton={{ primaryButton={{
text: "New issue", text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />, icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => commandPaletteStore.toggleCreateIssueModal(true), onClick: () => {
setTrackElement("ALL_ISSUES_EMPTY_STATE")
commandPaletteStore.toggleCreateIssueModal(true)
}
}} }}
/> />
)} )}

View File

@ -22,7 +22,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
// states // states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
const { moduleIssue: moduleIssueStore, commandPalette: commandPaletteStore } = useMobxStore(); const { moduleIssue: moduleIssueStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -58,7 +58,10 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
primaryButton={{ primaryButton={{
text: "New issue", text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />, icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => commandPaletteStore.toggleCreateIssueModal(true), onClick: () => {
setTrackElement("MODULE_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true)
}
}} }}
secondaryButton={ secondaryButton={
<Button <Button

View File

@ -8,7 +8,7 @@ import { EmptyState } from "components/common";
import emptyIssue from "public/empty-state/issue.svg"; import emptyIssue from "public/empty-state/issue.svg";
export const ProjectViewEmptyState: React.FC = observer(() => { export const ProjectViewEmptyState: React.FC = observer(() => {
const { commandPalette: commandPaletteStore } = useMobxStore(); const { commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
return ( return (
<div className="h-full w-full grid place-items-center"> <div className="h-full w-full grid place-items-center">
@ -19,7 +19,10 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
primaryButton={{ primaryButton={{
text: "New issue", text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />, icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => commandPaletteStore.toggleCreateIssueModal(true), onClick: () => {
setTrackElement("VIEW_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true)
}
}} }}
/> />
</div> </div>

View File

@ -8,7 +8,7 @@ import { EmptyState } from "components/common";
import emptyIssue from "public/empty-state/issue.svg"; import emptyIssue from "public/empty-state/issue.svg";
export const ProjectEmptyState: React.FC = observer(() => { export const ProjectEmptyState: React.FC = observer(() => {
const { commandPalette: commandPaletteStore } = useMobxStore(); const { commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
return ( return (
<div className="h-full w-full grid place-items-center"> <div className="h-full w-full grid place-items-center">
@ -19,7 +19,10 @@ export const ProjectEmptyState: React.FC = observer(() => {
primaryButton={{ primaryButton={{
text: "New issue", text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />, icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => commandPaletteStore.toggleCreateIssueModal(true), onClick: () => {
setTrackElement("PROJECT_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true)
}
}} }}
/> />
</div> </div>

View File

@ -72,6 +72,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
cycleIssue: cycleIssueStore, cycleIssue: cycleIssueStore,
moduleIssue: moduleIssueStore, moduleIssue: moduleIssueStore,
user: userStore, user: userStore,
trackEvent: { postHogEventTracker }
} = useMobxStore(); } = useMobxStore();
const user = userStore.currentUser; const user = userStore.currentUser;
@ -208,16 +209,27 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
title: "Success!", title: "Success!",
message: "Issue created successfully.", message: "Issue created successfully.",
}); });
postHogEventTracker(
"ISSUE_CREATE",
{
...res,
state: "SUCCESS"
}
);
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
} }
}) }).catch(() => {
.catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Issue could not be created. Please try again.", message: "Issue could not be created. Please try again.",
}); });
postHogEventTracker(
"ISSUE_CREATE",
{
state: "FAILED"
}
);
}); });
if (!createMore) onFormSubmitClose(); if (!createMore) onFormSubmitClose();
@ -232,13 +244,12 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
await issueDraftService await issueDraftService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload) .createDraftIssue(workspaceSlug as string, activeProject ?? "", payload)
.then(() => { .then((res) => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Draft Issue created successfully.", message: "Draft Issue created successfully.",
}); });
handleClose(); handleClose();
setActiveProject(null); setActiveProject(null);
setFormDirtyState(null); setFormDirtyState(null);
@ -262,7 +273,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
await issueDetailStore await issueDetailStore
.updateIssue(workspaceSlug.toString(), activeProject, data.id, payload) .updateIssue(workspaceSlug.toString(), activeProject, data.id, payload)
.then(() => { .then((res) => {
if (!createMore) onFormSubmitClose(); if (!createMore) onFormSubmitClose();
setToastAlert({ setToastAlert({
@ -270,6 +281,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
title: "Success!", title: "Success!",
message: "Issue updated successfully.", message: "Issue updated successfully.",
}); });
postHogEventTracker(
"ISSUE_UPDATE",
{
...res,
state: "SUCCESS"
}
);
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
@ -277,6 +295,12 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
title: "Error!", title: "Error!",
message: "Issue could not be updated. Please try again.", message: "Issue could not be updated. Please try again.",
}); });
postHogEventTracker(
"ISSUE_UPDATE",
{
state: "FAILED"
}
);
}); });
}; };

View File

@ -1,7 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { CheckCircle2, Search } from "lucide-react";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { trackEvent } from "helpers/event-tracker.helper";
// components // components
import { Button, Loader } from "@plane/ui"; import { Button, Loader } from "@plane/ui";
@ -17,6 +15,8 @@ import { USER_WORKSPACES, USER_WORKSPACE_INVITATIONS } from "constants/fetch-key
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
// types // types
import { IWorkspaceMemberInvitation } from "types"; import { IWorkspaceMemberInvitation } from "types";
// icons
import { CheckCircle2, Search } from "lucide-react";
type Props = { type Props = {
handleNextStep: () => void; handleNextStep: () => void;
@ -32,6 +32,7 @@ const Invitations: React.FC<Props> = (props) => {
const { const {
workspace: workspaceStore, workspace: workspaceStore,
user: { currentUser, updateCurrentUser }, user: { currentUser, updateCurrentUser },
trackEvent: { postHogEventTracker }
} = useMobxStore(); } = useMobxStore();
const { const {
@ -63,14 +64,17 @@ const Invitations: React.FC<Props> = (props) => {
await workspaceService await workspaceService
.joinWorkspaces({ invitations: invitationsRespond }) .joinWorkspaces({ invitations: invitationsRespond })
.then(async (res) => { .then(async (res) => {
trackEvent("WORKSPACE_USER_INVITE_ACCEPT", res); postHogEventTracker("WORKSPACE_USER_INVITE_ACCEPT", { ...res, state: "SUCCESS" });
await mutateInvitations(); await mutateInvitations();
await workspaceStore.fetchWorkspaces(); await workspaceStore.fetchWorkspaces();
await mutate(USER_WORKSPACES); await mutate(USER_WORKSPACES);
await updateLastWorkspace(); await updateLastWorkspace();
await handleNextStep(); await handleNextStep();
}) })
.finally(() => setIsJoiningWorkspaces(false)); .catch((error) => {
console.log(error);
postHogEventTracker("WORKSPACE_USER_INVITE_ACCEPT", { state: "FAILED" });
}).finally(() => setIsJoiningWorkspaces(false));
}; };
return invitations && invitations.length > 0 ? ( return invitations && invitations.length > 0 ? (
@ -85,11 +89,10 @@ const Invitations: React.FC<Props> = (props) => {
return ( return (
<div <div
key={invitation.id} key={invitation.id}
className={`flex cursor-pointer items-center gap-2 border p-3.5 rounded ${ className={`flex cursor-pointer items-center gap-2 border p-3.5 rounded ${isSelected
isSelected ? "border-custom-primary-100"
? "border-custom-primary-100" : "border-onboarding-border-200 hover:bg-onboarding-background-300/30"
: "border-onboarding-border-200 hover:bg-onboarding-background-300/30" }`}
}`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")} onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">

View File

@ -166,8 +166,7 @@ const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => {
key={key} key={key}
value={parseInt(key)} value={parseInt(key)}
className={({ active, selected }) => className={({ active, selected }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${ `cursor-pointer select-none truncate rounded px-1 py-1.5 ${active || selected ? "bg-onboarding-background-400/40" : ""
active || selected ? "bg-onboarding-background-400/40" : ""
} ${selected ? "text-onboarding-text-100" : "text-custom-text-200"}` } ${selected ? "text-onboarding-text-100" : "text-custom-text-200"}`
} }
> >

View File

@ -79,7 +79,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
// states // states
const [step, setStep] = useState<TTourSteps>("welcome"); const [step, setStep] = useState<TTourSteps>("welcome");
const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore(); const { user: userStore, commandPalette: commandPaletteStore, trackEvent: {setTrackElement} } = useMobxStore();
const user = userStore.currentUser; const user = userStore.currentUser;
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step); const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
@ -157,6 +157,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
variant="primary" variant="primary"
onClick={() => { onClick={() => {
onComplete(); onComplete();
setTrackElement("ONBOARDING_TOUR")
commandPaletteStore.toggleCreateProjectModal(true); commandPaletteStore.toggleCreateProjectModal(true);
}} }}
> >

View File

@ -19,7 +19,7 @@ export const WorkspaceDashboardView = observer(() => {
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { user: userStore, project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { user: userStore, project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const user = userStore.currentUser; const user = userStore.currentUser;
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
@ -69,7 +69,11 @@ export const WorkspaceDashboardView = observer(() => {
<div className="p-5 md:p-8 pr-0"> <div className="p-5 md:p-8 pr-0">
<h5 className="text-xl font-semibold">Create a project</h5> <h5 className="text-xl font-semibold">Create a project</h5>
<p className="mt-2 mb-5">Manage your projects by creating issues, cycles, modules, views and pages.</p> <p className="mt-2 mb-5">Manage your projects by creating issues, cycles, modules, views and pages.</p>
<Button variant="primary" size="sm" onClick={() => commandPaletteStore.toggleCreateProjectModal(true)}> <Button variant="primary" size="sm" onClick={() => {
setTrackElement("DASHBOARD_PAGE");
commandPaletteStore.toggleCreateProjectModal(true)
}
}>
Create Project Create Project
</Button> </Button>
</div> </div>

View File

@ -18,7 +18,7 @@ export interface IProjectCardList {
export const ProjectCardList: FC<IProjectCardList> = observer((props) => { export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
const { workspaceSlug } = props; const { workspaceSlug } = props;
// store // store
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
@ -57,7 +57,10 @@ export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
primaryButton={{ primaryButton={{
icon: <Plus className="h-4 w-4" />, icon: <Plus className="h-4 w-4" />,
text: "Start something new", text: "Start something new",
onClick: () => commandPaletteStore.toggleCreateProjectModal(true), onClick: () => {
setTrackElement("PROJECTS_EMPTY_STATE");
commandPaletteStore.toggleCreateProjectModal(true)
}
}} }}
/> />
)} )}

View File

@ -20,8 +20,6 @@ import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
import { IWorkspaceMember } from "types"; import { IWorkspaceMember } from "types";
// constants // constants
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
// track events
import { trackEvent } from "helpers/event-tracker.helper";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -68,6 +66,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
const { const {
project: projectStore, project: projectStore,
workspaceMember: { workspaceMembers }, workspaceMember: { workspaceMembers },
trackEvent: { postHogEventTracker }
} = useMobxStore(); } = useMobxStore();
// states // states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
@ -132,11 +131,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
.createProject(workspaceSlug.toString(), payload) .createProject(workspaceSlug.toString(), payload)
.then((res) => { .then((res) => {
const newPayload = { const newPayload = {
...payload, ...res,
id: res.id state: "SUCCESS"
} }
trackEvent( postHogEventTracker(
"CREATE_PROJECT", "PROJECT_CREATE",
newPayload, newPayload,
) )
setToastAlert({ setToastAlert({
@ -150,12 +149,19 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
handleClose(); handleClose();
}) })
.catch((err) => { .catch((err) => {
Object.keys(err.data).map((key) => Object.keys(err.data).map((key) => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: err.data[key], message: err.data[key],
}) });
postHogEventTracker(
"PROJECT_CREATE",
{
state: "FAILED"
},
)
}
); );
}); });
}; };
@ -380,7 +386,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
control={control} control={control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<WorkspaceMemberSelect <WorkspaceMemberSelect
value={workspaceMembers?.filter((member: IWorkspaceMember) => member.member.id ===value)[0]} value={workspaceMembers?.filter((member: IWorkspaceMember) => member.member.id === value)[0]}
onChange={onChange} onChange={onChange}
options={workspaceMembers || []} options={workspaceMembers || []}
placeholder="Select Lead" placeholder="Select Lead"

View File

@ -16,7 +16,6 @@ import { ProjectService } from "services/project";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { trackEvent } from "helpers/event-tracker.helper";
export interface IProjectDetailsForm { export interface IProjectDetailsForm {
project: IProject; project: IProject;
@ -29,7 +28,7 @@ const projectService = new ProjectService();
export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => { export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
const { project, workspaceSlug, isAdmin } = props; const { project, workspaceSlug, isAdmin } = props;
// store // store
const { project: projectStore } = useMobxStore(); const { project: projectStore, trackEvent: { postHogEventTracker } } = useMobxStore();
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// form data // form data
@ -63,7 +62,10 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
return projectStore return projectStore
.updateProject(workspaceSlug.toString(), project.id, payload) .updateProject(workspaceSlug.toString(), project.id, payload)
.then((res) => { .then((res) => {
trackEvent("UPDATE_PROJECT", res); postHogEventTracker(
'PROJECT_UPDATE',
{...res, state: "SUCCESS"}
);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -71,7 +73,12 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
}); });
}) })
.catch((error) => { .catch((error) => {
trackEvent("UPDATE_PROJECT/FAIL"); postHogEventTracker(
'PROJECT_UPDATE',
{
state: "FAILED"
}
);
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",

View File

@ -37,6 +37,7 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
// store // store
const { const {
user: { leaveProject }, user: { leaveProject },
trackEvent: { postHogEventTracker }
} = useMobxStore(); } = useMobxStore();
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -63,6 +64,12 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
.then(() => { .then(() => {
handleClose(); handleClose();
router.push(`/${workspaceSlug}/projects`); router.push(`/${workspaceSlug}/projects`);
postHogEventTracker(
"PROJECT_MEMBER_LEAVE",
{
state: "SUCCESS"
}
);
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
@ -70,6 +77,12 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
title: "Error!", title: "Error!",
message: "Something went wrong please try again later.", message: "Something went wrong please try again later.",
}); });
postHogEventTracker(
"PROJECT_MEMBER_LEAVE",
{
state: "FAILED"
}
);
}); });
} else { } else {
setToastAlert({ setToastAlert({

View File

@ -19,6 +19,7 @@ export const ProjectMemberList: React.FC = observer(() => {
// store // store
const { const {
projectMember: { projectMembers, fetchProjectMembers }, projectMember: { projectMembers, fetchProjectMembers },
trackEvent: { setTrackElement }
} = useMobxStore(); } = useMobxStore();
// states // states
@ -56,7 +57,12 @@ export const ProjectMemberList: React.FC = observer(() => {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</div> </div>
<Button variant="primary" onClick={() => setInviteModal(true)}> <Button variant="primary" onClick={() => {
setTrackElement("PROJECT_SETTINGS_MEMBERS_PAGE_HEADER");
setInviteModal(true)
}
}
>
Add Member Add Member
</Button> </Button>
</div> </div>

View File

@ -58,6 +58,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
const { const {
user: { currentProjectRole }, user: { currentProjectRole },
workspaceMember: { workspaceMembers }, workspaceMember: { workspaceMembers },
trackEvent: { postHogEventTracker }
} = useMobxStore(); } = useMobxStore();
const { const {
@ -85,18 +86,30 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
await projectMemberService await projectMemberService
.bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload) .bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload)
.then(() => { .then((res) => {
onSuccess(); onSuccess();
onClose(); onClose();
trackEvent("PROJECT_MEMBER_INVITE");
setToastAlert({ setToastAlert({
title: "Success", title: "Success",
type: "success", type: "success",
message: "Member added successfully", message: "Member added successfully",
}); });
postHogEventTracker(
'PROJECT_MEMBER_INVITE',
{
...res,
state: "SUCCESS"
}
);
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
postHogEventTracker(
'PROJECT_MEMBER_INVITE',
{
state: "FAILED",
}
);
}) })
.finally(() => { .finally(() => {
reset(defaultValues); reset(defaultValues);

View File

@ -51,8 +51,10 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store // store
const { const {
workspace: { currentWorkspace },
project: { currentProjectDetails, updateProject }, project: { currentProjectDetails, updateProject },
user: { currentUser, currentProjectRole }, user: { currentUser, currentProjectRole },
trackEvent: { setTrackElement, postHogEventTracker },
} = useMobxStore(); } = useMobxStore();
const isAdmin = currentProjectRole === 20; const isAdmin = currentProjectRole === 20;
// hooks // hooks
@ -87,6 +89,16 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
<ToggleSwitch <ToggleSwitch
value={currentProjectDetails?.[feature.property as keyof IProject]} value={currentProjectDetails?.[feature.property as keyof IProject]}
onChange={() => { onChange={() => {
console.log(currentProjectDetails?.[feature.property as keyof IProject]);
setTrackElement("PROJECT_SETTINGS_FEATURES_PAGE");
postHogEventTracker(`TOGGLE_${feature.title.toUpperCase()}`, {
workspace_id: currentWorkspace?.id,
workspace_slug: currentWorkspace?.slug,
project_id: currentProjectDetails?.id,
project_name: currentProjectDetails?.name,
project_identifier: currentProjectDetails?.identifier,
enabled: !currentProjectDetails?.[feature.property as keyof IProject]
});
handleSubmit({ handleSubmit({
[feature.property]: !currentProjectDetails?.[feature.property as keyof IProject], [feature.property]: !currentProjectDetails?.[feature.property as keyof IProject],
}); });

View File

@ -75,7 +75,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { project, provided, snapshot, handleCopyText, shortContextMenu = false } = props; const { project, provided, snapshot, handleCopyText, shortContextMenu = false } = props;
// store // store
const { project: projectStore, theme: themeStore } = useMobxStore(); const { project: projectStore, theme: themeStore, trackEvent: { setTrackElement } } = useMobxStore();
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -118,6 +118,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
}; };
const handleLeaveProject = () => { const handleLeaveProject = () => {
setTrackElement("APP_SIDEBAR_PROJECT_DROPDOWN");
setLeaveProjectModal(true); setLeaveProjectModal(true);
}; };

View File

@ -30,6 +30,7 @@ export const ProjectSidebarList: FC = observer(() => {
theme: { sidebarCollapsed }, theme: { sidebarCollapsed },
project: { joinedProjects, favoriteProjects, orderProjectsWithSortOrder, updateProjectView }, project: { joinedProjects, favoriteProjects, orderProjectsWithSortOrder, updateProjectView },
commandPalette: { toggleCreateProjectModal }, commandPalette: { toggleCreateProjectModal },
trackEvent: { setTrackElement }
} = useMobxStore(); } = useMobxStore();
// router // router
const router = useRouter(); const router = useRouter();
@ -111,9 +112,8 @@ export const ProjectSidebarList: FC = observer(() => {
)} )}
<div <div
ref={containerRef} ref={containerRef}
className={`h-full overflow-y-auto px-4 space-y-2 ${ className={`h-full overflow-y-auto px-4 space-y-2 ${isScrolled ? "border-t border-custom-sidebar-border-300" : ""
isScrolled ? "border-t border-custom-sidebar-border-300" : "" }`}
}`}
> >
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="favorite-projects"> <Droppable droppableId="favorite-projects">
@ -140,6 +140,7 @@ export const ProjectSidebarList: FC = observer(() => {
<button <button
className="group-hover:opacity-100 opacity-0" className="group-hover:opacity-100 opacity-0"
onClick={() => { onClick={() => {
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK")
setIsFavoriteProjectCreate(true); setIsFavoriteProjectCreate(true);
setIsProjectModalOpen(true); setIsProjectModalOpen(true);
}} }}
@ -262,7 +263,11 @@ export const ProjectSidebarList: FC = observer(() => {
<button <button
type="button" type="button"
className="flex w-full items-center gap-2 px-3 text-sm text-custom-sidebar-text-200" className="flex w-full items-center gap-2 px-3 text-sm text-custom-sidebar-text-200"
onClick={() => toggleCreateProjectModal(true)} onClick={() => {
setTrackElement("APP_SIDEBAR");
toggleCreateProjectModal(true)
}
}
> >
<Plus className="h-5 w-5" /> <Plus className="h-5 w-5" />
{!isCollapsed && "Add Project"} {!isCollapsed && "Add Project"}

View File

@ -18,7 +18,7 @@ import type { IState } from "types";
import { STATES_LIST } from "constants/fetch-keys"; import { STATES_LIST } from "constants/fetch-keys";
// constants // constants
import { GROUP_CHOICES } from "constants/project"; import { GROUP_CHOICES } from "constants/project";
import { trackEvent } from "helpers/event-tracker.helper"; import { stat } from "fs";
type Props = { type Props = {
data: IState | null; data: IState | null;
@ -44,7 +44,7 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store // store
const { projectState: projectStateStore } = useMobxStore(); const { projectState: projectStateStore, trackEvent: { postHogEventTracker, setTrackElement } } = useMobxStore();
// hooks // hooks
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -90,15 +90,18 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
.createState(workspaceSlug.toString(), projectId.toString(), formData) .createState(workspaceSlug.toString(), projectId.toString(), formData)
.then((res) => { .then((res) => {
handleClose(); handleClose();
trackEvent(
'STATE_CREATE',
res
)
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "State created successfully.", message: "State created successfully.",
}); });
postHogEventTracker(
'STATE_CREATE',
{
...res,
state: "SUCCESS"
}
);
}) })
.catch((error) => { .catch((error) => {
if (error.status === 400) if (error.status === 400)
@ -113,6 +116,12 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: "State could not be created. Please try again.", message: "State could not be created. Please try again.",
}); });
postHogEventTracker(
'STATE_CREATE',
{
state: "FAILED"
}
);
}); });
}; };
@ -124,10 +133,13 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
.then((res) => { .then((res) => {
mutate(STATES_LIST(projectId.toString())); mutate(STATES_LIST(projectId.toString()));
handleClose(); handleClose();
trackEvent( postHogEventTracker(
'STATE_UPDATE', 'STATE_UPDATE',
res {
) ...res,
state: "SUCCESS",
}
);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -147,6 +159,12 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: "State could not be updated. Please try again.", message: "State could not be updated. Please try again.",
}); });
postHogEventTracker(
'STATE_UPDATE',
{
state: "FAILED",
}
);
}); });
}; };
@ -274,7 +292,10 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
<Button variant="neutral-primary" onClick={handleClose}> <Button variant="neutral-primary" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button variant="primary" type="submit" loading={isSubmitting}> <Button variant="primary" type="submit" loading={isSubmitting} onClick={() => {
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
}
}>
{isSubmitting ? (data ? "Updating..." : "Creating...") : data ? "Update" : "Create"} {isSubmitting ? (data ? "Updating..." : "Creating...") : data ? "Update" : "Create"}
</Button> </Button>
</form> </form>

View File

@ -13,7 +13,6 @@ import useToast from "hooks/use-toast";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
import type { IState } from "types"; import type { IState } from "types";
import { trackEvent } from "helpers/event-tracker.helper";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -29,7 +28,7 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { projectState: projectStateStore } = useMobxStore(); const { projectState: projectStateStore, trackEvent: { postHogEventTracker } } = useMobxStore();
// states // states
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
@ -49,9 +48,12 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
await projectStateStore await projectStateStore
.deleteState(workspaceSlug.toString(), data.project, data.id) .deleteState(workspaceSlug.toString(), data.project, data.id)
.then((res) => { .then((res) => {
trackEvent( postHogEventTracker(
'STATE_DELETE', 'STATE_DELETE',
) {
state: "SUCCESS"
}
);
handleClose(); handleClose();
}) })
.catch((err) => { .catch((err) => {
@ -68,6 +70,12 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: "State could not be deleted. Please try again.", message: "State could not be deleted. Please try again.",
}); });
postHogEventTracker(
'STATE_DELETE',
{
state: "FAILED"
}
)
}) })
.finally(() => { .finally(() => {
setIsDeleteLoading(false); setIsDeleteLoading(false);

View File

@ -30,6 +30,7 @@ export const ProjectSettingListItem: React.FC<Props> = observer((props) => {
// store // store
const { const {
projectState: { markStateAsDefault, moveStatePosition }, projectState: { markStateAsDefault, moveStatePosition },
trackEvent: { setTrackElement }
} = useMobxStore(); } = useMobxStore();
// states // states
@ -107,7 +108,12 @@ export const ProjectSettingListItem: React.FC<Props> = observer((props) => {
className={`group-hover:opacity-100 opacity-0 ${ className={`group-hover:opacity-100 opacity-0 ${
state.default || groupLength === 1 ? "cursor-not-allowed" : "" state.default || groupLength === 1 ? "cursor-not-allowed" : ""
} grid place-items-center`} } grid place-items-center`}
onClick={handleDeleteState} onClick={
() => {
setTrackElement("PROJECT_SETTINGS_STATE_PAGE");
handleDeleteState()
}
}
disabled={state.default || groupLength === 1} disabled={state.default || groupLength === 1}
> >
{state.default ? ( {state.default ? (

View File

@ -23,6 +23,7 @@ export const ProjectSettingStateList: React.FC = observer(() => {
// store // store
const { const {
projectState: { groupedProjectStates, projectStates, fetchProjectStates }, projectState: { groupedProjectStates, projectStates, fetchProjectStates },
trackEvent: { setTrackElement }
} = useMobxStore(); } = useMobxStore();
// state // state
const [activeGroup, setActiveGroup] = useState<StateGroup>(null); const [activeGroup, setActiveGroup] = useState<StateGroup>(null);
@ -58,7 +59,11 @@ export const ProjectSettingStateList: React.FC = observer(() => {
<button <button
type="button" type="button"
className="flex items-center gap-2 text-custom-primary-100 px-2 hover:text-custom-primary-200 outline-none" className="flex items-center gap-2 text-custom-primary-100 px-2 hover:text-custom-primary-200 outline-none"
onClick={() => setActiveGroup(group as keyof StateGroup)} onClick={() => {
setTrackElement("PROJECT_SETTINGS_STATES_PAGE");
setActiveGroup(group as keyof StateGroup)
}
}
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</button> </button>

View File

@ -51,7 +51,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspace: workspaceStore } = useMobxStore(); const { workspace: workspaceStore, trackEvent: { postHogEventTracker } } = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -73,15 +73,12 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
await workspaceStore await workspaceStore
.createWorkspace(formData) .createWorkspace(formData)
.then(async (res) => { .then(async (res) => {
const payload = { postHogEventTracker(
name: formData.name, "WORKSPACE_CREATE",
slug: formData.slug, {
workspace_url: formData.url, ...res,
organization_size: formData.organization_size state: "SUCCESS"
}; },
trackEvent(
"CREATE_WORKSPACE",
payload
) )
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -92,11 +89,19 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
if (onSubmit) await onSubmit(res); if (onSubmit) await onSubmit(res);
}) })
.catch(() => .catch(() =>
{
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Workspace could not be created. Please try again.", message: "Workspace could not be created. Please try again.",
}) })
postHogEventTracker(
"WORKSPACE_CREATE",
{
state: "FAILED"
},
)
}
); );
} else setSlugError(true); } else setSlugError(true);
}) })
@ -106,6 +111,12 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
title: "Error!", title: "Error!",
message: "Some error occurred while creating workspace. Please try again.", message: "Some error occurred while creating workspace. Please try again.",
}); });
postHogEventTracker(
"WORKSPACE_CREATE",
{
state: "FAILED"
},
)
}); });
}; };

View File

@ -12,7 +12,6 @@ import useToast from "hooks/use-toast";
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// types // types
import type { IWorkspace } from "types"; import type { IWorkspace } from "types";
import { trackEvent } from "helpers/event-tracker.helper";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -30,7 +29,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspace: workspaceStore } = useMobxStore(); const { workspace: workspaceStore, trackEvent: { postHogEventTracker } } = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -60,14 +59,16 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
.deleteWorkspace(data.slug) .deleteWorkspace(data.slug)
.then((res) => { .then((res) => {
handleClose(); handleClose();
console.log('DELETE WORKPSACE', res);
router.push("/"); router.push("/");
const payload = { const payload = {
slug: data.slug slug: data.slug
}; };
trackEvent( postHogEventTracker(
'DELETE_WORKSPACE', 'WORKSPACE_DELETE',
payload {
res,
state: "SUCCESS"
}
); );
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -76,11 +77,19 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
}); });
}) })
.catch(() => .catch(() =>
{
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Something went wrong. Please try again later.", message: "Something went wrong. Please try again later.",
}) })
postHogEventTracker(
'WORKSPACE_DELETE',
{
state: "FAILED"
}
);
}
); );
}; };

View File

@ -40,6 +40,7 @@ export const WorkspaceDetails: FC = observer(() => {
const { const {
workspace: { currentWorkspace, updateWorkspace }, workspace: { currentWorkspace, updateWorkspace },
user: { currentWorkspaceRole }, user: { currentWorkspaceRole },
trackEvent: { postHogEventTracker }
} = useMobxStore(); } = useMobxStore();
const isAdmin = currentWorkspaceRole === 20; const isAdmin = currentWorkspaceRole === 20;
// hooks // hooks
@ -66,14 +67,28 @@ export const WorkspaceDetails: FC = observer(() => {
await updateWorkspace(currentWorkspace.slug, payload) await updateWorkspace(currentWorkspace.slug, payload)
.then((res) => { .then((res) => {
trackEvent("UPDATE_WORKSPACE", res); postHogEventTracker(
'WORKSPACE_UPDATE',
{
...res,
state: "SUCCESS"
}
)
setToastAlert({ setToastAlert({
title: "Success", title: "Success",
type: "success", type: "success",
message: "Workspace updated successfully", message: "Workspace updated successfully",
}); });
}) }).catch((err) => {
.catch((err) => console.error(err)); postHogEventTracker(
'WORKSPACE_UPDATE',
{
state: "FAILED"
}
);
console.error(err)
}
);
}; };
const handleRemoveLogo = () => { const handleRemoveLogo = () => {
@ -262,10 +277,9 @@ export const WorkspaceDetails: FC = observer(() => {
id="url" id="url"
name="url" name="url"
type="url" type="url"
value={`${ value={`${typeof window !== "undefined" &&
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "") window.location.origin.replace("http://", "").replace("https://", "")
}/${currentWorkspace.slug}`} }/${currentWorkspace.slug}`}
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.url)} hasError={Boolean(errors.url)}

View File

@ -56,6 +56,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
theme: { sidebarCollapsed }, theme: { sidebarCollapsed },
workspace: { workspaces, currentWorkspace: activeWorkspace }, workspace: { workspaces, currentWorkspace: activeWorkspace },
user: { currentUser, updateCurrentUser, isUserInstanceAdmin }, user: { currentUser, updateCurrentUser, isUserInstanceAdmin },
trackEvent: { setTrackElement }
} = useMobxStore(); } = useMobxStore();
// hooks // hooks
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -203,6 +204,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
as="button" as="button"
type="button" type="button"
onClick={() => { onClick={() => {
setTrackElement("APP_SIEDEBAR_WORKSPACE_DROPDOWN");
router.push("/create-workspace"); router.push("/create-workspace");
}} }}
className="flex w-full items-center gap-2 px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80" className="flex w-full items-center gap-2 px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"

View File

@ -14,7 +14,7 @@ export const WorkspaceSidebarQuickAction = observer(() => {
// states // states
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
const { theme: themeStore, commandPalette: commandPaletteStore } = useMobxStore(); const { theme: themeStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", JSON.stringify({})); const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", JSON.stringify({}));
@ -34,23 +34,24 @@ export const WorkspaceSidebarQuickAction = observer(() => {
/> />
<div <div
className={`flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${ className={`flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${isSidebarCollapsed ? "flex-col gap-1" : "gap-2"
isSidebarCollapsed ? "flex-col gap-1" : "gap-2" }`}
}`}
> >
<div <div
className={`relative flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 group ${ className={`relative flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 group ${isSidebarCollapsed
isSidebarCollapsed ? "px-2 hover:bg-custom-sidebar-background-80"
? "px-2 hover:bg-custom-sidebar-background-80" : "px-3 shadow-custom-sidebar-shadow-2xs border-[0.5px] border-custom-border-200"
: "px-3 shadow-custom-sidebar-shadow-2xs border-[0.5px] border-custom-border-200" }`}
}`}
> >
<button <button
type="button" type="button"
className={`relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 outline-none ${ className={`relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 outline-none ${isSidebarCollapsed ? "justify-center" : ""
isSidebarCollapsed ? "justify-center" : "" }`}
}`} onClick={() => {
onClick={() => commandPaletteStore.toggleCreateIssueModal(true)} setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
commandPaletteStore.toggleCreateIssueModal(true);
}
}
> >
<PenSquare className="h-4 w-4 text-custom-sidebar-text-300" /> <PenSquare className="h-4 w-4 text-custom-sidebar-text-300" />
{!isSidebarCollapsed && <span className="text-sm font-medium">New Issue</span>} {!isSidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
@ -62,9 +63,8 @@ export const WorkspaceSidebarQuickAction = observer(() => {
<button <button
type="button" type="button"
className={`flex items-center justify-center rounded flex-shrink-0 py-1.5 ml-1.5 ${ className={`flex items-center justify-center rounded flex-shrink-0 py-1.5 ml-1.5 ${isSidebarCollapsed ? "hidden" : "block"
isSidebarCollapsed ? "hidden" : "block" }`}
}`}
> >
<ChevronDown <ChevronDown
size={16} size={16}
@ -73,9 +73,8 @@ export const WorkspaceSidebarQuickAction = observer(() => {
</button> </button>
<div <div
className={`fixed h-10 pt-2 w-[203px] left-4 opacity-0 group-hover:opacity-100 mt-0 pointer-events-none group-hover:pointer-events-auto ${ className={`fixed h-10 pt-2 w-[203px] left-4 opacity-0 group-hover:opacity-100 mt-0 pointer-events-none group-hover:pointer-events-auto ${isSidebarCollapsed ? "top-[5.5rem]" : "top-24"
isSidebarCollapsed ? "top-[5.5rem]" : "top-24" }`}
}`}
> >
<div className="w-full h-full"> <div className="w-full h-full">
<button <button
@ -92,11 +91,10 @@ export const WorkspaceSidebarQuickAction = observer(() => {
</div> </div>
<button <button
className={`flex items-center justify-center rounded flex-shrink-0 p-2 outline-none ${ className={`flex items-center justify-center rounded flex-shrink-0 p-2 outline-none ${isSidebarCollapsed
isSidebarCollapsed ? "hover:bg-custom-sidebar-background-80"
? "hover:bg-custom-sidebar-background-80" : "shadow-custom-sidebar-shadow-2xs border-[0.5px] border-custom-border-200"
: "shadow-custom-sidebar-shadow-2xs border-[0.5px] border-custom-border-200" }`}
}`}
onClick={() => commandPaletteStore.toggleCommandPaletteModal(true)} onClick={() => commandPaletteStore.toggleCommandPaletteModal(true)}
> >
<Search className="h-4 w-4 text-custom-sidebar-text-300" /> <Search className="h-4 w-4 text-custom-sidebar-text-300" />

View File

@ -23,6 +23,7 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
const { const {
project: { workspaceProjects }, project: { workspaceProjects },
commandPalette: { toggleCreateProjectModal }, commandPalette: { toggleCreateProjectModal },
trackEvent: { setTrackElement }
} = useMobxStore(); } = useMobxStore();
return ( return (
@ -35,11 +36,10 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
<Tab <Tab
key={tab.key} key={tab.key}
className={({ selected }) => className={({ selected }) =>
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${ `rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${selected ? "bg-custom-background-80" : ""
selected ? "bg-custom-background-80" : ""
}` }`
} }
onClick={() => {}} onClick={() => { }}
> >
{tab.title} {tab.title}
</Tab> </Tab>
@ -64,7 +64,10 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
primaryButton={{ primaryButton={{
icon: <Plus className="h-4 w-4" />, icon: <Plus className="h-4 w-4" />,
text: "New Project", text: "New Project",
onClick: () => toggleCreateProjectModal(true), onClick: () => {
setTrackElement("ANALYTICS_EMPTY_STATE");
toggleCreateProjectModal(true)
}
}} }}
/> />
</> </>

View File

@ -26,6 +26,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
// store // store
const { const {
workspaceMember: { inviteMembersToWorkspace }, workspaceMember: { inviteMembersToWorkspace },
trackEvent: { postHogEventTracker, setTrackElement }
} = useMobxStore(); } = useMobxStore();
// states // states
const [inviteModal, setInviteModal] = useState(false); const [inviteModal, setInviteModal] = useState(false);
@ -37,9 +38,9 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
return inviteMembersToWorkspace(workspaceSlug.toString(), data) return inviteMembersToWorkspace(workspaceSlug.toString(), data)
.then(async () => { .then(async (res) => {
setInviteModal(false); setInviteModal(false);
trackEvent("WORKSPACE_USER_INVITE"); postHogEventTracker("WORKSPACE_USER_INVITE", { ...res, state: "SUCCESS" });
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
@ -47,7 +48,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
}); });
}) })
.catch((err) => { .catch((err) => {
trackEvent("WORKSPACE_USER_INVITE/FAIL"); postHogEventTracker("WORKSPACE_USER_INVITE", { state: "FAILED" });
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
@ -78,7 +79,11 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</div> </div>
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}> <Button variant="primary" size="sm" onClick={() => {
setTrackElement("WORKSPACE_SETTINGS_MEMBERS_PAGE_HEADER");
setInviteModal(true)
}
}>
Add Member Add Member
</Button> </Button>
</div> </div>

View File

@ -0,0 +1,80 @@
import { action, makeObservable, observable } from "mobx";
import posthog from "posthog-js";
import { RootStore } from "./root";
export interface ITrackEventStore {
trackElement: string;
setTrackElement: (element: string) => void;
postHogEventTracker: (
eventName: string,
payload: object | [] | null
// group: { isGrouping: boolean; groupType: string; gorupId: string } | null
) => void;
}
export class TrackEventStore implements ITrackEventStore {
trackElement: string = "";
rootStore;
constructor(_rootStore: RootStore) {
makeObservable(this, {
trackElement: observable,
setTrackElement: action,
postHogEventTracker: action,
});
this.rootStore = _rootStore;
}
setTrackElement = (element: string) => {
this.trackElement = element;
};
postHogEventTracker = (
eventName: string,
payload: object | [] | null
// group: { isGrouping: boolean; groupType: string; gorupId: string } | null
) => {
try {
console.log("POSTHOG_EVENT: ", eventName);
let extras: any = {
workspace_name: this.rootStore.workspace.currentWorkspace?.name ?? "",
workspace_id: this.rootStore.workspace.currentWorkspace?.id ?? "",
workspace_slug: this.rootStore.workspace.currentWorkspace?.slug ?? "",
project_name: this.rootStore.project.currentProjectDetails?.name ?? "",
project_id: this.rootStore.project.currentProjectDetails?.id ?? "",
project_identifier: this.rootStore.project.currentProjectDetails?.identifier ?? "",
};
if (["PROJECT_CREATE", "PROJECT_UPDATE"].includes(eventName)) {
const project_details: any = payload as object;
extras = {
...extras,
project_name: project_details?.name ?? "",
project_id: project_details?.id ?? "",
project_identifier: project_details?.identifier ?? "",
};
}
// if (group!.isGrouping === true) {
// posthog?.group(group!.groupType, group!.gorupId, {
// name: "PostHog",
// subscription: "subscription",
// date_joined: "2020-01-23T00:00:00.000Z",
// });
// console.log("END OF GROUPING");
// posthog?.capture(eventName, {
// ...payload,
// element: this.trackElement ?? "",
// });
// } else {
posthog?.capture(eventName, {
...payload,
extras: extras,
element: this.trackElement ?? "",
});
// }
console.log(payload);
} catch (error) {
console.log(error);
}
this.setTrackElement("");
};
}

View File

@ -160,6 +160,8 @@ import { ModuleIssueFiltersStore, IModuleIssueFiltersStore } from "store/module-
import { IMentionsStore, MentionsStore } from "store/editor"; import { IMentionsStore, MentionsStore } from "store/editor";
// pages // pages
import { PageStore, IPageStore } from "store/page.store"; import { PageStore, IPageStore } from "store/page.store";
// event tracking
import { TrackEventStore, ITrackEventStore } from "./event-tracker.store";
enableStaticRendering(typeof window === "undefined"); enableStaticRendering(typeof window === "undefined");
@ -261,6 +263,8 @@ export class RootStore {
page: IPageStore; page: IPageStore;
trackEvent: ITrackEventStore;
constructor() { constructor() {
this.instance = new InstanceStore(this); this.instance = new InstanceStore(this);
@ -357,5 +361,7 @@ export class RootStore {
this.moduleIssueFilters = new ModuleIssueFiltersStore(this); this.moduleIssueFilters = new ModuleIssueFiltersStore(this);
this.page = new PageStore(this); this.page = new PageStore(this);
this.trackEvent = new TrackEventStore(this);
} }
} }