app/src/utils/logging.tstypescript
/**
 * Logging utilities for console and BetterStack integration
 * @module Util:Logging
 */

import i18n from "@/plugins/vue-i18n";
import VersionService from "@/services/VersionService";
import store from "@/store";
import { Logtail } from "@logtail/browser";
import ERROR_CODES from "./error_codes";
import { isPrerender } from "./isPrerender";
import env from "./env";

const SOURCE_TOKEN = env.VITE_BETTERSTACK_SOURCE_TOKEN;
const INGESTING_HOST = env.VITE_BETTERSTACK_INGESTING_HOST;
const isTestEnvironment = localStorage.getItem("isTestEnvironment") == "true";

let logtail: Logtail | undefined;
if (SOURCE_TOKEN && INGESTING_HOST && !isTestEnvironment)
  logtail = new Logtail(SOURCE_TOKEN as string, {
    endpoint: INGESTING_HOST as string,
  });

const SESSION_ID =
  (isPrerender() ? "PRERENDER-" : env.PROD ? "PROD-" : "DEV-") +
  (Math.random() + 1).toString(36).substring(7);

const sendLogsToIngest = SOURCE_TOKEN && INGESTING_HOST && !isTestEnvironment;
// const sendLogsToIngest = env.PROD;

type ConsoleImage = (url: string, size?: number) => Promise<void>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(console as any).image = async function (url: string, size = 100) {
  const img = await fetch(url);
  const blob = await img.blob();
  await new Promise<void>((resolve) => {
    const reader = new FileReader();
    reader.onloadend = () => {
      const image = new Image();
      image.src = reader.result as string;
      image.onload = function () {
        var style = [
          "font-size: 1px;",
          "padding: " + size + "px " + size + "px;",
          "background: url(" + reader.result + ") no-repeat;",
          "background-size: contain;",
        ].join(" ");
        console.log("%c ", style);
        setTimeout(resolve, 100);
      };
    };
    reader.readAsDataURL(blob);
  });
};

const DEFAULT_TEXT_STYLE =
  "font-family: Avenir, Helvetica, Arial, sans-serif; font-size: 12px;";

/**
 * Log a welcome message in the console
 * @async
 * @returns {void}
 */
export async function logWelcomeMessage(): Promise<void> {
  console.clear();

  await (console as unknown as { image: ConsoleImage }).image("/Icon.png");

  console.log(
    `${i18n.global.t("logging.welcome")}`,
    `${DEFAULT_TEXT_STYLE} font-size: 36px; font-weight: bolder;`,
    `${DEFAULT_TEXT_STYLE}`
  );

  console.log(
    i18n.global.t("logging.warning"),
    `${DEFAULT_TEXT_STYLE} color: #dc3545; font-weight: bold; font-size: 1.2rem;`,
    `${DEFAULT_TEXT_STYLE} color: #dc3545;`
  );

  const details = [
    {
      key: i18n.global.t("logging.app-version"),
      value: VersionService.getAppVersion(),
    },
    {
      key: i18n.global.t("logging.server-version"),
      value:
        (await VersionService.getServerVersion()) ||
        i18n.global.t("errors.unknown"),
    },
    {
      key: i18n.global.t("logging.user-agent"),
      value: window.navigator.userAgent,
    },
    {
      key: i18n.global.t("logging.current-locale"),
      value: i18n.global.locale.value,
    },
    {
      key: i18n.global.t("logging.available-locales"),
      value: i18n.global.availableLocales.join(", "),
    },
  ];
  const styles: string[] = [];
  details.forEach(() => {
    styles.push(`${DEFAULT_TEXT_STYLE} font-weight: bold;`);
    styles.push(`${DEFAULT_TEXT_STYLE}`);
  });
  const result = details
    .map(({ key, value }) => {
      return `%c${key}: %c${value}`;
    })
    .join("\n");
  console.log(result, ...styles);
}

function generateTimeStamp(): string {
  return new Date().toISOString();
}

/**
 * Log an INFO message with a timestamp
 * @param {...unknown[]} messages - Messages to log
 */
export function log(...messages: unknown[]): void {
  console.log(generateTimeStamp(), "LOG", ...messages);
  if (sendLogsToIngest) {
    logtail!.info(messages.join(), {
      state: store?.state,
      version: VersionService.getAppVersion(),
      user_locale: i18n.global.locale.value,
      SESSION_ID,
    });
  }
}

/**
 * Log a DEBUG message with a timestamp
 * @param {...unknown[]} messages - Messages to log
 */
export function debug(...messages: unknown[]): void {
  console.debug(generateTimeStamp(), "DEBUG", ...messages);
  if (sendLogsToIngest) {
    const message = messages
      .map((m) => (typeof m !== "object" ? m : JSON.stringify(m)))
      .join(" ");
    logtail!.debug(message, {
      state: store?.state,
      version: VersionService.getAppVersion(),
      user_locale: i18n.global.locale.value,
      SESSION_ID,
    });
  }
}

/**
 * Log a WARN message with a timestamp
 * @param {...unknown[]} messages - Messages to log
 */
export function warn(...messages: unknown[]): void {
  console.warn(generateTimeStamp(), "WARN", ...messages);
  if (sendLogsToIngest && !isPrerender()) {
    logtail!.warn(messages.join(), {
      state: store?.state,
      version: VersionService.getAppVersion(),
      user_locale: i18n.global.locale.value,
      SESSION_ID,
    });
  }
}

/**
 * Log an ERROR message with a timestamp and error code
 * @param {unknown} message - Message to log
 * @param {string} [errorCode] - Error code (defaults to UNKNOWN_ERROR)
 */
export function error(
  message: unknown,
  errorCode: string = ERROR_CODES.UNKNOWN_ERROR ?? "E000"
): void {
  console.error(generateTimeStamp(), "ERROR", errorCode, message);
  if (sendLogsToIngest) {
    logtail!.error(`${errorCode} ${message}`, {
      state: store?.state,
      version: VersionService.getAppVersion(),
      user_locale: i18n.global.locale.value,
      errorCode,
      SESSION_ID,
    });
  }
}

/**
 * Log request results
 * @param {number} status - Response status code
 * @param {number} time - Milliseconds between request and response
 * @param {string} url - Path of the request
 */
export function logRequest(status: number, time: number, url: string): void {
  const message = `${url} responded with status ${status} in ${time} ms`;
  if (!status || status >= 400) {
    if (sendLogsToIngest && !isPrerender()) {
      logtail!.warn(message, {
        state: store?.state,
        version: VersionService.getAppVersion(),
        request: {
          status,
          time,
          url,
        },
        user_locale: i18n.global.locale.value,
        SESSION_ID,
      });
    }
  } else {
    console.debug(message);
    if (sendLogsToIngest) {
      logtail!.debug(message, {
        state: store?.state,
        version: VersionService.getAppVersion(),
        request: {
          status,
          time,
          url,
        },
        user_locale: i18n.global.locale.value,
        SESSION_ID,
      });
    }
  }
}