diff --git a/.agents/skills/create-adapter/SKILL.md b/.agents/skills/create-adapter/SKILL.md index 0cfdf11..847d9b7 100644 --- a/.agents/skills/create-adapter/SKILL.md +++ b/.agents/skills/create-adapter/SKILL.md @@ -51,7 +51,7 @@ Read [references/adapter-template.md](references/adapter-template.md) for the fu Key architecture rules: 1. **Config interface** -- service-specific fields (API key, endpoint, etc.) plus optional `timeout?: number` -2. **`getRuntimeConfig()` helper** -- dynamic `require('nitropack/runtime')` wrapped in try/catch +2. **`getRuntimeConfig()`** -- import from `./_utils` (shared helper, do NOT redefine locally) 3. **Config priority** (highest to lowest): - Overrides passed to `create{Name}Drain()` - `runtimeConfig.evlog.{name}` diff --git a/.agents/skills/create-adapter/references/adapter-template.md b/.agents/skills/create-adapter/references/adapter-template.md index 4e9f796..eb6e1ec 100644 --- a/.agents/skills/create-adapter/references/adapter-template.md +++ b/.agents/skills/create-adapter/references/adapter-template.md @@ -6,6 +6,7 @@ Replace `{Name}`, `{name}`, and `{NAME}` with the actual service name. ```typescript import type { DrainContext, WideEvent } from '../types' +import { getRuntimeConfig } from './_utils' // --- 1. Config Interface --- // Define all service-specific configuration fields. @@ -45,23 +46,7 @@ export function to{Name}Event(event: WideEvent): {Name}Event { } } -// --- 3. Runtime Config Helper --- -// Dynamic require to avoid bundling issues outside Nitro. -// Returns undefined when not in a Nitro context. -function getRuntimeConfig(): { - evlog?: { {name}?: Partial<{Name}Config> } - {name}?: Partial<{Name}Config> -} | undefined { - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { useRuntimeConfig } = require('nitropack/runtime') - return useRuntimeConfig() - } catch { - return undefined - } -} - -// --- 4. Factory Function --- +// --- 3. Factory Function --- // Returns a drain function that resolves config at call time. // Config priority: overrides > runtimeConfig.evlog.{name} > runtimeConfig.{name} > env vars diff --git a/bun.lock b/bun.lock index 409efd2..5cdd598 100644 --- a/bun.lock +++ b/bun.lock @@ -50,9 +50,6 @@ "packages/evlog": { "name": "evlog", "version": "1.6.0", - "dependencies": { - "defu": "^6.1.4", - }, "devDependencies": { "@nuxt/devtools": "^3.1.1", "@nuxt/schema": "^4.3.1", diff --git a/packages/evlog/package.json b/packages/evlog/package.json index 4c8caf6..c38608a 100644 --- a/packages/evlog/package.json +++ b/packages/evlog/package.json @@ -101,9 +101,6 @@ "test:coverage": "vitest run --coverage", "typecheck": "echo 'Typecheck handled by build'" }, - "dependencies": { - "defu": "^6.1.4" - }, "devDependencies": { "@nuxt/devtools": "^3.1.1", "@nuxt/schema": "^4.3.1", diff --git a/packages/evlog/src/adapters/_utils.ts b/packages/evlog/src/adapters/_utils.ts new file mode 100644 index 0000000..3812227 --- /dev/null +++ b/packages/evlog/src/adapters/_utils.ts @@ -0,0 +1,13 @@ +/** + * Try to get runtime config from Nitro/Nuxt environment. + * Returns undefined if not in a Nitro context. + */ +export function getRuntimeConfig(): Record | undefined { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { useRuntimeConfig } = require('nitropack/runtime') + return useRuntimeConfig() + } catch { + return undefined + } +} diff --git a/packages/evlog/src/adapters/axiom.ts b/packages/evlog/src/adapters/axiom.ts index b8bae94..495e041 100644 --- a/packages/evlog/src/adapters/axiom.ts +++ b/packages/evlog/src/adapters/axiom.ts @@ -1,4 +1,5 @@ import type { DrainContext, WideEvent } from '../types' +import { getRuntimeConfig } from './_utils' export interface AxiomConfig { /** Axiom dataset name */ @@ -13,21 +14,6 @@ export interface AxiomConfig { timeout?: number } -/** - * Try to get runtime config from Nitro/Nuxt environment. - * Returns undefined if not in a Nitro context. - */ -function getRuntimeConfig(): { evlog?: { axiom?: Partial }, axiom?: Partial } | undefined { - try { - // Dynamic import to avoid bundling issues when not in Nitro - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { useRuntimeConfig } = require('nitropack/runtime') - return useRuntimeConfig() - } catch { - return undefined - } -} - /** * Create a drain function for sending logs to Axiom. * diff --git a/packages/evlog/src/adapters/otlp.ts b/packages/evlog/src/adapters/otlp.ts index 49c44a4..b9849b2 100644 --- a/packages/evlog/src/adapters/otlp.ts +++ b/packages/evlog/src/adapters/otlp.ts @@ -1,4 +1,5 @@ import type { DrainContext, LogLevel, WideEvent } from '../types' +import { getRuntimeConfig } from './_utils' export interface OTLPConfig { /** OTLP HTTP endpoint (e.g., http://localhost:4318) */ @@ -70,21 +71,6 @@ const SEVERITY_TEXT_MAP: Record = { error: 'ERROR', } -/** - * Try to get runtime config from Nitro/Nuxt environment. - * Returns undefined if not in a Nitro context. - */ -function getRuntimeConfig(): { evlog?: { otlp?: Partial }, otlp?: Partial } | undefined { - try { - // Dynamic import to avoid bundling issues when not in Nitro - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { useRuntimeConfig } = require('nitropack/runtime') - return useRuntimeConfig() - } catch { - return undefined - } -} - /** * Convert a value to OTLP attribute value format. */ diff --git a/packages/evlog/src/adapters/posthog.ts b/packages/evlog/src/adapters/posthog.ts index c498b8a..2966232 100644 --- a/packages/evlog/src/adapters/posthog.ts +++ b/packages/evlog/src/adapters/posthog.ts @@ -1,4 +1,5 @@ import type { DrainContext, WideEvent } from '../types' +import { getRuntimeConfig } from './_utils' export interface PostHogConfig { /** PostHog project API key */ @@ -21,21 +22,6 @@ export interface PostHogEvent { properties: Record } -/** - * Try to get runtime config from Nitro/Nuxt environment. - * Returns undefined if not in a Nitro context. - */ -function getRuntimeConfig(): { evlog?: { posthog?: Partial }, posthog?: Partial } | undefined { - try { - // Dynamic import to avoid bundling issues when not in Nitro - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { useRuntimeConfig } = require('nitropack/runtime') - return useRuntimeConfig() - } catch { - return undefined - } -} - /** * Convert a WideEvent to a PostHog event format. */ diff --git a/packages/evlog/src/adapters/sentry.ts b/packages/evlog/src/adapters/sentry.ts index 8c6e44d..45f765b 100644 --- a/packages/evlog/src/adapters/sentry.ts +++ b/packages/evlog/src/adapters/sentry.ts @@ -1,4 +1,5 @@ import type { DrainContext, LogLevel, WideEvent } from '../types' +import { getRuntimeConfig } from './_utils' export interface SentryConfig { /** Sentry DSN */ @@ -45,21 +46,6 @@ const SEVERITY_MAP: Record = { error: 17, } -/** - * Try to get runtime config from Nitro/Nuxt environment. - * Returns undefined if not in a Nitro context. - */ -function getRuntimeConfig(): { evlog?: { sentry?: Partial }, sentry?: Partial } | undefined { - try { - // Dynamic import to avoid bundling issues when not in Nitro - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { useRuntimeConfig } = require('nitropack/runtime') - return useRuntimeConfig() - } catch { - return undefined - } -} - function parseSentryDsn(dsn: string): SentryDsnParts { const url = new URL(dsn) const publicKey = url.username diff --git a/packages/evlog/src/logger.ts b/packages/evlog/src/logger.ts index 4e997d2..e8988c2 100644 --- a/packages/evlog/src/logger.ts +++ b/packages/evlog/src/logger.ts @@ -1,7 +1,24 @@ -import { defu } from 'defu' import type { EnvironmentContext, Log, LogLevel, LoggerConfig, RequestLogger, RequestLoggerOptions, SamplingConfig, TailSamplingContext, WideEvent } from './types' import { colors, detectEnvironment, formatDuration, getConsoleMethod, getLevelColor, isDev, matchesPattern } from './utils' +function isPlainObject(val: unknown): val is Record { + return val !== null && typeof val === 'object' && !Array.isArray(val) +} + +function deepDefaults(base: Record, defaults: Record): Record { + const result = { ...base } + for (const key in defaults) { + const baseVal = result[key] + const defaultVal = defaults[key] + if (baseVal === undefined || baseVal === null) { + result[key] = defaultVal + } else if (isPlainObject(baseVal) && isPlainObject(defaultVal)) { + result[key] = deepDefaults(baseVal, defaultVal) + } + } + return result +} + let globalEnv: EnvironmentContext = { service: 'app', environment: 'development', @@ -221,7 +238,7 @@ export function createRequestLogger(options: RequestLoggerOptions = {}): Request return { set>(data: T): void { - context = defu(data, context) as Record + context = deepDefaults(data, context) as Record }, error(error: Error | string, errorContext?: Record): void { @@ -236,7 +253,7 @@ export function createRequestLogger(options: RequestLoggerOptions = {}): Request stack: err.stack, }, } - context = defu(errorData, context) as Record + context = deepDefaults(errorData, context) as Record }, emit(overrides?: Record & { _forceKeep?: boolean }): WideEvent | null {