@sinequa/atomic

Logging

Setting the log level, and the reason DevTools links every entry to your code instead of to the library.

Four functions, one level, one switch:

configuring it
import { LogLevel, configureLogger, debug, info, warn, error } from "@sinequa/atomic";

configureLogger({ level: LogLevel.DEBUG });

debug("query sent", { name: "<your-query>" });

LogLevel is DEBUG (0), INFO (1), WARN (2), ERROR (3), NONE (4) — and the default is INFO, so debug() is silent until you ask for it.

This is the part worth understanding, because it explains a constraint further down.

The four functions are not wrappers. They are the bound console methods, reassigned by configureLogger:

one internal logMessage()a wrapper DevTools blameslogger.ts, always "console.debug.bind(console, '[DEBUG

The browser attributes a console entry to the frame that called console.*. Routing every level through one helper made every line of every application link to logger.ts, and the real caller was unreachable. Exporting the bound method makes the caller the invoking frame.

Two consequences follow, and both are observable:

  • The level prefix is a separate argument — console.error("[ERROR]", message, data) rather than a concatenated string. It renders the same, but a %c or %s directive inside your message is no longer interpreted, and a call with no data no longer logs a trailing "".
  • The console method is captured when the binding is built. Anything replacing console.* afterwards is bypassed — see the pitfalls, this one bites in tests.

Turning it down in production

Verbose in development, quiet in production
src/logging.ts
import { LogLevel, configureLogger } from "@sinequa/atomic";

const isProduction = process.env.NODE_ENV === "production";

// Call this before anything else logs — the library logs during sign-in.
configureLogger({
  level: isProduction ? LogLevel.WARN : LogLevel.DEBUG,
  enableConsoleOutput: true,
});

What the library logs, and where

Worth knowing when a level is too quiet to diagnose something:

LevelWhat
debugBest-effort failures it deliberately absorbs — a CSRF probe that failed, a recovery hook that threw, a refused cookie exchange in bearer mode
infoDecisions with visible consequences — falling back to a credentials form, continuing on the token alone
warnA stored token that could not be parsed, an impersonation refusal
errorA logout the server did not confirm, empty credentials

The info lines are the ones to read first when a sign-in behaves unexpectedly: the fallback to a credentials form is announced there rather than happening silently.

Injecting a logger instead

configureLogger is global. For a per-client logger — two backends, or a test asserting on what was logged — pass one through the configuration:

a logger scoped to one client
const client = createAtomicClient({
  app: "<your-app>",
  auth: {
    logger: {
      debug: (message, data) => sink.push(["debug", message, data]),
      info: (message, data) => sink.push(["info", message, data]),
      warn: (message, data) => sink.push(["warn", message, data]),
      error: (message, data) => sink.push(["error", message, data]),
    },
  },
});

That is the AtomicLogger interface, and it is what the authentication layer uses. Without one it falls back to the module-level functions — through getters, so the fallback follows a later configureLogger rather than freezing the level in force when the module was evaluated.

Pitfalls

What's next

On this page