fix(Analytics): no tags tracking for upload & crawl (#1024)

* 🚚 create useCrawlApi to use in useCrawler hook

* 🚑 fix tracking in Crawl

* 🧑‍💻 add hot reloading within docker containers

* 🚑 fix tracking for upload

* 🚚 create useUploadApi for fileUpload request

* 📈 add june tag for Language change

* 🩹 revert dependencies
This commit is contained in:
Zineb El Bachiri 2023-08-23 10:03:10 +02:00 committed by GitHub
parent 0ca25e2af5
commit 2b74ebc1f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 196 additions and 101 deletions

View File

@ -32,7 +32,7 @@ services:
context: backend context: backend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: backend-core container_name: backend-core
command: uvicorn main:app --host 0.0.0.0 --port 5050 command: uvicorn main:app --reload --host 0.0.0.0 --port 5050
restart: always restart: always
volumes: volumes:
- ./backend/:/code/ - ./backend/:/code/
@ -49,7 +49,7 @@ services:
context: backend context: backend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: backend-chat container_name: backend-chat
command: uvicorn chat_service:app --host 0.0.0.0 --port 5050 command: uvicorn --reload chat_service:app --host 0.0.0.0 --port 5050
restart: always restart: always
volumes: volumes:
- ./backend/:/code/ - ./backend/:/code/
@ -66,7 +66,7 @@ services:
context: backend context: backend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: backend-crawl container_name: backend-crawl
command: uvicorn crawl_service:app --host 0.0.0.0 --port 5050 command: uvicorn --reload crawl_service:app --host 0.0.0.0 --port 5050
restart: always restart: always
volumes: volumes:
- ./backend/:/code/ - ./backend/:/code/
@ -83,7 +83,7 @@ services:
context: backend context: backend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: backend-upload container_name: backend-upload
command: uvicorn upload_service:app --host 0.0.0.0 --port 5050 command: uvicorn --reload upload_service:app --host 0.0.0.0 --port 5050
restart: always restart: always
volumes: volumes:
- ./backend/:/code/ - ./backend/:/code/

View File

@ -1,41 +1,41 @@
/* eslint-disable */ import { UUID } from "crypto";
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useCrawlApi } from "@/lib/api/crawl/useCrawlApi";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useAxios, useToast } from "@/lib/hooks"; import { useToast } from "@/lib/hooks";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { useEventTracking } from "@/services/analytics/useEventTracking"; import { useEventTracking } from "@/services/analytics/useEventTracking";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { UUID } from "crypto";
import { isValidUrl } from "../helpers/isValidUrl"; import { isValidUrl } from "../helpers/isValidUrl";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useCrawler = () => { export const useCrawler = () => {
const [isCrawling, setCrawling] = useState(false); const [isCrawling, setCrawling] = useState(false);
const urlInputRef = useRef<HTMLInputElement | null>(null); const urlInputRef = useRef<HTMLInputElement | null>(null);
const { session } = useSupabase(); const { session } = useSupabase();
const { publish } = useToast(); const { publish } = useToast();
const { axiosInstance } = useAxios();
const { track } = useEventTracking(); const { track } = useEventTracking();
const { crawlWebsiteUrl } = useCrawlApi();
if (session === null) { if (session === null) {
redirectToLogin(); redirectToLogin();
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars const { t } = useTranslation(["translation", "upload"]);
const {t, i18n} = useTranslation(["translation","upload"]);
const crawlWebsite = useCallback( const crawlWebsite = useCallback(
async (brainId: UUID | undefined) => { async (brainId: UUID | undefined) => {
// Validate URL // Validate URL
const url = urlInputRef.current ? urlInputRef.current.value : null; const url = urlInputRef.current ? urlInputRef.current.value : null;
if (!url || !isValidUrl(url)) { if (url === null || !isValidUrl(url)) {
void track("URL_INVALID"); void track("URL_INVALID");
publish({ publish({
variant: "danger", variant: "danger",
text: t("invalidUrl",{ns:"upload"}) text: t("invalidUrl", { ns: "upload" }),
}); });
return; return;
@ -51,15 +51,16 @@ export const useCrawler = () => {
}; };
setCrawling(true); setCrawling(true);
void track("URL_CRAWLED"); void track("URL_CRAWLED");
try { try {
console.log("Crawling website...", brainId); console.log("Crawling website...", brainId);
if (brainId !== undefined) { if (brainId !== undefined) {
const response = await axiosInstance.post( const response = await crawlWebsiteUrl({
`/crawl?brain_id=${brainId}`, brainId,
config config,
); });
publish({ publish({
variant: response.data.type, variant: response.data.type,
@ -69,19 +70,21 @@ export const useCrawler = () => {
} catch (error: unknown) { } catch (error: unknown) {
publish({ publish({
variant: "danger", variant: "danger",
text: t("crawlFailed",{ message: JSON.stringify(error),ns:"upload"}) text: t("crawlFailed", {
message: JSON.stringify(error),
ns: "upload",
}),
}); });
} finally { } finally {
setCrawling(false); setCrawling(false);
} }
}, },
[session.access_token, publish] [session.access_token]
); );
return { return {
isCrawling, isCrawling,
urlInputRef, urlInputRef,
crawlWebsite, crawlWebsite,
}; };
}; };

View File

@ -1,6 +1,4 @@
/* eslint-disable */
"use client"; "use client";
import { Suspense } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
@ -14,44 +12,35 @@ export const Crawler = (): JSX.Element => {
const { urlInputRef, isCrawling, crawlWebsite } = useCrawler(); const { urlInputRef, isCrawling, crawlWebsite } = useCrawler();
const { currentBrain } = useBrainContext(); const { currentBrain } = useBrainContext();
const {t, i18n} = useTranslation(["translation","upload"]); const { t } = useTranslation(["translation", "upload"]);
function Crawler() { return (
return ( <div className="w-full">
<div className="w-full"> <div className="flex justify-center gap-5 px-6">
<div className="flex justify-center gap-5 px-6"> <div className="max-w-xl w-full">
<div className="max-w-xl w-full"> <div className="flex-col justify-center gap-5">
<div className="flex-col justify-center gap-5"> <Card className="h-32 flex gap-5 justify-center items-center px-5">
<Card className="h-32 flex gap-5 justify-center items-center px-5"> <div className="text-center max-w-sm w-full flex flex-col gap-5 items-center">
<div className="text-center max-w-sm w-full flex flex-col gap-5 items-center"> <Field
<Field name="crawlurl"
name="crawlurl" ref={urlInputRef}
ref={urlInputRef} type="text"
type="text" placeholder={t("webSite", { ns: "upload" })}
placeholder={t("webSite",{ ns: "upload"})} className="w-full"
className="w-full" />
/> </div>
</div> <div className="flex flex-col items-center justify-center gap-5">
<div className="flex flex-col items-center justify-center gap-5"> <Button
<Button isLoading={isCrawling}
isLoading={isCrawling} onClick={() => void crawlWebsite(currentBrain?.id)}
onClick={() => void crawlWebsite(currentBrain?.id)} >
> {t("crawlButton")}
{t("crawlButton")} </Button>
</Button> </div>
</div> </Card>
</Card>
</div>
</div> </div>
</div> </div>
</div> </div>
); </div>
} );
return (
<Suspense fallback = {"Loading..."}>
<Crawler />
</Suspense>
)
}; };

View File

@ -1,32 +1,32 @@
/* eslint-disable */ /* eslint-disable max-lines */
import { useCallback, useState } from "react";
import { FileRejection, useDropzone } from "react-dropzone";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useAxios, useToast } from "@/lib/hooks";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { useEventTracking } from "@/services/analytics/useEventTracking";
import axios from "axios"; import axios from "axios";
import { UUID } from "crypto"; import { UUID } from "crypto";
import { useCallback, useState } from "react";
import { FileRejection, useDropzone } from "react-dropzone";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useUploadApi } from "@/lib/api/upload/useUploadApi";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useToast } from "@/lib/hooks";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { useEventTracking } from "@/services/analytics/useEventTracking";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useFileUploader = () => { export const useFileUploader = () => {
const { track } = useEventTracking(); const { track } = useEventTracking();
const [isPending, setIsPending] = useState(false); const [isPending, setIsPending] = useState(false);
const { publish } = useToast(); const { publish } = useToast();
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const { session } = useSupabase(); const { session } = useSupabase();
const { uploadFile } = useUploadApi();
const { currentBrain } = useBrainContext(); const { currentBrain } = useBrainContext();
const { axiosInstance } = useAxios();
if (session === null) { if (session === null) {
redirectToLogin(); redirectToLogin();
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars const { t } = useTranslation(["upload"]);
const {t, i18n} = useTranslation(["upload"]);
const upload = useCallback( const upload = useCallback(
async (file: File, brainId: UUID) => { async (file: File, brainId: UUID) => {
@ -34,18 +34,13 @@ export const useFileUploader = () => {
formData.append("uploadFile", file); formData.append("uploadFile", file);
try { try {
void track("FILE_UPLOADED"); void track("FILE_UPLOADED");
const response = await axiosInstance.post( const response = await uploadFile({ brainId, formData });
`/upload?brain_id=${brainId}`,
formData
);
track("FILE_UPLOADED");
publish({ publish({
variant: response.data.type, variant: response.data.type,
text: text:
(response.data.type === "success" response.data.type === "success"
? t("success",{ ns: "upload" }) ? t("success", { ns: "upload" })
: t("error",{ message: response.data.message, ns: "upload" }) : t("error", { message: response.data.message, ns: "upload" }),
)
}); });
} catch (e: unknown) { } catch (e: unknown) {
if (axios.isAxiosError(e) && e.response?.status === 403) { if (axios.isAxiosError(e) && e.response?.status === 403) {
@ -62,17 +57,17 @@ export const useFileUploader = () => {
} else { } else {
publish({ publish({
variant: "danger", variant: "danger",
text: t("error",{ message: e, ns: "upload" }) text: t("error", { message: e, ns: "upload" }),
}); });
} }
} }
}, },
[session.access_token, publish] [session.access_token]
); );
const onDrop = (acceptedFiles: File[], fileRejections: FileRejection[]) => { const onDrop = (acceptedFiles: File[], fileRejections: FileRejection[]) => {
if (fileRejections.length > 0) { if (fileRejections.length > 0) {
publish({ variant: "danger", text: t("maxSizeError",{ ns: "upload" }) }); publish({ variant: "danger", text: t("maxSizeError", { ns: "upload" }) });
return; return;
} }
@ -85,11 +80,12 @@ export const useFileUploader = () => {
if (isAlreadyInFiles) { if (isAlreadyInFiles) {
publish({ publish({
variant: "warning", variant: "warning",
text: t("alreadyAdded",{ fileName: file.name, ns: "upload" }), text: t("alreadyAdded", { fileName: file.name, ns: "upload" }),
}); });
acceptedFiles.splice(i, 1); acceptedFiles.splice(i, 1);
} }
} }
// eslint-disable-next-line @typescript-eslint/no-shadow
setFiles((files) => [...files, ...acceptedFiles]); setFiles((files) => [...files, ...acceptedFiles]);
}; };
@ -104,7 +100,7 @@ export const useFileUploader = () => {
} }
setIsPending(true); setIsPending(true);
if (currentBrain?.id !== undefined) { if (currentBrain?.id !== undefined) {
await Promise.all(files.map((file) => upload(file, currentBrain?.id))); await Promise.all(files.map((file) => upload(file, currentBrain.id)));
setFiles([]); setFiles([]);
} else { } else {
publish({ publish({
@ -128,7 +124,6 @@ export const useFileUploader = () => {
isDragActive, isDragActive,
open, open,
uploadAllFiles, uploadAllFiles,
files, files,
setFiles, setFiles,
}; };

View File

@ -1,4 +1,3 @@
/* eslint-disable */
"use client"; "use client";
import { AnimatePresence } from "framer-motion"; import { AnimatePresence } from "framer-motion";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -21,8 +20,7 @@ export const FileUploader = (): JSX.Element => {
setFiles, setFiles,
} = useFileUploader(); } = useFileUploader();
// eslint-disable-next-line @typescript-eslint/no-unused-vars const { t } = useTranslation(["translation", "upload"]);
const {t, i18n} = useTranslation(["translation","upload"]);
return ( return (
<section <section
@ -35,13 +33,13 @@ export const FileUploader = (): JSX.Element => {
<input {...getInputProps()} /> <input {...getInputProps()} />
<div className="text-center p-6 max-w-sm w-full flex flex-col gap-5 items-center"> <div className="text-center p-6 max-w-sm w-full flex flex-col gap-5 items-center">
{isDragActive ? ( {isDragActive ? (
<p className="text-blue-600">{t("drop",{"ns":"upload"})}</p> <p className="text-blue-600">{t("drop", { ns: "upload" })}</p>
) : ( ) : (
<button <button
onClick={open} onClick={open}
className="opacity-50 h-full cursor-pointer hover:opacity-100 hover:underline transition-opacity" className="opacity-50 h-full cursor-pointer hover:opacity-100 hover:underline transition-opacity"
> >
{t("dragAndDrop",{"ns":"upload"})} {t("dragAndDrop", { ns: "upload" })}
</button> </button>
)} )}
</div> </div>
@ -73,6 +71,4 @@ export const FileUploader = (): JSX.Element => {
</div> </div>
</section> </section>
); );
}; };

View File

@ -57,8 +57,8 @@ const UploadPage = (): JSX.Element => {
); );
} }
const Upload = () => { return (
return ( <Suspense fallback="Loading...">
<main className="pt-10"> <main className="pt-10">
<PageHeading <PageHeading
title={t("title", { ns: "upload" })} title={t("title", { ns: "upload" })}
@ -75,12 +75,6 @@ const UploadPage = (): JSX.Element => {
</Link> </Link>
</div> </div>
</main> </main>
);
};
return (
<Suspense fallback="Loading...">
<Upload />
</Suspense> </Suspense>
); );
}; };

View File

@ -0,0 +1,43 @@
import { renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { CrawlInputProps } from "../crawl";
import { useCrawlApi } from "../useCrawlApi";
const axiosPostMock = vi.fn(() => ({}));
vi.mock("@/lib/hooks", () => ({
useAxios: () => ({
axiosInstance: {
post: axiosPostMock,
},
}),
}));
describe("useCrawlApi", () => {
// TODO: Create a test user within preview and prod databases to interact with
it("should call updateUserIdentity with the correct parameters", async () => {
const {
result: {
current: { crawlWebsiteUrl },
},
} = renderHook(() => useCrawlApi());
const crawlInputProps: CrawlInputProps = {
brainId: "e7001ccd-6d90-4eab-8c50-2f23d39441e4",
config: {
url: "https://en.wikipedia.org/wiki/Mali",
js: false,
depth: 1,
max_pages: 100,
max_time: 60,
},
};
await crawlWebsiteUrl(crawlInputProps);
expect(axiosPostMock).toHaveBeenCalledTimes(1);
expect(axiosPostMock).toHaveBeenCalledWith(
`/crawl?brain_id=${crawlInputProps.brainId}`,
crawlInputProps.config
);
});
});

View File

@ -0,0 +1,25 @@
import { AxiosInstance } from "axios";
import { UUID } from "crypto";
import { ToastData } from "@/lib/components/ui/Toast/domain/types";
export type CrawlInputProps = {
brainId: UUID;
config: {
url: string;
js: boolean;
depth: number;
max_pages: number;
max_time: number;
};
};
export type CrawlResponse = {
data: { type: ToastData["variant"]; message: ToastData["text"] };
};
export const crawlWebsiteUrl = async (
props: CrawlInputProps,
axiosInstance: AxiosInstance
): Promise<CrawlResponse> =>
axiosInstance.post(`/crawl?brain_id=${props.brainId}`, props.config);

View File

@ -0,0 +1,13 @@
import { useAxios } from "@/lib/hooks";
import { CrawlInputProps, crawlWebsiteUrl } from "./crawl";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useCrawlApi = () => {
const { axiosInstance } = useAxios();
return {
crawlWebsiteUrl: async (props: CrawlInputProps) =>
crawlWebsiteUrl(props, axiosInstance),
};
};

View File

@ -0,0 +1,19 @@
import { AxiosInstance } from "axios";
import { UUID } from "crypto";
import { ToastData } from "@/lib/components/ui/Toast/domain/types";
export type UploadResponse = {
data: { type: ToastData["variant"]; message: ToastData["text"] };
};
export type UploadInputProps = {
brainId: UUID;
formData: FormData;
};
export const uploadFile = async (
props: UploadInputProps,
axiosInstance: AxiosInstance
): Promise<UploadResponse> =>
axiosInstance.post(`/upload?brain_id=${props.brainId}`, props.formData);

View File

@ -0,0 +1,13 @@
import { useAxios } from "@/lib/hooks";
import { uploadFile, UploadInputProps } from "./upload";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useUploadApi = () => {
const { axiosInstance } = useAxios();
return {
uploadFile: async (props: UploadInputProps) =>
uploadFile(props, axiosInstance),
};
};

View File

@ -1,6 +1,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useEventTracking } from "@/services/analytics/useEventTracking";
export type Language = { export type Language = {
id: string; id: string;
name: string; name: string;
@ -14,6 +16,8 @@ export const useLanguageHook = (): {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const [allLanguages, setAllLanguages] = useState<Language[]>([]); const [allLanguages, setAllLanguages] = useState<Language[]>([]);
const [currentLanguage, setCurrentLanguage] = useState<Language | null>(null); const [currentLanguage, setCurrentLanguage] = useState<Language | null>(null);
const { track } = useEventTracking();
useEffect(() => { useEffect(() => {
const languages = [ const languages = [
{ {
@ -63,6 +67,7 @@ export const useLanguageHook = (): {
}, [i18n]); }, [i18n]);
const change = (newLanguage: Language) => { const change = (newLanguage: Language) => {
void track("CHANGE_LANGUAGE");
setCurrentLanguage(newLanguage); setCurrentLanguage(newLanguage);
localStorage.setItem("selectedLanguage", newLanguage.id); localStorage.setItem("selectedLanguage", newLanguage.id);
void i18n.changeLanguage(newLanguage.id); void i18n.changeLanguage(newLanguage.id);