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:
@@ -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
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
16
lib/utils/logger.ts
Normal 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)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user