Merge pull request #212 from presenton/feat/basic-telemetry

feat/basic telemetry
This commit is contained in:
Saurav Niraula 2025-08-13 13:53:47 +05:45 committed by GitHub
commit dffe9f5098
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 58 additions and 76 deletions

View file

@ -1,5 +1,5 @@
import React from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { Button } from "@/components/ui/button";
import { LoadingState, LayoutGroup } from "../types/index";
@ -18,7 +18,6 @@ const GenerateButton: React.FC<GenerateButtonProps> = ({
onSubmit
}) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const isDisabled =
loadingState.isLoading ||
@ -36,8 +35,13 @@ const GenerateButton: React.FC<GenerateButtonProps> = ({
<Button
disabled={isDisabled}
onClick={() => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Generate_Presentation_Button_Clicked, { pathname, query });
if (!streamState.isLoading && !streamState.isStreaming) {
if (!selectedLayoutGroup) {
trackEvent(MixpanelEvent.Outline_Select_Template_Button_Clicked, { pathname });
} else {
trackEvent(MixpanelEvent.Outline_Generate_Presentation_Button_Clicked, { pathname });
}
}
onSubmit();
}}
className="bg-[#5146E5] w-full rounded-lg text-base sm:text-lg py-4 sm:py-6 font-instrument_sans font-semibold hover:bg-[#5146E5]/80 text-white disabled:opacity-50 disabled:cursor-not-allowed"

View file

