Captures console output in production builds for debugging and error reporting.

Maintains an in-memory buffer of log entries with subscriptions for real-time updates. Supports format string processing and sensitive data redaction. Tracks console group hierarchy to preserve structural information about grouped logs.

class LogCollector {
#entries: LogEntry[] = [];
readonly #listeners = new Set<(entries: LogEntry[]) => void>();
readonly #groupStack: string[] = [];

/**
* Retrieves all captured log entries.
* @returns Array of all captured log entries in chronological order.
* @source
*/
getEntries(): LogEntry[] {
return this.#entries;
}

/**
* Subscribes to log entry changes with a callback.
* @param listener - Function called with current entries on each update.
* @returns Unsubscribe function to remove the listener.
* @source
*/
subscribe(listener: (entries: LogEntry[]) => void): () => void {
this.#listeners.add(listener);
listener(this.#entries);
return () => {
this.#listeners.delete(listener);
};
}

/**
* Called when console.group/groupCollapsed is invoked; tracks group label in hierarchy stack.
* @param label - The group label.
* @source
*/
enterGroup(label: string): void {
this.#groupStack.push(label);
}

/**
* Called when console.groupEnd is invoked; removes most recent group from hierarchy stack.
* @source
*/
exitGroup(): void {
this.#groupStack.pop();
}

/**
* Adds a new log entry to the collection; processes format specifiers and maintains max buffer size.
* @param level - The log severity level.
* @param args - Arguments passed to the console method.
* @source
*/
addEntry(level: LogLevel, args: unknown[]): void {
const timestamp = new Date().toISOString();
const [primary, ...rest] = args;

let message = "";
let details: string[] = [];

if (
typeof primary === "string" &&
/%[sdifoOc%]/.test(primary) &&
rest.length
) {
// apply format specifiers against the first N args; remaining args become details
const consumed = countPlaceholders(primary);
const consumedArgs = rest.slice(0, consumed);
message = applyFormat(primary, consumedArgs);
details = rest.slice(consumed).map(serializeArgument);
} else {
message =
primary === undefined && rest.length === 0
? ""
: serializeArgument(primary);
details = rest.map(serializeArgument);
}

const entry: LogEntry = {
id: generateLogId(),
level,
message,
details,
timestamp,
source: extractSourceFromStack(new Error(message).stack),
isDebug: inferIsDebug(level),
groupPath:
this.#groupStack.length > 0 ? [...this.#groupStack] : undefined,
groupDepth:
this.#groupStack.length > 0 ? this.#groupStack.length : undefined,
};

this.#entries = [...this.#entries, entry].slice(-MAX_LOG_ENTRIES);
this.#notify();
}

/**
* Clears all captured log entries and notifies subscribers.
* @source
*/
clear(): void {
this.#entries = [];
this.#notify();
}

/**
* Notifies all subscribers with current snapshot of log entries.
* @source
*/
#notify() {
const snapshot = this.#entries;
for (const listener of this.#listeners) {
listener(snapshot);
}
}
}

Constructors

Methods

  • Retrieves all captured log entries.

    Returns LogEntry[]

    Array of all captured log entries in chronological order.

      getEntries(): LogEntry[] {
    return this.#entries;
    }
  • Subscribes to log entry changes with a callback.

    Parameters

    • listener: (entries: LogEntry[]) => void

      Function called with current entries on each update.

    Returns () => void

    Unsubscribe function to remove the listener.

      subscribe(listener: (entries: LogEntry[]) => void): () => void {
    this.#listeners.add(listener);
    listener(this.#entries);
    return () => {
    this.#listeners.delete(listener);
    };
    }
  • Called when console.group/groupCollapsed is invoked; tracks group label in hierarchy stack.

    Parameters

    • label: string

      The group label.

    Returns void

      enterGroup(label: string): void {
    this.#groupStack.push(label);
    }
  • Called when console.groupEnd is invoked; removes most recent group from hierarchy stack.

    Returns void

      exitGroup(): void {
    this.#groupStack.pop();
    }
  • Adds a new log entry to the collection; processes format specifiers and maintains max buffer size.

    Parameters

    • level: LogLevel

      The log severity level.

    • args: unknown[]

      Arguments passed to the console method.

    Returns void

      addEntry(level: LogLevel, args: unknown[]): void {
    const timestamp = new Date().toISOString();
    const [primary, ...rest] = args;

    let message = "";
    let details: string[] = [];

    if (
    typeof primary === "string" &&
    /%[sdifoOc%]/.test(primary) &&
    rest.length
    ) {
    // apply format specifiers against the first N args; remaining args become details
    const consumed = countPlaceholders(primary);
    const consumedArgs = rest.slice(0, consumed);
    message = applyFormat(primary, consumedArgs);
    details = rest.slice(consumed).map(serializeArgument);
    } else {
    message =
    primary === undefined && rest.length === 0
    ? ""
    : serializeArgument(primary);
    details = rest.map(serializeArgument);
    }

    const entry: LogEntry = {
    id: generateLogId(),
    level,
    message,
    details,
    timestamp,
    source: extractSourceFromStack(new Error(message).stack),
    isDebug: inferIsDebug(level),
    groupPath:
    this.#groupStack.length > 0 ? [...this.#groupStack] : undefined,
    groupDepth:
    this.#groupStack.length > 0 ? this.#groupStack.length : undefined,
    };

    this.#entries = [...this.#entries, entry].slice(-MAX_LOG_ENTRIES);
    this.#notify();
    }
  • Clears all captured log entries and notifies subscribers.

    Returns void

      clear(): void {
    this.#entries = [];
    this.#notify();
    }