@ -18,10 +18,12 @@ import { consoleTransport } from './transports.ts';
import { captureSource , extractError } from './source.ts' ;
/ * *
* Unique entry id . Uses ` crypto.randomUUID() ` when available ( modern browsers ,
* Node 16 + ) ; falls back to a compact ` timestamp-random ` hex in older hosts .
* Default unique entry id . Uses ` crypto.randomUUID() ` when available
* ( modern browsers , Node 16 + ) ; falls back to a compact ` timestamp-random `
* hex in older hosts . Hosts that need deterministic ids in tests can
* override via ` LoggerOptions.idFactory ` .
* /
function generateId ( ) : string {
function defaultG enerateId( ) : string {
if ( typeof crypto !== 'undefined' && typeof crypto . randomUUID === 'function' ) {
return crypto . randomUUID ( ) ;
}
@ -78,6 +80,12 @@ function generateId(): string {
* /
export function createEngineLogger ( options : LoggerOptions = { } ) : EngineLogger {
const captureSourceEnabled : boolean = options . captureSource ? ? DEV ;
const onInternalError = options . onInternalError ;
// Late-bind `Date.now` so test suites that `vi.spyOn(Date, 'now')`
// after logger construction still see their stub. Callers that
// inject `options.clock` get their function captured directly.
const clockNow = options . clock ? . now ? ? ( ( ) : number = > Date . now ( ) ) ;
const generateId = options . idFactory ? ? defaultGenerateId ;
const timers = new Map < string , number > ( ) ;
// Per-transport buffers and interval timer ids — indexed by reference.
@ -93,9 +101,20 @@ export function createEngineLogger(options: LoggerOptions = {}): EngineLogger {
transports : [ . . . ( options . transports ? ? [ consoleTransport ( ) ] ) ]
} ;
const built = buildLogger ( captureSourceEnabled , timers , buffers , flushTimers , state , {
. . . options . globalContext
} ) ;
const built = buildLogger (
captureSourceEnabled ,
timers ,
buffers ,
flushTimers ,
state ,
{
. . . options . globalContext
} ,
undefined ,
onInternalError ,
clockNow ,
generateId
) ;
// Browser: flush everything before the page unloads so buffered entries
// get a chance to reach their destinations. The handler reference is kept
@ -190,7 +209,10 @@ function buildLogger(
flushTimers : WeakMap < Transport , ReturnType < typeof setTimeout > > ,
state : LoggerState ,
globalContext : Record < string , unknown > ,
failureThrottle : WeakMap < Transport , FailureThrottleState > = new WeakMap ( )
failureThrottle : WeakMap < Transport , FailureThrottleState > = new WeakMap ( ) ,
onInternalError ? : ( info : { readonly transport : string ; readonly error : unknown } ) = > void ,
clockNow : ( ) = > number = Date . now ,
generateId : ( ) = > string = defaultGenerateId
) : EngineLogger {
function mergeContext ( input? : LogInput ) : Record < string , unknown > | undefined {
const inputCtx = input ? . context ;
@ -239,7 +261,7 @@ function buildLogger(
const entry : LogEntry = {
id : generateId ( ) ,
timestamp : new Date ( ) ,
timestamp : new Date ( clockNow ( ) ) ,
level : lvl ,
category ,
message : resolved ,
@ -416,7 +438,16 @@ function buildLogger(
const failedName = transportName ( failed ) ;
context . deniedFor . add ( failedName ) ;
console . error ( ` [ ${ ENGINE_NAME } ] Transport " ${ failedName } " error: ` , err ) ;
// `onInternalError` lets enterprise hosts capture engine-internal
// failures somewhere other than the runtime console (Sentry's
// captureException, an audit log, etc.). The synthetic failure
// entry below is independent of this hook — it always flows to
// the remaining transports.
if ( onInternalError !== undefined ) {
onInternalError ( { transport : failedName , error : err } ) ;
} else {
console . error ( ` [ ${ ENGINE_NAME } ] Transport " ${ failedName } " error: ` , err ) ;
}
// Per-transport failure throttle. Inside an active window we suppress
// the synthetic failure entry; the actual `write()` keeps being
@ -424,7 +455,7 @@ function buildLogger(
const throttleMs = failed . failureThrottleMs ? ? 0 ;
let suppressedCount = 0 ;
if ( throttleMs > 0 ) {
const now = Date . n ow( ) ;
const now = clockN ow( ) ;
const current = failureThrottle . get ( failed ) ;
if ( current && current . until > now ) {
current . suppressed ++ ;
@ -445,7 +476,7 @@ function buildLogger(
const failureEntry : LogEntry = {
id : generateId ( ) ,
timestamp : new Date ( ) ,
timestamp : new Date ( clockNow ( ) ) ,
level : LogLevel.ERROR ,
category : ENGINE_NAME ,
message ,
@ -577,7 +608,10 @@ function buildLogger(
flushTimers ,
state ,
{ . . . globalContext , . . . ctx } ,
failureThrottle
failureThrottle ,
onInternalError ,
clockNow ,
generateId
) ;
} ,