feat: add ga events

This commit is contained in:
AykutSarac 2024-05-09 17:32:52 +03:00
parent e4d9fff148
commit 4a5dcfb704
No known key found for this signature in database
23 changed files with 191 additions and 62 deletions

View File

@ -15,6 +15,7 @@ import { BiSolidDockLeft } from "react-icons/bi";
import { MdOutlineCheckCircleOutline } from "react-icons/md";
import { TbTransform } from "react-icons/tb";
import { VscError, VscFeedback, VscSourceControl, VscSync, VscSyncIgnored } from "react-icons/vsc";
import { gaEvent } from "src/lib/utils/gaEvent";
import { documentSvc } from "src/services/document.service";
import useConfig from "src/store/useConfig";
import useFile from "src/store/useFile";
@ -106,7 +107,10 @@ export const BottomBar = () => {
const [isPrivate, setIsPrivate] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const toggleEditor = () => toggleFullscreen(!fullscreen);
const toggleEditor = () => {
toggleFullscreen(!fullscreen);
gaEvent("Bottom Bar", "toggle fullscreen");
};
React.useEffect(() => {
setIsPrivate(data?.private ?? true);
@ -239,12 +243,22 @@ export const BottomBar = () => {
Share
</StyledBottomBarItem>
{liveTransformEnabled ? (
<StyledBottomBarItem onClick={() => toggleLiveTransform(false)}>
<StyledBottomBarItem
onClick={() => {
toggleLiveTransform(false);
gaEvent("Bottom Bar", "toggle live transform", "manual");
}}
>
<VscSync />
<Text fz="xs">Live Transform</Text>
</StyledBottomBarItem>
) : (
<StyledBottomBarItem onClick={() => toggleLiveTransform(true)}>
<StyledBottomBarItem
onClick={() => {
toggleLiveTransform(true);
gaEvent("Bottom Bar", "toggle live transform", "live");
}}
>
<VscSyncIgnored />
<Text fz="xs">Manual Transform</Text>
</StyledBottomBarItem>

View File

@ -11,6 +11,7 @@ import {
Badge,
} from "@mantine/core";
import { IoRocketSharp } from "react-icons/io5";
import { gaEvent } from "src/lib/utils/gaEvent";
import useModal from "src/store/useModal";
import useUser from "src/store/useUser";
@ -62,7 +63,10 @@ export const AccountModal: React.FC<ModalProps> = ({ opened, onClose }) => {
variant="gradient"
gradient={{ from: "teal", to: "lime", deg: 105 }}
leftSection={<IoRocketSharp />}
onClick={() => setVisible("premium")(true)}
onClick={() => {
setVisible("premium")(true);
gaEvent("Account Modal", "click upgrade premium");
}}
>
Upgrade to Premium
</Button>

View File

@ -30,6 +30,7 @@ import { FaTrash } from "react-icons/fa";
import { SlOptionsVertical } from "react-icons/sl";
import { VscAdd } from "react-icons/vsc";
import { FileFormat } from "src/enums/file.enum";
import { gaEvent } from "src/lib/utils/gaEvent";
import { documentSvc } from "src/services/document.service";
import useFile, { File } from "src/store/useFile";
@ -124,6 +125,7 @@ export const CloudModal: React.FC<ModalProps> = ({ opened, onClose }) => {
if (error) throw new Error(error.message);
if (data[0]) setFile(data[0]);
gaEvent("Cloud Modal", "open file");
} catch (error) {
if (error instanceof Error) toast.error(error.message);
} finally {
@ -134,14 +136,23 @@ export const CloudModal: React.FC<ModalProps> = ({ opened, onClose }) => {
);
const onDeleteClick = React.useCallback(
(file: File) => {
toast
.promise(documentSvc.delete(file.id), {
loading: "Deleting file...",
error: "An error occurred while deleting the file!",
success: `Deleted ${file.name}!`,
})
.then(() => refetch());
async (file: File) => {
try {
toast.loading("Deleting file...", { id: "delete-file" });
const { error } = await documentSvc.delete(file.id);
if (error) throw new Error(error.message);
await refetch();
toast.success(`Deleted ${file.name}!`, { id: "delete-file" });
gaEvent("Cloud Modal", "delete file");
} catch (error) {
if (error instanceof Error) {
toast.error(error.message, { id: "delete-file" });
}
} finally {
toast.dismiss("delete-file");
}
},
[refetch]
);

View File

@ -90,7 +90,7 @@ export const DownloadModal: React.FC<ModalProps> = ({ opened, onClose }) => {
]);
toast.success("Copied to clipboard");
gaEvent("click", "clipboard image");
gaEvent("Download Modal", "clipboard image");
} catch (error) {
toast.error("Failed to copy to clipboard");
} finally {
@ -111,7 +111,7 @@ export const DownloadModal: React.FC<ModalProps> = ({ opened, onClose }) => {
});
downloadURI(dataURI, `${fileDetails.filename}.${extension}`);
gaEvent("download", "download graph image", extension);
gaEvent("Download Modal", "download image", extension);
} catch (error) {
toast.error("Failed to download image!");
} finally {

View File

@ -14,6 +14,7 @@ import { Dropzone } from "@mantine/dropzone";
import toast from "react-hot-toast";
import { AiOutlineUpload } from "react-icons/ai";
import { FileFormat } from "src/enums/file.enum";
import { gaEvent } from "src/lib/utils/gaEvent";
import useFile from "src/store/useFile";
export const ImportModal: React.FC<ModalProps> = ({ opened, onClose }) => {
@ -28,6 +29,8 @@ export const ImportModal: React.FC<ModalProps> = ({ opened, onClose }) => {
setFile(null);
toast.loading("Loading...", { id: "toastFetch" });
gaEvent("Import Modal", "fetch url");
return fetch(url)
.then(res => res.json())
.then(json => {
@ -47,6 +50,8 @@ export const ImportModal: React.FC<ModalProps> = ({ opened, onClose }) => {
setURL("");
onClose();
});
gaEvent("Import Modal", "import file", format);
}
};

View File

@ -1,6 +1,7 @@
import React from "react";
import { Modal, Button, ModalProps, Textarea, Divider, Group } from "@mantine/core";
import { decode } from "jsonwebtoken";
import { gaEvent } from "src/lib/utils/gaEvent";
import useFile from "src/store/useFile";
export const JWTModal: React.FC<ModalProps> = ({ opened, onClose }) => {
@ -10,8 +11,9 @@ export const JWTModal: React.FC<ModalProps> = ({ opened, onClose }) => {
const resolve = () => {
if (!token) return;
const json = decode(token);
setContents({ contents: JSON.stringify(json, null, 2) });
gaEvent("JWT Modal", "resolve");
setToken("");
onClose();
};

View File

@ -2,6 +2,7 @@ import React from "react";
import { Modal, Stack, Text, ScrollArea, ModalProps, Button } from "@mantine/core";
import { CodeHighlight } from "@mantine/code-highlight";
import { VscLock } from "react-icons/vsc";
import { gaEvent } from "src/lib/utils/gaEvent";
import useGraph from "src/store/useGraph";
import useModal from "src/store/useModal";
@ -32,7 +33,10 @@ export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
</ScrollArea.Autosize>
</Stack>
<Button
onClick={() => setVisible("premium")(true)}
onClick={() => {
setVisible("premium")(true);
gaEvent("Node Modal", "edit");
}}
rightSection={<VscLock strokeWidth={0.5} />}
>
Edit

View File

@ -13,6 +13,7 @@ import {
} from "@mantine/core";
import { BsCheck } from "react-icons/bs";
import { MdChevronRight } from "react-icons/md";
import { gaEvent } from "src/lib/utils/gaEvent";
export const PremiumModal: React.FC<ModalProps> = ({ opened, onClose }) => {
return (
@ -42,6 +43,7 @@ export const PremiumModal: React.FC<ModalProps> = ({ opened, onClose }) => {
<Stack gap="xs">
<Title order={3}>Premium</Title>
<Button
onClick={() => gaEvent("Premium Modal", "click upgrade premium")}
component={Link}
prefetch={false}
href="/pricing"

View File

@ -3,6 +3,7 @@ import { Stack, Modal, Button, ModalProps, Text, Anchor, Group, Divider } from "
import Editor from "@monaco-editor/react";
import { toast } from "react-hot-toast";
import { VscLinkExternal } from "react-icons/vsc";
import { gaEvent } from "src/lib/utils/gaEvent";
import useConfig from "src/store/useConfig";
import useFile from "src/store/useFile";
@ -32,8 +33,9 @@ export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
const onApply = () => {
try {
const parsedSchema = JSON.parse(schema);
setJsonSchema(parsedSchema);
gaEvent("Schema Modal", "apply");
toast.success("Applied schema!");
onClose();
} catch (error) {

View File

@ -13,6 +13,7 @@ import {
} from "@mantine/core";
import { FiExternalLink } from "react-icons/fi";
import { MdCheck, MdCopyAll } from "react-icons/md";
import { gaEvent } from "src/lib/utils/gaEvent";
export const ShareModal: React.FC<ModalProps> = ({ opened, onClose }) => {
const { query } = useRouter();
@ -32,7 +33,13 @@ export const ShareModal: React.FC<ModalProps> = ({ opened, onClose }) => {
<CopyButton value={shareURL} timeout={2000}>
{({ copied, copy }) => (
<Tooltip label={copied ? "Copied" : "Copy"} withArrow position="right">
<ActionIcon color={copied ? "teal" : "gray"} onClick={copy}>
<ActionIcon
color={copied ? "teal" : "gray"}
onClick={() => {
copy();
gaEvent("Share Modal", "copy");
}}
>
{copied ? <MdCheck size="1rem" /> : <MdCopyAll size="1rem" />}
</ActionIcon>
</Tooltip>

View File

@ -1,6 +1,7 @@
import React from "react";
import { Stack, Modal, ModalProps, Select, ScrollArea } from "@mantine/core";
import { CodeHighlight } from "@mantine/code-highlight";
import { gaEvent } from "src/lib/utils/gaEvent";
import useJson from "src/store/useJson";
enum Language {
@ -93,7 +94,10 @@ export const TypeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
<Select
value={selectedType}
data={typeOptions}
onChange={e => setSelectedType(e as Language)}
onChange={e => {
setSelectedType(e as Language);
gaEvent("Type Modal", "generate", e as string);
}}
allowDeselect={false}
/>
<ScrollArea.Autosize mah={400} maw={700}>

View File

@ -19,7 +19,7 @@ export const FileMenu = () => {
a.download = `jsoncrack.${getFormat()}`;
a.click();
gaEvent("download", "file download");
gaEvent("File Menu", "download", getFormat());
};
return (

View File

@ -2,6 +2,7 @@ import React from "react";
import { Menu, Text, Flex } from "@mantine/core";
import { BsCheck2 } from "react-icons/bs";
import { MdSettings } from "react-icons/md";
import { gaEvent } from "src/lib/utils/gaEvent";
import useConfig from "src/store/useConfig";
import * as Styles from "./styles";
@ -32,37 +33,55 @@ export const OptionsMenu = () => {
<Menu.Dropdown>
<Menu.Item
leftSection={<BsCheck2 opacity={rulersEnabled ? 100 : 0} />}
onClick={() => toggleRulers(!rulersEnabled)}
onClick={() => {
toggleRulers(!rulersEnabled);
gaEvent("Options Menu", "toggle rulers", rulersEnabled ? "on" : "off");
}}
>
<Text size="xs">Rulers</Text>
</Menu.Item>
<Menu.Item
leftSection={<BsCheck2 opacity={gesturesEnabled ? 100 : 0} />}
onClick={() => toggleGestures(!gesturesEnabled)}
onClick={() => {
toggleGestures(!gesturesEnabled);
gaEvent("Options Menu", "toggle gestures", gesturesEnabled ? "on" : "off");
}}
>
<Text size="xs">Trackpad Gestures</Text>
</Menu.Item>
<Menu.Item
leftSection={<BsCheck2 opacity={childrenCountVisible ? 100 : 0} />}
onClick={() => toggleChildrenCount(!childrenCountVisible)}
onClick={() => {
toggleChildrenCount(!childrenCountVisible);
gaEvent("Options Menu", "toggle children count", childrenCountVisible ? "on" : "off");
}}
>
<Text size="xs">Item Count</Text>
</Menu.Item>
<Menu.Item
leftSection={<BsCheck2 opacity={imagePreviewEnabled ? 100 : 0} />}
onClick={() => toggleImagePreview(!imagePreviewEnabled)}
onClick={() => {
toggleImagePreview(!imagePreviewEnabled);
gaEvent("Options Menu", "toggle image preview", imagePreviewEnabled ? "on" : "off");
}}
>
<Text size="xs">Image Link Preview</Text>
</Menu.Item>
<Menu.Item
leftSection={<BsCheck2 opacity={collapseButtonVisible ? 100 : 0} />}
onClick={() => toggleCollapseButton(!collapseButtonVisible)}
onClick={() => {
toggleCollapseButton(!collapseButtonVisible);
gaEvent("Options Menu", "toggle collapse button", collapseButtonVisible ? "on" : "off");
}}
>
<Text size="xs">Show Expand/Collapse</Text>
</Menu.Item>
<Menu.Item
leftSection={<BsCheck2 opacity={darkmodeEnabled ? 100 : 0} />}
onClick={() => toggleDarkMode(!darkmodeEnabled)}
onClick={() => {
toggleDarkMode(!darkmodeEnabled);
gaEvent("Options Menu", "toggle dark mode", darkmodeEnabled ? "on" : "off");
}}
>
<Text size="xs">Dark Mode</Text>
</Menu.Item>

View File

@ -14,7 +14,7 @@ export const ToolsMenu = () => {
return (
<Menu shadow="md" withArrow>
<Menu.Target>
<Styles.StyledToolElement onClick={() => gaEvent("click", "tools menu")}>
<Styles.StyledToolElement onClick={() => gaEvent("Tools Menu", "toggle menu")}>
<Flex align="center" gap={3}>
Tools <CgChevronDown />
</Flex>
@ -25,27 +25,50 @@ export const ToolsMenu = () => {
fz={12}
leftSection={<MdCompare />}
rightSection={<VscLock />}
onClick={() => setVisible("premium")(true)}
onClick={() => {
setVisible("premium")(true);
gaEvent("Tools Menu", "open", "Compare Data");
}}
>
Compare Data
</Menu.Item>
<Menu.Item fz={12} leftSection={<VscSearchFuzzy />} onClick={() => setVisible("jq")(true)}>
<Menu.Item
fz={12}
leftSection={<VscSearchFuzzy />}
onClick={() => {
setVisible("jq")(true);
gaEvent("Tools Menu", "open", "JSON Query");
}}
>
JSON Query (jq)
</Menu.Item>
<Menu.Item fz={12} leftSection={<VscJson />} onClick={() => setVisible("schema")(true)}>
<Menu.Item
fz={12}
leftSection={<VscJson />}
onClick={() => {
setVisible("schema")(true);
gaEvent("Tools Menu", "open", "JSON Schema");
}}
>
JSON Schema
</Menu.Item>
<Menu.Item
fz={12}
leftSection={<SiJsonwebtokens />}
onClick={() => setVisible("jwt")(true)}
onClick={() => {
setVisible("jwt")(true);
gaEvent("Tools Menu", "open", "Decode JWT");
}}
>
Decode JWT
</Menu.Item>
<Menu.Item
fz={12}
leftSection={<VscGroupByRefType />}
onClick={() => setVisible("type")(true)}
onClick={() => {
setVisible("type")(true);
gaEvent("Tools Menu", "open", "Generate Type");
}}
>
Generate Type
</Menu.Item>

View File

@ -65,7 +65,7 @@ export const ViewMenu = () => {
return (
<Menu shadow="md" closeOnItemClick={false} withArrow>
<Menu.Target>
<Styles.StyledToolElement onClick={() => gaEvent("click", "view menu")}>
<Styles.StyledToolElement onClick={() => gaEvent("View Menu", "open menu")}>
<Flex align="center" gap={3}>
View <CgChevronDown />
</Flex>
@ -76,7 +76,10 @@ export const ViewMenu = () => {
miw={205}
size="xs"
value={viewMode}
onChange={e => setViewMode(e as ViewMode)}
onChange={e => {
setViewMode(e as ViewMode);
gaEvent("View Menu", "change view mode", e as string);
}}
data={[
{ value: ViewMode.Graph, label: "Graph" },
{ value: ViewMode.Tree, label: "Tree" },
@ -88,7 +91,10 @@ export const ViewMenu = () => {
<Menu.Item
mt="xs"
fz={12}
onClick={toggleDirection}
onClick={() => {
toggleDirection();
gaEvent("View Menu", "rotate layout");
}}
leftSection={<Styles.StyledFlowIcon rotate={rotateLayout(direction || "RIGHT")} />}
rightSection={
<Text ml="md" fz={10} c="dimmed">
@ -100,7 +106,10 @@ export const ViewMenu = () => {
</Menu.Item>
<Menu.Item
fz={12}
onClick={toggleExpandCollapseGraph}
onClick={() => {
toggleExpandCollapseGraph();
gaEvent("View Menu", "expand collapse graph");
}}
leftSection={graphCollapsed ? <VscExpandAll /> : <VscCollapseAll />}
rightSection={
<Text ml="md" fz={10} c="dimmed">

View File

@ -27,7 +27,7 @@ export const ZoomMenu = () => {
return (
<Menu shadow="md" trigger="click" closeOnItemClick={false} withArrow>
<Menu.Target>
<Styles.StyledToolElement onClick={() => gaEvent("click", "zoom menu")}>
<Styles.StyledToolElement onClick={() => gaEvent("Zoom Menu", "open menu")}>
<Flex gap={4} align="center">
{Math.round(zoomFactor * 100)}%
<CgChevronDown />
@ -45,22 +45,56 @@ export const ZoomMenu = () => {
rightSection="%"
/>
</Menu.Item>
<Menu.Item rightSection="+" onClick={zoomIn}>
<Menu.Item
rightSection="+"
onClick={() => {
zoomIn();
gaEvent("Zoom Menu", "zoom in");
}}
>
<Text size="xs">Zoom in</Text>
</Menu.Item>
<Menu.Item rightSection="-" onClick={zoomOut}>
<Menu.Item
rightSection="-"
onClick={() => {
zoomOut();
gaEvent("Zoom Menu", "zoom out");
}}
>
<Text size="xs">Zoom out</Text>
</Menu.Item>
<Menu.Item rightSection="⇧ 1" onClick={centerView}>
<Menu.Item
rightSection="⇧ 1"
onClick={() => {
centerView();
gaEvent("Zoom Menu", "center view");
}}
>
<Text size="xs">Zoom to fit</Text>
</Menu.Item>
<Menu.Item onClick={() => setZoomFactor(50 / 100)}>
<Menu.Item
onClick={() => {
setZoomFactor(50 / 100);
gaEvent("Zoom Menu", "zoom to 50%");
}}
>
<Text size="xs">Zoom to %50</Text>
</Menu.Item>
<Menu.Item rightSection="⇧ 0" onClick={() => setZoomFactor(100 / 100)}>
<Menu.Item
rightSection="⇧ 0"
onClick={() => {
setZoomFactor(100 / 100);
gaEvent("Zoom Menu", "zoom to 100%");
}}
>
<Text size="xs">Zoom to %100</Text>
</Menu.Item>
<Menu.Item onClick={() => setZoomFactor(200 / 100)}>
<Menu.Item
onClick={() => {
setZoomFactor(200 / 100);
gaEvent("Zoom Menu", "zoom to 200%");
}}
>
<Text size="xs">Zoom to %200</Text>
</Menu.Item>
</Menu.Dropdown>

View File

@ -8,6 +8,7 @@ import { FiDownload } from "react-icons/fi";
import { SearchInput } from "src/components/SearchInput";
import { FileFormat } from "src/enums/file.enum";
import { JSONCrackLogo } from "src/layout/JsonCrackLogo";
import { gaEvent } from "src/lib/utils/gaEvent";
import useFile from "src/store/useFile";
import useModal from "src/store/useModal";
import { AccountMenu } from "./AccountMenu";
@ -80,6 +81,7 @@ export const Toolbar: React.FC<{ isWidget?: boolean }> = ({ isWidget = false })
onClick={() => {
setSeenPremium(true);
setVisible("premium")(true);
gaEvent("Toolbar", "click upgrade premium");
}}
>
<Indicator

View File

@ -4,6 +4,7 @@ import { Button, Title } from "@mantine/core";
import styled from "styled-components";
import { MdChevronRight } from "react-icons/md";
import { JSONCrackLogo } from "src/layout/JsonCrackLogo";
import { gaEvent } from "src/lib/utils/gaEvent";
const StyledPremiumView = styled.div`
position: relative;
@ -166,6 +167,7 @@ export const PremiumView = () => (
</StyledInfo>
<Button
onClick={() => gaEvent("Premium View", "click upgrade premium")}
component={Link}
prefetch={false}
href="/pricing"

View File

@ -39,7 +39,7 @@ export const useFocusNode = () => {
setNodeCount(0);
}
gaEvent("input", "search node in graph");
gaEvent("Graph", "search");
}, [selectedNode, debouncedValue, value, viewPort]);
return [value, setValue, skip, nodeCount, selectedNode] as const;

View File

@ -15,6 +15,7 @@ import styled from "styled-components";
import { AiOutlineInfoCircle } from "react-icons/ai";
import { VscArrowRight } from "react-icons/vsc";
import Layout from "src/layout/Layout";
import { gaEvent } from "src/lib/utils/gaEvent";
import useUser from "src/store/useUser";
const purchaseLinks = {
@ -161,6 +162,7 @@ export const PricingCards = () => {
</Flex>
<Button
component="a"
onClick={() => gaEvent("Pricing", "click upgrade premium")}
href={paymentURL(isMonthly ? purchaseLinks.monthly : purchaseLinks.annual)}
target="_blank"
size="lg"

View File

@ -45,6 +45,6 @@ export const documentSvc = {
return await supabase.from("document").update(data).eq("id", id).select("private");
},
delete: async (id: string) => {
await supabase.from("document").delete().eq("id", id);
return await supabase.from("document").delete().eq("id", id);
},
};

View File

@ -3,7 +3,6 @@ import { toast } from "react-hot-toast";
import { create } from "zustand";
import { defaultJson } from "src/constants/data";
import { FileFormat } from "src/enums/file.enum";
import { gaEvent } from "src/lib/utils/gaEvent";
import { contentToJson, jsonToContent } from "src/lib/utils/json/jsonAdapter";
import { isIframe } from "src/lib/utils/widget";
import { documentSvc } from "src/services/document.service";
@ -69,19 +68,6 @@ const debouncedUpdateJson = debounce((value: unknown) => {
useJson.getState().setJson(JSON.stringify(value, null, 2));
}, 800);
const filterArrayAndObjectFields = (obj: object) => {
const result = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (Array.isArray(obj[key]) || typeof obj[key] === "object") {
result[key] = obj[key];
}
}
}
return result;
};
const useFile = create<FileStates & JsonActions>()((set, get) => ({
...initialStates,
clear: () => {
@ -105,7 +91,6 @@ const useFile = create<FileStates & JsonActions>()((set, get) => ({
const jsonContent = await jsonToContent(JSON.stringify(contentJson, null, 2), format);
get().setContents({ contents: jsonContent });
gaEvent("input", "file format change");
} catch (error) {
get().clear();
console.warn("The content was unable to be converted, so it was cleared instead.");

View File

@ -1,6 +1,5 @@
import { create } from "zustand";
import { Modal } from "src/containers/Modals";
import { gaEvent } from "src/lib/utils/gaEvent";
import useUser from "./useUser";
type ModalState = {
@ -40,7 +39,6 @@ const useModal = create<ModalState & ModalActions>()(set => ({
return set({ login: true });
}
if (visible) gaEvent("modal", `open ${modal}`);
set({ [modal]: visible });
},
}));