{"version":3,"file":"sentry-log.mjs","names":[],"sources":["../../../../../../../@warlock.js/logger/src/channels/sentry-log.ts"],"sourcesContent":["import { LogChannel } from \"../log-channel\";\r\nimport type { BasicLogConfigurations, LoggingData, LogLevel } from \"../types\";\r\nimport { safeJsonStringify } from \"../utils/safe-json-stringify\";\r\n\r\n// ── @sentry/node as an optional peer ─────────────────────────────────────────\r\n// @sentry/node is referenced ONLY through the local minimal types below — never\r\n// via `typeof import(\"@sentry/node\")`. This package is served as source\r\n// (`main` → `./src/index.ts`), so its `.ts` becomes part of a consumer's TS\r\n// program; a static type reference to the SDK would force module resolution and\r\n// break every consumer who (correctly) never installs the optional peer with a\r\n// TS2307 \"cannot find module\". The runtime import uses an indirect specifier for\r\n// the same reason, and its result is cast to these shapes. The shapes mirror the\r\n// stable `@sentry/node` public API (unchanged across v8–v10).\r\n\r\n/**\r\n * Sentry severity levels — the `@sentry/node` `SeverityLevel` union.\r\n */\r\ntype SentrySeverityLevel =\r\n  | \"fatal\"\r\n  | \"error\"\r\n  | \"warning\"\r\n  | \"log\"\r\n  | \"info\"\r\n  | \"debug\";\r\n\r\n/**\r\n * The Sentry `Scope` surface used while building a single event.\r\n */\r\nexport interface SentryScopeLike {\r\n  setLevel(level: SentrySeverityLevel): void;\r\n  setTags(tags: Record<string, string>): void;\r\n  setContext(name: string, context: Record<string, unknown> | null): void;\r\n}\r\n\r\n/**\r\n * A Sentry breadcrumb, as passed to `addBreadcrumb`.\r\n */\r\nexport type SentryBreadcrumb = {\r\n  category?: string;\r\n  message?: string;\r\n  level?: SentrySeverityLevel;\r\n  data?: Record<string, unknown>;\r\n};\r\n\r\n/**\r\n * The subset of the `@sentry/node` API `SentryLog` calls. The `@sentry/node`\r\n * namespace satisfies this shape, so an app can pass it straight through as\r\n * `client`; a test (or a custom forwarder) can supply a compatible stand-in.\r\n */\r\nexport interface SentryForwarder {\r\n  captureException(exception: unknown): string;\r\n  captureMessage(message: string, level?: SentrySeverityLevel): string;\r\n  addBreadcrumb(breadcrumb: SentryBreadcrumb): void;\r\n  withScope(callback: (scope: SentryScopeLike) => void): void;\r\n  flush(timeout?: number): Promise<boolean>;\r\n}\r\n\r\n/**\r\n * Sentry initialization options. Mirrors the common `@sentry/node` `NodeOptions`\r\n * fields; the index signature keeps any other SDK option valid without coupling\r\n * to the SDK's types.\r\n */\r\nexport type SentryInitOptions = {\r\n  dsn?: string;\r\n  environment?: string;\r\n  release?: string;\r\n  sampleRate?: number;\r\n  [key: string]: unknown;\r\n};\r\n\r\n/**\r\n * The lazily-imported `@sentry/node` namespace surface — the forwarder plus the\r\n * lifecycle calls the channel uses when it owns initialization.\r\n */\r\ninterface SentryNamespace extends SentryForwarder {\r\n  init(options?: SentryInitOptions): unknown;\r\n  getClient(): unknown;\r\n}\r\n\r\nlet Sentry: SentryNamespace | undefined;\r\nlet isModuleExists: boolean | null = null;\r\nlet loadingPromise: Promise<void> | undefined;\r\n\r\nconst SENTRY_INSTALL_INSTRUCTIONS = `\r\nThe Sentry log channel requires the @sentry/node package.\r\nInstall it with:\r\n\r\n  npm install @sentry/node\r\n\r\nOr with your preferred package manager:\r\n\r\n  pnpm add @sentry/node\r\n  yarn add @sentry/node\r\n`.trim();\r\n\r\n/**\r\n * Load @sentry/node once, lazily and concurrency-safely. A bare catch maps any\r\n * import failure to \"not installed\" — the curated install message surfaces at\r\n * `log()` time, never as a boot-time module-resolution crash.\r\n */\r\nfunction loadSentry(): Promise<void> {\r\n  if (isModuleExists !== null) {\r\n    return Promise.resolve();\r\n  }\r\n\r\n  if (loadingPromise) {\r\n    return loadingPromise;\r\n  }\r\n\r\n  loadingPromise = (async () => {\r\n    try {\r\n      // Indirect specifier (typed `string`, not a literal) so TypeScript never\r\n      // statically resolves the optional peer — see the file header.\r\n      const sentryModule: string = \"@sentry/node\";\r\n      Sentry = (await import(sentryModule)) as unknown as SentryNamespace;\r\n      isModuleExists = true;\r\n    } catch {\r\n      isModuleExists = false;\r\n    }\r\n  })();\r\n\r\n  return loadingPromise;\r\n}\r\n\r\nexport type SentryLogConfig = BasicLogConfigurations & {\r\n  /**\r\n   * Reuse an already-initialized Sentry instance — typically the `@sentry/node`\r\n   * namespace from an app that already calls `Sentry.init(...)`. When set, the\r\n   * channel forwards through it and never imports or re-initializes the SDK.\r\n   */\r\n  client?: SentryForwarder;\r\n  /**\r\n   * Initialize Sentry from these options instead of reusing a host client. The\r\n   * channel lazily imports `@sentry/node` and calls `Sentry.init(options)` once,\r\n   * guarded so it never clobbers an existing client.\r\n   */\r\n  options?: SentryInitOptions;\r\n  /**\r\n   * Levels delivered as Sentry *events* (these consume the error quota). Every\r\n   * other level is recorded as a breadcrumb that rides along with the next\r\n   * event, costing no quota.\r\n   *\r\n   * @default [\"fatal\", \"error\", \"warn\"]\r\n   */\r\n  eventLevels?: LogLevel[];\r\n  /**\r\n   * Milliseconds `flush()` waits for the transport to drain on shutdown.\r\n   *\r\n   * @default 2000\r\n   */\r\n  flushTimeout?: number;\r\n};\r\n\r\n/**\r\n * Forwards log entries to Sentry.\r\n *\r\n * Entries at an `eventLevels` level (`error` / `warn` by default) become Sentry\r\n * **events**: an `Error` message via `captureException` (preserving the real\r\n * stack), any other message via `captureMessage`. Every other level becomes a\r\n * **breadcrumb** — buffered and attached to the next event, consuming no error\r\n * quota. `module` / `action` are attached as searchable tags and the entry's\r\n * `context` as a structured Sentry context.\r\n *\r\n * The SDK is an optional peer: pass an existing `client` (reused as-is) or\r\n * `options` (the channel lazily imports `@sentry/node` and initializes it). On\r\n * graceful shutdown, `await log.flush()` drains pending events via\r\n * `Sentry.flush(timeout)`.\r\n *\r\n * @example\r\n * // Existing app — reuse the initialized Sentry client\r\n * import * as Sentry from \"@sentry/node\";\r\n * log.addChannel(new SentryLog({ client: Sentry }));\r\n *\r\n * @example\r\n * // New app — let the channel initialize Sentry\r\n * log.addChannel(new SentryLog({ options: { dsn: process.env.SENTRY_DSN } }));\r\n */\r\nexport class SentryLog extends LogChannel<SentryLogConfig> {\r\n  /**\r\n   * {@inheritdoc}\r\n   */\r\n  public name = \"sentry\";\r\n\r\n  /**\r\n   * {@inheritdoc}\r\n   */\r\n  public description =\r\n    \"Forwards entries to Sentry as events (error/warn) or breadcrumbs (everything else)\";\r\n\r\n  /**\r\n   * {@inheritdoc}\r\n   */\r\n  protected defaultConfigurations: SentryLogConfig = {\r\n    eventLevels: [\"fatal\", \"error\", \"warn\"],\r\n    flushTimeout: 2_000,\r\n  };\r\n\r\n  /**\r\n   * The resolved forwarder — the injected `client` or the lazily-imported\r\n   * `@sentry/node` namespace. `undefined` until `init()` resolves, and when the\r\n   * SDK is absent (then `log()` surfaces the install message once).\r\n   */\r\n  private sentry?: SentryForwarder;\r\n\r\n  /**\r\n   * Guards the one-time \"@sentry/node is not installed\" notice so a missing SDK\r\n   * doesn't spam stderr on every entry.\r\n   */\r\n  private warnedMissing = false;\r\n\r\n  /**\r\n   * Resolve an injected `client` (the Sentry namespace) **synchronously**, so an\r\n   * entry logged on the same tick as construction — e.g. at app boot, before\r\n   * the base schedules `init()` on the next tick via `setTimeout(0)` — is not\r\n   * silently dropped. The `options` (lazy-import) path is inherently async and\r\n   * still resolves in `init()`.\r\n   */\r\n  public constructor(configurations?: SentryLogConfig) {\r\n    super(configurations);\r\n\r\n    const injected = this.config(\"client\");\r\n\r\n    if (injected) {\r\n      this.sentry = injected;\r\n    }\r\n  }\r\n\r\n  /**\r\n   * Resolve the forwarder: reuse the injected client, otherwise lazily import\r\n   * `@sentry/node` and (only when explicit `options` are supplied and no client\r\n   * exists yet) initialize it. Never throws — the base runs `init()` inside an\r\n   * un-awaited `setTimeout`, so a throw would become an unhandled rejection and\r\n   * `isInitialized` would never flip; a missing SDK is reported from `log()`.\r\n   */\r\n  protected async init(): Promise<void> {\r\n    const injected = this.config(\"client\");\r\n\r\n    if (injected) {\r\n      this.sentry = injected;\r\n\r\n      return;\r\n    }\r\n\r\n    await loadSentry();\r\n\r\n    if (!Sentry) {\r\n      return;\r\n    }\r\n\r\n    const options = this.config(\"options\");\r\n\r\n    if (options && !Sentry.getClient()) {\r\n      Sentry.init(options);\r\n    }\r\n\r\n    this.sentry = Sentry;\r\n  }\r\n\r\n  /**\r\n   * {@inheritdoc}\r\n   */\r\n  public async log(data: LoggingData): Promise<void> {\r\n    if (!this.shouldBeLogged(data)) {\r\n      return;\r\n    }\r\n\r\n    if (isModuleExists === null) {\r\n      // wait until module is fully loaded\r\n      await loadSentry();\r\n    }\r\n\r\n    if (!this.sentry) {\r\n      this.reportMissingSdk();\r\n\r\n      return;\r\n    }\r\n\r\n    const { module, action, message, type: level, context } = data;\r\n\r\n    if (this.isEventLevel(level)) {\r\n      this.captureEvent(this.sentry, {\r\n        module,\r\n        action,\r\n        message,\r\n        level,\r\n        context,\r\n      });\r\n\r\n      return;\r\n    }\r\n\r\n    this.sentry.addBreadcrumb({\r\n      category: module,\r\n      message: this.toText(message),\r\n      level: this.toSentryLevel(level),\r\n      data: context,\r\n    });\r\n  }\r\n\r\n  /**\r\n   * Drain pending Sentry events. Bounded by `flushTimeout` so an unreachable\r\n   * Sentry can never hang a graceful shutdown. No-op when the SDK is absent.\r\n   */\r\n  public async flush(): Promise<void> {\r\n    if (!this.sentry) {\r\n      return;\r\n    }\r\n\r\n    await this.sentry.flush(this.config(\"flushTimeout\"));\r\n  }\r\n\r\n  /**\r\n   * Whether the level should be sent as a Sentry event (vs a breadcrumb).\r\n   */\r\n  private isEventLevel(level: LogLevel): boolean {\r\n    return Boolean(this.config(\"eventLevels\")?.includes(level));\r\n  }\r\n\r\n  /**\r\n   * Send an entry as a Sentry event. An `Error` message goes through\r\n   * `captureException` so Sentry parses the real stack and groups properly;\r\n   * any other message goes through `captureMessage`. `module` / `action` are\r\n   * attached as tags and `context` as a structured context, scoped to this\r\n   * event only via `withScope`.\r\n   */\r\n  private captureEvent(\r\n    sentry: SentryForwarder,\r\n    entry: {\r\n      module: string;\r\n      action: string;\r\n      message: unknown;\r\n      level: LogLevel;\r\n      context?: Record<string, any>;\r\n    },\r\n  ): void {\r\n    const { module, action, message, level, context } = entry;\r\n    const sentryLevel = this.toSentryLevel(level);\r\n\r\n    sentry.withScope((scope) => {\r\n      scope.setLevel(sentryLevel);\r\n      scope.setTags({ module, action });\r\n\r\n      if (context) {\r\n        scope.setContext(\"context\", context);\r\n      }\r\n\r\n      if (message instanceof Error) {\r\n        sentry.captureException(message);\r\n      } else {\r\n        sentry.captureMessage(this.toText(message), sentryLevel);\r\n      }\r\n    });\r\n  }\r\n\r\n  /**\r\n   * Map a logger level to a Sentry severity. `success` has no Sentry\r\n   * equivalent, so it is reported as informational.\r\n   */\r\n  private toSentryLevel(level: LogLevel): SentrySeverityLevel {\r\n    switch (level) {\r\n      case \"warn\":\r\n        return \"warning\";\r\n      case \"success\":\r\n        return \"info\";\r\n      default:\r\n        return level; // debug | info | error | fatal map 1:1\r\n    }\r\n  }\r\n\r\n  /**\r\n   * Coerce a message into the string Sentry's APIs expect — an `Error`'s\r\n   * `.message`, a string as-is, anything else safely JSON-serialized.\r\n   */\r\n  private toText(message: unknown): string {\r\n    if (typeof message === \"string\") {\r\n      return message;\r\n    }\r\n\r\n    if (message instanceof Error) {\r\n      return message.message;\r\n    }\r\n\r\n    return safeJsonStringify(message);\r\n  }\r\n\r\n  /**\r\n   * Surface the install instructions exactly once when the SDK is absent. The\r\n   * logger can't log through itself here, so this writes to stderr — matching\r\n   * how the file channels report write failures.\r\n   */\r\n  private reportMissingSdk(): void {\r\n    if (isModuleExists === false && !this.warnedMissing) {\r\n      this.warnedMissing = true;\r\n      console.error(SENTRY_INSTALL_INSTRUCTIONS);\r\n    }\r\n  }\r\n}\r\n"],"mappings":";;;;AA+EA,IAAI;AACJ,IAAI,iBAAiC;AACrC,IAAI;AAEJ,MAAM,8BAA8B;;;;;;;;;;EAUlC,KAAK;;;;;;AAOP,SAAS,aAA4B;CACnC,IAAI,mBAAmB,MACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GAIF,SAAU,MAAM,OAAO;GACvB,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,IAAa,YAAb,cAA+B,WAA4B;;;;;;;;CAwCzD,AAAO,YAAY,gBAAkC;EACnD,MAAM,cAAc;cArCR;qBAMZ;+BAKiD;GACjD,aAAa;IAAC;IAAS;IAAS;GAAM;GACtC,cAAc;EAChB;uBAawB;EAYtB,MAAM,WAAW,KAAK,OAAO,QAAQ;EAErC,IAAI,UACF,KAAK,SAAS;CAElB;;;;;;;;CASA,MAAgB,OAAsB;EACpC,MAAM,WAAW,KAAK,OAAO,QAAQ;EAErC,IAAI,UAAU;GACZ,KAAK,SAAS;GAEd;EACF;EAEA,MAAM,WAAW;EAEjB,IAAI,CAAC,QACH;EAGF,MAAM,UAAU,KAAK,OAAO,SAAS;EAErC,IAAI,WAAW,CAAC,OAAO,UAAU,GAC/B,OAAO,KAAK,OAAO;EAGrB,KAAK,SAAS;CAChB;;;;CAKA,MAAa,IAAI,MAAkC;EACjD,IAAI,CAAC,KAAK,eAAe,IAAI,GAC3B;EAGF,IAAI,mBAAmB,MAErB,MAAM,WAAW;EAGnB,IAAI,CAAC,KAAK,QAAQ;GAChB,KAAK,iBAAiB;GAEtB;EACF;EAEA,MAAM,EAAE,QAAQ,QAAQ,SAAS,MAAM,OAAO,YAAY;EAE1D,IAAI,KAAK,aAAa,KAAK,GAAG;GAC5B,KAAK,aAAa,KAAK,QAAQ;IAC7B;IACA;IACA;IACA;IACA;GACF,CAAC;GAED;EACF;EAEA,KAAK,OAAO,cAAc;GACxB,UAAU;GACV,SAAS,KAAK,OAAO,OAAO;GAC5B,OAAO,KAAK,cAAc,KAAK;GAC/B,MAAM;EACR,CAAC;CACH;;;;;CAMA,MAAa,QAAuB;EAClC,IAAI,CAAC,KAAK,QACR;EAGF,MAAM,KAAK,OAAO,MAAM,KAAK,OAAO,cAAc,CAAC;CACrD;;;;CAKA,AAAQ,aAAa,OAA0B;EAC7C,OAAO,QAAQ,KAAK,OAAO,aAAa,CAAC,EAAE,SAAS,KAAK,CAAC;CAC5D;;;;;;;;CASA,AAAQ,aACN,QACA,OAOM;EACN,MAAM,EAAE,QAAQ,QAAQ,SAAS,OAAO,YAAY;EACpD,MAAM,cAAc,KAAK,cAAc,KAAK;EAE5C,OAAO,WAAW,UAAU;GAC1B,MAAM,SAAS,WAAW;GAC1B,MAAM,QAAQ;IAAE;IAAQ;GAAO,CAAC;GAEhC,IAAI,SACF,MAAM,WAAW,WAAW,OAAO;GAGrC,IAAI,mBAAmB,OACrB,OAAO,iBAAiB,OAAO;QAE/B,OAAO,eAAe,KAAK,OAAO,OAAO,GAAG,WAAW;EAE3D,CAAC;CACH;;;;;CAMA,AAAQ,cAAc,OAAsC;EAC1D,QAAQ,OAAR;GACE,KAAK,QACH,OAAO;GACT,KAAK,WACH,OAAO;GACT,SACE,OAAO;EACX;CACF;;;;;CAMA,AAAQ,OAAO,SAA0B;EACvC,IAAI,OAAO,YAAY,UACrB,OAAO;EAGT,IAAI,mBAAmB,OACrB,OAAO,QAAQ;EAGjB,OAAO,kBAAkB,OAAO;CAClC;;;;;;CAOA,AAAQ,mBAAyB;EAC/B,IAAI,mBAAmB,SAAS,CAAC,KAAK,eAAe;GACnD,KAAK,gBAAgB;GACrB,QAAQ,MAAM,2BAA2B;EAC3C;CACF;AACF"}