Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,33 @@ export class ErrorHandler {
postTo.options.method ||= 'POST'
let options = { ...postTo.options }

let dataStr = this.logger.data ? '\n\n' + JSON.stringify(this.logger.data, null, ' ') : ''
let causeStr = ''
let currentCause = err.cause
const seenCauses = new WeakSet()
if (typeof err === 'object' && err !== null) seenCauses.add(err)
while (currentCause) {
Comment thread
treeder marked this conversation as resolved.
if (typeof currentCause === 'object' && currentCause !== null) {
if (seenCauses.has(currentCause)) {
causeStr += '\n\nCaused by: [Circular Reference]'
break
}
seenCauses.add(currentCause)
}
if (currentCause instanceof Error) {
causeStr += `\n\nCaused by: ${currentCause.name}: ${currentCause.message}\n${currentCause.stack || ''}`
currentCause = currentCause.cause
} else if (typeof currentCause === 'object') {
causeStr += `\n\nCaused by: ${JSON.stringify(currentCause, null, ' ')}`
currentCause = currentCause.cause
} else {
causeStr += `\n\nCaused by: ${String(currentCause)}`
break
}
}

let dataStr = this.logger?.data ? '\n\n' + JSON.stringify(this.logger.data, null, ' ') : ''

let message = `${err.name}: ${err.message}${dataStr}\n\n${err.stack}`
let message = `${err.name}: ${err.message}${causeStr}${dataStr}\n\n${err.stack}`
Comment thread
treeder marked this conversation as resolved.
if (this.options.appName) message = `${this.options.appName}\n${message}`

if (options.body) {
Expand Down
63 changes: 57 additions & 6 deletions logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ export class CloudflareLogger {
console[method](data)
}


/**
* This returns the object that will be passed to console.log()
*
Expand Down Expand Up @@ -107,11 +106,16 @@ export class CloudflareLogger {
if (err) {
data.message = err.message
data.level = 'error'
data.error = {
message: err.message,
stack: err.stack,
status: err.status,
cause: err.cause,
data.error = serializeError(err)
if (err.cause) {
const causeMsg = formatCauseChain(err.cause, new WeakSet([err]))
if (causeMsg) {
if (data.message) {
data.message += ` (caused by: ${causeMsg})`
} else {
data.message = `caused by: ${causeMsg}`
}
}
}
}
if (params.length > 0) {
Expand Down Expand Up @@ -145,3 +149,50 @@ export class CloudflareLogger {
return typeof value === 'object' && value !== null && !Array.isArray(value) && value.constructor === Object
}
}

export function serializeError(err, seen = new WeakSet()) {
if (!err) return null
if (!(err instanceof Error)) {
if (typeof err === 'object') return err
return { message: String(err) }
}
if (seen.has(err)) {
return { name: err.name, message: '[Circular Reference]', stack: err.stack }
}
seen.add(err)
const data = {
name: err.name,
message: err.message,
status: err.status,
stack: err.stack,
}
if (err.cause !== undefined) {
data.cause = serializeError(err.cause, seen)
}
return data
}

function formatCauseChain(cause, seen = new WeakSet()) {
const messages = []
let curr = cause
while (curr) {
Comment thread
treeder marked this conversation as resolved.
if (typeof curr === 'object' && curr !== null) {
if (seen.has(curr)) {
messages.push('[Circular Reference]')
break
}
seen.add(curr)
}
if (curr instanceof Error) {
if (curr.message) messages.push(curr.message)
curr = curr.cause
} else if (typeof curr === 'object' && curr.message) {
messages.push(curr.message)
curr = curr.cause
} else {
messages.push(String(curr))
break
}
}
return messages.join(': ')
}
52 changes: 52 additions & 0 deletions test/logger.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,55 @@ test('CloudflareLogger contextual cloning with .with()', () => {

spyError.mockRestore()
})

test('CloudflareLogger formats and serializes error cause chains', () => {
const logger = new CloudflareLogger({ data: { env: 'test' } })
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {})

const rootErr = new Error('root cause failure')
const outerErr = new Error('operation failed', { cause: rootErr })

logger.error('an error occurred', outerErr)

expect(spyError).toHaveBeenCalledTimes(1)
const logged = spyError.mock.calls[0][0]
expect(logged.level).toBe('error')
expect(logged.message).toBe('an error occurred operation failed (caused by: root cause failure)')
expect(logged.error).toBeDefined()
expect(logged.error.message).toBe('operation failed')
expect(logged.error.cause).toBeDefined()
expect(logged.error.cause.message).toBe('root cause failure')
expect(logged.error.cause.stack).toBeDefined()

// Test nested cause chains
const deepRoot = new Error('database connection timeout')
const midErr = new Error('query execution failed', { cause: deepRoot })
const topErr = new Error('request handler failed', { cause: midErr })

logger.error(topErr)
expect(spyError).toHaveBeenCalledTimes(2)
const loggedNested = spyError.mock.calls[1][0]
expect(loggedNested.message).toBe(
'request handler failed (caused by: query execution failed: database connection timeout)',
)
expect(loggedNested.error.cause.cause.message).toBe('database connection timeout')

spyError.mockRestore()
})

test('CloudflareLogger handles circular references in error causes gracefully', () => {
const logger = new CloudflareLogger({ data: { env: 'test' } })
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {})

const cyclicErr = new Error('cyclic error')
cyclicErr.cause = cyclicErr

logger.error(cyclicErr)
expect(spyError).toHaveBeenCalledTimes(1)
const logged = spyError.mock.calls[0][0]
expect(logged.level).toBe('error')
expect(logged.message).toBe('cyclic error (caused by: [Circular Reference])')
expect(logged.error.cause.message).toBe('[Circular Reference]')

spyError.mockRestore()
})
Loading