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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ export interface ICyclesList {
export const CyclesList: FC<ICyclesList> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId } = props;
const { commandPalette: commandPaletteStore } = useMobxStore();
const { commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
return (
<>
@ -57,7 +57,11 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
<button
type="button"
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
</button>

View File

@ -24,7 +24,7 @@ interface ICycleDelete {
export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props;
// store
const { cycle: cycleStore } = useMobxStore();
const { cycle: cycleStore, trackEvent: { postHogEventTracker } } = useMobxStore();
// toast
const { setToastAlert } = useToast();
// states
@ -36,11 +36,25 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
setLoader(true);
if (cycle?.id)
try {
await cycleStore.removeCycle(workspaceSlug, projectId, cycle?.id);
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle deleted successfully.",
await cycleStore.removeCycle(workspaceSlug, projectId, cycle?.id).then((res) => {
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle deleted successfully.",
});
postHogEventTracker(
"CYCLE_DELETE",
{
state: "SUCCESS"
}
);
}).catch((error) => {
postHogEventTracker(
"CYCLE_DELETE",
{
state: "FAILED"
}
);
});
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) => {
const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
// store
const { cycle: cycleStore } = useMobxStore();
const { cycle: cycleStore, trackEvent: { postHogEventTracker } } = useMobxStore();
// states
const [activeProject, setActiveProject] = useState<string>(projectId);
// toast
@ -35,12 +35,19 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore
.createCycle(workspaceSlug, selectedProjectId, payload)
.then(() => {
.then((res) => {
setToastAlert({
type: "success",
title: "Success!",
message: "Cycle created successfully.",
});
postHogEventTracker(
"CYCLE_CREATE",
{
...res,
state: "SUCCESS",
}
);
})
.catch(() => {
setToastAlert({
@ -48,6 +55,12 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
title: "Error!",
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 { workspaceSlug, projectId, peekCycle } = router.query;
const { cycle: cycleDetailsStore } = useMobxStore();
const { cycle: cycleDetailsStore, trackEvent: { setTrackElement, postHogEventTracker } } = useMobxStore();
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
@ -74,8 +74,27 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
cycleService
.patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data)
.then(() => mutate(CYCLE_DETAILS(cycleId as string)))
.catch((e) => console.log(e));
.then((res) => {
mutate(CYCLE_DETAILS(cycleId as string));
postHogEventTracker(
"CYCLE_UPDATE",
{
...res,
state: "SUCCESS"
}
);
}
)
.catch((e) => {
console.log(e);
postHogEventTracker(
"CYCLE_UPDATE",
{
state: "FAILED"
}
);
}
);
};
const handleCopyText = () => {
@ -284,10 +303,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
cycleDetails.total_issues === 0
? "0 Issue"
: cycleDetails.total_issues === cycleDetails.completed_issues
? cycleDetails.total_issues > 1
? `${cycleDetails.total_issues}`
: `${cycleDetails.total_issues}`
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
? cycleDetails.total_issues > 1
? `${cycleDetails.total_issues}`
: `${cycleDetails.total_issues}`
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
return (
<>
@ -317,7 +336,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</button>
{!isCompleted && (
<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">
<Trash2 className="h-3 w-3" />
<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}
onChange={(val) => {
if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_START_DATE_BUTTON");
handleStartDateChange(val);
}
}}
@ -402,6 +426,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
onChange={(val) => {
if (val) {
setTrackElement("CYCLE_PAGE_SIDEBAR_END_DATE_BUTTON");
handleEndDateChange(val);
}
}}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ export const ProjectsHeader = observer(() => {
const { workspaceSlug } = router.query;
// store
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: {setTrackElement} } = useMobxStore();
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : [];
@ -41,7 +41,10 @@ export const ProjectsHeader = observer(() => {
</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
</Button>
</div>

View File

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

View File

@ -46,7 +46,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId, inboxId } = router.query;
const { inboxIssueDetails: inboxIssueDetailsStore } = useMobxStore();
const { inboxIssueDetails: inboxIssueDetailsStore, trackEvent: { postHogEventTracker } } = useMobxStore();
const {
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}`);
handleClose();
} 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)}
>
<span className="text-xs">Create more</span>
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
<ToggleSwitch value={createMore} onChange={() => { }} size="md" />
</div>
<div className="flex items-center gap-2">
<Button variant="neutral-primary" size="sm" onClick={() => handleClose()}>

View File

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

View File

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

View File

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

View File

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

View File

@ -72,6 +72,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
cycleIssue: cycleIssueStore,
moduleIssue: moduleIssueStore,
user: userStore,
trackEvent: { postHogEventTracker }
} = useMobxStore();
const user = userStore.currentUser;
@ -208,16 +209,27 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
title: "Success!",
message: "Issue created successfully.",
});
postHogEventTracker(
"ISSUE_CREATE",
{
...res,
state: "SUCCESS"
}
);
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
}
})
.catch(() => {
}).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be created. Please try again.",
});
postHogEventTracker(
"ISSUE_CREATE",
{
state: "FAILED"
}
);
});
if (!createMore) onFormSubmitClose();
@ -232,13 +244,12 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
await issueDraftService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload)
.then(() => {
.then((res) => {
setToastAlert({
type: "success",
title: "Success!",
message: "Draft Issue created successfully.",
});
handleClose();
setActiveProject(null);
setFormDirtyState(null);
@ -262,7 +273,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
await issueDetailStore
.updateIssue(workspaceSlug.toString(), activeProject, data.id, payload)
.then(() => {
.then((res) => {
if (!createMore) onFormSubmitClose();
setToastAlert({
@ -270,6 +281,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
title: "Success!",
message: "Issue updated successfully.",
});
postHogEventTracker(
"ISSUE_UPDATE",
{
...res,
state: "SUCCESS"
}
);
})
.catch(() => {
setToastAlert({
@ -277,6 +295,12 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
title: "Error!",
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 { CheckCircle2, Search } from "lucide-react";
import useSWR, { mutate } from "swr";
import { trackEvent } from "helpers/event-tracker.helper";
// components
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";
// types
import { IWorkspaceMemberInvitation } from "types";
// icons
import { CheckCircle2, Search } from "lucide-react";
type Props = {
handleNextStep: () => void;
@ -32,6 +32,7 @@ const Invitations: React.FC<Props> = (props) => {
const {
workspace: workspaceStore,
user: { currentUser, updateCurrentUser },
trackEvent: { postHogEventTracker }
} = useMobxStore();
const {
@ -63,14 +64,17 @@ const Invitations: React.FC<Props> = (props) => {
await workspaceService
.joinWorkspaces({ invitations: invitationsRespond })
.then(async (res) => {
trackEvent("WORKSPACE_USER_INVITE_ACCEPT", res);
postHogEventTracker("WORKSPACE_USER_INVITE_ACCEPT", { ...res, state: "SUCCESS" });
await mutateInvitations();
await workspaceStore.fetchWorkspaces();
await mutate(USER_WORKSPACES);
await updateLastWorkspace();
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 ? (
@ -85,11 +89,10 @@ const Invitations: React.FC<Props> = (props) => {
return (
<div
key={invitation.id}
className={`flex cursor-pointer items-center gap-2 border p-3.5 rounded ${
isSelected
? "border-custom-primary-100"
: "border-onboarding-border-200 hover:bg-onboarding-background-300/30"
}`}
className={`flex cursor-pointer items-center gap-2 border p-3.5 rounded ${isSelected
? "border-custom-primary-100"
: "border-onboarding-border-200 hover:bg-onboarding-background-300/30"
}`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
>
<div className="flex-shrink-0">

View File

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

View File

@ -79,7 +79,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
// states
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 currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
@ -157,6 +157,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
variant="primary"
onClick={() => {
onComplete();
setTrackElement("ONBOARDING_TOUR")
commandPaletteStore.toggleCreateProjectModal(true);
}}
>

View File

@ -19,7 +19,7 @@ export const WorkspaceDashboardView = observer(() => {
const { workspaceSlug } = router.query;
// store
const { user: userStore, project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
const { user: userStore, project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const user = userStore.currentUser;
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">
<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>
<Button variant="primary" size="sm" onClick={() => commandPaletteStore.toggleCreateProjectModal(true)}>
<Button variant="primary" size="sm" onClick={() => {
setTrackElement("DASHBOARD_PAGE");
commandPaletteStore.toggleCreateProjectModal(true)
}
}>
Create Project
</Button>
</div>

View File

@ -18,7 +18,7 @@ export interface IProjectCardList {
export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
const { workspaceSlug } = props;
// store
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
const { project: projectStore, commandPalette: commandPaletteStore, trackEvent: { setTrackElement } } = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
@ -57,7 +57,10 @@ export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
primaryButton={{
icon: <Plus className="h-4 w-4" />,
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";
// constants
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
// track events
import { trackEvent } from "helpers/event-tracker.helper";
type Props = {
isOpen: boolean;
@ -68,6 +66,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
const {
project: projectStore,
workspaceMember: { workspaceMembers },
trackEvent: { postHogEventTracker }
} = useMobxStore();
// states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
@ -132,11 +131,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
.createProject(workspaceSlug.toString(), payload)
.then((res) => {
const newPayload = {
...payload,
id: res.id
...res,
state: "SUCCESS"
}
trackEvent(
"CREATE_PROJECT",
postHogEventTracker(
"PROJECT_CREATE",
newPayload,
)
setToastAlert({
@ -150,12 +149,19 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
handleClose();
})
.catch((err) => {
Object.keys(err.data).map((key) =>
Object.keys(err.data).map((key) => {
setToastAlert({
type: "error",
title: "Error!",
message: err.data[key],
})
});
postHogEventTracker(
"PROJECT_CREATE",
{
state: "FAILED"
},
)
}
);
});
};
@ -380,7 +386,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
control={control}
render={({ field: { value, onChange } }) => (
<WorkspaceMemberSelect
value={workspaceMembers?.filter((member: IWorkspaceMember) => member.member.id ===value)[0]}
value={workspaceMembers?.filter((member: IWorkspaceMember) => member.member.id === value)[0]}
onChange={onChange}
options={workspaceMembers || []}
placeholder="Select Lead"

View File

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

View File

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

View File

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

View File

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

View File

@ -51,8 +51,10 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
const { workspaceSlug, projectId } = router.query;
// store
const {
workspace: { currentWorkspace },
project: { currentProjectDetails, updateProject },
user: { currentUser, currentProjectRole },
trackEvent: { setTrackElement, postHogEventTracker },
} = useMobxStore();
const isAdmin = currentProjectRole === 20;
// hooks
@ -87,6 +89,16 @@ export const ProjectFeaturesList: FC<Props> = observer(() => {
<ToggleSwitch
value={currentProjectDetails?.[feature.property as keyof IProject]}
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({
[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
const { project, provided, snapshot, handleCopyText, shortContextMenu = false } = props;
// store
const { project: projectStore, theme: themeStore } = useMobxStore();
const { project: projectStore, theme: themeStore, trackEvent: { setTrackElement } } = useMobxStore();
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -118,6 +118,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
};
const handleLeaveProject = () => {
setTrackElement("APP_SIDEBAR_PROJECT_DROPDOWN");
setLeaveProjectModal(true);
};

View File

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

View File

@ -18,7 +18,7 @@ import type { IState } from "types";
import { STATES_LIST } from "constants/fetch-keys";
// constants
import { GROUP_CHOICES } from "constants/project";
import { trackEvent } from "helpers/event-tracker.helper";
import { stat } from "fs";
type Props = {
data: IState | null;
@ -44,7 +44,7 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = router.query;
// store
const { projectState: projectStateStore } = useMobxStore();
const { projectState: projectStateStore, trackEvent: { postHogEventTracker, setTrackElement } } = useMobxStore();
// hooks
const { setToastAlert } = useToast();
@ -90,15 +90,18 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
.createState(workspaceSlug.toString(), projectId.toString(), formData)
.then((res) => {
handleClose();
trackEvent(
'STATE_CREATE',
res
)
setToastAlert({
type: "success",
title: "Success!",
message: "State created successfully.",
});
postHogEventTracker(
'STATE_CREATE',
{
...res,
state: "SUCCESS"
}
);
})
.catch((error) => {
if (error.status === 400)
@ -113,6 +116,12 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Error!",
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) => {
mutate(STATES_LIST(projectId.toString()));
handleClose();
trackEvent(
postHogEventTracker(
'STATE_UPDATE',
res
)
{
...res,
state: "SUCCESS",
}
);
setToastAlert({
type: "success",
title: "Success!",
@ -147,6 +159,12 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
title: "Error!",
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}>
Cancel
</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"}
</Button>
</form>

View File

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

View File

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

View File

@ -23,6 +23,7 @@ export const ProjectSettingStateList: React.FC = observer(() => {
// store
const {
projectState: { groupedProjectStates, projectStates, fetchProjectStates },
trackEvent: { setTrackElement }
} = useMobxStore();
// state
const [activeGroup, setActiveGroup] = useState<StateGroup>(null);
@ -58,7 +59,11 @@ export const ProjectSettingStateList: React.FC = observer(() => {
<button
type="button"
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" />
</button>

View File

@ -51,7 +51,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
const router = useRouter();
const { workspace: workspaceStore } = useMobxStore();
const { workspace: workspaceStore, trackEvent: { postHogEventTracker } } = useMobxStore();
const { setToastAlert } = useToast();
@ -73,15 +73,12 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
await workspaceStore
.createWorkspace(formData)
.then(async (res) => {
const payload = {
name: formData.name,
slug: formData.slug,
workspace_url: formData.url,
organization_size: formData.organization_size
};
trackEvent(
"CREATE_WORKSPACE",
payload
postHogEventTracker(
"WORKSPACE_CREATE",
{
...res,
state: "SUCCESS"
},
)
setToastAlert({
type: "success",
@ -92,11 +89,19 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
if (onSubmit) await onSubmit(res);
})
.catch(() =>
{
setToastAlert({
type: "error",
title: "Error!",
message: "Workspace could not be created. Please try again.",
})
postHogEventTracker(
"WORKSPACE_CREATE",
{
state: "FAILED"
},
)
}
);
} else setSlugError(true);
})
@ -106,6 +111,12 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
title: "Error!",
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";
// types
import type { IWorkspace } from "types";
import { trackEvent } from "helpers/event-tracker.helper";
type Props = {
isOpen: boolean;
@ -30,7 +29,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspace: workspaceStore } = useMobxStore();
const { workspace: workspaceStore, trackEvent: { postHogEventTracker } } = useMobxStore();
const { setToastAlert } = useToast();
@ -60,14 +59,16 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
.deleteWorkspace(data.slug)
.then((res) => {
handleClose();
console.log('DELETE WORKPSACE', res);
router.push("/");
const payload = {
slug: data.slug
};
trackEvent(
'DELETE_WORKSPACE',
payload
postHogEventTracker(
'WORKSPACE_DELETE',
{
res,
state: "SUCCESS"
}
);
setToastAlert({
type: "success",
@ -76,11 +77,19 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
});
})
.catch(() =>
{
setToastAlert({
type: "error",
title: "Error!",
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 {
workspace: { currentWorkspace, updateWorkspace },
user: { currentWorkspaceRole },
trackEvent: { postHogEventTracker }
} = useMobxStore();
const isAdmin = currentWorkspaceRole === 20;
// hooks
@ -66,14 +67,28 @@ export const WorkspaceDetails: FC = observer(() => {
await updateWorkspace(currentWorkspace.slug, payload)
.then((res) => {
trackEvent("UPDATE_WORKSPACE", res);
postHogEventTracker(
'WORKSPACE_UPDATE',
{
...res,
state: "SUCCESS"
}
)
setToastAlert({
title: "Success",
type: "success",
message: "Workspace updated successfully",
});
})
.catch((err) => console.error(err));
}).catch((err) => {
postHogEventTracker(
'WORKSPACE_UPDATE',
{
state: "FAILED"
}
);
console.error(err)
}
);
};
const handleRemoveLogo = () => {
@ -262,10 +277,9 @@ export const WorkspaceDetails: FC = observer(() => {
id="url"
name="url"
type="url"
value={`${
typeof window !== "undefined" &&
value={`${typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${currentWorkspace.slug}`}
}/${currentWorkspace.slug}`}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.url)}

View File

@ -56,6 +56,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
theme: { sidebarCollapsed },
workspace: { workspaces, currentWorkspace: activeWorkspace },
user: { currentUser, updateCurrentUser, isUserInstanceAdmin },
trackEvent: { setTrackElement }
} = useMobxStore();
// hooks
const { setToastAlert } = useToast();
@ -203,6 +204,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
as="button"
type="button"
onClick={() => {
setTrackElement("APP_SIEDEBAR_WORKSPACE_DROPDOWN");
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"

View File

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

View File

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

View File

@ -26,6 +26,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
// store
const {
workspaceMember: { inviteMembersToWorkspace },
trackEvent: { postHogEventTracker, setTrackElement }
} = useMobxStore();
// states
const [inviteModal, setInviteModal] = useState(false);
@ -37,9 +38,9 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
if (!workspaceSlug) return;
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
.then(async () => {
.then(async (res) => {
setInviteModal(false);
trackEvent("WORKSPACE_USER_INVITE");
postHogEventTracker("WORKSPACE_USER_INVITE", { ...res, state: "SUCCESS" });
setToastAlert({
type: "success",
title: "Success!",
@ -47,7 +48,7 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
});
})
.catch((err) => {
trackEvent("WORKSPACE_USER_INVITE/FAIL");
postHogEventTracker("WORKSPACE_USER_INVITE", { state: "FAILED" });
setToastAlert({
type: "error",
title: "Error!",
@ -78,7 +79,11 @@ const WorkspaceMembersSettingsPage: NextPageWithLayout = observer(() => {
onChange={(e) => setSearchQuery(e.target.value)}
/>
</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
</Button>
</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";
// pages
import { PageStore, IPageStore } from "store/page.store";
// event tracking
import { TrackEventStore, ITrackEventStore } from "./event-tracker.store";
enableStaticRendering(typeof window === "undefined");
@ -261,6 +263,8 @@ export class RootStore {
page: IPageStore;
trackEvent: ITrackEventStore;
constructor() {
this.instance = new InstanceStore(this);
@ -357,5 +361,7 @@ export class RootStore {
this.moduleIssueFilters = new ModuleIssueFiltersStore(this);
this.page = new PageStore(this);
this.trackEvent = new TrackEventStore(this);
}
}