Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/regexp-redact-patterns-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'evlog': patch
---

Fix custom RegExp `redact.patterns` declared in `nuxt.config.ts` (or Nitro module options) being silently dropped: patterns are now serialized as `{ source, flags }` across the config bridges and rebuilt into RegExp on the server. A pattern object without a `source` field is reported instead of skipped, and a function-valued `redact.replacement` / `redact.transform` in the Nuxt module now prints the same warning the standalone Nitro modules already print.
2 changes: 1 addition & 1 deletion apps/docs/app/components/features/FeatureSampling.vue
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ function getLevelColor(level: string): string {
<div class="size-3 rounded-full bg-accented" />
<div class="size-3 rounded-full bg-accented" />
</div>
<span class="ml-3 font-mono text-xs text-dimmed">evlog.config.ts</span>
<span class="ml-3 font-mono text-xs text-dimmed">logger.ts</span>
</div>
<div class="p-5 font-mono text-xs sm:text-sm leading-relaxed overflow-x-auto">
<pre><code><span class="text-amber-400">initLogger</span>({
Expand Down
10 changes: 5 additions & 5 deletions packages/evlog/src/nitro-v3/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'
import type { Nitro } from 'nitro/types'
import type { NitroModuleOptions } from '../nitro'
import { prependNitroErrorHandler } from '../nitro'
import { FUNCTION_REDACT_POLICY_WARNING, hasFunctionRedactPolicy } from '../redact'
import { prepareRedactForBridge } from '../redact'

export type { NitroModuleOptions }

Expand Down Expand Up @@ -39,10 +39,10 @@ export default function evlog(options?: NitroModuleOptions) {
)
nitro.options.errorHandler = Array.isArray(handlers) ? handlers : [handlers]

// JSON.stringify below drops function-valued redact policy silently.
if (hasFunctionRedactPolicy(options?.redact)) {
console.warn(FUNCTION_REDACT_POLICY_WARNING)
}
// runtimeConfig and the JSON bridges below serialize the config:
// RegExps would collapse to {} and function-valued redact policy
// would vanish silently.
prepareRedactForBridge(options?.redact)

// Inject config into runtimeConfig — works in production where the
// plugin is bundled through Nitro's builder and the virtual
Expand Down
10 changes: 5 additions & 5 deletions packages/evlog/src/nitro/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'
import type { Nitro } from 'nitropack'
import type { NitroModuleOptions } from '../nitro'
import { prependNitroErrorHandler } from '../nitro'
import { FUNCTION_REDACT_POLICY_WARNING, hasFunctionRedactPolicy } from '../redact'
import { prepareRedactForBridge } from '../redact'

export type { NitroModuleOptions }

Expand Down Expand Up @@ -35,10 +35,10 @@ export default function evlog(options?: NitroModuleOptions) {

nitro.options.noExternals = true

// JSON.stringify below drops function-valued redact policy silently.
if (hasFunctionRedactPolicy(options?.redact)) {
console.warn(FUNCTION_REDACT_POLICY_WARNING)
}
// runtimeConfig and the JSON bridges below serialize the config:
// RegExps would collapse to {} and function-valued redact policy
// would vanish silently.
prepareRedactForBridge(options?.redact)

// Inject config into runtimeConfig — works in production where the
// plugin is bundled through Nitro's builder and the virtual
Expand Down
9 changes: 8 additions & 1 deletion packages/evlog/src/nuxt/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { NitroConfig } from 'nitropack'
import type { EnvironmentContext, LogLevel, RedactConfig, RouteConfig, SamplingConfig, TransportConfig } from '../types'
import type { DevTerminalInput } from '../shared/dev-terminal'
import { prependNitroErrorHandler } from '../nitro'
import { prepareRedactForBridge } from '../redact'
import { createStripPlugin } from '../vite/strip'
import { createSourceLocationPlugin } from '../vite/source-location'
import { name, version } from '../../package.json'
Expand Down Expand Up @@ -314,7 +315,7 @@ export interface ModuleOptions {
/**
* How long to retain events before cleanup (used by @evlog/nuxthub).
* Supports "30d" (days), "24h" (hours), "60m" (minutes).
* @default '30d'
* @default '7d'
*/
retention?: string
}
Expand Down Expand Up @@ -357,6 +358,12 @@ export default defineNuxtModule<ModuleOptions>({
const streamEnabled = normalizedStream !== false
options.stream = normalizedStream

// runtimeConfig and the JSON bridges in the nitro:config hook below
// serialize the config: RegExps in redact.patterns would collapse to {}
// and be dropped server-side, and function-valued redact policy would
// vanish silently.
prepareRedactForBridge(options.redact)

nuxt.options.runtimeConfig.evlog = options

// Register custom error handler for proper EvlogError serialization
Expand Down
34 changes: 29 additions & 5 deletions packages/evlog/src/redact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,27 @@ export const FUNCTION_REDACT_POLICY_WARNING
= '[evlog] redact.replacement / redact.transform is a function and cannot be serialized into the build config. '
+ 'Declare it at runtime instead — initLogger({ redact: { ... } }) from a server plugin — or use a string replacement.'

/**
* Prepare a `redact` config for the config bridges (`__EVLOG_CONFIG__`,
* `process.env.__EVLOG_CONFIG`, a framework runtimeConfig). RegExp patterns
* are rewritten in place to `{ source, flags }` — the shape
* `deserializeRegexList` reads back — because JSON.stringify reduces a
* RegExp to `{}` and the pattern is silently dropped. A function-valued
* policy cannot survive any bridge; warn so the missing redaction is loud.
* Modules that serialize user config call this before `JSON.stringify`.
*/
export function prepareRedactForBridge(redact: unknown): void {
if (hasFunctionRedactPolicy(redact)) {
console.warn(FUNCTION_REDACT_POLICY_WARNING)
}
if (!redact || typeof redact !== 'object') return
const { patterns } = redact as { patterns?: unknown }
if (!Array.isArray(patterns)) return
;(redact as { patterns: unknown[] }).patterns = patterns.map(p =>
p instanceof RegExp ? { source: p.source, flags: p.flags } : p,
)
}

/**
* Normalize a redact config that may have been deserialized from JSON
* (e.g. via `process.env.__EVLOG_CONFIG`). Converts pattern strings
Expand Down Expand Up @@ -595,12 +616,15 @@ function deserializeRegexList(raw: unknown[]): RegExp[] {
patterns.push(new RegExp(p, 'g'))
continue
}
if (typeof p === 'object' && p !== null && typeof (p as { source?: unknown }).source === 'string') {
const flags = typeof (p as { flags?: unknown }).flags === 'string'
? (p as { flags: string }).flags
: 'g'
patterns.push(new RegExp((p as { source: string }).source, flags))
if (typeof p !== 'object' || p === null) continue
if (typeof (p as { source?: unknown }).source !== 'string') {
console.warn('[normalizeRedactConfig] Ignoring redact pattern object without a `source` field')
continue
}
const flags = typeof (p as { flags?: unknown }).flags === 'string'
? (p as { flags: string }).flags
: 'g'
patterns.push(new RegExp((p as { source: string }).source, flags))
} catch {
console.warn('[normalizeRedactConfig] Ignoring invalid redact regex entry')
}
Expand Down
2 changes: 1 addition & 1 deletion packages/evlog/src/shared/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { pickBaseEvlogOptions } from './middleware'

/**
* Single-config shape accepted everywhere evlog is bootstrapped: at
* `initLogger`, in framework middleware, and in the Nuxt module. Authored
* `initLogger` and in framework middleware. Authored
* with {@link defineEvlog} and split via {@link toLoggerConfig} /
* {@link toMiddlewareOptions}.
*/
Expand Down
42 changes: 41 additions & 1 deletion packages/evlog/test/core/redact.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { redactEvent, normalizeRedactConfig, resolveRedactConfig, builtinPatterns, hasFunctionRedactPolicy, compileRedactPathMatchers, redactPathsInTree } from '../../src/redact'
import { redactEvent, normalizeRedactConfig, resolveRedactConfig, builtinPatterns, hasFunctionRedactPolicy, FUNCTION_REDACT_POLICY_WARNING, prepareRedactForBridge, compileRedactPathMatchers, redactPathsInTree } from '../../src/redact'
import type { RedactConfig } from '../../src/types'
import { createLogger, initLogger } from '../../src/logger'
import { defined } from '../helpers/defined'
Expand Down Expand Up @@ -609,6 +609,17 @@ describe('normalizeRedactConfig', () => {
warn.mockRestore()
})

it('warns on pattern objects without a source field instead of dropping them silently', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const config = normalizeRedactConfig({
builtins: false,
patterns: [{ source: '\\d+' }, {}],
})
expect(config?.patterns).toHaveLength(1)
expect(warn).toHaveBeenCalledWith('[normalizeRedactConfig] Ignoring redact pattern object without a `source` field')
warn.mockRestore()
})

it('handles builtins field from deserialized JSON', () => {
const config = normalizeRedactConfig({
builtins: ['email', 'creditCard'],
Expand Down Expand Up @@ -845,4 +856,33 @@ describe('function-valued redact policy across the config bridge', () => {
expect(hasFunctionRedactPolicy(serialized)).toBe(false)
expect(normalizeRedactConfig(serialized)?.replacement).toBeUndefined()
})

it('warns when a function-valued policy crosses the bridge', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
prepareRedactForBridge({ replacement: () => '[x]' })
expect(warn).toHaveBeenCalledWith(FUNCTION_REDACT_POLICY_WARNING)
warn.mockRestore()
})
})

describe('RegExp patterns across the config bridge', () => {
it('rewrites RegExp patterns to source/flags so a JSON round-trip still redacts', () => {
const redact = { builtins: false, patterns: [/sk_live_\w+/g] }
prepareRedactForBridge(redact)
const serialized = JSON.parse(JSON.stringify(redact))

const resolved = defined(normalizeRedactConfig(serialized), 'redact config')
expect(resolved.patterns).toHaveLength(1)
expect(defined(resolved.patterns?.[0], 'patterns[0]').flags).toBe('g')

const event = redactEvent({ key: 'sk_live_abc123' }, resolved)
expect(event.key).toBe('[REDACTED]')
})

it('keeps patterns intact when the config is passed live, not through the bridge', () => {
const redact = { builtins: false, patterns: [/sk_live_\w+/g] }
prepareRedactForBridge(redact)
const resolved = defined(normalizeRedactConfig(redact), 'redact config')
expect(defined(resolved.patterns?.[0], 'patterns[0]')).toBeInstanceOf(RegExp)
})
})
2 changes: 2 additions & 0 deletions packages/signals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export default defineEvlog({
})
```

`defineEvlog` only authors the config. Nothing registers until the object reaches evlog: pass it to `initLogger` or the framework integration you already use.

Every event that passes a signal's `when` gets judged. All due signals for one event go into a single model call.

## What lands on the event
Expand Down
Loading