diff --git a/.env.docker.example b/.env.docker.example index 8cfd56ce2..b26be84cb 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -65,7 +65,6 @@ NEXT_PUBLIC_GA_ID="your_google_analytics_id_here" NEXT_PUBLIC_APP_ENV=local NEXT_PUBLIC_LOCAL_STORAGE_PREFIX="@DockerEmuReady_" NEXT_PUBLIC_EMUREADY_BETA_URL="https://play.google.com/store/apps/details?id=com.producdevity.emureadyapp" -NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED=false NEXT_PUBLIC_ENABLE_ANALYTICS=false NEXT_PUBLIC_ENABLE_KOFI_WIDGET=false NEXT_PUBLIC_ENABLE_SENTRY=false diff --git a/.env.example b/.env.example index 5e80fcf9f..6bbbb6ae8 100644 --- a/.env.example +++ b/.env.example @@ -39,7 +39,6 @@ NEXT_PUBLIC_APP_ENV=local NEXT_PUBLIC_GA_ID="Google-Analytics-ID" NEXT_PUBLIC_LOCAL_STORAGE_PREFIX="@LocalEmuReady_" NEXT_PUBLIC_EMUREADY_BETA_URL="https://play.google.com/store/apps/details?id=com.producdevity.emureadyapp" -NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED=false NEXT_PUBLIC_ENABLE_ANALYTICS=false NEXT_PUBLIC_ENABLE_KOFI_WIDGET=false NEXT_PUBLIC_ENABLE_SENTRY=false diff --git a/.env.test.example b/.env.test.example index 9dc7a9b4a..19d4b7a8a 100644 --- a/.env.test.example +++ b/.env.test.example @@ -30,7 +30,6 @@ NEXT_PUBLIC_APP_ENV=test NEXT_PUBLIC_GA_ID="" NEXT_PUBLIC_LOCAL_STORAGE_PREFIX="@TestEmuReady_" NEXT_PUBLIC_EMUREADY_BETA_URL="https://play.google.com/store/apps/details?id=com.producdevity.emureadyapp" -NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED=false NEXT_PUBLIC_ENABLE_ANALYTICS=false NEXT_PUBLIC_ENABLE_KOFI_WIDGET=false NEXT_PUBLIC_ENABLE_SENTRY=false diff --git a/next.config.ts b/next.config.ts index bb0d6ef1a..9069d8859 100644 --- a/next.config.ts +++ b/next.config.ts @@ -21,7 +21,6 @@ const contentSecurityPolicyDirectives = [ "'unsafe-eval'", 'https://www.googletagmanager.com', 'https://static.cloudflareinsights.com', - 'https://va.vercel-scripts.com', 'https://*.clerk.com', 'https://*.clerk.accounts.dev', 'https://clerk.emuready.com', @@ -94,7 +93,6 @@ const contentSecurityPolicyDirectives = [ 'https://clerk.emuready.com', 'wss://*.clerk.accounts.dev', 'wss://clerk.emuready.com', - 'https://va.vercel-scripts.com', 'https://challenges.cloudflare.com', 'https://storage.ko-fi.com', 'https://clerk-telemetry.com', diff --git a/package.json b/package.json index a872e5d6c..7cf8ae0a3 100644 --- a/package.json +++ b/package.json @@ -80,8 +80,6 @@ "@trpc/react-query": "11.17.0", "@trpc/server": "11.17.0", "@types/react-syntax-highlighter": "^15.5.13", - "@vercel/analytics": "^1.5.0", - "@vercel/speed-insights": "^1.2.0", "axios": "1.16.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/playwright.config.ts b/playwright.config.ts index 99c81291c..ac11fb179 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -21,7 +21,6 @@ function createWebServerEnv(): { [key: string]: string } { env.NEXT_PUBLIC_ENABLE_ANALYTICS = 'false' env.NEXT_PUBLIC_ENABLE_KOFI_WIDGET = 'false' env.NEXT_PUBLIC_ENABLE_SENTRY = 'false' - env.NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED = 'false' env.NEXT_PUBLIC_DISABLE_COOKIE_BANNER = 'true' env.PLAYWRIGHT_TEST = 'true' return env diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37146b6ad..dd31e3803 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,12 +96,6 @@ importers: '@types/react-syntax-highlighter': specifier: ^15.5.13 version: 15.5.13 - '@vercel/analytics': - specifier: ^1.5.0 - version: 1.5.0(next@16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(vue-router@4.5.1(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3)) - '@vercel/speed-insights': - specifier: ^1.2.0 - version: 1.2.0(next@16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(vue-router@4.5.1(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3)) axios: specifier: 1.16.0 version: 1.16.0 @@ -3968,55 +3962,6 @@ packages: cpu: [x64] os: [win32] - '@vercel/analytics@1.5.0': - resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==} - peerDependencies: - '@remix-run/react': ^2 - '@sveltejs/kit': ^1 || ^2 - next: '>= 13' - react: ^18 || ^19 || ^19.0.0-rc - svelte: '>= 4' - vue: ^3 - vue-router: ^4 - peerDependenciesMeta: - '@remix-run/react': - optional: true - '@sveltejs/kit': - optional: true - next: - optional: true - react: - optional: true - svelte: - optional: true - vue: - optional: true - vue-router: - optional: true - - '@vercel/speed-insights@1.2.0': - resolution: {integrity: sha512-y9GVzrUJ2xmgtQlzFP2KhVRoCglwfRQgjyfY607aU0hh0Un6d0OUyrJkjuAlsV18qR4zfoFPs/BiIj9YDS6Wzw==} - peerDependencies: - '@sveltejs/kit': ^1 || ^2 - next: '>= 13' - react: ^18 || ^19 || ^19.0.0-rc - svelte: '>= 4' - vue: ^3 - vue-router: ^4 - peerDependenciesMeta: - '@sveltejs/kit': - optional: true - next: - optional: true - react: - optional: true - svelte: - optional: true - vue: - optional: true - vue-router: - optional: true - '@vitejs/plugin-react@4.6.0': resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -12350,20 +12295,6 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.9.2': optional: true - '@vercel/analytics@1.5.0(next@16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(vue-router@4.5.1(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3))': - optionalDependencies: - next: 16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - vue: 3.5.17(typescript@5.8.3) - vue-router: 4.5.1(vue@3.5.17(typescript@5.8.3)) - - '@vercel/speed-insights@1.2.0(next@16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(vue-router@4.5.1(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3))': - optionalDependencies: - next: 16.2.6(@babel/core@7.27.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - react: 19.2.6 - vue: 3.5.17(typescript@5.8.3) - vue-router: 4.5.1(vue@3.5.17(typescript@5.8.3)) - '@vitejs/plugin-react@4.6.0(vite@7.2.4(@types/node@20.19.1)(jiti@2.7.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.7 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 407952f3a..e3236a9c4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,7 +4,6 @@ allowBuilds: "@prisma/engines": true "@sentry/cli": true "@tailwindcss/oxide": true - "@vercel/speed-insights": false esbuild: true prisma: true sharp: true diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6a4b15df3..12cee3b15 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,8 +2,6 @@ import './globals.css' import { ClerkProvider } from '@clerk/nextjs' import { shadesOfPurple } from '@clerk/themes' import { GoogleAnalytics } from '@next/third-parties/google' -import { Analytics } from '@vercel/analytics/next' -import { SpeedInsights } from '@vercel/speed-insights/next' import { type Metadata, type Viewport } from 'next' import { Inter } from 'next/font/google' import { connection } from 'next/server' @@ -55,18 +53,12 @@ export default function RootLayout(props: PropsWithChildren) { <> - {env.GA_ID && } )} {env.ENABLE_KOFI_WIDGET && } )} - {env.ENABLE_ANALYTICS && env.VERCEL_ANALYTICS_ENABLED && ( - - - - )} diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index e5a312329..1a6d47a1f 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -14,7 +14,7 @@ function PrivacyPolicyPage() {

Privacy Policy

- Last updated: August 1, 2025 + Last updated: June 6, 2026

@@ -169,7 +169,7 @@ function PrivacyPolicyPage() { Clerk: User authentication and account management
  • - Vercel: Website hosting and analytics + Vercel: Website hosting
  • Supabase: Database and storage services diff --git a/src/components/SessionTracker.test.tsx b/src/components/SessionTracker.test.tsx new file mode 100644 index 000000000..a857e80da --- /dev/null +++ b/src/components/SessionTracker.test.tsx @@ -0,0 +1,169 @@ +import { fireEvent, render, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import SessionTracker from './SessionTracker' + +type MockUser = { + id: string + externalAccounts?: { provider?: string }[] + primaryEmailAddress?: { id: string } | null +} + +const testState = vi.hoisted(() => ({ + analyticsAllowed: true, + pathname: '/', + user: null as MockUser | null, + analytics: { + user: { + signedIn: vi.fn(), + }, + session: { + featureDiscovered: vi.fn(), + pageView: vi.fn(), + sessionEnded: vi.fn(), + sessionStarted: vi.fn(), + }, + }, +})) + +vi.mock('@clerk/nextjs', () => ({ + useUser: () => ({ user: testState.user }), +})) + +vi.mock('next/navigation', () => ({ + usePathname: () => testState.pathname, +})) + +vi.mock('@/hooks', () => ({ + useCookieConsent: () => ({ analyticsAllowed: testState.analyticsAllowed }), +})) + +vi.mock('@/lib/analytics', () => ({ + default: testState.analytics, +})) + +describe('SessionTracker', () => { + beforeEach(() => { + vi.clearAllMocks() + testState.analyticsAllowed = true + testState.pathname = '/' + testState.user = null + }) + + it('does not track session activity when analytics are disabled', () => { + testState.analyticsAllowed = false + + render() + + fireEvent.click(document.body) + window.dispatchEvent(new Event('beforeunload')) + + expect(testState.analytics.session.sessionStarted).not.toHaveBeenCalled() + expect(testState.analytics.session.pageView).not.toHaveBeenCalled() + expect(testState.analytics.session.sessionEnded).not.toHaveBeenCalled() + }) + + it('tracks real page view and interaction counts when the session ends', async () => { + const view = render() + + await waitFor(() => { + expect(testState.analytics.session.sessionStarted).toHaveBeenCalledOnce() + expect(testState.analytics.session.pageView).toHaveBeenCalledOnce() + }) + expect(testState.analytics.session.pageView).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + loadTime: expect.any(Number), + pathname: '/', + }), + ) + + testState.pathname = '/games' + view.rerender() + + await waitFor(() => { + expect(testState.analytics.session.pageView).toHaveBeenCalledTimes(2) + }) + expect(testState.analytics.session.pageView).toHaveBeenLastCalledWith({ + pathname: '/games', + userId: undefined, + }) + + fireEvent.click(document.body) + fireEvent.keyDown(document, { key: 'Enter' }) + window.dispatchEvent(new Event('beforeunload')) + + expect(testState.analytics.session.sessionEnded).toHaveBeenCalledWith( + expect.objectContaining({ + duration: expect.any(Number), + interactions: 2, + pageViews: 2, + sessionId: expect.any(String), + }), + ) + }) + + it('reports the Clerk OAuth provider without counting sign-in as a page view', async () => { + const view = render() + + await waitFor(() => { + expect(testState.analytics.session.sessionStarted).toHaveBeenCalledOnce() + }) + + testState.user = { + id: 'user-1', + externalAccounts: [{ provider: 'oauth_google' }], + primaryEmailAddress: null, + } + view.rerender() + + await waitFor(() => { + expect(testState.analytics.user.signedIn).toHaveBeenCalledWith({ + method: 'google', + userId: 'user-1', + }) + }) + expect(testState.analytics.session.pageView).toHaveBeenCalledOnce() + }) + + it('falls back to email sign-in when the Clerk user has no OAuth provider', async () => { + const view = render() + + await waitFor(() => { + expect(testState.analytics.session.sessionStarted).toHaveBeenCalledOnce() + }) + + testState.user = { + id: 'user-2', + primaryEmailAddress: { id: 'email-1' }, + } + view.rerender() + + await waitFor(() => { + expect(testState.analytics.user.signedIn).toHaveBeenCalledWith({ + method: 'email', + userId: 'user-2', + }) + }) + }) + + it('falls back to clerk sign-in when the user has no OAuth provider or email', async () => { + const view = render() + + await waitFor(() => { + expect(testState.analytics.session.sessionStarted).toHaveBeenCalledOnce() + }) + + testState.user = { + id: 'user-3', + primaryEmailAddress: null, + } + view.rerender() + + await waitFor(() => { + expect(testState.analytics.user.signedIn).toHaveBeenCalledWith({ + method: 'clerk', + userId: 'user-3', + }) + }) + }) +}) diff --git a/src/components/SessionTracker.tsx b/src/components/SessionTracker.tsx index 45266c9f8..6e4d3ce96 100644 --- a/src/components/SessionTracker.tsx +++ b/src/components/SessionTracker.tsx @@ -6,10 +6,22 @@ import { useEffect, useRef } from 'react' import { useCookieConsent } from '@/hooks' import analytics from '@/lib/analytics' -// Generate a UUID compatible with older browsers +type SignInMethod = NonNullable[0]['method']> +type ClerkUser = NonNullable['user']> + +const INTERACTION_EVENTS: (keyof DocumentEventMap)[] = ['click', 'keydown', 'change', 'submit'] +const FEATURE_BY_PATHNAME: Partial> = { + '/pc-listings/new': 'pc-listing_creation', + '/listings/new': 'listing_creation', + '/profile': 'profile_management', + '/admin': 'admin_panel', + '/listings': 'listing_browser', + '/pc-listings': 'pc-listing_browser', + '/games': 'game_browser', +} + function generateUUID() { if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID() - // Fallback for browsers that don't support crypto.randomUUID return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 @@ -17,14 +29,43 @@ function generateUUID() { }) } +function mapExternalProvider(provider: string | undefined): SignInMethod | null { + switch (provider) { + case 'google': + case 'oauth_google': + return 'google' + case 'discord': + case 'oauth_discord': + return 'discord' + case 'github': + case 'oauth_github': + return 'github' + default: + return null + } +} + +function getSignInMethod(user: ClerkUser | null | undefined): SignInMethod { + const externalProvider = mapExternalProvider(user?.externalAccounts?.[0]?.provider) + if (externalProvider) return externalProvider + if (user?.primaryEmailAddress) return 'email' + return 'clerk' +} + function SessionTracker() { const { user } = useUser() const pathname = usePathname() const { analyticsAllowed } = useCookieConsent() + const userId = user?.id + const signInMethod = getSignInMethod(user) const sessionStartRef = useRef(null) - const pageLoadTimeRef = useRef(null) + const initialPageViewStartedAtRef = useRef(null) const sessionIdRef = useRef(null) const hasTrackedSessionStart = useRef(false) + const hasTrackedPageViewRef = useRef(false) + const pageViewCountRef = useRef(0) + const interactionCountRef = useRef(0) + const currentUserIdRef = useRef(undefined) const discoveredFeatures = useRef>(new Set()) const previousUserIdRef = useRef(undefined) @@ -33,85 +74,97 @@ function SessionTracker() { const now = Date.now() sessionStartRef.current = now - pageLoadTimeRef.current = now + initialPageViewStartedAtRef.current = now sessionIdRef.current = generateUUID() }, []) - // Track user sign-in when a user transitions from null/undefined to having a user + useEffect(() => { + currentUserIdRef.current = userId + }, [userId]) + useEffect(() => { if (!analyticsAllowed) return - const currentUserId = user?.id const previousUserId = previousUserIdRef.current - // If we now have a user but didn't before, and it's not the first load, track sign-in - if (currentUserId && !previousUserId && hasTrackedSessionStart.current) { + if (userId && !previousUserId && hasTrackedSessionStart.current) { analytics.user.signedIn({ - userId: currentUserId, - method: 'clerk', // TODO: figure out if we can get the SSO method from Clerk + userId, + method: signInMethod, }) } - // Update the previous user ID for next comparison - previousUserIdRef.current = currentUserId - }, [analyticsAllowed, user?.id]) + previousUserIdRef.current = userId + }, [analyticsAllowed, signInMethod, userId]) - // Track session start on the first load useEffect(() => { if (!analyticsAllowed || hasTrackedSessionStart.current || !sessionIdRef.current) return hasTrackedSessionStart.current = true analytics.session.sessionStarted({ - userId: user?.id, + userId, sessionId: sessionIdRef.current, referrer: document.referrer, userAgent: navigator.userAgent, }) - }, [analyticsAllowed, user?.id]) + }, [analyticsAllowed, userId]) - // Track page views when pathname changes useEffect(() => { - if (!analyticsAllowed || pageLoadTimeRef.current === null) return + if (!analyticsAllowed || initialPageViewStartedAtRef.current === null) return + + const initialLoadTime = hasTrackedPageViewRef.current + ? undefined + : Date.now() - initialPageViewStartedAtRef.current + const currentUserId = currentUserIdRef.current + const pageViewEvent: Parameters[0] = { + pathname, + userId: currentUserId, + } + if (initialLoadTime !== undefined) pageViewEvent.loadTime = initialLoadTime - const loadTime = Date.now() - pageLoadTimeRef.current + hasTrackedPageViewRef.current = true + pageViewCountRef.current += 1 if (process.env.NODE_ENV === 'development') { - return console.log('📊 Page View:', { + return console.log('Page View:', { pathname, - loadTime, - userSession: user ? 'authenticated' : 'anonymous', + loadTime: initialLoadTime, + userSession: currentUserId ? 'authenticated' : 'anonymous', }) } - analytics.session.pageView({ pathname, loadTime, userId: user?.id }) - - // Track feature discovery based on page visits - const featureMap: Record = { - '/pc-listings/new': 'pc-listing_creation', - '/listings/new': 'listing_creation', - '/profile': 'profile_management', - '/admin': 'admin_panel', - '/listings': 'listing_browser', - '/pc-listings': 'pc-listing_browser', - '/games': 'game_browser', - } + analytics.session.pageView(pageViewEvent) - const feature = featureMap[pathname] + const feature = FEATURE_BY_PATHNAME[pathname] if (feature && !discoveredFeatures.current.has(feature)) { discoveredFeatures.current.add(feature) analytics.session.featureDiscovered({ - userId: user?.id, - feature: feature, + userId: currentUserId, + feature, context: pathname, }) } + }, [analyticsAllowed, pathname]) + + useEffect(() => { + if (!analyticsAllowed) return + + const handleInteraction = () => { + interactionCountRef.current += 1 + } - // Reset page load timer - pageLoadTimeRef.current = Date.now() - }, [pathname, analyticsAllowed, user]) + for (const eventName of INTERACTION_EVENTS) { + document.addEventListener(eventName, handleInteraction, true) + } + + return () => { + for (const eventName of INTERACTION_EVENTS) { + document.removeEventListener(eventName, handleInteraction, true) + } + } + }, [analyticsAllowed]) - // Track session duration on page unloading useEffect(() => { if (!analyticsAllowed || sessionStartRef.current === null || !sessionIdRef.current) return @@ -121,17 +174,17 @@ function SessionTracker() { const sessionDuration = Date.now() - sessionStartRef.current analytics.session.sessionEnded({ - userId: user?.id, + userId: currentUserIdRef.current, sessionId: sessionIdRef.current, duration: sessionDuration, - pageViews: 1, // TODO: Track page views separately - interactions: 0, // TODO: Track interactions separately + pageViews: pageViewCountRef.current, + interactions: interactionCountRef.current, }) } window.addEventListener('beforeunload', handleBeforeUnload) return () => window.removeEventListener('beforeunload', handleBeforeUnload) - }, [analyticsAllowed, user]) + }, [analyticsAllowed]) return null } diff --git a/src/lib/analytics/analytics.ts b/src/lib/analytics/analytics.ts index 9c44e30ff..7d9205001 100644 --- a/src/lib/analytics/analytics.ts +++ b/src/lib/analytics/analytics.ts @@ -1026,7 +1026,7 @@ const analytics = { }) }, - pageView: (params: { pathname: string; loadTime: number; userId?: string }) => { + pageView: (params: { pathname: string; loadTime?: number; userId?: string }) => { sendAnalyticsEvent({ category: ANALYTICS_CATEGORIES.SESSION, action: SESSION_ACTIONS.PAGE_VIEW, diff --git a/src/lib/analytics/utils/sendAnalyticsEvent.test.ts b/src/lib/analytics/utils/sendAnalyticsEvent.test.ts index bd149e207..07b7a3757 100644 --- a/src/lib/analytics/utils/sendAnalyticsEvent.test.ts +++ b/src/lib/analytics/utils/sendAnalyticsEvent.test.ts @@ -5,17 +5,12 @@ const mocks = vi.hoisted(() => ({ isTrackingAllowed: vi.fn(() => true), loggerLog: vi.fn(), sendGAEvent: vi.fn(), - track: vi.fn(), })) vi.mock('@next/third-parties/google', () => ({ sendGAEvent: mocks.sendGAEvent, })) -vi.mock('@vercel/analytics', () => ({ - track: mocks.track, -})) - vi.mock('@/lib/logger', () => ({ logger: { log: mocks.loggerLog, @@ -50,7 +45,6 @@ describe('sendAnalyticsEvent', () => { NEXT_PUBLIC_APP_ENV: 'production', NEXT_PUBLIC_ENABLE_ANALYTICS: 'false', NEXT_PUBLIC_GA_ID: 'G-TEST', - NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: 'true', }) sendAnalyticsEvent({ @@ -58,17 +52,15 @@ describe('sendAnalyticsEvent', () => { action: 'support_banner_shown', }) - expect(mocks.track).not.toHaveBeenCalled() expect(mocks.sendGAEvent).not.toHaveBeenCalled() }) - it('sends enabled analytics services when the master analytics flag is enabled', async () => { + it('sends Google Analytics events when analytics are enabled', async () => { const { sendAnalyticsEvent } = await loadSendAnalyticsEvent({ NODE_ENV: 'production', NEXT_PUBLIC_APP_ENV: 'production', NEXT_PUBLIC_ENABLE_ANALYTICS: 'true', NEXT_PUBLIC_GA_ID: 'G-TEST', - NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: 'true', }) sendAnalyticsEvent({ @@ -76,10 +68,6 @@ describe('sendAnalyticsEvent', () => { action: 'support_banner_shown', }) - expect(mocks.track).toHaveBeenCalledWith( - 'support_banner_shown', - expect.objectContaining({ category: ANALYTICS_CATEGORIES.ENGAGEMENT }), - ) expect(mocks.sendGAEvent).toHaveBeenCalledWith( 'event', 'support_banner_shown', diff --git a/src/lib/analytics/utils/sendAnalyticsEvent.ts b/src/lib/analytics/utils/sendAnalyticsEvent.ts index e641c518f..1a367cff7 100644 --- a/src/lib/analytics/utils/sendAnalyticsEvent.ts +++ b/src/lib/analytics/utils/sendAnalyticsEvent.ts @@ -1,16 +1,12 @@ import { sendGAEvent } from '@next/third-parties/google' -import { track } from '@vercel/analytics' import { type AnalyticsEventData } from '@/lib/analytics/analytics.types' import { env } from '@/lib/env' import { logger } from '@/lib/logger' import { isTrackingAllowed } from './isTrackingAllowed' -/** - * Send analytics event with proper consent checking and environment handling - */ + export function sendAnalyticsEvent(params: AnalyticsEventData) { if (!isTrackingAllowed(params.category)) return - // Build event data with proper typing const eventData: Record = { category: params.category, action: params.action, @@ -40,17 +36,15 @@ export function sendAnalyticsEvent(params: AnalyticsEventData) { if (params.duration) eventData.duration = params.duration if (params.value !== undefined) eventData.value = params.value - // Add metadata if (params.metadata) { Object.entries(params.metadata).forEach(([key, value]) => { eventData[key] = value }) } - // Log in development, send it to external services only when explicitly enabled. if (env.IS_DEVELOPMENT_BUILD) { const context = typeof window !== 'undefined' ? 'CLIENT' : 'SERVER' - return logger.log(`📊 Analytics Event [${context}]:`, { + return logger.log(`Analytics Event [${context}]:`, { category: params.category, action: params.action, data: eventData, @@ -58,7 +52,6 @@ export function sendAnalyticsEvent(params: AnalyticsEventData) { } if (typeof window !== 'undefined' && env.ENABLE_ANALYTICS) { - if (env.VERCEL_ANALYTICS_ENABLED) track(params.action, eventData) if (env.GA_ID) { sendGAEvent('event', params.action, { event_category: params.category, diff --git a/src/lib/env.ts b/src/lib/env.ts index 06b815593..47f68f09d 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -18,7 +18,6 @@ interface Env { GA_ID: string LOCAL_STORAGE_PREFIX: string ENABLE_SW: boolean - VERCEL_ANALYTICS_ENABLED: boolean DISABLE_COOKIE_BANNER: boolean APP_ENV: AppEnv IS_PUBLIC_PRODUCTION: boolean @@ -79,8 +78,6 @@ export const env = { ENABLE_SW: process.env.NEXT_PUBLIC_ENABLE_SW === 'true', - VERCEL_ANALYTICS_ENABLED: process.env.NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED === 'true', - DISABLE_COOKIE_BANNER: process.env.NEXT_PUBLIC_DISABLE_COOKIE_BANNER === 'true', APP_ENV, diff --git a/tests/helpers/external-services.ts b/tests/helpers/external-services.ts index 3d69adcf8..3a438082b 100644 --- a/tests/helpers/external-services.ts +++ b/tests/helpers/external-services.ts @@ -6,14 +6,6 @@ const transparentPng = Buffer.from( ) export async function registerExternalServiceMocks(page: Page) { - await page.route('**/_vercel/speed-insights/script.js*', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/javascript', - body: '', - }) - }) - await page.route( 'https://storage.ko-fi.com/cdn/scripts/floating-chat-wrapper.css*', async (route) => { diff --git a/tests/third-party-services.spec.ts b/tests/third-party-services.spec.ts index 1e630a5aa..0a495b022 100644 --- a/tests/third-party-services.spec.ts +++ b/tests/third-party-services.spec.ts @@ -4,8 +4,6 @@ const OPTIONAL_SERVICE_REQUEST_PATTERNS = [ 'storage.ko-fi.com', 'googletagmanager.com', 'google-analytics.com', - '_vercel/insights', - '_vercel/speed-insights', 'ingest.us.sentry.io', ] as const