fix: enhance error logging by replacing console.error with a centralized logger across the application to improve security and maintainability

This commit is contained in:
Usman Baig
2026-02-22 20:57:21 +01:00
parent 837c677b51
commit 2d0307d328
17 changed files with 62 additions and 31 deletions

View File

@@ -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. - **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. - **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. - **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 ## [0.10.0-alpha] - 2026-02-21

View File

@@ -1,6 +1,7 @@
'use server' 'use server'
import { cookies } from 'next/headers' 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' 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) { } catch (error: unknown) {
console.error('Auth Exchange Error:', error) logger.error('Auth Exchange Error:', error)
const isNetwork = const isNetwork =
error instanceof TypeError || error instanceof TypeError ||
(error instanceof Error && (error.name === 'AbortError' || /fetch|network|ECONNREFUSED|ETIMEDOUT/i.test(error.message))) (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) { } catch (e) {
console.error('[setSessionAction] Error:', e) logger.error('[setSessionAction] Error:', e)
return { success: false as const, error: 'invalid' } return { success: false as const, error: 'invalid' }
} }
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useEffect, useState, Suspense, useRef, useCallback } from 'react' import { useEffect, useState, Suspense, useRef, useCallback } from 'react'
import { logger } from '@/lib/utils/logger'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
import { AUTH_URL, default as apiRequest } from '@/lib/api/client' import { AUTH_URL, default as apiRequest } from '@/lib/api/client'
@@ -96,7 +97,7 @@ function AuthCallbackContent() {
return return
} }
if (state !== storedState) { if (state !== storedState) {
console.error('State mismatch', { received: state, stored: storedState }) logger.error('State mismatch', { received: state, stored: storedState })
setError('Invalid state') setError('Invalid state')
return return
} }

View File

