src/lib/logger/index.ts

Total Symbols
21
Lines of Code
288
Avg Complexity
2.6
Symbol Types
4

File Relationships

graph LR flushBuffer["flushBuffer"] shouldLog["shouldLog"] formatArgs["formatArgs"] emit["emit"] initLogger["initLogger"] configureLogger["configureLogger"] createLogger["createLogger"] formatPrefix["formatPrefix"] debug["debug"] info["info"] warn["warn"] error["error"] groupCollapsed["groupCollapsed"] group["group"] logAuditFill["logAuditFill"] maskValue["maskValue"] flushBuffer -->|calls| shouldLog flushBuffer -->|calls| formatArgs emit -->|calls| shouldLog emit -->|calls| formatArgs initLogger -->|calls| flushBuffer initLogger -->|calls| configureLogger createLogger -->|calls| formatPrefix debug -->|calls| emit info -->|calls| emit warn -->|calls| emit error -->|calls| emit groupCollapsed -->|calls| emit group -->|calls| emit logAuditFill -->|calls| maskValue click flushBuffer "../symbols/a9a9c3e65488e23e.html" click shouldLog "../symbols/49f7fde9c48523cd.html" click formatArgs "../symbols/7295db40a071fe9d.html" click emit "../symbols/131b8d3f5dc1f207.html" click initLogger "../symbols/ad03e6e0ac6d4c9a.html" click configureLogger "../symbols/b16387a5be04329d.html" click createLogger "../symbols/70597a0a6b5e9ebb.html" click formatPrefix "../symbols/74382caf5b83f901.html" click debug "../symbols/6a1e70b4f3a8c4bb.html" click info "../symbols/6f3556cbea8826f9.html" click warn "../symbols/9f8badd5bb39f102.html" click error "../symbols/4c0bc34e62d3e8bf.html" click groupCollapsed "../symbols/4ab6085ac424455a.html" click group "../symbols/07d14529b23f8e28.html" click logAuditFill "../symbols/221e9f6c8e5f6850.html" click maskValue "../symbols/cddf00b5b286b0a8.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'initLogger' has cyclomatic complexity 16 (max 10)

Symbols by Kind

function 10
method 7
interface 3
type 1

All Symbols

Name Kind Visibility Status Lines Signature
LogLevel type exported- 37-37 type LogLevel
LoggerState interface - 49-52 interface LoggerState
BufferedEntry interface - 62-66 interface BufferedEntry
shouldLog function - 73-77 shouldLog(level: LogLevel): : boolean
formatPrefix function - 79-81 formatPrefix(namespace: string): : string
formatArgs function - 83-97 formatArgs(args: unknown[]): : string
flushBuffer function - 99-113 flushBuffer(): : void
emit function - 115-134 emit( level: LogLevel | "group", prefix: string, args: unknown[], ): : void
configureLogger function exported- 142-145 configureLogger(options: Partial<LoggerState>): : void
initLogger function exported- 152-201 initLogger(): : Promise<void>
Logger interface exported- 205-214 interface Logger
createLogger function exported- 224-250 createLogger(namespace: string): : Logger
debug method - 228-230 debug(...args: unknown[])
info method - 231-233 info(...args: unknown[])
warn method - 234-236 warn(...args: unknown[])
error method - 237-239 error(...args: unknown[])
groupCollapsed method - 240-242 groupCollapsed(label: string)
group method - 243-245 group(label: string)
groupEnd method - 246-248 groupEnd()
maskValue function - 254-261 maskValue(value: string): : string
logAuditFill function exported- 272-287 logAuditFill(opts: { selector: string; fieldType: string | null | undefined; source: string; value: string; }): : void

Full Source

