From feda87fdc7faf6cdb9f929223971bf35b61000ee Mon Sep 17 00:00:00 2001 From: Jakub Buciuto <46843555+MrJacob12@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:32:28 +0000 Subject: [PATCH] feat: implement GA4 with events for packs, upgrades and dimensions --- .github/workflows/deploy.yml | 2 ++ package.json | 1 + src/App.tsx | 17 ++++++++++++++ src/lib/analytics.ts | 43 ++++++++++++++++++++++++++++++++++++ src/store/gameStore.ts | 29 ++++++++++++++++++------ 5 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 src/lib/analytics.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6123356..d72682a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,6 +34,8 @@ jobs: run: npm install - name: Build + env: + VITE_GA_ID: ${{ secrets.VITE_GA_ID }} run: npm run build - name: Setup Pages diff --git a/package.json b/package.json index 15c4693..0671f94 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-ga4": "^2.1.0", "react-hook-form": "^7.61.1", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", diff --git a/src/App.tsx b/src/App.tsx index 492b8d4..ad44f5c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,12 +16,28 @@ import { useGameStore } from "@/store/gameStore"; import { toast } from "sonner"; import { formatCurrency } from "@/lib/utils"; import { GAME_CONFIG, calculateCurrentIncome } from "@/config/gameConfig"; +import { initGA, trackPageView } from "@/lib/analytics"; +import { useLocation } from "react-router-dom"; const queryClient = new QueryClient(); +const AnalyticsTracker = () => { + const location = useLocation(); + + useEffect(() => { + trackPageView(location.pathname + location.search); + }, [location]); + + return null; +}; + const App = () => { const offlineProcessed = useRef(false); + useEffect(() => { + initGA(); + }, []); + // Offline income calculation — runs once on mount useEffect(() => { if (offlineProcessed.current) return; @@ -65,6 +81,7 @@ const App = () => { + } /> } /> diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts new file mode 100644 index 0000000..c8fab25 --- /dev/null +++ b/src/lib/analytics.ts @@ -0,0 +1,43 @@ +import ReactGA from "react-ga4"; + +const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_ID; + +export const initGA = () => { + if (GA_MEASUREMENT_ID) { + ReactGA.initialize(GA_MEASUREMENT_ID); + console.log("GA Initialized"); + } +}; + +export const trackPageView = (path: string) => { + if (GA_MEASUREMENT_ID) { + ReactGA.send({ hitType: "pageview", page: path }); + } +}; + +export const trackEvent = (category: string, action: string, label?: string, value?: number) => { + if (GA_MEASUREMENT_ID) { + ReactGA.event({ + category, + action, + label, + value, + }); + } +}; + +export const trackPackOpening = (packName: string, cost: number) => { + trackEvent("Game", "Pack Opened", packName, cost); +}; + +export const trackUpgrade = (upgradeType: string, level: number, cost: number) => { + trackEvent("Game", "Upgrade Purchased", `${upgradeType} - Level ${level}`, cost); +}; + +export const trackDimensionStart = (level: number) => { + trackEvent("Game", "Dimension Started", `Level ${level}`); +}; + +export const trackDimensionEnd = (level: number, reward: number) => { + trackEvent("Game", "Dimension Ended", `Reached Level ${level}`, reward); +}; diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts index 11e4e8e..92940d4 100644 --- a/src/store/gameStore.ts +++ b/src/store/gameStore.ts @@ -10,6 +10,12 @@ import { Character, GameState as BaseGameState, } from "@/types/game"; +import { + trackPackOpening, + trackUpgrade, + trackDimensionStart, + trackDimensionEnd, +} from "@/lib/analytics"; interface GameState extends BaseGameState { addCard: (card: GameCard) => boolean; @@ -191,6 +197,8 @@ export const useGameStore = create()( seeds: state.seeds - pack.cost, })); + trackPackOpening(pack.name, pack.cost); + return newCards; }, @@ -230,6 +238,7 @@ export const useGameStore = create()( dimensionLevel: 1, currentEnemy: enemy, })); + trackDimensionStart(1); return true; }, @@ -268,6 +277,8 @@ export const useGameStore = create()( }, resetDimension: (reward) => { + const { dimensionLevel } = get(); + trackDimensionEnd(dimensionLevel, reward); set((s) => ({ seeds: s.seeds + reward, isDimensionActive: false, @@ -296,13 +307,17 @@ export const useGameStore = create()( if (seeds < cost) return false; - set((s) => ({ - seeds: s.seeds - cost, - upgrades: { - ...s.upgrades, - [type]: s.upgrades[type] + 1, - }, - })); + set((s) => { + const newLevel = s.upgrades[type] + 1; + trackUpgrade(type, newLevel, cost); + return { + seeds: s.seeds - cost, + upgrades: { + ...s.upgrades, + [type]: newLevel, + }, + }; + }); return true; },