chore: new analytic events (#699)

* feat: tracking events for issues marked as DONE, issue property update, issue moved to cycle, issue moved to modules

* fix: changed events names

* chore: sync analytic

* chore: new analytic events
This commit is contained in:
Dakshesh Jain 2023-04-06 12:08:52 +05:30 committed by GitHub
parent 65037b5031
commit cf662f6e6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 428 additions and 31 deletions

View File

@ -34,7 +34,6 @@ import {
TrashIcon,
XMarkIcon,
ArrowTopRightOnSquareIcon,
} from "@heroicons/react/24/outline";
// helpers
import { handleIssuesMutation } from "constants/issue";

View File

@ -10,6 +10,7 @@ import { DragDropContext, DropResult } from "react-beautiful-dnd";
import issuesService from "services/issues.service";
import stateService from "services/state.service";
import modulesService from "services/modules.service";
import trackEventServices from "services/track-event.service";
// hooks
import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
@ -259,7 +260,22 @@ export const IssuesView: React.FC<Props> = ({
state: draggedItem.state,
sort_order: draggedItem.sort_order,
})
.then(() => {
.then((response) => {
const sourceStateBeforeDrag = states.find((state) => state.name === source.droppableId);
if (
sourceStateBeforeDrag?.group !== "completed" &&
response?.state_detail?.group === "completed"
)
trackEventServices.trackIssueMarkedAsDoneEvent({
workspaceSlug,
workspaceId: draggedItem.workspace_detail.id,
projectName: draggedItem.project_detail.name,
projectIdentifier: draggedItem.project_detail.identifier,
projectId,
issueId: draggedItem.id,
});
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
mutate(CYCLE_DETAILS(cycleId as string));
@ -282,6 +298,7 @@ export const IssuesView: React.FC<Props> = ({
orderBy,
handleDeleteIssue,
params,
states,
]
);

View File

@ -6,6 +6,7 @@ import useSWR from "swr";
// services
import projectService from "services/project.service";
import trackEventServices from "services/track-event.service";
// ui
import { AssigneesList, Avatar, CustomSearchSelect, Tooltip } from "components/ui";
// icons
@ -71,6 +72,18 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
else newData.push(data);
partialUpdateIssue({ assignees_list: data });
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_ASSIGNEE"
);
}}
options={options}
label={

View File

@ -2,6 +2,8 @@
import { CustomDatePicker, Tooltip } from "components/ui";
// helpers
import { findHowManyDaysLeft } from "helpers/date-time.helper";
// services
import trackEventServices from "services/track-event.service";
// types
import { IIssue } from "types";
@ -25,13 +27,24 @@ export const ViewDueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue,
<CustomDatePicker
placeholder="N/A"
value={issue?.target_date}
onChange={(val) =>
onChange={(val) => {
partialUpdateIssue({
target_date: val,
priority: issue.priority,
state: issue.state,
})
}
});
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_DUE_DATE"
);
}}
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
disabled={isNotAllowed}
/>

View File