/**
 * Fill All — Centralized Logger
 *
 * Usage:
 *   import { createLogger } from '@/lib/logger';
 *   const log = createLogger('MyModule');
 *
 *   log.debug('detalhe interno');
 *   log.info('campo detectado', field);
 *   log.warn('fallback activado');
 *   log.error('falha ao carregar modelo', err);
 *
 * O nível mínimo e habilitação são controlados via Settings (debugLog / logLevel).
 * Nenhum `if` é necessário no código consumidor — o filtro é feito aqui.
 *
 * Configuração via storage:
 *   { debugLog: true, logLevel: 'debug' }   → mostra tudo
 *   { debugLog: false }                      → silencia tudo (padrão em prod)
 *
 * Também escuta `chrome.storage.onChanged` para atualização em tempo real.
 *
 * Os logs são persistidos em chrome.storage.session via log-store.ts,
 * acessíveis de todas as abas de log (painel, devtools, options).
 */

import { addLogEntry, initLogStore, configureLogStore } from "./log-store";
import type { LogEntry } from "./log-store";

export type { LogEntry };
export {
  onLogUpdate,
  getLogEntries,
  loadLogEntries,
  clearLogEntries,
} from "./log-store";

export type LogLevel = "debug" | "info" | "warn" | "error" | "audit";

const LEVEL_PRIORITY: Record<LogLevel, number> = {
  debug: 0,
  info: 1,
  warn: 2,
  error: 3,
  audit: 4,
};

// ── Estado interno ─────────────────────────────────────────────────────────────

interface LoggerState {
  enabled: boolean;
  level: LogLevel;
}

const state: LoggerState = {
  enabled: false,
  level: "warn",
};

/** true enquanto initLogger() ainda não terminou */
let initializing = true;

interface BufferedEntry {
  level: LogLevel | "group";
  prefix: string;
  args: unknown[];
}

/** Fila de mensagens emitidas antes do init completar */
const buffer: BufferedEntry[] = [];

// ── Funções internas ───────────────────────────────────────────────────────────

function shouldLog(level: LogLevel): boolean {
  // audit, warn and error are always visible so critical issues surface even without debugLog
  if (level === "error" || level === "warn" || level === "audit") return true;
  return state.enabled && LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[state.level];
}

function formatPrefix(namespace: string): string {
  return `[FillAll/${namespace}]`;
}

function formatArgs(args: unknown[]): string {
  return args
    .map((a) => {
      if (a instanceof Error) return `${a.message}`;
      if (typeof a === "object" && a !== null) {
        try {
          return JSON.stringify(a);
        } catch {
          return String(a);
        }
      }
      return String(a);
    })
    .join(" ");
}

function flushBuffer(): void {
  for (const entry of buffer) {
    const lvl: LogLevel = entry.level === "group" ? "debug" : entry.level;
    if (shouldLog(lvl)) {
      const logEntry: LogEntry = {
        ts: new Date().toISOString(),
        level: lvl,
        ns: entry.prefix,
        msg: formatArgs(entry.args),
      };
      addLogEntry(logEntry);
    }
  }
  buffer.length = 0;
}

function emit(
  level: LogLevel | "group",
  prefix: string,
  args: unknown[],
): void {
  if (initializing) {
    buffer.push({ level, prefix, args });
    return;
  }
  const lvl: LogLevel = level === "group" ? "debug" : level;
  if (!shouldLog(lvl)) return;

  const logEntry: LogEntry = {
    ts: new Date().toISOString(),
    level: lvl,
    ns: prefix,
    msg: formatArgs(args),
  };
  addLogEntry(logEntry);
}

// ── API pública de configuração ────────────────────────────────────────────────

/**
 * Atualiza a configuração do logger em tempo de execução.
 * Chamado pelo initLogger() com os dados do storage.
 */
export function configureLogger(options: Partial<LoggerState>): void {
  if (options.enabled !== undefined) state.enabled = options.enabled;
  if (options.level !== undefined) state.level = options.level;
}

/**
 * Inicializa o logger lendo as configurações do chrome.storage.
 * Deve ser chamado uma vez no bootstrap de cada contexto (background, content, popup).
 * Se `chrome` não estiver disponível (ex.: testes unitários), usa os padrões.
 */
