diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 602b88cf2..ef4d663f6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,7 +7,6 @@ import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext"; import { SidebarProvider } from "./contexts/SidebarContext"; import ErrorBoundary from "./components/shared/ErrorBoundary"; import HomePage from "./pages/HomePage"; -import { ScarfPixel } from "./components/ScarfPixel"; // Import global styles import "./styles/tailwind.css"; @@ -37,7 +36,6 @@ export default function App() { - diff --git a/frontend/src/components/ScarfPixel.tsx b/frontend/src/components/ScarfPixel.tsx deleted file mode 100644 index 4d8a40ddd..000000000 --- a/frontend/src/components/ScarfPixel.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useRef } from "react"; -import { useNavigationState } from "../contexts/NavigationContext"; - -export function ScarfPixel() { - const { workbench, selectedTool } = useNavigationState(); - const lastUrlSent = useRef(null); // helps with React 18 StrictMode in dev - - useEffect(() => { - // Get current pathname from browser location - const pathname = window.location.pathname; - - const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7' - + '&path=' + encodeURIComponent(pathname) - + '&t=' + Date.now(); // cache-buster - - console.log("ScarfPixel: Navigation change", { workbench, selectedTool, pathname }); - - if (lastUrlSent.current !== url) { - lastUrlSent.current = url; - const img = new Image(); - img.referrerPolicy = "no-referrer-when-downgrade"; // optional - img.src = url; - - console.log("ScarfPixel: Fire to... " + pathname, url); - } - }, [workbench, selectedTool]); // Fire when navigation state changes - - return null; // Nothing visible in UI -} - diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 6da6f5c3e..f5fc1502e 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -174,7 +174,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { const handleToolSelect = useCallback((toolId: string) => { // Set the selected tool and determine the appropriate workbench actions.setSelectedTool(toolId); - + // Get the tool from registry to determine workbench const tool = getSelectedTool(toolId); if (tool && tool.workbench) { @@ -225,10 +225,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { // URL sync for proper tool navigation useNavigationUrlSync( - navigationState.workbench, navigationState.selectedTool, - actions.setWorkbench, - actions.setSelectedTool, handleToolSelect, () => actions.setSelectedTool(null), toolRegistry, diff --git a/frontend/src/hooks/useUrlSync.ts b/frontend/src/hooks/useUrlSync.ts index 57eefe843..a0366cebd 100644 --- a/frontend/src/hooks/useUrlSync.ts +++ b/frontend/src/hooks/useUrlSync.ts @@ -3,7 +3,7 @@ */ import { useEffect, useCallback } from 'react'; -import { WorkbenchType, ToolId, ToolRoute } from '../types/navigation'; +import { WorkbenchType, ToolId } from '../types/navigation'; import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting'; import { ToolRegistry } from '../data/toolsTaxonomy'; @@ -11,10 +11,7 @@ import { ToolRegistry } from '../data/toolsTaxonomy'; * Hook to sync workbench and tool with URL using registry */ export function useNavigationUrlSync( - workbench: WorkbenchType, selectedTool: ToolId | null, - setWorkbench: (workbench: WorkbenchType) => void, - setSelectedTool: (toolId: ToolId | null) => void, handleToolSelect: (toolId: string) => void, clearToolSelection: () => void, registry: ToolRegistry, @@ -28,7 +25,9 @@ export function useNavigationUrlSync( if (route.toolId !== selectedTool) { if (route.toolId) { handleToolSelect(route.toolId); - } else { + } else if (selectedTool !== null) { + // Only clear selection if we actually had a tool selected + // Don't clear on initial load when selectedTool starts as null clearToolSelection(); } } @@ -41,8 +40,10 @@ export function useNavigationUrlSync( if (selectedTool) { updateToolRoute(selectedTool, registry); } else { - // Clear URL when no tool is selected - clearToolRoute(); + // Only clear URL if we're not on the home page already + if (window.location.pathname !== '/') { + clearToolRoute(); + } } }, [selectedTool, registry, enableSync]); @@ -103,4 +104,4 @@ export function useCurrentRoute(registry: ToolRegistry) { }, [registry]); return getCurrentRoute; -} \ No newline at end of file +} diff --git a/frontend/src/utils/scarfTracking.ts b/frontend/src/utils/scarfTracking.ts new file mode 100644 index 000000000..27c16b238 --- /dev/null +++ b/frontend/src/utils/scarfTracking.ts @@ -0,0 +1,28 @@ +let lastFiredPathname: string | null = null; +let lastFiredTime = 0; + +/** + * Fire scarf pixel for analytics tracking + * Only fires if pathname is different from last call or enough time has passed + */ +export function firePixel(pathname: string): void { + const now = Date.now(); + + // Only fire if pathname changed or it's been at least 1 second since last fire + if (pathname === lastFiredPathname && now - lastFiredTime < 250) { + return; + } + + lastFiredPathname = pathname; + lastFiredTime = now; + + const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7' + + '&path=' + encodeURIComponent(pathname) + + const img = new Image(); + img.referrerPolicy = "no-referrer-when-downgrade"; + img.src = url; + + console.log("ScarfPixel: Fire to... " + pathname); +} + diff --git a/frontend/src/utils/urlRouting.ts b/frontend/src/utils/urlRouting.ts index 8432ff0a5..592aaab0d 100644 --- a/frontend/src/utils/urlRouting.ts +++ b/frontend/src/utils/urlRouting.ts @@ -8,6 +8,7 @@ import { getDefaultWorkbench } from '../types/navigation'; import { ToolRegistry, getToolWorkbench, getToolUrlPath, isValidToolId } from '../data/toolsTaxonomy'; +import { firePixel } from './scarfTracking'; /** * Parse the current URL to extract tool routing information @@ -44,33 +45,38 @@ export function parseToolRoute(registry: ToolRegistry): ToolRoute { }; } +/** + * Update URL and fire analytics pixel + */ +function updateUrl(newPath: string, searchParams: URLSearchParams): void { + const currentPath = window.location.pathname; + const queryString = searchParams.toString(); + const fullUrl = newPath + (queryString ? `?${queryString}` : ''); + + // Only update URL and fire pixel if something actually changed + if (currentPath !== newPath || window.location.search !== (queryString ? `?${queryString}` : '')) { + window.history.replaceState(null, '', fullUrl); + firePixel(newPath); + } +} + /** * Update the URL to reflect the current tool selection */ export function updateToolRoute(toolId: ToolId, registry: ToolRegistry): void { - const currentPath = window.location.pathname; - const searchParams = new URLSearchParams(window.location.search); - const tool = registry[toolId]; if (!tool) { console.warn(`Tool ${toolId} not found in registry`); return; } - const newPath = getToolUrlPath(toolId, tool); - + const searchParams = new URLSearchParams(window.location.search); + // Remove tool query parameter since we're using path-based routing searchParams.delete('tool'); - // Construct final URL - const queryString = searchParams.toString(); - const fullUrl = newPath + (queryString ? `?${queryString}` : ''); - - // Update URL without triggering page reload - if (currentPath !== newPath || window.location.search !== (queryString ? `?${queryString}` : '')) { - window.history.replaceState(null, '', fullUrl); - } + updateUrl(newPath, searchParams); } /** @@ -80,10 +86,7 @@ export function clearToolRoute(): void { const searchParams = new URLSearchParams(window.location.search); searchParams.delete('tool'); - const queryString = searchParams.toString(); - const url = '/' + (queryString ? `?${queryString}` : ''); - - window.history.replaceState(null, '', url); + updateUrl('/', searchParams); } /**