@ -8,6 +8,8 @@ import { getPriorityIcon } from "components/icons/priority-icon";
import { IIssue } from "types";
// constants
import { PRIORITIES } from "constants/project";
// services
import trackEventServices from "services/track-event.service";
type Props = {
issue: IIssue;
@ -26,9 +28,20 @@ export const ViewPrioritySelect: React.FC<Props> = ({
}) => (
<CustomSelect
value={issue.priority}
onChange={(data: string) =>
partialUpdateIssue({ priority: data, state: issue.state, target_date: issue.target_date })
}
onChange={(data: string) => {
partialUpdateIssue({ priority: data, state: issue.state, target_date: issue.target_date });
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_PRIORITY"
);
}}
maxHeight="md"
customButton={
<button

View File

@ -4,6 +4,7 @@ import useSWR from "swr";
// services
import stateService from "services/state.service";
import trackEventServices from "services/track-event.service";
// ui
import { CustomSearchSelect, Tooltip } from "components/ui";
// icons
@ -58,13 +59,38 @@ export const ViewStateSelect: React.FC<Props> = ({
return (
<CustomSearchSelect
value={issue.state}
onChange={(data: string) =>
onChange={(data: string) => {
partialUpdateIssue({
state: data,
priority: issue.priority,
target_date: issue.target_date,
})
}
});
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
},
"ISSUE_PROPERTY_UPDATE_STATE"
);
const oldState = states.find((s) => s.id === issue.state);
const newState = states.find((s) => s.id === data);
if (oldState?.group !== "completed" && newState?.group !== "completed") {
trackEventServices.trackIssueMarkedAsDoneEvent({
workspaceSlug: issue.workspace_detail.slug,
workspaceId: issue.workspace_detail.id,
projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name,
issueId: issue.id,
});
}
}}
options={options}
label={
<Tooltip

View File

@ -6,6 +6,7 @@ import useSWR, { mutate } from "swr";
// services
import projectService from "services/project.service";
import trackEventServices from "services/track-event.service";
// lib
import { requiredAdmin } from "lib/auth";
// layouts
@ -112,7 +113,19 @@ const FeaturesSettings: NextPage<UserAuth> = (props) => {
}`}
role="switch"
aria-checked={projectDetails?.cycle_view}
onClick={() => handleSubmit({ cycle_view: !projectDetails?.cycle_view })}
onClick={() => {
trackEventServices.trackMiscellaneousEvent(
{
workspaceId: (projectDetails?.workspace as any)?.id,
workspaceSlug,
projectId,
projectIdentifier: projectDetails?.identifier,
projectName: projectDetails?.name,
},
!projectDetails?.cycle_view ? "TOGGLE_CYCLE_ON" : "TOGGLE_CYCLE_OFF"
);
handleSubmit({ cycle_view: !projectDetails?.cycle_view });
}}
>
<span className="sr-only">Use cycles</span>
<span
@ -141,7 +154,19 @@ const FeaturesSettings: NextPage<UserAuth> = (props) => {
}`}
role="switch"
aria-checked={projectDetails?.module_view}
onClick={() => handleSubmit({ module_view: !projectDetails?.module_view })}
onClick={() => {
trackEventServices.trackMiscellaneousEvent(
{
workspaceId: (projectDetails?.workspace as any)?.id,
workspaceSlug,
projectId,
projectIdentifier: projectDetails?.identifier,
projectName: projectDetails?.name,
},
!projectDetails?.module_view ? "TOGGLE_MODULE_ON" : "TOGGLE_MODULE_OFF"
);
handleSubmit({ module_view: !projectDetails?.module_view });
}}
>
<span className="sr-only">Use cycles</span>
<span
@ -170,7 +195,19 @@ const FeaturesSettings: NextPage<UserAuth> = (props) => {
}`}
role="switch"
aria-checked={projectDetails?.issue_views_view}
onClick={() => handleSubmit({ issue_views_view: !projectDetails?.issue_views_view })}
onClick={() => {
trackEventServices.trackMiscellaneousEvent(
{
workspaceId: (projectDetails?.workspace as any)?.id,
workspaceSlug,
projectId,
projectIdentifier: projectDetails?.identifier,
projectName: projectDetails?.name,
},
!projectDetails?.issue_views_view ? "TOGGLE_VIEW_ON" : "TOGGLE_VIEW_OFF"
);
handleSubmit({ issue_views_view: !projectDetails?.issue_views_view });
}}
>
<span className="sr-only">Use views</span>
<span
@ -199,7 +236,19 @@ const FeaturesSettings: NextPage<UserAuth> = (props) => {
}`}
role="switch"
aria-checked={projectDetails?.page_view}
onClick={() => handleSubmit({ page_view: !projectDetails?.page_view })}
onClick={() => {
trackEventServices.trackMiscellaneousEvent(
{
workspaceId: (projectDetails?.workspace as any)?.id,
workspaceSlug,
projectId,
projectIdentifier: projectDetails?.identifier,
projectName: projectDetails?.name,
},
!projectDetails?.page_view ? "TOGGLE_PAGES_ON" : "TOGGLE_PAGES_OFF"
);
handleSubmit({ page_view: !projectDetails?.page_view });
}}
>
<span className="sr-only">Use cycles</span>
<span

View File