export async function initLogger(): Promise<void> {
  // Initialize the persistent log store
  await initLogStore();

  if (typeof chrome === "undefined" || !chrome.storage) {
    initializing = false;
    flushBuffer();
    return;
  }

  const SETTINGS_KEY = "fill_all_settings";

  try {
    const result = await chrome.storage.local.get(SETTINGS_KEY);
    const settings = result[SETTINGS_KEY] as
      | { debugLog?: boolean; logLevel?: LogLevel; logMaxEntries?: number }
      | undefined;

    if (settings) {
      const enabled = settings.debugLog ?? false;
      configureLogger({
        enabled,
        // Se debugLog ligado mas logLevel não salvo, assume "debug" — caso contrário "warn"
        level: settings.logLevel ?? (enabled ? "debug" : "warn"),
      });
      configureLogStore({ maxEntries: settings.logMaxEntries ?? 1000 });
    }
  } catch {
    // Silently ignore — logger stays with defaults.
  }

  // Libera a fila de mensagens acumuladas antes do init terminar.
  initializing = false;
  flushBuffer();

  // Atualiza em tempo real quando as configurações mudarem.
  chrome.storage.onChanged.addListener((changes, area) => {
    if (area !== "local" || !changes[SETTINGS_KEY]) return;
    const newSettings = changes[SETTINGS_KEY].newValue as
      | { debugLog?: boolean; logLevel?: LogLevel; logMaxEntries?: number }
      | undefined;
    if (!newSettings) return;
    const enabled = newSettings.debugLog ?? false;
    configureLogger({
      enabled,
      level: newSettings.logLevel ?? (enabled ? "debug" : "warn"),
    });
    configureLogStore({ maxEntries: newSettings.logMaxEntries ?? 1000 });
  });
}

// ── Interface do logger criado por namespace ───────────────────────────────────

export interface Logger {
  debug(...args: unknown[]): void;
  info(...args: unknown[]): void;
  warn(...args: unknown[]): void;
  error(...args: unknown[]): void;
  /** Imprime um cabeçalho de seção no lugar de um group. */
  group(label: string): void;
  groupCollapsed(label: string): void;
  groupEnd(): void;
}

/**
 * Cria um logger com namespace específico.
 * O prefixo `[FillAll/<namespace>]` é adicionado automaticamente.
 *
 * @example
 *   const log = createLogger('TFClassifier');
 *   log.debug('modelo carregado');  // [FillAll/TFClassifier] modelo carregado
 */
export function createLogger(namespace: string): Logger {
  const prefix = formatPrefix(namespace);

  return {
    debug(...args: unknown[]) {
      emit("debug", prefix, args);
    },
    info(...args: unknown[]) {
      emit("info", prefix, args);
    },
    warn(...args: unknown[]) {
      emit("warn", prefix, args);
    },
    error(...args: unknown[]) {
      emit("error", prefix, args);
    },
    groupCollapsed(label: string) {
      emit("group", `${prefix} ── ${label} ──`, []);
    },
    group(label: string) {
      emit("group", `${prefix} ── ${label} ──`, []);
    },
    groupEnd() {
      // no-op: sem group, não há nada para fechar
    },
  };
}

// ── Audit log ────────────────────────────────────────────────────────────────

function maskValue(value: string): string {
  if (value.length <= 4) return "****";
  return (
    value.slice(0, 2) +
    "*".repeat(Math.min(value.length - 4, 6)) +
    value.slice(-2)
  );
}

/**
 * Records an audit entry for a form field fill operation.
 * Always persisted regardless of debugLog setting.
 *
 * @param selector - CSS selector of the filled field
 * @param fieldType - detected type (e.g. "cpf", "email")
 * @param source - generation source (e.g. "generator", "ai", "rule")
 * @param value - generated value (partially masked for privacy)
 */
export function logAuditFill(opts: {
  selector: string;
  fieldType: string | null | undefined;
  source: string;
  value: string;
}): void {
  const masked = maskValue(opts.value);
  const msg = `fill | ${opts.fieldType ?? "unknown"} | ${opts.source} | ${masked} | ${opts.selector}`;
  const entry: LogEntry = {
    ts: new Date().toISOString(),
    level: "audit",
    ns: "[FillAll/Audit]",
    msg,
  };
  addLogEntry(entry);
}