diff --git a/CHANGELOG.md b/CHANGELOG.md index c877e31..83b8a44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **No more loading flicker.** Fast-loading pages no longer flash a loading state for a split second before showing content. - **Organization context switch.** Switching away from a deleted organization now stores the session correctly instead of using an insecure fallback. -- **Removed debug logs.** Auth and organization-switching details no longer leak into the browser console in production. +- **Removed debug logs.** Auth and organization-switching details no longer leak into the browser console in production. Error logs are now also suppressed in production and only appear during development. - **Dark mode uptime chart.** The response time chart on the uptime page now correctly follows your dark mode preference instead of always showing a white tooltip background. ## [0.10.0-alpha] - 2026-02-21 diff --git a/app/actions/auth.ts b/app/actions/auth.ts index f618157..869cfff 100644 --- a/app/actions/auth.ts +++ b/app/actions/auth.ts @@ -1,6 +1,7 @@ 'use server' import { cookies } from 'next/headers' +import { logger } from '@/lib/utils/logger' const AUTH_API_URL = process.env.NEXT_PUBLIC_AUTH_API_URL || process.env.NEXT_PUBLIC_AUTH_URL || 'http://localhost:8081' @@ -102,7 +103,7 @@ export async function exchangeAuthCode(code: string, codeVerifier: string, redir } } catch (error: unknown) { - console.error('Auth Exchange Error:', error) + logger.error('Auth Exchange Error:', error) const isNetwork = error instanceof TypeError || (error instanceof Error && (error.name === 'AbortError' || /fetch|network|ECONNREFUSED|ETIMEDOUT/i.test(error.message))) @@ -152,7 +153,7 @@ export async function setSessionAction(accessToken: string, refreshToken?: strin } } } catch (e) { - console.error('[setSessionAction] Error:', e) + logger.error('[setSessionAction] Error:', e) return { success: false as const, error: 'invalid' } } } diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx index 30396c4..b2a8c34 100644 --- a/app/auth/callback/page.tsx +++ b/app/auth/callback/page.tsx @@ -1,6 +1,7 @@ 'use client' import { useEffect, useState, Suspense, useRef, useCallback } from 'react' +import { logger } from '@/lib/utils/logger' import { useRouter, useSearchParams } from 'next/navigation' import { useAuth } from '@/lib/auth/context' import { AUTH_URL, default as apiRequest } from '@/lib/api/client' @@ -96,7 +97,7 @@ function AuthCallbackContent() { return } if (state !== storedState) { - console.error('State mismatch', { received: state, stored: storedState }) + logger.error('State mismatch', { received: state, stored: storedState }) setError('Invalid state') return } diff --git a/app/layout-content.tsx b/app/layout-content.tsx index 14d3b49..727254f 100644 --- a/app/layout-content.tsx +++ b/app/layout-content.tsx @@ -8,6 +8,7 @@ import { useAuth } from '@/lib/auth/context' import { useOnlineStatus } from '@/lib/hooks/useOnlineStatus' import Link from 'next/link' import { useEffect, useState } from 'react' +import { logger } from '@/lib/utils/logger' import { getUserOrganizations, switchContext } from '@/lib/api/organization' import { setSessionAction } from '@/app/actions/auth' import { LoadingOverlay } from '@ciphera-net/ui' @@ -39,7 +40,7 @@ export default function LayoutContent({ children }: { children: React.ReactNode if (auth.user) { getUserOrganizations() .then((organizations) => setOrgs(Array.isArray(organizations) ? organizations : [])) - .catch(err => console.error('Failed to fetch orgs for header', err)) + .catch(err => logger.error('Failed to fetch orgs for header', err)) } }, [auth.user]) @@ -51,7 +52,7 @@ export default function LayoutContent({ children }: { children: React.ReactNode sessionStorage.setItem(ORG_SWITCH_KEY, 'true') window.location.reload() } catch (err) { - console.error('Failed to switch organization', err) + logger.error('Failed to switch organization', err) } } diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 689b4b0..8622551 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -1,6 +1,7 @@ 'use client' import { useAuth } from '@/lib/auth/context' +import { logger } from '@/lib/utils/logger' import { useCallback, useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { motion } from 'framer-motion' @@ -85,7 +86,7 @@ export default function SiteDashboardPage() { if (settings.multiDayInterval) setMultiDayInterval(settings.multiDayInterval) } } catch (e) { - console.error('Failed to load dashboard settings', e) + logger.error('Failed to load dashboard settings', e) } finally { setIsSettingsLoaded(true) } @@ -103,7 +104,7 @@ export default function SiteDashboardPage() { } localStorage.setItem('pulse_dashboard_settings', JSON.stringify(settings)) } catch (e) { - console.error('Failed to save dashboard settings', e) + logger.error('Failed to save dashboard settings', e) } } diff --git a/app/sites/new/page.tsx b/app/sites/new/page.tsx index 7e651d6..da34c13 100644 --- a/app/sites/new/page.tsx +++ b/app/sites/new/page.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react' +import { logger } from '@/lib/utils/logger' import { useRouter } from 'next/navigation' import Link from 'next/link' import { createSite, listSites, getSite, type Site } from '@/lib/api/sites' @@ -65,7 +66,7 @@ export default function NewSitePage() { router.replace('/') } } catch (error) { - console.error('Failed to check limits', error) + logger.error('Failed to check limits', error) } finally { setLimitsChecked(true) } diff --git a/components/PricingSection.tsx b/components/PricingSection.tsx index d0f4587..42b7663 100644 --- a/components/PricingSection.tsx +++ b/components/PricingSection.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react' +import { logger } from '@/lib/utils/logger' import { useSearchParams } from 'next/navigation' import { motion } from 'framer-motion' import { Button, CheckCircleIcon } from '@ciphera-net/ui' @@ -140,7 +141,7 @@ export default function PricingSection() { // Clear intent localStorage.removeItem('pulse_pending_checkout') } catch (e) { - console.error('Failed to parse pending checkout', e) + logger.error('Failed to parse pending checkout', e) localStorage.removeItem('pulse_pending_checkout') } } @@ -203,7 +204,7 @@ export default function PricingSection() { } } catch (error: unknown) { - console.error('Checkout error:', error) + logger.error('Checkout error:', error) toast.error('Failed to start checkout — please try again') } finally { setLoadingPlan(null) diff --git a/components/WorkspaceSwitcher.tsx b/components/WorkspaceSwitcher.tsx index 4610e44..6d8ff26 100644 --- a/components/WorkspaceSwitcher.tsx +++ b/components/WorkspaceSwitcher.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation' import { PlusIcon, PersonIcon, CubeIcon, CheckIcon } from '@radix-ui/react-icons' import { switchContext, OrganizationMember } from '@/lib/api/organization' import { setSessionAction } from '@/app/actions/auth' +import { logger } from '@/lib/utils/logger' import Link from 'next/link' export default function OrganizationSwitcher({ orgs, activeOrgId }: { orgs: OrganizationMember[], activeOrgId: string | null }) { @@ -37,7 +38,7 @@ export default function OrganizationSwitcher({ orgs, activeOrgId }: { orgs: Orga window.location.reload() } catch (err) { - console.error('Failed to switch organization', err) + logger.error('Failed to switch organization', err) setSwitching(null) } } diff --git a/components/dashboard/Campaigns.tsx b/components/dashboard/Campaigns.tsx index f15efd2..31bed08 100644 --- a/components/dashboard/Campaigns.tsx +++ b/components/dashboard/Campaigns.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect, useMemo } from 'react' +import { logger } from '@/lib/utils/logger' import Link from 'next/link' import Image from 'next/image' import { formatNumber } from '@ciphera-net/ui' @@ -58,7 +59,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) { const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 10) setData(result) } catch (e) { - console.error(e) + logger.error(e) } finally { setIsLoading(false) } @@ -74,7 +75,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) { const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 100) setFullData(result) } catch (e) { - console.error(e) + logger.error(e) } finally { setIsLoadingFull(false) } diff --git a/components/dashboard/ContentStats.tsx b/components/dashboard/ContentStats.tsx index 42c8169..7f2e956 100644 --- a/components/dashboard/ContentStats.tsx +++ b/components/dashboard/ContentStats.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react' +import { logger } from '@/lib/utils/logger' import { formatNumber } from '@ciphera-net/ui' import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' import { TopPage, getTopPages, getEntryPages, getExitPages } from '@/lib/api/stats' @@ -50,7 +51,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain, } setFullData(filterGenericPaths(data)) } catch (e) { - console.error(e) + logger.error(e) } finally { setIsLoadingFull(false) } diff --git a/components/dashboard/Locations.tsx b/components/dashboard/Locations.tsx index e22b4bc..cb71101 100644 --- a/components/dashboard/Locations.tsx +++ b/components/dashboard/Locations.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react' +import { logger } from '@/lib/utils/logger' import { formatNumber } from '@ciphera-net/ui' import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' import * as Flags from 'country-flag-icons/react/3x2' @@ -48,7 +49,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = ' } setFullData(data) } catch (e) { - console.error(e) + logger.error(e) } finally { setIsLoadingFull(false) } diff --git a/components/dashboard/TechSpecs.tsx b/components/dashboard/TechSpecs.tsx index 0655650..8f3a82f 100644 --- a/components/dashboard/TechSpecs.tsx +++ b/components/dashboard/TechSpecs.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react' +import { logger } from '@/lib/utils/logger' import { formatNumber } from '@ciphera-net/ui' import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons' @@ -58,7 +59,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co } setFullData(filterUnknown(data)) } catch (e) { - console.error(e) + logger.error(e) } finally { setIsLoadingFull(false) } diff --git a/components/dashboard/TopReferrers.tsx b/components/dashboard/TopReferrers.tsx index fbfca1a..9e9fb1c 100644 --- a/components/dashboard/TopReferrers.tsx +++ b/components/dashboard/TopReferrers.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react' +import { logger } from '@/lib/utils/logger' import Image from 'next/image' import { formatNumber } from '@ciphera-net/ui' import { getReferrerDisplayName, getReferrerFavicon, getReferrerIcon, mergeReferrersByDisplayName } from '@/lib/utils/icons' @@ -66,7 +67,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI ) setFullData(filtered) } catch (e) { - console.error(e) + logger.error(e) } finally { setIsLoadingFull(false) } diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index b122f45..764eca7 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { setSessionAction } from '@/app/actions/auth' +import { logger } from '@/lib/utils/logger' import { useAuth } from '@/lib/auth/context' import { deleteOrganization, @@ -170,7 +171,7 @@ export default function OrganizationSettings() { setOrgName(orgData.name) setOrgSlug(orgData.slug) } catch (error) { - console.error('Failed to load data:', error) + logger.error('Failed to load data:', error) // toast.error('Failed to load members') } finally { setIsLoadingMembers(false) @@ -184,7 +185,7 @@ export default function OrganizationSettings() { const sub = await getSubscription() setSubscription(sub) } catch (error) { - console.error('Failed to load subscription:', error) + logger.error('Failed to load subscription:', error) // toast.error('Failed to load subscription details') } finally { setIsLoadingSubscription(false) @@ -198,7 +199,7 @@ export default function OrganizationSettings() { const invs = await getInvoices() setInvoices(invs) } catch (error) { - console.error('Failed to load invoices:', error) + logger.error('Failed to load invoices:', error) } finally { setIsLoadingInvoices(false) } @@ -247,7 +248,7 @@ export default function OrganizationSettings() { setAuditEntries(Array.isArray(entries) ? entries : []) setAuditTotal(typeof total === 'number' ? total : 0) } catch (error) { - console.error('Failed to load audit log', error) + logger.error('Failed to load audit log', error) toast.error(getAuthErrorMessage(error as Error) || 'Failed to load audit log entries') } finally { setIsLoadingAudit(false) @@ -279,7 +280,7 @@ export default function OrganizationSettings() { setNotificationSettings(res.settings || {}) setNotificationCategories(res.categories || []) } catch (error) { - console.error('Failed to load notification settings', error) + logger.error('Failed to load notification settings', error) toast.error(getAuthErrorMessage(error as Error) || 'Failed to load notification settings') } finally { setIsLoadingNotificationSettings(false) @@ -422,13 +423,13 @@ export default function OrganizationSettings() { sessionStorage.setItem('pulse_switching_org', 'true') window.location.href = '/' } catch (switchErr) { - console.error('Failed to switch to personal context after delete:', switchErr) + logger.error('Failed to switch to personal context after delete:', switchErr) sessionStorage.setItem('pulse_switching_org', 'true') window.location.href = '/' } } catch (err: unknown) { - console.error(err) + logger.error(err) toast.error(getAuthErrorMessage(err) || (err instanceof Error ? err.message : '') || 'Failed to delete organization') setIsDeleting(false) } diff --git a/components/tools/UtmBuilder.tsx b/components/tools/UtmBuilder.tsx index 3e7dd7f..7f5ad34 100644 --- a/components/tools/UtmBuilder.tsx +++ b/components/tools/UtmBuilder.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react' +import { logger } from '@/lib/utils/logger' import { CopyIcon, CheckIcon } from '@radix-ui/react-icons' import { listSites, Site } from '@/lib/api/sites' import { Select, Input, Button } from '@ciphera-net/ui' @@ -30,7 +31,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) { const data = await listSites() setSites(data) } catch (e) { - console.error('Failed to load sites for UTM builder', e) + logger.error('Failed to load sites for UTM builder', e) } } fetchSites() diff --git a/lib/auth/context.tsx b/lib/auth/context.tsx index ad834e1..537732b 100644 --- a/lib/auth/context.tsx +++ b/lib/auth/context.tsx @@ -6,6 +6,7 @@ import apiRequest from '@/lib/api/client' import { LoadingOverlay } from '@ciphera-net/ui' import { logoutAction, getSessionAction, setSessionAction } from '@/app/actions/auth' import { getUserOrganizations, switchContext } from '@/lib/api/organization' +import { logger } from '@/lib/utils/logger' interface User { id: string @@ -66,7 +67,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return merged }) }) - .catch((e) => console.error('Failed to fetch full profile after login', e)) + .catch((e) => logger.error('Failed to fetch full profile after login', e)) } const logout = useCallback(async () => { @@ -96,7 +97,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return merged }) } catch (e) { - console.error('Failed to refresh user data', e) + logger.error('Failed to refresh user data', e) } router.refresh() }, [router]) @@ -121,7 +122,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setUser(merged) localStorage.setItem('user', JSON.stringify(merged)) } catch (e) { - console.error('Failed to fetch full profile', e) + logger.error('Failed to fetch full profile', e) } } else { // * Session invalid/expired @@ -178,11 +179,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { router.refresh() } } catch (e) { - console.error('Failed to auto-switch context', e) + logger.error('Failed to auto-switch context', e) } } } catch (e) { - console.error("Failed to fetch organizations", e) + logger.error("Failed to fetch organizations", e) } } } diff --git a/lib/utils/logger.ts b/lib/utils/logger.ts new file mode 100644 index 0000000..52f53b9 --- /dev/null +++ b/lib/utils/logger.ts @@ -0,0 +1,16 @@ +/** + * Dev-only logger that suppresses client-side output in production. + * Server-side logs always pass through (they go to server logs, not the browser). + */ + +const isServer = typeof window === 'undefined' +const isDev = process.env.NODE_ENV === 'development' + +export const logger = { + error(...args: unknown[]) { + if (isServer || isDev) console.error(...args) + }, + warn(...args: unknown[]) { + if (isServer || isDev) console.warn(...args) + }, +}