Skip to content

Conversation

@daniel-graham-amplitude
Copy link
Collaborator

@daniel-graham-amplitude daniel-graham-amplitude commented Jan 10, 2026

Summary

Add a helper that overrides the MDN Console and add callbacks to the different log levels ('error', 'log', etc...) that get triggered when the function is called

Checklist

  • Does your PR title have the correct title format?
  • Does your PR have a breaking change?: No

Note

Adds a lightweight observer to intercept console calls and invoke callbacks per log level.

  • New consoleObserver in observers/console.ts uses a Proxy to wrap console and dispatch callbacks via observe/disconnectHandler (with _restoreConsole for tests)
  • Exported consoleObserver from src/index.ts
  • Unit tests for observing multiple callbacks and disconnect behavior in console.test.ts
  • Demo HTML page under test-server/observers/console.html showcasing usage

Written by Cursor Bugbot for commit 0a25305. This will update automatically on new commits. Configure here.

@macroscopeapp
Copy link
Contributor

macroscopeapp bot commented Jan 10, 2026

Add consoleObserver to @amplitude/analytics-core to capture console method calls for error click autocapture

Expose consoleObserver via index.ts and implement a Proxy-based override of the global console that invokes registered callbacks per log level before delegating to the original methods. Include registration, disconnection, and test-only restore utilities in console.ts, with tests in console.test.ts.

🖇️ Linked Issues

Addresses AMP-146148 under the story AMP-146047 within epic AMP-146043 by adding a console observer that captures console events.

📍Where to Start

Start with the overrideConsole and observe implementations in packages/analytics-core/src/observers/console.ts.


Macroscope summarized 0a25305. (Automatic summaries will resume when PR exits draft mode or review begins).

@daniel-graham-amplitude
Copy link
Collaborator Author

bugbot run

Comment on lines +21 to +33
get(target: Console, prop: ConsoleLogLevel) {
return function (...args: any[]) {
if (handlers.has(prop)) {
const callbacks = handlers.get(prop);
if (callbacks) {
callbacks.forEach((callback) => callback(prop, args));
}
}
/* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */
return (target[prop] as (...args: any[]) => void)(...args);
};
},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Proxy get trap wraps every console property. This breaks non-function props (e.g., console.memory). Consider only wrapping when target[prop] is a function; otherwise return the value as-is.

Suggested change
get(target: Console, prop: ConsoleLogLevel) {
return function (...args: any[]) {
if (handlers.has(prop)) {
const callbacks = handlers.get(prop);
if (callbacks) {
callbacks.forEach((callback) => callback(prop, args));
}
}
/* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */
return (target[prop] as (...args: any[]) => void)(...args);
};
},
};
get(target: Console, prop: ConsoleLogLevel) {
const value = target[prop];
if (typeof value !== 'function') return value;
return function (...args: any[]) {
if (handlers.has(prop)) {
const callbacks = handlers.get(prop);
if (callbacks) {
callbacks.forEach((callback) => callback(prop, args));
}
}
/* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */
return (value as (...args: any[]) => void)(...args);
};
},

🚀 Want me to fix this? Reply ex: "fix it for me".

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a console observer utility that intercepts global console methods and allows callbacks to be registered for different log levels. The implementation provides a Proxy-based approach to observe console calls without breaking existing functionality.

Key Changes:

  • New console observer utility with observe/disconnect/restore capabilities
  • Unit tests for basic observation and disconnection behavior
  • Demo HTML page for manual testing

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
packages/analytics-core/src/observers/console.ts Core implementation of console observer using Proxy API to intercept console methods
packages/analytics-core/src/index.ts Exports the consoleObserver for public use
packages/analytics-core/test/observers/console.test.ts Unit tests covering observe and disconnect functionality
test-server/observers/console.html Demo page for manual verification of console observation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 10 to 81
type Callback = (logLevel: ConsoleLogLevel, args: any[]) => void;

const handlers = new Map<ConsoleLogLevel, Array<Callback>>();