@@ -8,6 +8,7 @@ import { useAuth } from '@/lib/auth/context'
import { useOnlineStatus } from '@/lib/hooks/useOnlineStatus' import { useOnlineStatus } from '@/lib/hooks/useOnlineStatus'
import Link from 'next/link' import Link from 'next/link'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { logger } from '@/lib/utils/logger'
import { getUserOrganizations, switchContext } from '@/lib/api/organization' import { getUserOrganizations, switchContext } from '@/lib/api/organization'
import { setSessionAction } from '@/app/actions/auth' import { setSessionAction } from '@/app/actions/auth'
import { LoadingOverlay } from '@ciphera-net/ui' import { LoadingOverlay } from '@ciphera-net/ui'
@@ -39,7 +40,7 @@ export default function LayoutContent({ children }: { children: React.ReactNode
if (auth.user) { if (auth.user) {
getUserOrganizations() getUserOrganizations()
.then((organizations) => setOrgs(Array.isArray(organizations) ? organizations : [])) .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]) }, [auth.user])
@@ -51,7 +52,7 @@ export default function LayoutContent({ children }: { children: React.ReactNode
sessionStorage.setItem(ORG_SWITCH_KEY, 'true') sessionStorage.setItem(ORG_SWITCH_KEY, 'true')
window.location.reload() window.location.reload()
} catch (err) { } catch (err) {
console.error('Failed to switch organization', err) logger.error('Failed to switch organization', err)
} }
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
import { logger } from '@/lib/utils/logger'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
@@ -85,7 +86,7 @@ export default function SiteDashboardPage() {
if (settings.multiDayInterval) setMultiDayInterval(settings.multiDayInterval) if (settings.multiDayInterval) setMultiDayInterval(settings.multiDayInterval)
} }
} catch (e) { } catch (e) {
console.error('Failed to load dashboard settings', e) logger.error('Failed to load dashboard settings', e)
} finally { } finally {
setIsSettingsLoaded(true) setIsSettingsLoaded(true)
} }
@@ -103,7 +104,7 @@ export default function SiteDashboardPage() {
} }
localStorage.setItem('pulse_dashboard_settings', JSON.stringify(settings)) localStorage.setItem('pulse_dashboard_settings', JSON.stringify(settings))
} catch (e) { } catch (e) {
console.error('Failed to save dashboard settings', e) logger.error('Failed to save dashboard settings', e)
} }
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { logger } from '@/lib/utils/logger'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import { createSite, listSites, getSite, type Site } from '@/lib/api/sites' import { createSite, listSites, getSite, type Site } from '@/lib/api/sites'
@@ -65,7 +66,7 @@ export default function NewSitePage() {
router.replace('/') router.replace('/')
} }
} catch (error) { } catch (error) {
console.error('Failed to check limits', error) logger.error('Failed to check limits', error)
} finally { } finally {
setLimitsChecked(true) setLimitsChecked(true)
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { logger } from '@/lib/utils/logger'
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { Button, CheckCircleIcon } from '@ciphera-net/ui' import { Button, CheckCircleIcon } from '@ciphera-net/ui'
@@ -140,7 +141,7 @@ export default function PricingSection() {
// Clear intent // Clear intent
localStorage.removeItem('pulse_pending_checkout') localStorage.removeItem('pulse_pending_checkout')
} catch (e) { } catch (e) {
console.error('Failed to parse pending checkout', e) logger.error('Failed to parse pending checkout', e)
localStorage.removeItem('pulse_pending_checkout') localStorage.removeItem('pulse_pending_checkout')
} }
} }
@@ -203,7 +204,7 @@ export default function PricingSection() {
} }
} catch (error: unknown) { } catch (error: unknown) {
console.error('Checkout error:', error) logger.error('Checkout error:', error)
toast.error('Failed to start checkout — please try again') toast.error('Failed to start checkout — please try again')
} finally { } finally {
setLoadingPlan(null) setLoadingPlan(null)

View File

@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
import { PlusIcon, PersonIcon, CubeIcon, CheckIcon } from '@radix-ui/react-icons' import { PlusIcon, PersonIcon, CubeIcon, CheckIcon } from '@radix-ui/react-icons'
import { switchContext, OrganizationMember } from '@/lib/api/organization' import { switchContext, OrganizationMember } from '@/lib/api/organization'
import { setSessionAction } from '@/app/actions/auth' import { setSessionAction } from '@/app/actions/auth'
import { logger } from '@/lib/utils/logger'
import Link from 'next/link' import Link from 'next/link'
export default function OrganizationSwitcher({ orgs, activeOrgId }: { orgs: OrganizationMember[], activeOrgId: string | null }) { 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() window.location.reload()
} catch (err) { } catch (err) {
console.error('Failed to switch organization', err) logger.error('Failed to switch organization', err)
setSwitching(null) setSwitching(null)
} }
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useEffect, useMemo } from 'react' import { useState, useEffect, useMemo } from 'react'
import { logger } from '@/lib/utils/logger'
import Link from 'next/link' import Link from 'next/link'
import Image from 'next/image' import Image from 'next/image'
import { formatNumber } from '@ciphera-net/ui' 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) const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 10)
setData(result) setData(result)
} catch (e) { } catch (e) {
console.error(e) logger.error(e)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@@ -74,7 +75,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 100) const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 100)
setFullData(result) setFullData(result)
} catch (e) { } catch (e) {
console.error(e) logger.error(e)
} finally { } finally {
setIsLoadingFull(false) setIsLoadingFull(false)
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { logger } from '@/lib/utils/logger'
import { formatNumber } from '@ciphera-net/ui' import { formatNumber } from '@ciphera-net/ui'
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
import { TopPage, getTopPages, getEntryPages, getExitPages } from '@/lib/api/stats' import { TopPage, getTopPages, getEntryPages, getExitPages } from '@/lib/api/stats'
@@ -50,7 +51,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
} }
setFullData(filterGenericPaths(data)) setFullData(filterGenericPaths(data))
} catch (e) { } catch (e) {
console.error(e) logger.error(e)
} finally { } finally {
setIsLoadingFull(false) setIsLoadingFull(false)
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { logger } from '@/lib/utils/logger'
import { formatNumber } from '@ciphera-net/ui' import { formatNumber } from '@ciphera-net/ui'
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
import * as Flags from 'country-flag-icons/react/3x2' import * as Flags from 'country-flag-icons/react/3x2'
@@ -48,7 +49,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
} }
setFullData(data) setFullData(data)
} catch (e) { } catch (e) {
console.error(e) logger.error(e)
} finally { } finally {
setIsLoadingFull(false) setIsLoadingFull(false)
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { logger } from '@/lib/utils/logger'
import { formatNumber } from '@ciphera-net/ui' import { formatNumber } from '@ciphera-net/ui'
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons' import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons'
@@ -58,7 +59,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
} }
setFullData(filterUnknown(data)) setFullData(filterUnknown(data))
} catch (e) { } catch (e) {
console.error(e) logger.error(e)
} finally { } finally {
setIsLoadingFull(false) setIsLoadingFull(false)
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { logger } from '@/lib/utils/logger'
import Image from 'next/image' import Image from 'next/image'
import { formatNumber } from '@ciphera-net/ui' import { formatNumber } from '@ciphera-net/ui'
import { getReferrerDisplayName, getReferrerFavicon, getReferrerIcon, mergeReferrersByDisplayName } from '@/lib/utils/icons' import { getReferrerDisplayName, getReferrerFavicon, getReferrerIcon, mergeReferrersByDisplayName } from '@/lib/utils/icons'
@@ -66,7 +67,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
) )
setFullData(filtered) setFullData(filtered)
} catch (e) { } catch (e) {
console.error(e) logger.error(e)
} finally { } finally {
setIsLoadingFull(false) setIsLoadingFull(false)
} }

View File

@@ -3,6 +3,7 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import { setSessionAction } from '@/app/actions/auth' import { setSessionAction } from '@/app/actions/auth'
import { logger } from '@/lib/utils/logger'
import { useAuth } from '@/lib/auth/context' import { useAuth } from '@/lib/auth/context'
import { import {
deleteOrganization, deleteOrganization,
@@ -170,7 +171,7 @@ export default function OrganizationSettings() {
setOrgName(orgData.name) setOrgName(orgData.name)
setOrgSlug(orgData.slug) setOrgSlug(orgData.slug)
} catch (error) { } catch (error) {
console.error('Failed to load data:', error) logger.error('Failed to load data:', error)
// toast.error('Failed to load members') // toast.error('Failed to load members')
} finally { } finally {
setIsLoadingMembers(false) setIsLoadingMembers(false)
@@ -184,7 +185,7 @@ export default function OrganizationSettings() {
const sub = await getSubscription() const sub = await getSubscription()
setSubscription(sub) setSubscription(sub)
} catch (error) { } catch (error) {
console.error('Failed to load subscription:', error) logger.error('Failed to load subscription:', error)
// toast.error('Failed to load subscription details') // toast.error('Failed to load subscription details')
} finally { } finally {
setIsLoadingSubscription(false) setIsLoadingSubscription(false)
@@ -198,7 +199,7 @@ export default function OrganizationSettings() {
const invs = await getInvoices() const invs = await getInvoices()
setInvoices(invs) setInvoices(invs)
} catch (error) { } catch (error) {
console.error('Failed to load invoices:', error) logger.error('Failed to load invoices:', error)
} finally { } finally {
setIsLoadingInvoices(false) setIsLoadingInvoices(false)
} }
@@ -247,7 +248,7 @@ export default function OrganizationSettings() {
setAuditEntries(Array.isArray(entries) ? entries : []) setAuditEntries(Array.isArray(entries) ? entries : [])
setAuditTotal(typeof total === 'number' ? total : 0) setAuditTotal(typeof total === 'number' ? total : 0)
} catch (error) { } 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') toast.error(getAuthErrorMessage(error as Error) || 'Failed to load audit log entries')
} finally { } finally {
setIsLoadingAudit(false) setIsLoadingAudit(false)
@@ -279,7 +280,7 @@ export default function OrganizationSettings() {
setNotificationSettings(res.settings || {}) setNotificationSettings(res.settings || {})
setNotificationCategories(res.categories || []) setNotificationCategories(res.categories || [])
} catch (error) { } 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') toast.error(getAuthErrorMessage(error as Error) || 'Failed to load notification settings')
} finally { } finally {
setIsLoadingNotificationSettings(false) setIsLoadingNotificationSettings(false)
@@ -422,13 +423,13 @@ export default function OrganizationSettings() {
sessionStorage.setItem('pulse_switching_org', 'true') sessionStorage.setItem('pulse_switching_org', 'true')
window.location.href = '/' window.location.href = '/'
} catch (switchErr) { } 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') sessionStorage.setItem('pulse_switching_org', 'true')
window.location.href = '/' window.location.href = '/'
} }
} catch (err: unknown) { } catch (err: unknown) {
console.error(err) logger.error(err)
toast.error(getAuthErrorMessage(err) || (err instanceof Error ? err.message : '') || 'Failed to delete organization') toast.error(getAuthErrorMessage(err) || (err instanceof Error ? err.message : '') || 'Failed to delete organization')
setIsDeleting(false) setIsDeleting(false)
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { logger } from '@/lib/utils/logger'
import { CopyIcon, CheckIcon } from '@radix-ui/react-icons' import { CopyIcon, CheckIcon } from '@radix-ui/react-icons'
import { listSites, Site } from '@/lib/api/sites' import { listSites, Site } from '@/lib/api/sites'
import { Select, Input, Button } from '@ciphera-net/ui' import { Select, Input, Button } from '@ciphera-net/ui'
@@ -30,7 +31,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
const data = await listSites() const data = await listSites()
setSites(data) setSites(data)
} catch (e) { } catch (e) {
console.error('Failed to load sites for UTM builder', e) logger.error('Failed to load sites for UTM builder', e)
} }
} }
fetchSites() fetchSites()

View File

@@ -6,6 +6,7 @@ import apiRequest from '@/lib/api/client'
import { LoadingOverlay } from '@ciphera-net/ui' import { LoadingOverlay } from '@ciphera-net/ui'
import { logoutAction, getSessionAction, setSessionAction } from '@/app/actions/auth' import { logoutAction, getSessionAction, setSessionAction } from '@/app/actions/auth'
import { getUserOrganizations, switchContext } from '@/lib/api/organization' import { getUserOrganizations, switchContext } from '@/lib/api/organization'
import { logger } from '@/lib/utils/logger'
interface User { interface User {
id: string id: string
@@ -66,7 +67,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return merged 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 () => { const logout = useCallback(async () => {
@@ -96,7 +97,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return merged return merged
}) })
} catch (e) { } catch (e) {
console.error('Failed to refresh user data', e) logger.error('Failed to refresh user data', e)
} }
router.refresh() router.refresh()
}, [router]) }, [router])
@@ -121,7 +122,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setUser(merged) setUser(merged)
localStorage.setItem('user', JSON.stringify(merged)) localStorage.setItem('user', JSON.stringify(merged))
} catch (e) { } catch (e) {
console.error('Failed to fetch full profile', e) logger.error('Failed to fetch full profile', e)
} }
} else { } else {
// * Session invalid/expired // * Session invalid/expired
@@ -178,11 +179,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
router.refresh() router.refresh()
} }
} catch (e) { } catch (e) {
console.error('Failed to auto-switch context', e) logger.error('Failed to auto-switch context', e)
} }
} }
} catch (e) { } catch (e) {
console.error("Failed to fetch organizations", e) logger.error("Failed to fetch organizations", e)
} }
} }
} }

16
lib/utils/logger.ts Normal file
View File

@@ -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)
},
}