chore: added workspace settings events

This commit is contained in:
LAKHAN BAHETI 2024-03-11 17:25:19 +05:30
parent 35a9527325
commit 2b8569a0c9
16 changed files with 169 additions and 38 deletions

View File

@ -2,6 +2,8 @@ import { useState, Fragment, FC } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import { useEventTracker } from "hooks/store";
// services
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { API_TOKENS_LIST } from "constants/fetch-keys";
@ -9,6 +11,8 @@ import { APITokenService } from "services/api_token.service";
// ui
// types
import { IApiToken } from "@plane/types";
// constants
import { API_TOKEN_DELETED } from "constants/event-tracker";
// fetch-keys
type Props = {
@ -26,6 +30,8 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const { captureEvent } = useEventTracker();
const handleClose = () => {
onClose();
@ -45,6 +51,9 @@ export const DeleteApiTokenModal: FC<Props> = (props) => {
title: "Success!",
message: "Token deleted successfully.",
});
captureEvent(API_TOKEN_DELETED, {
token_id: tokenId,
});
mutate<IApiToken[]>(
API_TOKENS_LIST(workspaceSlug.toString()),

View File

@ -2,6 +2,8 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import { useEventTracker } from "hooks/store";
// services
import { TOAST_TYPE, setToast } from "@plane/ui";
@ -15,6 +17,8 @@ import { APITokenService } from "services/api_token.service";
// helpers
// types
import { IApiToken } from "@plane/types";
// constants
import { API_TOKEN_CREATED } from "constants/event-tracker";
// fetch-keys
type Props = {
@ -33,6 +37,8 @@ export const CreateApiTokenModal: React.FC<Props> = (props) => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const { captureEvent } = useEventTracker();
const handleClose = () => {
onClose();
@ -73,6 +79,11 @@ export const CreateApiTokenModal: React.FC<Props> = (props) => {
},
false
);
captureEvent(API_TOKEN_CREATED, {
token_id: res.id,
expiry_date: data.expired_at ?? undefined,
never_exprires: res.expired_at ? false : true,
});
})
.catch((err) => {
setToast({

View File

@ -5,12 +5,14 @@ import { Dialog, Transition } from "@headlessui/react";
// hooks
import { Button, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui";
import { useProject } from "hooks/store";
import { useEventTracker, useProject } from "hooks/store";
// services
import { ProjectExportService } from "services/project";
// ui
// types
import { IUser, IImporterService } from "@plane/types";
// constants
import { ISSUES_EXPORTED } from "constants/event-tracker";
type Props = {
isOpen: boolean;
@ -33,6 +35,7 @@ export const Exporter: React.FC<Props> = observer((props) => {
const { workspaceSlug } = router.query;
// store hooks
const { workspaceProjectIds, getProjectById } = useProject();
const { captureEvent } = useEventTracker();
const options = workspaceProjectIds?.map((projectId) => {
const projectDetails = getProjectById(projectId);
@ -68,6 +71,11 @@ export const Exporter: React.FC<Props> = observer((props) => {
mutateServices();
router.push(`/${workspaceSlug}/settings/exports`);
setExportLoading(false);
captureEvent(ISSUES_EXPORTED, {
format: provider,
project_ids: value,
export_seperate: multiple,
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Export Successful",

View File

@ -24,6 +24,7 @@ import {
import GithubLogo from "public/services/github.png";
import { IntegrationService, GithubIntegrationService } from "services/integrations";
// hooks
import { useEventTracker } from "hooks/store";
// components
// icons
// images
@ -31,6 +32,8 @@ import { IntegrationService, GithubIntegrationService } from "services/integrati
import { IGithubRepoCollaborator, IGithubServiceImportFormData } from "@plane/types";
// fetch-keys
import { APP_INTEGRATIONS, IMPORTER_SERVICES_LIST, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
// constants
import { GITHUB_ISSUES_IMPORTED } from "constants/event-tracker";
export type TIntegrationSteps = "import-configure" | "import-data" | "repo-details" | "import-users" | "import-confirm";
export interface IIntegrationData {
@ -90,9 +93,11 @@ export const GithubImporterRoot: React.FC = () => {
state: "import-configure",
});
const [users, setUsers] = useState<IUserDetails[]>([]);
// router
const router = useRouter();
const { workspaceSlug, provider } = router.query;
// store hooks
const { captureEvent } = useEventTracker();
const { handleSubmit, control, setValue, watch } = useForm<TFormValues>({
defaultValues: defaultFormValues,
@ -147,6 +152,7 @@ export const GithubImporterRoot: React.FC = () => {
.then(() => {
router.push(`/${workspaceSlug}/settings/imports`);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string));
captureEvent(GITHUB_ISSUES_IMPORTED);
})
.catch(() =>
setToast({

View File

@ -4,6 +4,8 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { FormProvider, useForm } from "react-hook-form";
import { mutate } from "swr";
// hooks
import { useEventTracker } from "hooks/store";
// icons
import { ArrowLeft, Check, List, Settings } from "lucide-react";
// services
@ -25,6 +27,8 @@ import {
TJiraIntegrationSteps,
IJiraIntegrationData,
} from ".";
// constants
import { JIRA_ISSUES_IMPORTED } from "constants/event-tracker";
const integrationWorkflowData: Array<{
title: string;
@ -61,9 +65,11 @@ export const JiraImporterRoot: React.FC = () => {
state: "import-configure",
});
const [disableTopBarAfter, setDisableTopBarAfter] = useState<TJiraIntegrationSteps | null>(null);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const { captureEvent } = useEventTracker();
const methods = useForm<IJiraImporterForm>({
defaultValues: jiraFormDefaultValues,
@ -81,6 +87,7 @@ export const JiraImporterRoot: React.FC = () => {
.then(() => {
mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString()));
router.push(`/${workspaceSlug}/settings/imports`);
captureEvent(JIRA_ISSUES_IMPORTED);
})
.catch((err) => {
console.error(err);

View File

@ -9,7 +9,7 @@ import { AlertTriangleIcon } from "lucide-react";
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// constants
import { PROJECT_MEMBER_LEAVE } from "constants/event-tracker";
import { PROJECT_MEMBER_LEFT } from "constants/event-tracker";
// hooks
import { useEventTracker, useUser } from "hooks/store";
// types
@ -64,7 +64,7 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
.then(() => {
handleClose();
router.push(`/${workspaceSlug}/projects`);
captureEvent(PROJECT_MEMBER_LEAVE, {
captureEvent(PROJECT_MEMBER_LEFT, {
state: "SUCCESS",
element: "Project settings members page",
});
@ -75,7 +75,7 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
title: "Error!",
message: "Something went wrong please try again later.",
});
captureEvent(PROJECT_MEMBER_LEAVE, {
captureEvent(PROJECT_MEMBER_LEFT, {
state: "FAILED",
element: "Project settings members page",
});

View File

@ -9,7 +9,7 @@ import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { ConfirmProjectMemberRemove } from "components/project";
// constants
import { PM_ROLE_CHANGED, PROJECT_MEMBER_LEAVE, PROJECT_MEMBER_REMOVED } from "constants/event-tracker";
import { PM_ROLE_CHANGED, PROJECT_MEMBER_LEFT, PROJECT_MEMBER_REMOVED } from "constants/event-tracker";
import { EUserProjectRoles } from "constants/project";
import { ROLE } from "constants/workspace";
// hooks
@ -49,7 +49,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
if (userDetails.member?.id === currentUser?.id) {
await leaveProject(workspaceSlug.toString(), projectId.toString())
.then(async () => {
captureEvent(PROJECT_MEMBER_LEAVE, {
captureEvent(PROJECT_MEMBER_LEFT, {
state: "SUCCESS",
element: "Project settings members page",
});

View File

@ -3,6 +3,8 @@ import { useRouter } from "next/router";
// ui
import { Dialog, Transition } from "@headlessui/react";
import { TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useEventTracker } from "hooks/store";
// components
// helpers
import { csvDownload } from "helpers/download.helper";
@ -12,6 +14,8 @@ import { WebhookForm } from "./form";
import { GeneratedHookDetails } from "./generated-hook-details";
// utils
import { getCurrentHookAsCSV } from "./utils";
// constants
import { WEBHOOK_CREATED } from "constants/event-tracker";
// ui
interface ICreateWebhookModal {
@ -35,6 +39,8 @@ export const CreateWebhookModal: React.FC<ICreateWebhookModal> = (props) => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const { captureEvent } = useEventTracker();
const handleCreateWebhook = async (formData: IWebhook, webhookEventType: TWebhookEventTypes) => {
if (!workspaceSlug) return;
@ -64,6 +70,14 @@ export const CreateWebhookModal: React.FC<ICreateWebhookModal> = (props) => {
await createWebhook(workspaceSlug.toString(), payload)
.then(({ webHook, secretKey }) => {
captureEvent(WEBHOOK_CREATED, {
webhook_id: webHook.id,
event_type: webhookEventType,
events:
webhookEventType === "individual"
? Object.keys(payload).filter((key) => key !== "url" && payload[key as keyof IWebhook] === true)
: undefined,
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",

View File

@ -5,7 +5,9 @@ import { AlertTriangle } from "lucide-react";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useWebhook } from "hooks/store";
import { useEventTracker, useWebhook } from "hooks/store";
// constants
import { WEBHOOK_DELETED } from "constants/event-tracker";
interface IDeleteWebhook {
isOpen: boolean;
@ -20,6 +22,7 @@ export const DeleteWebhookModal: FC<IDeleteWebhook> = (props) => {
const router = useRouter();
// store hooks
const { removeWebhook } = useWebhook();
const { captureEvent } = useEventTracker();
const { workspaceSlug, webhookId } = router.query;
@ -34,6 +37,9 @@ export const DeleteWebhookModal: FC<IDeleteWebhook> = (props) => {
removeWebhook(workspaceSlug.toString(), webhookId.toString())
.then(() => {
captureEvent(WEBHOOK_DELETED, {
webhook_id: webhookId.toString(),
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",

View File

@ -10,11 +10,13 @@ import {
WebhookSecretKey,
WebhookToggle,
} from "components/web-hooks";
import { useWebhook } from "hooks/store";
import { useEventTracker, useWebhook } from "hooks/store";
// components
// ui
// types
import { IWebhook, TWebhookEventTypes } from "@plane/types";
// constants
import { WEBHOOK_UPDATED } from "constants/event-tracker";
type Props = {
data?: Partial<IWebhook>;
@ -37,17 +39,25 @@ export const WebhookForm: FC<Props> = observer((props) => {
const [webhookEventType, setWebhookEventType] = useState<TWebhookEventTypes>("all");
// store hooks
const { webhookSecretKey } = useWebhook();
const { captureEvent } = useEventTracker();
// use form
const {
handleSubmit,
control,
formState: { isSubmitting, errors },
formState: { isSubmitting, errors, dirtyFields },
} = useForm<IWebhook>({
defaultValues: { ...initialWebhookPayload, ...data },
});
const handleFormSubmit = async (formData: IWebhook) => {
await onSubmit(formData, webhookEventType);
await onSubmit(formData, webhookEventType).then(() => {
if (!data) return;
captureEvent(WEBHOOK_UPDATED, {
webhook_id: data.id,
change_details: Object.keys(dirtyFields),
enabled: formData.is_active,
});
});
};
useEffect(() => {

View File

@ -9,11 +9,13 @@ import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
import { csvDownload } from "helpers/download.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// hooks
import { useWebhook, useWorkspace } from "hooks/store";
import { useEventTracker, useWebhook, useWorkspace } from "hooks/store";
// types
import { IWebhook } from "@plane/types";
// utils
import { getCurrentHookAsCSV } from "../utils";
// constants
import { WEBHOOK_KEY_REGEN } from "constants/event-tracker";
type Props = {
data: Partial<IWebhook>;
@ -30,6 +32,7 @@ export const WebhookSecretKey: FC<Props> = observer((props) => {
// store hooks
const { currentWorkspace } = useWorkspace();
const { currentWebhook, regenerateSecretKey, webhookSecretKey } = useWebhook();
const { captureEvent } = useEventTracker();
const handleCopySecretKey = () => {
if (!webhookSecretKey) return;
@ -63,6 +66,9 @@ export const WebhookSecretKey: FC<Props> = observer((props) => {
title: "Success!",
message: "New key regenerated successfully.",
});
captureEvent(WEBHOOK_KEY_REGEN, {
webhook_id: data.id,
});
if (currentWebhook && webhookSecretKey) {
const csvData = getCurrentHookAsCSV(currentWorkspace, currentWebhook, webhookSecretKey);

View File

@ -3,10 +3,12 @@ import Link from "next/link";
import { useRouter } from "next/router";
// hooks
import { ToggleSwitch } from "@plane/ui";
import { useWebhook } from "hooks/store";
import { useEventTracker, useWebhook } from "hooks/store";
// ui
// types
import { IWebhook } from "@plane/types";
// constants
import { WEBHOOK_DISABLED, WEBHOOK_ENABLED } from "constants/event-tracker";
interface IWebhookListItem {
webhook: IWebhook;
@ -19,11 +21,16 @@ export const WebhooksListItem: FC<IWebhookListItem> = (props) => {
const { workspaceSlug } = router.query;
// store hooks
const { updateWebhook } = useWebhook();
const { captureEvent } = useEventTracker();
const handleToggle = () => {
if (!workspaceSlug || !webhook.id) return;
updateWebhook(workspaceSlug.toString(), webhook.id, { is_active: !webhook.is_active });
updateWebhook(workspaceSlug.toString(), webhook.id, { is_active: !webhook.is_active }).then(() =>
captureEvent(!webhook.is_active ? WEBHOOK_ENABLED : WEBHOOK_DISABLED, {
webhook_id: webhook.id,
})
);
};
return (

View File

@ -8,8 +8,10 @@ import { ChevronDown, Dot, XCircle } from "lucide-react";
import { CustomSelect, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { ConfirmWorkspaceMemberRemove } from "components/workspace";
// helpers
import { getUserRole } from "helpers/user.helper";
// constants
import { WORKSPACE_MEMBER_lEAVE } from "constants/event-tracker";
import { WM_ROLE_CHANGED, WORKSPACE_MEMBER_REMOVED, WORKSPACE_MEMBER_LEFT } from "constants/event-tracker";
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
// hooks
import { useEventTracker, useMember, useUser } from "hooks/store";
@ -43,7 +45,9 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
await leaveWorkspace(workspaceSlug.toString())
.then(() => {
captureEvent(WORKSPACE_MEMBER_lEAVE, {
captureEvent(WORKSPACE_MEMBER_LEFT, {
member_id: currentUser?.id,
role: currentWorkspaceRole ? getUserRole(currentWorkspaceRole as number) : undefined,
state: "SUCCESS",
element: "Workspace settings members page",
});
@ -61,13 +65,23 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
const handleRemoveMember = async () => {
if (!workspaceSlug || !memberDetails) return;
await removeMemberFromWorkspace(workspaceSlug.toString(), memberDetails.member.id).catch((err) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error",
message: err?.error || "Something went wrong. Please try again.",
})
);
await removeMemberFromWorkspace(workspaceSlug.toString(), memberDetails.member.id)
.then(() =>
captureEvent(WORKSPACE_MEMBER_REMOVED, {
member_id: memberDetails.member.id,
removed_by_role: currentWorkspaceRole ? getUserRole(currentWorkspaceRole as number) : undefined,
role: memberDetails.role ? getUserRole(memberDetails.role as number) : undefined,
state: "SUCCESS",
element: "Workspace settings members page",
})
)
.catch((err) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error",
message: err?.error || "Something went wrong. Please try again.",
})
);
};
const handleRemove = async () => {
@ -162,13 +176,22 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
updateMember(workspaceSlug.toString(), memberDetails.member.id, {
role: value,
}).catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "An error occurred while updating member role. Please try again.",
})
.then(() =>
captureEvent(WM_ROLE_CHANGED, {
member_id: memberDetails.member.id,
changed_role: value ? getUserRole(value as number) : undefined,
state: "SUCCESS",
element: "Workspace settings members page",
})
)
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "An error occurred while updating member role. Please try again.",
});
});
});
}}
disabled={!hasRoleChangeAccess}
placement="bottom-end"

View File

@ -48,7 +48,7 @@ export const WorkspaceDetails: FC = observer(() => {
control,
reset,
watch,
formState: { errors },
formState: { errors, dirtyFields },
} = useForm<IWorkspace>({
defaultValues: { ...defaultValues, ...currentWorkspace },
});
@ -70,6 +70,7 @@ export const WorkspaceDetails: FC = observer(() => {
eventName: WORKSPACE_UPDATED,
payload: {
...res,
change_details: Object.keys(dirtyFields),
state: "SUCCESS",
element: "Workspace general settings page",
},

View File

@ -19,6 +19,7 @@ export const getWorkspaceEventPayload = (payload: any) => ({
organization_size: payload.organization_size,
first_time: payload.first_time,
state: payload.state,
change_details: payload.change_details,
element: payload.element,
});
@ -172,7 +173,9 @@ export const elementFromPath = (routePath?: string) => {
return {
element: element,
element_id: ["Project", "Draft", "Archive"].includes(element) ? routePath.split("/")?.at(-2) : routePath.split("/")?.at(-1),
element_id: ["Project", "Draft", "Archive"].includes(element)
? routePath.split("/")?.at(-2)
: routePath.split("/")?.at(-1),
};
};
@ -265,14 +268,34 @@ export const PAGE_RESTORED = "Page restored";
export const AI_TRIGGERED = "AI triggered";
export const AI_RES_USED = "AI response used";
export const AI_RES_REGENERATED = "AI response regenerated";
// Member Events
export const MEMBER_INVITED = "Member invited";
export const MEMBER_ACCEPTED = "Member accepted";
// Project Member Events
export const PROJECT_MEMBER_ADDED = "Project member added";
export const PROJECT_MEMBER_LEAVE = "Project member leave";
export const PROJECT_MEMBER_LEFT = "Project member left";
export const PROJECT_MEMBER_REMOVED = "Project member removed";
export const PM_ROLE_CHANGED = "Project member role changed";
export const WORKSPACE_MEMBER_lEAVE = "Workspace member leave";
// Workspace Member Events
export const MEMBER_INVITED = "Member invited";
export const MEMBER_ACCEPTED = "Member accepted";
export const WORKSPACE_MEMBER_REMOVED = "Workspace member removed";
export const WORKSPACE_MEMBER_LEFT = "Workspace member left";
export const WM_ROLE_CHANGED = "Workspace member role changed";
// Issues Export Events
export const ISSUES_EXPORTED = "Issues exported";
// Issues Import Events
export const GITHUB_ISSUES_IMPORTED = "Github issues imported";
export const JIRA_ISSUES_IMPORTED = "Jira issues imported";
// Webhook Events
export const WEBHOOK_CREATED = "Webhook created";
export const WEBHOOK_UPDATED = "Webhook updated";
export const WEBHOOK_DELETED = "Webhook deleted";
export const WEBHOOK_ENABLED = "Webhook enabled";
export const WEBHOOK_DISABLED = "Webhook diabled";
export const WEBHOOK_KEY_REGEN = "Webhook secret key regenerated";
// API Token Events
export const API_TOKEN_CREATED = "API token created";
export const API_TOKEN_UPDATED = "API token updated";
export const API_TOKEN_DELETED = "API token deleted";
export const API_TOKEN_REGEN = "API token regenerated";
// Sign-in & Sign-up Events
export const NAVIGATE_TO_SIGNUP = "Navigate to sign-up page";
export const NAVIGATE_TO_SIGNIN = "Navigate to sign-in page";

View File

@ -26,7 +26,7 @@ const PostHogProvider: FC<IPosthogWrapper> = (props) => {
currentWorkspaceId,
posthogAPIKey,
posthogHost,
isCloud = false,
isCloud = true,
telemetryEnabled = false,
} = props;
// states