feat: track onboarding events (#1388)

https://github.com/StanGirard/quivr/issues/1386
This commit is contained in:
Mamadou DICKO 2023-10-11 15:56:28 +02:00 committed by GitHub
parent 5503e104d4
commit b9172b7442
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 179 additions and 65 deletions

View File

@ -15,7 +15,7 @@ def update_user_onboarding(
supabase_db = get_supabase_db()
updated_onboarding = supabase_db.update_user_onboarding(user_id, onboarding)
if all(not value for value in onboarding.dict().values()):
if all(not value for value in updated_onboarding.dict().values()):
supabase_db.remove_user_onboarding(user_id)
return updated_onboarding

View File

@ -2,13 +2,11 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useChatApi } from "@/lib/api/chat/useChatApi";
import { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useToast } from "@/lib/hooks";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { useKnowledgeToFeed } from "./useKnowledgeToFeed";
import { useFeedBrainHandler } from "./useFeedBrainHandler";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useFeedBrain = ({
@ -19,17 +17,13 @@ export const useFeedBrain = ({
closeFeedInput?: () => void;
}) => {
const { publish } = useToast();
const { files, urls } = useKnowledgeToFeed();
const { t } = useTranslation(["upload"]);
const { updateOnboarding, onboarding } = useOnboarding();
const { currentBrainId } = useBrainContext();
const { setKnowledgeToFeed, knowledgeToFeed } = useKnowledgeToFeedContext();
const [hasPendingRequests, setHasPendingRequests] = useState(false);
const { handleFeedBrain } = useFeedBrainHandler();
const { createChat, deleteChat } = useChatApi();
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
const feedBrain = async (): Promise<void> => {
if (currentBrainId === null) {
publish({
@ -56,26 +50,10 @@ export const useFeedBrain = ({
dispatchHasPendingRequests?.();
closeFeedInput?.();
setHasPendingRequests(true);
const uploadPromises = files.map((file) =>
uploadFileHandler(file, currentBrainId, currentChatId)
);
const crawlPromises = urls.map((url) =>
crawlWebsiteHandler(url, currentBrainId, currentChatId)
);
const updateOnboardingPromise = async () => {
if (onboarding.onboarding_a) {
await updateOnboarding({
onboarding_a: false,
});
}
};
await Promise.all([
...uploadPromises,
...crawlPromises,
updateOnboardingPromise(),
]);
await handleFeedBrain({
brainId: currentBrainId,
chatId: currentChatId,
});
setKnowledgeToFeed([]);
} catch (e) {

View File

@ -0,0 +1,47 @@
import { UUID } from "crypto";
import { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { useKnowledgeToFeed } from "./useKnowledgeToFeed";
type FeedBrainProps = {
brainId: UUID;
chatId: UUID;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useFeedBrainHandler = () => {
const { files, urls } = useKnowledgeToFeed();
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
const { updateOnboarding, onboarding } = useOnboarding();
const updateOnboardingA = async () => {
if (onboarding.onboarding_a) {
await updateOnboarding({
onboarding_a: false,
});
}
};
const handleFeedBrain = async ({
brainId,
chatId,
}: FeedBrainProps): Promise<void> => {
const uploadPromises = files.map((file) =>
uploadFileHandler(file, brainId, chatId)
);
const crawlPromises = urls.map((url) =>
crawlWebsiteHandler(url, brainId, chatId)
);
await Promise.all([
...uploadPromises,
...crawlPromises,
updateOnboardingA(),
]);
};
return {
handleFeedBrain,
};
};

View File

@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next";
import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
import { QuestionId } from "../../../types";
import { questionIdToTradPath } from "../utils";
@ -10,6 +11,7 @@ import { questionIdToTradPath } from "../utils";
export const useOnboardingQuestion = (questionId: QuestionId) => {
const { updateOnboarding } = useOnboarding();
const { t } = useTranslation("chat");
const { trackOnboardingEvent } = useOnboardingTracker();
const onboardingStep = questionIdToTradPath[questionId];
@ -18,6 +20,7 @@ export const useOnboardingQuestion = (questionId: QuestionId) => {
const { addQuestion } = useChat();
const handleSuggestionClick = async () => {
trackOnboardingEvent(onboardingStep);
await Promise.all([
addQuestion(question),
updateOnboarding({ [questionId]: false }),

View File

@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { RiDownloadLine } from "react-icons/ri";
import Button from "@/lib/components/ui/Button";
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
import { useStreamText } from "./hooks/useStreamText";
import { stepsContainerStyle } from "./styles";
@ -20,6 +21,8 @@ export const Onboarding = (): JSX.Element => {
const shouldStepBeDisplayed = useFeatureIsOn("onboarding");
const { trackOnboardingEvent } = useOnboardingTracker();
const { streamingText: titleStream, isDone: isTitleDisplayed } =
useStreamText({
text: title,
@ -36,12 +39,11 @@ export const Onboarding = (): JSX.Element => {
enabled: isStep1Done,
});
const { streamingText: secondStepStrem, isDone: isStep2Done } = useStreamText(
{
const { streamingText: secondStepStream, isDone: isStep2Done } =
useStreamText({
text: step2,
enabled: isStep1DetailsDone,
}
);
});
const { streamingText: thirdStepStream } = useStreamText({
text: step3,
enabled: isStep2Done,
@ -62,10 +64,13 @@ export const Onboarding = (): JSX.Element => {
{firstStepDetailsStream}
{isStep1DetailsDone && (
<Link
href="/documents/doc.pdf"
href="/documents/quivr_documentation.pdf"
download
target="_blank"
referrerPolicy="no-referrer"
onClick={() => {
trackOnboardingEvent("QUIVR_DOCUMENTATION_DOWNLOADED");
}}
>
<Button className="bg-black p-2 ml-2 rounded-full inline-flex">
<RiDownloadLine />
@ -74,7 +79,7 @@ export const Onboarding = (): JSX.Element => {
)}
</div>
</div>
<p>{secondStepStrem}</p>
<p>{secondStepStream}</p>
<p>{thirdStepStream}</p>
</div>
</MessageRow>

View File

@ -11,6 +11,8 @@ import { useChatApi } from "@/lib/api/chat/useChatApi";
import { useChatContext } from "@/lib/context";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useToast } from "@/lib/hooks";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
import { useQuestion } from "./useQuestion";
@ -25,6 +27,8 @@ export const useChat = () => {
const [chatId, setChatId] = useState<string | undefined>(
params?.chatId as string | undefined
);
const { isOnboarding } = useOnboarding();
const { trackOnboardingEvent } = useOnboardingTracker();
const [generatingAnswer, setGeneratingAnswer] = useState(false);
const router = useRouter();
const { messages } = useChatContext();
@ -64,10 +68,17 @@ export const useChat = () => {
});
}
void track("QUESTION_ASKED", {
brainId: currentBrainId,
promptId: currentPromptId,
});
if (isOnboarding) {
void trackOnboardingEvent("QUESTION_ASKED", {
brainId: currentBrainId,
promptId: currentPromptId,
});
} else {
void track("QUESTION_ASKED", {
brainId: currentBrainId,
promptId: currentPromptId,
});
}
const chatConfig = getChatsConfigFromLocalStorage();

View File

@ -1,24 +0,0 @@
import { useTranslation } from "react-i18next";
import { ChatEntity } from "@/app/chat/[chatId]/types";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { ChatsListItem } from "./ChatsListItem";
export const WelcomeChat = (): JSX.Element => {
const { t } = useTranslation("chat");
const chat: ChatEntity = {
chat_name: t("welcome"),
// @ts-expect-error because we don't need to pass all the props
chat_id: "",
};
const { updateOnboarding } = useOnboarding();
return (
<ChatsListItem
onDelete={() => void updateOnboarding({ onboarding_a: false })}
editable={false}
chat={chat}
/>
);
};

View File

@ -0,0 +1,19 @@
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
import { useWelcomeChat } from "./hooks/useWelcomeChat";
import { ChatsListItem } from "../ChatsListItem";
export const WelcomeChat = (): JSX.Element => {
const { chat, handleWelcomeChatDelete } = useWelcomeChat();
const { trackOnboardingEvent } = useOnboardingTracker();
return (
<div onClick={() => trackOnboardingEvent("WELCOME_CHAT_CLICKED")}>
<ChatsListItem
onDelete={() => void handleWelcomeChatDelete()}
editable={false}
chat={chat}
/>
</div>
);
};

View File

@ -0,0 +1,28 @@
import { useTranslation } from "react-i18next";
import { ChatEntity } from "@/app/chat/[chatId]/types";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useWelcomeChat = () => {
const { t } = useTranslation("chat");
const { updateOnboarding } = useOnboarding();
const { trackOnboardingEvent } = useOnboardingTracker();
const chat: ChatEntity = {
chat_name: t("welcome"),
// @ts-expect-error because we don't need to pass all the props
chat_id: "",
};
const handleWelcomeChatDelete = () => {
trackOnboardingEvent("WELCOME_CHAT_DELETED");
void updateOnboarding({ onboarding_a: false });
};
return {
chat,
handleWelcomeChatDelete,
};
};

View File

@ -0,0 +1 @@
export * from "./WelcomeChat";

View File

@ -5,6 +5,8 @@ import { useTranslation } from "react-i18next";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useToast } from "@/lib/hooks";
import { useOnboarding } from "@/lib/hooks/useOnboarding";
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
@ -19,6 +21,8 @@ export const useCrawler = () => {
const { t } = useTranslation(["translation", "upload"]);
const [urlToCrawl, setUrlToCrawl] = useState<string>("");
const { track } = useEventTracking();
const { trackOnboardingEvent } = useOnboardingTracker();
const { isOnboarding } = useOnboarding();
if (session === null) {
redirectToLogin();
@ -37,7 +41,11 @@ export const useCrawler = () => {
return;
}
void track("URL_CRAWLED");
if (isOnboarding) {
void trackOnboardingEvent("URL_CRAWLED");
} else {
void track("URL_CRAWLED");
}
addKnowledgeToFeed({
source: "crawl",
url: urlToCrawl,

View File

@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
import { FeedItemUploadType } from "@/app/chat/[chatId]/components/ActionsBar/types";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
import { useOnboarding } from "./useOnboarding";
import { useOnboardingTracker } from "./useOnboardingTracker";
import { useToast } from "./useToast";
import { useKnowledgeToFeedContext } from "../context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { acceptedFormats } from "../helpers/acceptedFormats";
@ -12,6 +14,8 @@ import { acceptedFormats } from "../helpers/acceptedFormats";
export const useCustomDropzone = () => {
const { knowledgeToFeed, addKnowledgeToFeed, setShouldDisplayFeedCard } =
useKnowledgeToFeedContext();
const { isOnboarding } = useOnboarding();
const { trackOnboardingEvent } = useOnboardingTracker();
const files: File[] = (
knowledgeToFeed.filter((c) => c.source === "upload") as FeedItemUploadType[]
@ -49,7 +53,12 @@ export const useCustomDropzone = () => {
text: t("alreadyAdded", { fileName: file.name, ns: "upload" }),
});
} else {
void track("FILE_UPLOADED");
if (isOnboarding) {
void trackOnboardingEvent("FILE_UPLOADED");
} else {
void track("FILE_UPLOADED");
}
addKnowledgeToFeed({
source: "upload",
file: file,

View File

@ -29,6 +29,8 @@ export const useOnboarding = () => {
onboarding_b3: false,
};
const isOnboarding = Object.values(onboarding).some((value) => value);
const updateOnboardingHandler = async (
newOnboardingStatus: Partial<Onboarding>
) => {
@ -49,5 +51,6 @@ export const useOnboarding = () => {
shouldDisplayOnboardingAInstructions,
shouldDisplayWelcomeChat,
updateOnboarding: updateOnboardingHandler,
isOnboarding,
};
};

View File

@ -0,0 +1,26 @@
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
import { useOnboarding } from "./useOnboarding";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useOnboardingTracker = () => {
const { onboarding } = useOnboarding();
const { track } = useEventTracking();
const trackOnboardingEvent = (
event: string,
properties: Record<string, unknown> = {}
): void => {
void track(event, {
HOW_TO_USER_QUIVR: onboarding.onboarding_b1,
WHAT_IS_QUIVR: onboarding.onboarding_b2,
WHAT_IS_BRAIN: onboarding.onboarding_b3,
...properties,
});
};
return {
trackOnboardingEvent,
};
};