Source: src/js/common/EventLog.js

/* eslint-disable no-console */

define(["models/analytics/Analytics"], (Analytics) => {
  const DEFAULT_MAX_EVENTS = 500;
  const DEFAULT_CONSOLE_LEVEL = "info";
  // The entire Analytics.js file can be blocked by privacy extensions, so
  // we need to check if it exists before using it.
  const DEFAULT_ANALYTICS =
    MetacatUI?.analytics || Analytics ? new Analytics() : null;

  const LEVELS = {
    INFO: "info",
    WARNING: "warning",
    ERROR: "error",
  };

  const LEVEL_WEIGHTS = {
    info: 0,
    warning: 1,
    error: 2,
  };
  /**
   * @class EventLog
   * @classdesc A utility class for recording events and grouping by context.
   * Allows logging of events with a descriptive name, severity levels (info,
   * warning, error), and sending the data to an analytics service.
   * @since 2.34.0
   */
  class EventLog {
    /**
     * Constructor for EventLog
     * @param {object} options - Options for the event log
     * @param {string} [options.consoleLevel] - The level at which to log to the
     * events to the console. Must be one of the LEVELS or false to prevent all
     * console logging.
     * @param {Backbone.Model} [options.analyticsModel] - An existing analytics
     * model to use for sending log events. If not provided, a new Analytics
     * model will be created.
     * @param {number} [options.maxEvents] - Maximum number of events per log.
     * Defaults to 500.
     */
    constructor({
      consoleLevel = DEFAULT_CONSOLE_LEVEL,
      maxEvents = DEFAULT_MAX_EVENTS,
      analyticsModel = DEFAULT_ANALYTICS,
    } = {}) {
      this.logs = new Map();
      this.analytics =
        analyticsModel && analyticsModel instanceof Analytics
          ? analyticsModel
          : DEFAULT_ANALYTICS;
      this.maxEvents =
        Number.isInteger(maxEvents) && maxEvents > 0
          ? maxEvents
          : DEFAULT_MAX_EVENTS;
      const optLevel = consoleLevel;
      try {
        this.setConsoleLogLevel(optLevel);
      } catch (e) {
        this.setConsoleLogLevel(DEFAULT_CONSOLE_LEVEL);
      }
    }

    /**
     * Create a new collection to record events related by context or scope. If
     * a log with the same name already exists, it will return the existing log.
     * @param {string} name - Label and ID for the log. If not provided, the
     * default log will be used.
     * @returns {object} - The log object containing an ID, name, start time,
     * and events.
     */
    getOrCreateLog(name) {
      if (this.logs.has(name)) {
        return this.logs.get(name);
      }
      const log = {
        name: name || "Default Log",
        startTime: Date.now(),
        events: [],
      };
      this.logs.set(name, log);
      return log;
    }

    /**
     * Set the console log level for this EventLog instance.
     * @param {string|boolean} level - The log level to set. Must be one of the
     * LEVELS (info, warning, error) or false to disable console logging.
     * @throws {Error} If an invalid level is provided.
     */
    setConsoleLogLevel(level) {
      if (level === false) {
        this.consoleLevel = false;
      } else if (LEVELS[level.toUpperCase()]) {
        this.consoleLevel = LEVELS[level.toUpperCase()];
      } else {
        throw new Error("Invalid console log level");
      }
    }

    /**
     * Record an event to the specified log with a given level.
     * @param {object} log - The log object to record the event in
     * @param {"info"|"warning"|"error"} level - Log level, must be in LEVELS
     * @param {string} message - The log message
     * @param {object} [meta] - Optional metadata to include with the log
     */
    log(log, level, message, meta = {}) {
      if (!LEVELS[level.toUpperCase()]) throw new Error("Invalid level");
      const event = {
        timestamp: Date.now(),
        level,
        message,
        meta,
      };
      if (log.events.length >= this.maxEvents) {
        log.events.shift(); // Remove oldest event
      }
      log.events.push(event);

      const levelWeight = LEVEL_WEIGHTS[level];
      if (
        this.consoleLevel &&
        LEVEL_WEIGHTS[this.consoleLevel] <= levelWeight
      ) {
        this.consoleLog(message, log.name, level, meta);
      }
    }

    /**
     * Clear all events from a log, resetting its state.
     * @param {object} log - The log object to clear
     */
    clearLog(log) {
      if (!this.logs.has(log.name)) return;
      const logToClear = log;
      logToClear.events = [];
      logToClear.startTime = Date.now(); // Reset start time
    }

    /**
     * Log an event to the console with a prefix indicating the log name.
     * @param {string} message - The log message
     * @param {object} [logName] - The log object containing the log name
     * @param {"info"|"warning"|"error"} [level] - The log level
     * @param {object} [meta] - Optional metadata to include with the log
     */
    consoleLog(message, logName = "Default Log", level = "info", meta = {}) {
      if (!this.consoleLevel) return; // Skip if console logging is disabled
      if (!LEVELS[level.toUpperCase()]) throw new Error("Invalid level");
      const prefix = `[Log:${logName}]`;
      if (level === LEVELS.ERROR) {
        console.error(`${prefix} ERROR: ${message}`, meta);
      } else if (level === LEVELS.WARNING) {
        console.warn(`${prefix} WARNING: ${message}`, meta);
      } else {
        console.info(`${prefix} ${message}`, meta);
      }
    }

    /**
     * Manually send a log to analytics (e.g., GA)
     * @param {object} log - The log object to send
     * @param {string} [eventName] - The name of the event to send to analytics
     * @example
     * sendToAnalytics(
     *   resMapResolver.getLog(pid),
     *   "resource_map_resolution_failed"
     * );
     */
    sendToAnalytics(log, eventName = "EventLog") {
      log.events.forEach((event) => {
        const { timestamp, level, message, meta } = event;
        this.analytics.trackCustomEvent(eventName, {
          timestamp,
          level,
          message,
          ...meta,
        });
      });
    }

    /**
     * Return a log's full log for inspection
     * @param {object} log - The log object to inspect
     * @returns {object[]} - Array of log events
     */
    static getLogs(log) {
      return log.events;
    }

    /**
     * Optionally attach a specific analytics model (e.g. GoogleAnalytics)
     * @param {Backbone.Model} analyticsModel - An existing analytics model to
     * use for sending log events.
     */
    setAnalyticsModel(analyticsModel) {
      this.analytics = analyticsModel;
    }
  }

  EventLog.LEVELS = LEVELS;

  return EventLog;
});