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.
- **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

View File

@@ -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' }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()

View File

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

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