@ -1,6 +1,6 @@
import { CheckCircle } from "lucide-react";
import React from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { LayoutGroup } from "../types/index";
import { useLayout } from "../../context/LayoutContext";
@ -21,12 +21,10 @@ const GroupLayouts: React.FC<GroupLayoutsProps> = ({
const fonts = getCustomTemplateFonts(group.id.split("custom-")[1]);
useFontLoader(fonts || []);
const pathname = usePathname();
const searchParams = useSearchParams();
return (
<div
onClick={() => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Group_Layout_Selected_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Group_Layout_Selected_Clicked, { pathname });
onSelectLayoutGroup(group);
}}
className={`relative p-4 rounded-lg border cursor-pointer transition-all duration-200 ${

View file

@ -16,7 +16,7 @@ import {
import { OutlineItem } from "./OutlineItem";
import { Button } from "@/components/ui/button";
import { FileText } from "lucide-react";
import { usePathname, useSearchParams } from "next/navigation";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
interface OutlineContentProps {
@ -42,7 +42,6 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
);
const pathname = usePathname();
const searchParams = useSearchParams();
return (
<div className="space-y-6 font-instrument_sans">
@ -116,8 +115,7 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
<Button
variant="outline"
onClick={() => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Outline_Add_Slide_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Outline_Add_Slide_Button_Clicked, { pathname });
onAddSlide();
}}
disabled={isLoading || isStreaming}
@ -136,8 +134,7 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
<Button
variant="outline"
onClick={() => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Outline_Add_Slide_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Outline_Add_Slide_Button_Clicked, { pathname });
onAddSlide();
}}
className="text-blue-600 border-blue-200"

View file

@ -5,6 +5,7 @@ import { toast } from "sonner";
import { clearPresentationData } from "@/store/slices/presentationGeneration";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { LayoutGroup, LoadingState, TABS } from "../types/index";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
const DEFAULT_LOADING_STATE: LoadingState = {
message: "",
@ -37,7 +38,7 @@ export const usePresentationGeneration = (
});
return false;
}
if(!selectedLayoutGroup.slides.length){
if (!selectedLayoutGroup.slides.length) {
toast.error("No Slide Schema found", {
description: "Please select a Group before generating presentation",
});
@ -63,7 +64,7 @@ export const usePresentationGeneration = (
}
if (!validateInputs()) return;
setLoadingState({
message: "Generating presentation data...",
@ -74,8 +75,9 @@ export const usePresentationGeneration = (
try {
const layoutData = prepareLayoutData();
if (!layoutData) return;
trackEvent(MixpanelEvent.Presentation_Prepare_API_Call);
const response = await PresentationGenerationApi.presentationPrepare({
presentation_id: presentationId,
outlines: outlines,
@ -84,7 +86,7 @@ export const usePresentationGeneration = (
if (response) {
dispatch(clearPresentationData());
router.replace(`/presentation?id=${presentationId}&stream=true`);
router.replace(`/presentation?id=${presentationId}&stream=true`);
}
} catch (error: any) {
console.error('Error In Presentation Generation(prepare).', error);

View file

@ -5,7 +5,7 @@ import { RootState } from "@/store/store";
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { usePathname, useSearchParams } from "next/navigation";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { AlertCircle } from "lucide-react";
import { useGroupLayouts } from "../hooks/useGroupLayouts";
@ -19,7 +19,6 @@ import { useFontLoader } from "../hooks/useFontLoader";
const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
const { renderSlideContent, loading } = useGroupLayouts();
const pathname = usePathname();
const searchParams = useSearchParams();
const [contentLoading, setContentLoading] = useState(true);
const { getCustomTemplateFonts } = useLayout()
const dispatch = useDispatch();
@ -87,7 +86,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
<Button
className="mt-4 bg-red-500 text-white hover:bg-red-600 focus:ring-4 focus:ring-red-300"
onClick={() => {
trackEvent(MixpanelEvent.PdfMaker_Retry_Button_Clicked, { pathname, query: searchParams?.toString() });
trackEvent(MixpanelEvent.PdfMaker_Retry_Button_Clicked, { pathname });
window.location.reload();
}}
>

View file

@ -7,7 +7,7 @@ import {
} from "lucide-react";
import React, { useState } from "react";
import Wrapper from "@/components/Wrapper";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
import {
Popover,
PopoverContent,
@ -42,7 +42,6 @@ const Header = ({
const [showLoader, setShowLoader] = useState(false);
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const { presentationData, isStreaming } = useSelector(
@ -145,8 +144,7 @@ const Header = ({
<div className={`space-y-2 max-md:mt-4 ${mobile ? "" : "bg-white"} rounded-lg`}>
<Button
onClick={() => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Header_Export_PDF_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Header_Export_PDF_Button_Clicked, { pathname });
handleExportPdf();
}}
variant="ghost"
@ -156,8 +154,7 @@ const Header = ({
</Button>
<Button
onClick={() => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Header_Export_PPTX_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Header_Export_PPTX_Button_Clicked, { pathname });
handleExportPptx();
}}
variant="ghost"

View file

@ -8,7 +8,7 @@ import SidePanel from "./SidePanel";
import SlideContent from "./SlideContent";
import Header from "./Header";
import { Button } from "@/components/ui/button";
import { usePathname, useSearchParams } from "next/navigation";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { AlertCircle, Loader2 } from "lucide-react";
import Help from "./Help";
@ -26,7 +26,6 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
presentation_id,
}) => {
const pathname = usePathname();
const searchParams = useSearchParams();
// State management
const [loading, setLoading] = useState(true);
const [selectedSlide, setSelectedSlide] = useState(0);
@ -114,7 +113,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
<p className="text-center mb-4">
We couldn't load your presentation. Please try again.
</p>
<Button onClick={() => { const query = searchParams?.toString(); trackEvent(MixpanelEvent.PresentationPage_Refresh_Page_Button_Clicked, { pathname, query }); window.location.reload(); }}>Refresh Page</Button>
<Button onClick={() => { trackEvent(MixpanelEvent.PresentationPage_Refresh_Page_Button_Clicked, { pathname }); window.location.reload(); }}>Refresh Page</Button>
</div>
</div>
);

View file

@ -17,7 +17,7 @@ import {
updateSlide,
} from "@/store/slices/presentationGeneration";
import { useGroupLayouts } from "../../hooks/useGroupLayouts";
import { usePathname, useSearchParams } from "next/navigation";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import NewSlide from "../../components/NewSlide";
@ -38,7 +38,6 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
// Use the centralized group layouts hook
const { renderSlideContent, loading } = useGroupLayouts();
const pathname = usePathname();
const searchParams = useSearchParams();
const handleSubmit = async () => {
const element = document.getElementById(
@ -73,6 +72,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
};
const onDeleteSlide = async () => {
try {
trackEvent(MixpanelEvent.Slide_Delete_API_Call);
dispatch(deletePresentationSlide(slide.index));
} catch (error: any) {
console.error("Error deleting slide:", error);
@ -113,7 +113,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
return;
}
if (slide.layout.includes("custom")) {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
@ -155,8 +155,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
{!isStreaming && !loading && (
<div
onClick={() => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Slide_Add_New_Slide_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Slide_Add_New_Slide_Button_Clicked, { pathname });
setShowNewSlideSelection(true);
}}
className=" bg-white shadow-md w-[80px] py-2 border hover:border-[#5141e5] duration-300 flex items-center justify-center rounded-lg cursor-pointer mx-auto"
@ -179,8 +178,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
<ToolTip content="Delete slide">
<div
onClick={() => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Slide_Delete_Slide_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Slide_Delete_Slide_Button_Clicked, { pathname });
onDeleteSlide();
}}
className="absolute top-2 z-20 sm:top-4 right-2 sm:right-4 hidden md:block transition-transform"
@ -232,12 +230,10 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
<button
disabled={isUpdating}
type="submit"
className={`bg-gradient-to-r from-[#9034EA] to-[#5146E5] rounded-[32px] px-4 py-2 text-white flex items-center justify-end gap-2 ml-auto ${
isUpdating ? "opacity-70 cursor-not-allowed" : ""
}`}
className={`bg-gradient-to-r from-[#9034EA] to-[#5146E5] rounded-[32px] px-4 py-2 text-white flex items-center justify-end gap-2 ml-auto ${isUpdating ? "opacity-70 cursor-not-allowed" : ""
}`}
onClick={() => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Slide_Update_From_Prompt_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Slide_Update_From_Prompt_Button_Clicked, { pathname });
}}
>
{isUpdating ? "Updating..." : "Update"}

View file

@ -7,6 +7,7 @@ import {
} from "@/store/slices/presentationGeneration";
import { jsonrepair } from "jsonrepair";
import { toast } from "sonner";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
export const usePresentationStreaming = (
presentationId: string,
@ -26,6 +27,8 @@ export const usePresentationStreaming = (
dispatch(setStreaming(true));
dispatch(clearPresentationData());
trackEvent(MixpanelEvent.Presentation_Stream_API_Call);
eventSource = new EventSource(
`/api/v1/ppt/presentation/stream?presentation_id=${presentationId}`
);
@ -99,7 +102,7 @@ export const usePresentationStreaming = (
setLoading(false);
dispatch(setStreaming(false));
setError(true);
break;
break;
}
});

View file

@ -9,7 +9,7 @@ import {
checkIfSelectedOllamaModelIsPulled,
pullOllamaModel,
} from "@/utils/providerUtils";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
import LLMProviderSelection from "@/components/LLMSelection";
import Header from "../dashboard/components/Header";
import { LLMConfig } from "@/types/llm_config";
@ -28,7 +28,6 @@ interface ButtonState {
const SettingsPage = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const userConfigState = useSelector((state: RootState) => state.userConfig);
const [llmConfig, setLlmConfig] = useState<LLMConfig>(
userConfigState.llm_config
@ -64,8 +63,7 @@ const SettingsPage = () => {
}, [downloadingModel?.downloaded, downloadingModel?.size]);
const handleSaveConfig = async () => {
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Settings_SaveConfiguration_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Settings_SaveConfiguration_Button_Clicked, { pathname });
try {
setButtonState(prev => ({
...prev,

View file

@ -1,6 +1,6 @@
"use client";
import React, { useEffect, useState } from "react";
import { useParams, useRouter, usePathname, useSearchParams } from "next/navigation";
import { useParams, useRouter, usePathname } from "next/navigation";
import LoadingStates from "../components/LoadingStates";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@ -21,7 +21,6 @@ const GroupLayoutPreview = () => {
const router = useRouter();
const slug = params.slug as string;
const pathname = usePathname();
const searchParams = useSearchParams();
const { getFullDataByGroup, loading, refetch } = useLayout();
const layoutGroup = getFullDataByGroup(slug);
@ -181,8 +180,7 @@ const GroupLayoutPreview = () => {
variant="outline"
size="sm"
onClick={() => {
const query = searchParams?.toString?.() as string | undefined;
trackEvent(MixpanelEvent.TemplatePreview_Back_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.TemplatePreview_Back_Button_Clicked, { pathname });
router.back();
}}
className="flex items-center gap-2"
@ -194,8 +192,7 @@ const GroupLayoutPreview = () => {
variant="outline"
size="sm"
onClick={() => {
const query = searchParams?.toString?.() as string | undefined;
trackEvent(MixpanelEvent.TemplatePreview_All_Groups_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.TemplatePreview_All_Groups_Button_Clicked, { pathname });
router.push("/template-preview");
}}
className="flex items-center gap-2"
@ -204,8 +201,7 @@ const GroupLayoutPreview = () => {
All Groups
</Button>
{slug.includes('custom-') && <button className=" border border-red-200 flex justify-center items-center gap-2 text-red-700 px-4 py-1 rounded-md" onClick={() => {
const query = searchParams?.toString?.() as string | undefined;
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_API_Call);
deleteLayouts();
}}><Trash2 className="w-4 h-4" />Delete</button>}
@ -265,8 +261,7 @@ const GroupLayoutPreview = () => {
size="sm"
className="flex items-center gap-2 bg-blue-50 border border-blue-400 text-blue-700"
onClick={() => {
const query = searchParams?.toString?.() as string | undefined;
trackEvent(MixpanelEvent.TemplatePreview_Open_Editor_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.TemplatePreview_Open_Editor_Button_Clicked, { pathname });
openEditor(fileName);
}}
disabled={!layoutsMap[fileName]}

View file

@ -11,7 +11,7 @@
"use client";
import React, { useState } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
import { useDispatch } from "react-redux";
import { clearOutlines, setPresentationId } from "@/store/slices/presentationGeneration";
import { ConfigurationSelects } from "./ConfigurationSelects";
@ -39,7 +39,6 @@ interface LoadingState {
const UploadPage = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const dispatch = useDispatch();
// State management

View file

@ -1,26 +1,20 @@
'use client';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { initMixpanel, trackEvent, MixpanelEvent } from '@/utils/mixpanel';
import { initMixpanel, MixpanelEvent, trackEvent } from '@/utils/mixpanel';
import { usePathname } from 'next/navigation';
export function MixpanelInitializer({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const searchParams = useSearchParams();
// Initialize once
useEffect(() => {
initMixpanel();
}, []);
// Track page views on route changes
useEffect(() => {
if (!pathname) return;
const query = searchParams?.toString();
const url = query ? `${pathname}?${query}` : pathname;
trackEvent(MixpanelEvent.PageView, { url });
}, [pathname, searchParams]);
trackEvent(MixpanelEvent.PageView, { url: pathname });
}, [pathname]);
return <>{children}</>;
}

View file

@ -2,7 +2,7 @@
import { useState, useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { Loader2, Download, CheckCircle, X } from "lucide-react";
import { Loader2, Download, CheckCircle } from "lucide-react";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { handleSaveLLMConfig } from "@/utils/storeHelpers";
@ -13,7 +13,7 @@ import {
} from "@/utils/providerUtils";
import { LLMConfig } from "@/types/llm_config";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { usePathname, useSearchParams } from "next/navigation";
import { usePathname } from "next/navigation";
// Button state interface
interface ButtonState {
@ -28,7 +28,6 @@ interface ButtonState {
export default function Home() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const config = useSelector((state: RootState) => state.userConfig);
const [llmConfig, setLlmConfig] = useState<LLMConfig>(config.llm_config);
@ -56,9 +55,7 @@ export default function Home() {
}, [downloadingModel?.downloaded, downloadingModel?.size]);
const handleSaveConfig = async () => {
// Track button click with pathname and searchParams
const query = searchParams?.toString();
trackEvent(MixpanelEvent.Home_SaveConfiguration_Button_Clicked, { pathname, query });
trackEvent(MixpanelEvent.Home_SaveConfiguration_Button_Clicked, { pathname });
try {
setButtonState(prev => ({
...prev,

View file

@ -11,8 +11,11 @@ export enum MixpanelEvent {
Home_SaveConfiguration_API_Call = 'Home Save Configuration API Call',
Home_CheckOllamaModelPulled_API_Call = 'Home Check Ollama Model Pulled API Call',
Home_DownloadOllamaModel_API_Call = 'Home Download Ollama Model API Call',
Generate_Presentation_Button_Clicked = 'Generate Presentation Button Clicked',
Outline_Generate_Presentation_Button_Clicked = 'Outline Generate Presentation Button Clicked',
Outline_Select_Template_Button_Clicked = 'Outline Select Template Button Clicked',
Outline_Add_Slide_Button_Clicked = 'Outline Add Slide Button Clicked',
Presentation_Prepare_API_Call = 'Presentation Prepare API Call',
Presentation_Stream_API_Call = 'Presentation Stream API Call',
Group_Layout_Selected_Clicked = 'Group Layout Selected Clicked',
Header_Export_PDF_Button_Clicked = 'Header Export PDF Button Clicked',
Header_Export_PPTX_Button_Clicked = 'Header Export PPTX Button Clicked',
@ -24,6 +27,7 @@ export enum MixpanelEvent {
Slide_Delete_Slide_Button_Clicked = 'Slide Delete Slide Button Clicked',
Slide_Update_From_Prompt_Button_Clicked = 'Slide Update From Prompt Button Clicked',
Slide_Edit_API_Call = 'Slide Edit API Call',
Slide_Delete_API_Call = 'Slide Delete API Call',
TemplatePreview_Back_Button_Clicked = 'Template Preview Back Button Clicked',
TemplatePreview_All_Groups_Button_Clicked = 'Template Preview All Groups Button Clicked',
TemplatePreview_Delete_Templates_Button_Clicked = 'Template Preview Delete Templates Button Clicked',