@ -99,7 +99,22 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`,
data
)
.then((response) => response?.data)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
{
workspaceSlug,
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
projectId,
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
issueId: response?.data?.[0]?.issue_detail?.id,
cycleId,
},
response.data.length > 1 ? "ISSUE_MOVED_TO_CYCLE_IN_BULK" : "ISSUE_MOVED_TO_CYCLE"
);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
@ -158,7 +173,11 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`,
data
)
.then((response) => response?.data)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE");
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
@ -175,7 +194,11 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`,
data
)
.then((response) => response?.data)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE");
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
@ -190,7 +213,17 @@ class ProjectIssuesServices extends APIService {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`
)
.then((response) => response?.data)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueCommentEvent(
{
issueId,
commentId,
},
"ISSUE_COMMENT_DELETE"
);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
@ -204,9 +237,29 @@ class ProjectIssuesServices extends APIService {
});
}
async createIssueLabel(workspaceSlug: string, projectId: string, data: any): Promise<any> {
async createIssueLabel(
workspaceSlug: string,
projectId: string,
data: any
): Promise<IIssueLabels> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`, data)
.then((response) => response?.data)
.then((response: { data: IIssueLabels; [key: string]: any }) => {
if (trackEvent)
trackEventServices.trackIssueLabelEvent(
{
workSpaceId: response?.data?.workspace_detail?.id,
workSpaceName: response?.data?.workspace_detail?.name,
workspaceSlug,
projectId,
projectIdentifier: response?.data?.project_detail?.identifier,
projectName: response?.data?.project_detail?.name,
labelId: response?.data?.id,
color: response?.data?.color,
},
"ISSUE_LABEL_CREATE"
);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
@ -222,7 +275,23 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`,
data
)
.then((response) => response?.data)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueLabelEvent(
{
workSpaceId: response?.data?.workspace_detail?.id,
workSpaceName: response?.data?.workspace_detail?.name,
workspaceSlug,
projectId,
projectIdentifier: response?.data?.project_detail?.identifier,
projectName: response?.data?.project_detail?.name,
labelId: response?.data?.id,
color: response?.data?.color,
},
"ISSUE_LABEL_UPDATE"
);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
@ -232,7 +301,17 @@ class ProjectIssuesServices extends APIService {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`
)
.then((response) => response?.data)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueLabelEvent(
{
workspaceSlug,
projectId,
},
"ISSUE_LABEL_DELETE"
);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});

View File

@ -138,7 +138,22 @@ class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`,
data
)
.then((response) => response?.data)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
{
workspaceSlug,
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
projectId,
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
issueId: response?.data?.[0]?.issue_detail?.id,
moduleId,
},
response?.data?.length > 1 ? "ISSUE_MOVED_TO_MODULE_IN_BULK" : "ISSUE_MOVED_TO_MODULE"
);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});

View File