function overrideConsole() {
/* istanbul ignore if */
if (isOverridden || !originalConsole || !globalScope) return;

// use Proxy to override the console method
const handler = {
// get args from console method call
get(target: Console, prop: ConsoleLogLevel) {
return function (...args: any[]) {
if (handlers.has(prop)) {
const callbacks = handlers.get(prop);
if (callbacks) {
callbacks.forEach((callback) => callback(prop, args));
}
}
/* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */
return (target[prop] as (...args: any[]) => void)(...args);
};
},
};
const proxy = new Proxy(originalConsole, handler);
globalScope.console = proxy;
isOverridden = true;
}

async function observe(level: ConsoleLogLevel, callback: Callback) {
if (handlers.has(level)) {
// using ! is safe because we know the key exists based on has() condition
handlers.get(level)!.push(callback);
} else {
handlers.set(level, [callback]);
}
overrideConsole();
}

function disconnectHandler(callback: Callback) {
handlers.forEach((callbacks, level) => {
if (callbacks.includes(callback)) {
handlers.delete(level);
}
});
}

// this should only be used for testing
// restoring console can break other console overrides
function _restoreConsole() {
if (globalScope) {
/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */
(globalScope as any).console = originalConsole;
}
isOverridden = false;
handlers.clear();
}

const consoleObserver = {
observe,
disconnectHandler,
_restoreConsole,
};
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Callback type and consoleObserver methods lack documentation. Public APIs should have JSDoc comments explaining parameters, return values, and behavior, especially for an exported utility that overrides global console behavior.

Copilot uses AI. Check for mistakes.
handlers.delete(level);
}
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disconnectHandler removes all callbacks for a log level

High Severity

The disconnectHandler function uses handlers.delete(level) which removes the entire level entry from the map, rather than just removing the specific callback from the array. When multiple callbacks are registered for the same log level (e.g., two callbacks for 'log'), calling disconnectHandler with one callback removes all callbacks for that level, not just the specified one. The fix would be to splice/filter the specific callback from the array instead of deleting the entire level.

🔬 Verification Test

Test code:

// Added to packages/analytics-core/test/observers/console.test.ts
it('should only disconnect the specified callback when multiple exist', async () => {
  const callback1 = jest.fn();
  const callback2 = jest.fn();

  await consoleObserver.observe('log', callback1);
  await consoleObserver.observe('log', callback2);

  console.log('before disconnect');
  expect(callback1).toHaveBeenCalledTimes(1);
  expect(callback2).toHaveBeenCalledTimes(1);

  // Disconnect only callback1
  consoleObserver.disconnectHandler(callback1);

  console.log('after disconnect');
  // callback1 should not be called again
  expect(callback1).toHaveBeenCalledTimes(1);
  // callback2 should still be called
  expect(callback2).toHaveBeenCalledTimes(2);
});

Command run:

cd packages/analytics-core && npx jest test/observers/console.test.ts --testNamePattern="should only disconnect"

Output:

 FAIL  test/observers/console.test.ts
  consoleObserver
    disconnect
      ✕ should only disconnect the specified callback when multiple exist (11 ms)

  ● consoleObserver › disconnect › should only disconnect the specified callback when multiple exist

    expect(received).toHaveBeenCalledTimes(expected)

    Expected number of calls: 2
    Received number of calls: 1

      at Object.<anonymous> (test/observers/console.test.ts:XX:XX)

Why this proves the bug: The test expects callback2 to still receive console.log events after only callback1 is disconnected, but it doesn't. This proves disconnectHandler removes all callbacks for the level instead of just the specified one.

Fix in Cursor Fix in Web

@daniel-graham-amplitude daniel-graham-amplitude force-pushed the AMP-146148-console-observer-helper branch from 0a25305 to 21c96dd Compare January 10, 2026 00:25
@daniel-graham-amplitude daniel-graham-amplitude force-pushed the AMP-146148-console-observer-helper branch from 21c96dd to cdf10a6 Compare January 10, 2026 00:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants