From 4b2a84b320cc971095321135b46752eb510989ef Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Tue, 12 Aug 2025 20:15:17 +0545 Subject: [PATCH] feat(nextjs): adds basic anonymous tracking that tracks navigation, api calls (Just name of endpoint is tracked) and button clicks --- README.md | 5 +- docker-compose.yml | 4 + .../components/HeaderNab.tsx | 5 + .../components/ImageEditor.tsx | 4 + .../custom-template/page.tsx | 5 +- .../dashboard/components/Header.tsx | 5 +- .../components/DocumentPreviewPage.tsx | 6 +- .../outline/components/GenerateButton.tsx | 11 +- .../outline/components/GroupLayouts.tsx | 10 +- .../outline/components/OutlineContent.tsx | 17 +- .../pdf-maker/PdfMakerPage.tsx | 36 ++-- .../presentation/components/Header.tsx | 28 +++- .../components/PresentationPage.tsx | 6 +- .../presentation/components/SlideContent.tsx | 21 ++- .../settings/SettingPage.tsx | 11 +- .../template-preview/[slug]/page.tsx | 26 ++- .../template-preview/page.tsx | 19 ++- .../upload/components/UploadPage.tsx | 14 +- servers/nextjs/app/MixpanelInitializer.tsx | 30 ++++ .../nextjs/app/api/tracking-status/route.ts | 11 ++ servers/nextjs/app/layout.tsx | 9 +- servers/nextjs/components/Home.tsx | 15 ++ servers/nextjs/package-lock.json | 87 ++++++++++ servers/nextjs/package.json | 1 + servers/nextjs/utils/mixpanel.ts | 156 ++++++++++++++++++ 25 files changed, 497 insertions(+), 45 deletions(-) create mode 100644 servers/nextjs/app/MixpanelInitializer.tsx create mode 100644 servers/nextjs/app/api/tracking-status/route.ts create mode 100644 servers/nextjs/utils/mixpanel.ts diff --git a/README.md b/README.md index 9cd5d2c7..550d73c8 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,6 @@ Presenton gives you complete control over your AI presentation workflow. Choose * ✅ **Versatile Image Generation** — Choose from DALL-E 3, Gemini Flash, Pexels, or Pixabay * ✅ **Rich Media Support** — Icons, charts, and custom graphics for professional presentations * ✅ **Runs Locally** — All processing happens on your device, no cloud dependencies -* ✅ **Privacy-First** — Zero tracking, no data stored by us, complete data sovereignty * ✅ **API Deployment** — Host as your own API service for your team * ✅ **Fully Open-Source** — Apache 2.0 licensed, inspect, modify, and contribute * ✅ **Docker Ready** — One-command deployment with GPU support for local models @@ -103,6 +102,10 @@ You can also set the following environment variables to customize the image gene - **GOOGLE_API_KEY=[Your Google API Key]**: Required if using **gemini_flash** as the image provider. - **OPENAI_API_KEY=[Your OpenAI API Key]**: Required if using **dall-e-3** as the image provider. +You can disable anonymous tracking using the following environment variable: +- **DISABLE_ANONYMOUS_TRACKING=[true/false]**: Set this to **true** to disable anonymous usage tracking. + + > **Note:** You can freely choose both the LLM (text generation) and the image provider. Supported image providers: **pexels**, **pixabay**, **gemini_flash** (Google), and **dall-e-3** (OpenAI). ### Using OpenAI diff --git a/docker-compose.yml b/docker-compose.yml index 85ca9210..39a24c97 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: - DISABLE_THINKING=${DISABLE_THINKING} - WEB_GROUNDING=${WEB_GROUNDING} - DATABASE_URL=${DATABASE_URL} + - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING} production-gpu: # image: ghcr.io/presenton/presenton:latest @@ -67,6 +68,7 @@ services: - DISABLE_THINKING=${DISABLE_THINKING} - WEB_GROUNDING=${WEB_GROUNDING} - DATABASE_URL=${DATABASE_URL} + - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING} development: build: @@ -97,6 +99,7 @@ services: - DISABLE_THINKING=${DISABLE_THINKING} - WEB_GROUNDING=${WEB_GROUNDING} - DATABASE_URL=${DATABASE_URL} + - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING} development-gpu: build: @@ -134,3 +137,4 @@ services: - DISABLE_THINKING=${DISABLE_THINKING} - WEB_GROUNDING=${WEB_GROUNDING} - DATABASE_URL=${DATABASE_URL} + - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING} diff --git a/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx index d960ad8d..cb0f0338 100644 --- a/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx @@ -2,12 +2,15 @@ import { LayoutDashboard, Settings, Upload } from "lucide-react"; import React from "react"; import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; import { RootState } from "@/store/store"; import { useSelector } from "react-redux"; const HeaderNav = () => { const canChangeKeys = useSelector((state: RootState) => state.userConfig.can_change_keys); + const pathname = usePathname(); return (
@@ -17,6 +20,7 @@ const HeaderNav = () => { prefetch={false} className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none" role="menuitem" + onClick={() => trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" })} > @@ -29,6 +33,7 @@ const HeaderNav = () => { prefetch={false} className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none" role="menuitem" + onClick={() => trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/settings" })} > diff --git a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx index 29e67cc6..fb1c8337 100644 --- a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx @@ -15,6 +15,7 @@ import { PresentationGenerationApi } from "../services/api/presentation-generati import { Skeleton } from "@/components/ui/skeleton"; import { toast } from "sonner"; import { PreviousGeneratedImagesResponse } from "../services/api/params"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; interface ImageEditorProps { initialImage: string | null; imageIdx?: number; @@ -90,6 +91,7 @@ const ImageEditor = ({ const getPreviousGeneratedImage = async () => { try { + trackEvent(MixpanelEvent.ImageEditor_GetPreviousGeneratedImages_API_Call); const response = await PresentationGenerationApi.getPreviousGeneratedImages(); setPreviousGeneratedImages(response); @@ -187,6 +189,7 @@ const ImageEditor = ({ try { setIsGenerating(true); setError(null); + trackEvent(MixpanelEvent.ImageEditor_GenerateImage_API_Call); const response = await PresentationGenerationApi.generateImage({ prompt: prompt, }); @@ -228,6 +231,7 @@ const ImageEditor = ({ const formData = new FormData(); formData.append("file", file); + trackEvent(MixpanelEvent.ImageEditor_UploadImage_API_Call); const response = await fetch("/api/upload-image", { method: "POST", body: formData, diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx index 6becf5ac..b77ded5d 100644 --- a/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx @@ -10,16 +10,18 @@ import { useFileUpload } from "./hooks/useFileUpload"; import { useSlideProcessing } from "./hooks/useSlideProcessing"; import { useLayoutSaving } from "./hooks/useLayoutSaving"; import { useAPIKeyCheck } from "./hooks/useAPIKeyCheck"; -import { useRouter } from "next/navigation"; +import { useRouter, usePathname } from "next/navigation"; import { LoadingSpinner } from "./components/LoadingSpinner"; import { FileUploadSection } from "./components/FileUploadSection"; import { SaveLayoutButton } from "./components/SaveLayoutButton"; import { SaveLayoutModal } from "./components/SaveLayoutModal"; import EachSlide from "./components/EachSlide/NewEachSlide"; import { APIKeyWarning } from "./components/APIKeyWarning"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; const CustomTemplatePage = () => { const router = useRouter(); + const pathname = usePathname(); const { refetch } = useLayout(); // Custom hooks for different concerns @@ -42,6 +44,7 @@ const CustomTemplatePage = () => { ); const handleSaveTemplate = async (layoutName: string, description: string): Promise => { + trackEvent(MixpanelEvent.CustomTemplate_Save_Templates_API_Call); const id = await saveLayout(layoutName, description); if (id) { router.push(`/template-preview/custom-${id}`); diff --git a/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx b/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx index 3708a494..4636347c 100644 --- a/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx +++ b/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx @@ -7,6 +7,7 @@ import BackBtn from "@/components/BackBtn"; import { usePathname } from "next/navigation"; import HeaderNav from "@/app/(presentation-generator)/components/HeaderNab"; import { Layout, FilePlus2 } from "lucide-react"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; const Header = () => { const pathname = usePathname(); return ( @@ -15,7 +16,7 @@ const Header = () => {
{pathname !== "/upload" && } - + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" })}> Presentation logo { trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/custom-template" })} className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none" role="menuitem" > @@ -36,6 +38,7 @@ const Header = () => { trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/template-preview" })} className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none" role="menuitem" > diff --git a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx index 718049fb..8ed2f14a 100644 --- a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx @@ -19,7 +19,7 @@ import { OverlayLoader } from "@/components/ui/overlay-loader"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; import { setPresentationId } from "@/store/slices/presentationGeneration"; import { useDispatch, useSelector } from "react-redux"; -import { useRouter } from "next/navigation"; +import { useRouter, usePathname } from "next/navigation"; import { RootState } from "@/store/store"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; @@ -28,6 +28,7 @@ import { getIconFromFile } from "../../utils/others"; import { ChevronRight, PanelRightOpen, X } from "lucide-react"; import ToolTip from "@/components/ToolTip"; import Header from "@/app/(presentation-generator)/dashboard/components/Header"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; // Types interface LoadingState { @@ -50,6 +51,7 @@ const DocumentsPreviewPage: React.FC = () => { // Hooks const dispatch = useDispatch(); const router = useRouter(); + const pathname = usePathname(); const textareaRef = useRef(null); // Redux state @@ -144,6 +146,7 @@ const DocumentsPreviewPage: React.FC = () => { const documentPaths = fileItems.map( (fileItem: FileItem) => fileItem.file_path ); + trackEvent(MixpanelEvent.DocumentsPreview_Create_Presentation_API_Call); const createResponse = await PresentationGenerationApi.createPresentation( { prompt: config?.prompt ?? "", @@ -154,6 +157,7 @@ const DocumentsPreviewPage: React.FC = () => { ); dispatch(setPresentationId(createResponse.id)); + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/outline" }); router.replace("/outline"); } catch (error: any) { console.error("Error in radar presentation creation:", error); diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx index 29359bb5..63b375d3 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx @@ -1,4 +1,6 @@ import React from "react"; +import { usePathname, useSearchParams } from "next/navigation"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; import { Button } from "@/components/ui/button"; import { LoadingState, LayoutGroup } from "../types/index"; @@ -15,6 +17,9 @@ const GenerateButton: React.FC = ({ selectedLayoutGroup, onSubmit }) => { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const isDisabled = loadingState.isLoading || streamState.isLoading || @@ -30,7 +35,11 @@ const GenerateButton: React.FC = ({ return ( @@ -95,10 +102,10 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { className="mx-auto flex flex-col items-center overflow-hidden justify-center " > {!presentationData || - loading || - contentLoading || - !presentationData?.slides || - presentationData?.slides.length === 0 ? ( + loading || + contentLoading || + !presentationData?.slides || + presentationData?.slides.length === 0 ? (
{Array.from({ length: 2 }).map((_, index) => ( @@ -115,7 +122,8 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { presentationData.slides && presentationData.slides.length > 0 && presentationData.slides.map((slide: any, index: number) => ( -
+ // [data-speaker-note] is used to extract the speaker note from the slide for export to pptx +
{renderSlideContent(slide, true)}
))} diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx index eef0e152..7f2859af 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx @@ -7,7 +7,7 @@ import { } from "lucide-react"; import React, { useState } from "react"; import Wrapper from "@/components/Wrapper"; -import { useRouter } from "next/navigation"; +import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Popover, PopoverContent, @@ -29,6 +29,7 @@ import HeaderNav from "../../components/HeaderNab"; import PDFIMAGE from "@/public/pdf.svg"; import PPTXIMAGE from "@/public/pptx.svg"; import Image from "next/image"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; const Header = ({ presentation_id, @@ -40,6 +41,8 @@ const Header = ({ const [open, setOpen] = useState(false); const [showLoader, setShowLoader] = useState(false); const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); const { presentationData, isStreaming } = useSelector( @@ -59,13 +62,16 @@ const Header = ({ setOpen(false); setShowLoader(true); // Save the presentation data before exporting + trackEvent(MixpanelEvent.Header_UpdatePresentationContent_API_Call); await PresentationGenerationApi.updatePresentationContent(presentationData); + trackEvent(MixpanelEvent.Header_GetPptxModel_API_Call); const pptx_model = await get_presentation_pptx_model(presentation_id); if (!pptx_model) { throw new Error("Failed to get presentation PPTX model"); } + trackEvent(MixpanelEvent.Header_ExportAsPPTX_API_Call); const pptx_path = await PresentationGenerationApi.exportAsPPTX(pptx_model); if (pptx_path) { // window.open(pptx_path, '_self'); @@ -92,8 +98,10 @@ const Header = ({ setOpen(false); setShowLoader(true); // Save the presentation data before exporting + trackEvent(MixpanelEvent.Header_UpdatePresentationContent_API_Call); await PresentationGenerationApi.updatePresentationContent(presentationData); + trackEvent(MixpanelEvent.Header_ExportAsPDF_API_Call); const response = await fetch('/api/export-as-pdf', { method: 'POST', body: JSON.stringify({ @@ -136,14 +144,22 @@ const Header = ({ const ExportOptions = ({ mobile }: { mobile: boolean }) => (
+
); diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx index 1953926e..e94f7ef2 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx @@ -17,6 +17,8 @@ import { updateSlide, } from "@/store/slices/presentationGeneration"; import { useGroupLayouts } from "../../hooks/useGroupLayouts"; +import { usePathname, useSearchParams } from "next/navigation"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; import NewSlide from "../../components/NewSlide"; interface SlideContentProps { @@ -35,6 +37,8 @@ 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( @@ -48,6 +52,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => { setIsUpdating(true); try { + trackEvent(MixpanelEvent.Slide_Edit_API_Call); const response = await PresentationGenerationApi.editSlide( slide.id, value @@ -149,7 +154,11 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => { {!isStreaming && !loading && (
setShowNewSlideSelection(true)} + onClick={() => { + const query = searchParams?.toString(); + trackEvent(MixpanelEvent.Slide_Add_New_Slide_Button_Clicked, { pathname, query }); + 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" > @@ -169,7 +178,11 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => { {!isStreaming && !loading && (
{ + const query = searchParams?.toString(); + trackEvent(MixpanelEvent.Slide_Delete_Slide_Button_Clicked, { pathname, query }); + onDeleteSlide(); + }} className="absolute top-2 z-20 sm:top-4 right-2 sm:right-4 hidden md:block transition-transform" > @@ -222,6 +235,10 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => { 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 }); + }} > {isUpdating ? "Updating..." : "Update"} diff --git a/servers/nextjs/app/(presentation-generator)/settings/SettingPage.tsx b/servers/nextjs/app/(presentation-generator)/settings/SettingPage.tsx index 980d2d05..83b6ec25 100644 --- a/servers/nextjs/app/(presentation-generator)/settings/SettingPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/settings/SettingPage.tsx @@ -9,10 +9,11 @@ import { checkIfSelectedOllamaModelIsPulled, pullOllamaModel, } from "@/utils/providerUtils"; -import { useRouter } from "next/navigation"; +import { useRouter, usePathname, useSearchParams } from "next/navigation"; import LLMProviderSelection from "@/components/LLMSelection"; import Header from "../dashboard/components/Header"; import { LLMConfig } from "@/types/llm_config"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; // Button state interface interface ButtonState { @@ -26,6 +27,8 @@ interface ButtonState { const SettingsPage = () => { const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); const userConfigState = useSelector((state: RootState) => state.userConfig); const [llmConfig, setLlmConfig] = useState( userConfigState.llm_config @@ -61,6 +64,8 @@ const SettingsPage = () => { }, [downloadingModel?.downloaded, downloadingModel?.size]); const handleSaveConfig = async () => { + const query = searchParams?.toString(); + trackEvent(MixpanelEvent.Settings_SaveConfiguration_Button_Clicked, { pathname, query }); try { setButtonState(prev => ({ ...prev, @@ -68,13 +73,16 @@ const SettingsPage = () => { isDisabled: true, text: "Saving Configuration...", })); + trackEvent(MixpanelEvent.Settings_SaveConfiguration_API_Call); await handleSaveLLMConfig(llmConfig); if (llmConfig.LLM === "ollama" && llmConfig.OLLAMA_MODEL) { + trackEvent(MixpanelEvent.Settings_CheckOllamaModelPulled_API_Call); const isPulled = await checkIfSelectedOllamaModelIsPulled( llmConfig.OLLAMA_MODEL ); if (!isPulled) { setShowDownloadModal(true); + trackEvent(MixpanelEvent.Settings_DownloadOllamaModel_API_Call); await handleModelDownload(); } } @@ -85,6 +93,7 @@ const SettingsPage = () => { isDisabled: false, text: "Save Configuration", })); + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" }); router.push("/upload"); } catch (error) { toast.info(error instanceof Error ? error.message : "Failed to save configuration"); diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx index 11f9af4c..0b54db1e 100644 --- a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useEffect, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; +import { useParams, useRouter, usePathname, useSearchParams } from "next/navigation"; import LoadingStates from "../components/LoadingStates"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -14,11 +14,14 @@ import "prismjs/components/prism-markup"; import "prismjs/components/prism-jsx"; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { useFontLoader } from "../../hooks/useFontLoader"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; const GroupLayoutPreview = () => { const params = useParams(); const router = useRouter(); const slug = params.slug as string; + const pathname = usePathname(); + const searchParams = useSearchParams(); const { getFullDataByGroup, loading, refetch } = useLayout(); const layoutGroup = getFullDataByGroup(slug); @@ -177,7 +180,11 @@ const GroupLayoutPreview = () => { {slug.includes('custom-') && }
@@ -250,7 +264,11 @@ const GroupLayoutPreview = () => { variant="outline" size="sm" className="flex items-center gap-2 bg-blue-50 border border-blue-400 text-blue-700" - onClick={() => openEditor(fileName)} + onClick={() => { + const query = searchParams?.toString?.() as string | undefined; + trackEvent(MixpanelEvent.TemplatePreview_Open_Editor_Button_Clicked, { pathname, query }); + openEditor(fileName); + }} disabled={!layoutsMap[fileName]} title={!layoutsMap[fileName] ? "Loading layout code..." : "Edit layout code"} > diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx index 34d495e6..b51955a8 100644 --- a/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx @@ -1,11 +1,12 @@ "use client"; import React, { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter, usePathname } from "next/navigation"; import LoadingStates from "./components/LoadingStates"; import { Card } from "@/components/ui/card"; import { ExternalLink } from "lucide-react"; import Header from "@/app/(presentation-generator)/dashboard/components/Header"; import { useLayout } from "../context/LayoutContext"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; const LayoutPreview = () => { const { @@ -17,6 +18,7 @@ const LayoutPreview = () => { error, } = useLayout(); const router = useRouter(); + const pathname = usePathname(); const [summaryMap, setSummaryMap] = useState>({}); @@ -114,7 +116,10 @@ const LayoutPreview = () => { router.push(`/template-preview/${group.groupName}`)} + onClick={() => { + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/template-preview/${group.groupName}` }); + router.push(`/template-preview/${group.groupName}`) + }} >
@@ -166,7 +171,10 @@ const LayoutPreview = () => { router.push(`/template-preview/${group.groupName}`)} + onClick={() => { + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/template-preview/${group.groupName}` }); + router.push(`/template-preview/${group.groupName}`) + }} >
@@ -196,7 +204,10 @@ const LayoutPreview = () => { ) : ( router.push(`/custom-template`)} + onClick={() => { + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/custom-template` }); + router.push(`/custom-template`) + }} >
diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx index 8b14b69e..f9689180 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx @@ -11,7 +11,7 @@ "use client"; import React, { useState } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { useDispatch } from "react-redux"; import { clearOutlines, setPresentationId } from "@/store/slices/presentationGeneration"; import { ConfigurationSelects } from "./ConfigurationSelects"; @@ -25,6 +25,7 @@ import { PresentationGenerationApi } from "../../services/api/presentation-gener import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; import { setPptGenUploadState } from "@/store/slices/presentationGenUpload"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; // Types for loading state interface LoadingState { @@ -37,6 +38,8 @@ interface LoadingState { const UploadPage = () => { const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); const dispatch = useDispatch(); // State management @@ -115,6 +118,7 @@ const UploadPage = () => { let documents = []; if (files.length > 0) { + trackEvent(MixpanelEvent.Upload_Upload_Documents_API_Call); const uploadResponse = await PresentationGenerationApi.uploadDoc(files); documents = uploadResponse; } @@ -122,9 +126,8 @@ const UploadPage = () => { const promises: Promise[] = []; if (documents.length > 0) { - promises.push( - PresentationGenerationApi.decomposeDocuments(documents) - ); + trackEvent(MixpanelEvent.Upload_Decompose_Documents_API_Call); + promises.push(PresentationGenerationApi.decomposeDocuments(documents)); } const responses = await Promise.all(promises); dispatch(setPptGenUploadState({ @@ -132,6 +135,7 @@ const UploadPage = () => { files: responses, })); dispatch(clearOutlines()) + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/documents-preview" }); router.push("/documents-preview"); }; @@ -147,6 +151,7 @@ const UploadPage = () => { }); // Use the first available layout group for direct generation + trackEvent(MixpanelEvent.Upload_Create_Presentation_API_Call); const createResponse = await PresentationGenerationApi.createPresentation({ prompt: config?.prompt ?? "", n_slides: config?.slides ? parseInt(config.slides) : null, @@ -156,6 +161,7 @@ const UploadPage = () => { dispatch(setPresentationId(createResponse.id)); dispatch(clearOutlines()) + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/outline" }); router.push("/outline"); }; diff --git a/servers/nextjs/app/MixpanelInitializer.tsx b/servers/nextjs/app/MixpanelInitializer.tsx new file mode 100644 index 00000000..65b23fd6 --- /dev/null +++ b/servers/nextjs/app/MixpanelInitializer.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { useEffect } from 'react'; +import { usePathname, useSearchParams } from 'next/navigation'; +import { initMixpanel, trackEvent, MixpanelEvent } from '@/utils/mixpanel'; + +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]); + + + return <>{children}; +} + +export default MixpanelInitializer; + + diff --git a/servers/nextjs/app/api/tracking-status/route.ts b/servers/nextjs/app/api/tracking-status/route.ts new file mode 100644 index 00000000..3cc727ff --- /dev/null +++ b/servers/nextjs/app/api/tracking-status/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + const isDisabled = process.env.DISABLE_ANONYMOUS_TRACKING === 'true' || process.env.DISABLE_ANONYMOUS_TRACKING === 'True'; + const trackingEnabled = !isDisabled; + return NextResponse.json({ trackingEnabled }); +} + + diff --git a/servers/nextjs/app/layout.tsx b/servers/nextjs/app/layout.tsx index 4b739142..e575b51f 100644 --- a/servers/nextjs/app/layout.tsx +++ b/servers/nextjs/app/layout.tsx @@ -3,6 +3,7 @@ import localFont from "next/font/local"; import { Roboto, Instrument_Sans } from "next/font/google"; import "./globals.css"; import { Providers } from "./providers"; +import MixpanelInitializer from "./MixpanelInitializer"; import { LayoutProvider } from "./(presentation-generator)/context/LayoutContext"; import { Toaster } from "@/components/ui/sonner"; const inter = localFont({ @@ -85,9 +86,11 @@ export default function RootLayout({ className={`${inter.variable} ${roboto.variable} ${instrument_sans.variable} antialiased`} > - - {children} - + + + {children} + + diff --git a/servers/nextjs/components/Home.tsx b/servers/nextjs/components/Home.tsx index a62bd349..414e78cc 100644 --- a/servers/nextjs/components/Home.tsx +++ b/servers/nextjs/components/Home.tsx @@ -12,6 +12,8 @@ import { pullOllamaModel, } from "@/utils/providerUtils"; import { LLMConfig } from "@/types/llm_config"; +import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; +import { usePathname, useSearchParams } from "next/navigation"; // Button state interface interface ButtonState { @@ -25,6 +27,8 @@ 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(config.llm_config); @@ -52,6 +56,9 @@ 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 }); try { setButtonState(prev => ({ ...prev, @@ -59,11 +66,17 @@ export default function Home() { isDisabled: true, text: "Saving Configuration..." })); + // API: save config + trackEvent(MixpanelEvent.Home_SaveConfiguration_API_Call); await handleSaveLLMConfig(llmConfig); if (llmConfig.LLM === "ollama" && llmConfig.OLLAMA_MODEL) { + // API: check model pulled + trackEvent(MixpanelEvent.Home_CheckOllamaModelPulled_API_Call); const isPulled = await checkIfSelectedOllamaModelIsPulled(llmConfig.OLLAMA_MODEL); if (!isPulled) { setShowDownloadModal(true); + // API: download model + trackEvent(MixpanelEvent.Home_DownloadOllamaModel_API_Call); await handleModelDownload(); } } @@ -74,6 +87,8 @@ export default function Home() { isDisabled: false, text: "Save Configuration" })); + // Track navigation from -> to + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" }); router.push("/upload"); } catch (error) { toast.info(error instanceof Error ? error.message : "Failed to save configuration"); diff --git a/servers/nextjs/package-lock.json b/servers/nextjs/package-lock.json index 94b61767..54604588 100644 --- a/servers/nextjs/package-lock.json +++ b/servers/nextjs/package-lock.json @@ -45,6 +45,7 @@ "lucide-react": "^0.447.0", "marked": "^15.0.11", "mermaid": "^11.9.0", + "mixpanel-browser": "^2.67.0", "next": "^14.2.14", "next-themes": "^0.4.6", "prismjs": "^1.30.0", @@ -2827,6 +2828,16 @@ "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", "license": "MIT" }, + "node_modules/@rrweb/types": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.18.tgz", + "integrity": "sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==" + }, + "node_modules/@rrweb/utils": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmjs.org/@rrweb/utils/-/utils-2.0.0-alpha.18.tgz", + "integrity": "sha512-qV8azQYo9RuwW4NGRtOiQfTBdHNL1B0Q//uRLMbCSjbaKqJYd88Js17Bdskj65a0Vgp2dwTLPIZ0gK47dfjfaA==" + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -3334,6 +3345,11 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -3724,6 +3740,11 @@ "@types/node": "*" } }, + "node_modules/@xstate/fsm": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", + "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -7110,6 +7131,14 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/mixpanel-browser": { + "version": "2.67.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.67.0.tgz", + "integrity": "sha512-LudY4eRIkvjEpAlIAg10i2T2mbtiKZ4XlMGbTyF1kcAhEqMa9JhEEdEcjxYPwiKhuMVSBM3RVkNCZaNqcnE4ww==", + "dependencies": { + "rrweb": "2.0.0-alpha.18" + } + }, "node_modules/mlly": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", @@ -8417,6 +8446,64 @@ "points-on-path": "^0.2.1" } }, + "node_modules/rrdom": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmjs.org/rrdom/-/rrdom-2.0.0-alpha.18.tgz", + "integrity": "sha512-fSFzFFxbqAViITyYVA4Z0o5G6p1nEqEr/N8vdgSKie9Rn0FJxDSNJgjV0yiCIzcDs0QR+hpvgFhpbdZ6JIr5Nw==", + "dependencies": { + "rrweb-snapshot": "^2.0.0-alpha.18" + } + }, + "node_modules/rrweb": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmjs.org/rrweb/-/rrweb-2.0.0-alpha.18.tgz", + "integrity": "sha512-1mjZcB+LVoGSx1+i9E2ZdAP90fS3MghYVix2wvGlZvrgRuLCbTCCOZMztFCkKpgp7/EeCdYM4nIHJkKX5J1Nmg==", + "dependencies": { + "@rrweb/types": "^2.0.0-alpha.18", + "@rrweb/utils": "^2.0.0-alpha.18", + "@types/css-font-loading-module": "0.0.7", + "@xstate/fsm": "^1.4.0", + "base64-arraybuffer": "^1.0.1", + "mitt": "^3.0.0", + "rrdom": "^2.0.0-alpha.18", + "rrweb-snapshot": "^2.0.0-alpha.18" + } + }, + "node_modules/rrweb-snapshot": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.18.tgz", + "integrity": "sha512-hBHZL/NfgQX6wO1D9mpwqFu1NJPpim+moIcKhFEjVTZVRUfCln+LOugRc4teVTCISYHN8Cw5e2iNTWCSm+SkoA==", + "dependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/rrweb-snapshot/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/servers/nextjs/package.json b/servers/nextjs/package.json index 8fdd24c3..f8a7d656 100644 --- a/servers/nextjs/package.json +++ b/servers/nextjs/package.json @@ -47,6 +47,7 @@ "lucide-react": "^0.447.0", "marked": "^15.0.11", "mermaid": "^11.9.0", + "mixpanel-browser": "^2.67.0", "next": "^14.2.14", "next-themes": "^0.4.6", "prismjs": "^1.30.0", diff --git a/servers/nextjs/utils/mixpanel.ts b/servers/nextjs/utils/mixpanel.ts new file mode 100644 index 00000000..0261861f --- /dev/null +++ b/servers/nextjs/utils/mixpanel.ts @@ -0,0 +1,156 @@ +'use client'; + +import mixpanel from 'mixpanel-browser'; + +const MIXPANEL_TOKEN = 'd726e8bea8ec147f4c7720060cb2e6d1'; + +export enum MixpanelEvent { + PageView = 'Page View', + Navigation = 'Navigation', + Home_SaveConfiguration_Button_Clicked = 'Home Save Configuration Button Clicked', + 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_Add_Slide_Button_Clicked = 'Outline Add Slide Button Clicked', + 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', + Header_UpdatePresentationContent_API_Call = 'Header Update Presentation Content API Call', + Header_ExportAsPDF_API_Call = 'Header Export As PDF API Call', + Header_GetPptxModel_API_Call = 'Header Get PPTX Model API Call', + Header_ExportAsPPTX_API_Call = 'Header Export As PPTX API Call', + Slide_Add_New_Slide_Button_Clicked = 'Slide Add New Slide Button Clicked', + 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', + 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', + TemplatePreview_Delete_Templates_API_Call = 'Template Preview Delete Templates API Call', + TemplatePreview_Open_Editor_Button_Clicked = 'Template Preview Open Editor Button Clicked', + CustomTemplate_Save_Templates_API_Call = 'Custom Template Save Templates API Call', + PdfMaker_Retry_Button_Clicked = 'PDF Maker Retry Button Clicked', + Upload_Upload_Documents_API_Call = 'Upload Upload Documents API Call', + Upload_Decompose_Documents_API_Call = 'Upload Decompose Documents API Call', + Upload_Create_Presentation_API_Call = 'Upload Create Presentation API Call', + DocumentsPreview_Create_Presentation_API_Call = 'Documents Preview Create Presentation API Call', + DocumentsPreview_Next_Button_Clicked = 'Documents Preview Next Button Clicked', + Settings_SaveConfiguration_Button_Clicked = 'Settings Save Configuration Button Clicked', + Settings_SaveConfiguration_API_Call = 'Settings Save Configuration API Call', + Settings_CheckOllamaModelPulled_API_Call = 'Settings Check Ollama Model Pulled API Call', + Settings_DownloadOllamaModel_API_Call = 'Settings Download Ollama Model API Call', + PresentationPage_Refresh_Page_Button_Clicked = 'Presentation Page Refresh Page Button Clicked', + PresentationMode_Fullscreen_Toggle_Clicked = 'Presentation Mode Fullscreen Toggle Clicked', + PresentationMode_Exit_Clicked = 'Presentation Mode Exit Clicked', + ImageEditor_GetPreviousGeneratedImages_API_Call = 'Image Editor Get Previous Generated Images API Call', + ImageEditor_GenerateImage_API_Call = 'Image Editor Generate Image API Call', + ImageEditor_UploadImage_API_Call = 'Image Editor Upload Image API Call', +} + +export type MixpanelProps = Record; + +declare global { + interface Window { + __mixpanel_initialized?: boolean; + __mixpanel_tracking_enabled?: boolean; + } +} + +function canUseMixpanel(): boolean { + return typeof window !== 'undefined' && Boolean(MIXPANEL_TOKEN); +} + +let trackingCheckPromise: Promise | null = null; + +async function ensureTrackingStatus(): Promise { + if (typeof window === 'undefined') return false; + if (typeof window.__mixpanel_tracking_enabled === 'boolean') { + return window.__mixpanel_tracking_enabled; + } + if (!trackingCheckPromise) { + trackingCheckPromise = fetch('/api/tracking-status') + .then(async (res) => { + try { + const data = await res.json(); + const enabled = Boolean(data?.trackingEnabled); + window.__mixpanel_tracking_enabled = enabled; + return enabled; + } catch { + // If the API response is malformed, default to enabling tracking + window.__mixpanel_tracking_enabled = true; + return true; + } + }) + .catch(() => { + // If the API call fails, default to enabling tracking + window.__mixpanel_tracking_enabled = true; + return true; + }); + } + return trackingCheckPromise; +} + +export function initMixpanel(): void { + if (!canUseMixpanel()) return; + if (window.__mixpanel_initialized) return; + // Ensure tracking is allowed before initializing + void ensureTrackingStatus().then((enabled) => { + if (!enabled) return; + if (window.__mixpanel_initialized) return; + mixpanel.init(MIXPANEL_TOKEN as string, { track_pageview: false }); + mixpanel.identify(mixpanel.get_distinct_id()); + window.__mixpanel_initialized = true; + }); +} + +export function track(eventName: string, props?: Record): void { + if (!canUseMixpanel()) return; + if (typeof window !== 'undefined' && window.__mixpanel_tracking_enabled === false) { + return; + } + if (!window.__mixpanel_initialized) { + initMixpanel(); + return; + } + mixpanel.track(eventName, props); +} + +export function trackEvent(event: MixpanelEvent, props?: MixpanelProps): void { + track(event, props); +} + +export function getDistinctId(): string | undefined { + if (!canUseMixpanel()) return undefined; + if (typeof window !== 'undefined' && window.__mixpanel_tracking_enabled === false) { + return undefined; + } + if (!window.__mixpanel_initialized) { + initMixpanel(); + return undefined; + } + if (!window.__mixpanel_initialized) return undefined; + return mixpanel.get_distinct_id(); +} + +export function identifyAnonymous(): void { + if (!canUseMixpanel()) return; + if (typeof window !== 'undefined' && window.__mixpanel_tracking_enabled === false) { + return; + } + if (!window.__mixpanel_initialized) { + initMixpanel(); + return; + } + mixpanel.identify(mixpanel.get_distinct_id()); +} + +export default { + initMixpanel, + track, + trackEvent, + getDistinctId, + identifyAnonymous, +}; + +