@ -89,7 +89,20 @@ class ProjectServices extends APIService {
async inviteProject(workspaceSlug: string, projectId: string, data: any): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/add/`, data)
.then((response) => response?.data)
.then((response) => {
if (trackEvent)
trackEventServices.trackProjectEvent(
{
workspaceId: response?.data?.workspace?.id,
workspaceSlug,
projectId,
projectName: response?.data?.project?.name,
memberEmail: response?.data?.member?.email,
},
"PROJECT_MEMBER_INVITE"
);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});

View File

@ -9,6 +9,7 @@ import type {
ICycle,
IGptResponse,
IIssue,
IIssueComment,
IModule,
IPage,
IPageBlock,
@ -26,7 +27,11 @@ type WorkspaceEventType =
| "WORKSPACE_USER_INVITE_ACCEPT"
| "WORKSPACE_USER_BULK_INVITE_ACCEPT";
type ProjectEventType = "CREATE_PROJECT" | "UPDATE_PROJECT" | "DELETE_PROJECT";
type ProjectEventType =
| "CREATE_PROJECT"
| "UPDATE_PROJECT"
| "DELETE_PROJECT"
| "PROJECT_MEMBER_INVITE";
type IssueEventType = "ISSUE_CREATE" | "ISSUE_UPDATE" | "ISSUE_DELETE";
@ -40,12 +45,32 @@ type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE";
type ViewEventType = "VIEW_CREATE" | "VIEW_UPDATE" | "VIEW_DELETE";
type IssueCommentType = "ISSUE_COMMENT_CREATE" | "ISSUE_COMMENT_UPDATE" | "ISSUE_COMMENT_DELETE";
type MiscellaneousEventType =
| "TOGGLE_CYCLE_ON"
| "TOGGLE_CYCLE_OFF"
| "TOGGLE_MODULE_ON"
| "TOGGLE_MODULE_OFF"
| "TOGGLE_VIEW_ON"
| "TOGGLE_VIEW_OFF"
| "TOGGLE_PAGES_ON"
| "TOGGLE_PAGES_OFF"
| "TOGGLE_STATE_ON"
| "TOGGLE_STATE_OFF";
type IntegrationEventType = "ADD_WORKSPACE_INTEGRATION" | "REMOVE_WORKSPACE_INTEGRATION";
type GitHubSyncEventType = "GITHUB_REPO_SYNC";
type PageBlocksEventType =
| "PAGE_BLOCK_CREATE"
| "PAGE_BLOCK_UPDATE"
| "PAGE_BLOCK_DELETE"
| "PAGE_BLOCK_CONVERTED_TO_ISSUE";
type IssueLabelEventType = "ISSUE_LABEL_CREATE" | "ISSUE_LABEL_UPDATE" | "ISSUE_LABEL_DELETE";
type GptEventType = "ASK_GPT" | "USE_GPT_RESPONSE_IN_ISSUE" | "USE_GPT_RESPONSE_IN_PAGE_BLOCK";
class TrackEventServices extends APIService {
@ -85,7 +110,7 @@ class TrackEventServices extends APIService {
eventName: ProjectEventType
): Promise<any> {
let payload: any;
if (eventName !== "DELETE_PROJECT")
if (eventName !== "DELETE_PROJECT" && eventName !== "PROJECT_MEMBER_INVITE")
payload = {
workspaceId: data?.workspace_detail?.id,
workspaceName: data?.workspace_detail?.name,
@ -131,7 +156,6 @@ class TrackEventServices extends APIService {
projectName: data?.project_detail?.name,
projectIdentifier: data?.project_detail?.identifier,
issueId: data?.id,
issueTitle: data?.name,
};
else payload = data;
@ -147,6 +171,89 @@ class TrackEventServices extends APIService {
});
}
async trackIssueMarkedAsDoneEvent(data: any): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
data: {
eventName: "ISSUES_MARKED_AS_DONE",
extra: {
...data,
},
},
});
}
async trackIssuePartialPropertyUpdateEvent(
data: any,
propertyName:
| "ISSUE_PROPERTY_UPDATE_PRIORITY"
| "ISSUE_PROPERTY_UPDATE_STATE"
| "ISSUE_PROPERTY_UPDATE_ASSIGNEE"
| "ISSUE_PROPERTY_UPDATE_DUE_DATE"
): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
data: {
eventName: propertyName,
extra: {
...data,
},
},
});
}
async trackIssueCommentEvent(
data: Partial<IIssueComment> | any,
eventName: IssueCommentType
): Promise<any> {
let payload: any;
if (eventName !== "ISSUE_COMMENT_DELETE")
payload = {
workspaceId: data?.workspace_detail?.id,
workspaceName: data?.workspace_detail?.name,
workspaceSlug: data?.workspace_detail?.slug,
projectId: data?.project_detail?.id,
projectName: data?.project_detail?.name,
projectIdentifier: data?.project_detail?.identifier,
issueId: data?.issue,
};
else payload = data;
return this.request({
url: "/api/track-event",
method: "POST",
data: {
eventName,
extra: {
...payload,
},
},
});
}
async trackIssueMovedToCycleOrModuleEvent(
data: any,
eventName:
| "ISSUE_MOVED_TO_CYCLE"
| "ISSUE_MOVED_TO_MODULE"
| "ISSUE_MOVED_TO_CYCLE_IN_BULK"
| "ISSUE_MOVED_TO_MODULE_IN_BULK"
): Promise<any> {
return this.request({
url: "/api/track-event",
method: "POST",
data: {
eventName,
extra: {
...data,
},
},
});
}
async trackIssueBulkDeleteEvent(data: any): Promise<any> {
return this.request({
url: "/api/track-event",
@ -160,6 +267,19 @@ class TrackEventServices extends APIService {
});
}
async trackIssueLabelEvent(data: any, eventName: IssueLabelEventType): Promise<any> {
return this.request({
url: "/api/track-event",
method: "POST",
data: {
eventName,
extra: {
...data,
},
},
});
}
async trackStateEvent(data: IState | any, eventName: StateEventType): Promise<any> {
let payload: any;
if (eventName !== "STATE_DELETE")
@ -171,7 +291,6 @@ class TrackEventServices extends APIService {
projectName: data?.project_detail?.name,
projectIdentifier: data?.project_detail?.identifier,
stateId: data.id,
stateName: data.name,
};
else payload = data;
@ -198,7 +317,6 @@ class TrackEventServices extends APIService {
projectName: data?.project_detail?.name,
projectIdentifier: data?.project_detail?.identifier,
cycleId: data.id,
cycleName: data.name,
};
else payload = data;
@ -225,7 +343,6 @@ class TrackEventServices extends APIService {
projectName: data.project_detail?.name,
projectIdentifier: data?.project_detail?.identifier,
moduleId: data.id,
moduleName: data.name,
};
else payload = data;
@ -392,6 +509,45 @@ class TrackEventServices extends APIService {
},
});
}
async trackMiscellaneousEvent(data: any, eventName: MiscellaneousEventType): Promise<any> {
return this.request({
url: "/api/track-event",
method: "POST",
data: {
eventName,
extra: {
...data,
},
},
});
}
async trackAppIntegrationEvent(data: any, eventName: IntegrationEventType): Promise<any> {
return this.request({
url: "/api/track-event",
method: "POST",
data: {
eventName,
extra: {
...data,
},
},
});
}
async trackGitHubSyncEvent(data: any, eventName: GitHubSyncEventType): Promise<any> {
return this.request({
url: "/api/track-event",
method: "POST",
data: {
eventName,
extra: {
...data,
},
},
});
}
}
const trackEventServices = new TrackEventServices();

View File

@ -160,7 +160,9 @@ export interface IIssueComment {
created_by: string;
updated_by: string;
project: string;
project_detail: IProjectLite;
workspace: string;
workspace_detail: IWorkspaceLite;
issue: string;
}
@ -195,7 +197,9 @@ export interface IIssueLabels {
created_by: string;
updated_by: string;
project: string;
project_detail: IProjectLite;
workspace: string;
workspace_detail: IWorkspaceLite;
parent: string | null;
}