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
dockerfile: Dockerfile
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
volumes:
- ./backend/:/code/
@ -49,7 +49,7 @@ services:
context: backend
dockerfile: Dockerfile
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
volumes:
- ./backend/:/code/
@ -66,7 +66,7 @@ services:
context: backend
dockerfile: Dockerfile
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
volumes:
- ./backend/:/code/
@ -83,7 +83,7 @@ services:
context: backend
dockerfile: Dockerfile
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
volumes:
- ./backend/:/code/

View File

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

View File

@ -1,6 +1,4 @@
/* eslint-disable */
"use client";
import { Suspense } from "react";
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
@ -14,9 +12,8 @@ export const Crawler = (): JSX.Element => {
const { urlInputRef, isCrawling, crawlWebsite } = useCrawler();
const { currentBrain } = useBrainContext();
const {t, i18n} = useTranslation(["translation","upload"]);
const { t } = useTranslation(["translation", "upload"]);
function Crawler() {
return (
<div className="w-full">
<div className="flex justify-center gap-5 px-6">
@ -28,7 +25,7 @@ export const Crawler = (): JSX.Element => {
name="crawlurl"
ref={urlInputRef}
type="text"
placeholder={t("webSite",{ ns: "upload"})}
placeholder={t("webSite", { ns: "upload" })}
className="w-full"
/>
</div>
@ -46,12 +43,4 @@ export const Crawler = (): JSX.Element => {
</div>
</div>
);
}
return (
<Suspense fallback = {"Loading..."}>
<Crawler />
</Suspense>
)
};

View File

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

View File

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

View File

@ -57,8 +57,8 @@ const UploadPage = (): JSX.Element => {
);
}
const Upload = () => {
return (
<Suspense fallback="Loading...">
<main className="pt-10">
<PageHeading
title={t("title", { ns: "upload" })}
@ -75,12 +75,6 @@ const UploadPage = (): JSX.Element => {
</Link>
</div>
</main>
);
};
return (
<Suspense fallback="Loading...">
<Upload />
</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 { useTranslation } from "react-i18next";
import { useEventTracking } from "@/services/analytics/useEventTracking";
export type Language = {
id: string;
name: string;
@ -14,6 +16,8 @@ export const useLanguageHook = (): {
const { i18n } = useTranslation();
const [allLanguages, setAllLanguages] = useState<Language[]>([]);
const [currentLanguage, setCurrentLanguage] = useState<Language | null>(null);
const { track } = useEventTracking();
useEffect(() => {
const languages = [
{
@ -63,6 +67,7 @@ export const useLanguageHook = (): {
}, [i18n]);
const change = (newLanguage: Language) => {
void track("CHANGE_LANGUAGE");
setCurrentLanguage(newLanguage);
localStorage.setItem("selectedLanguage", newLanguage.id);
void i18n.changeLanguage(newLanguage.id);