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/brown-glasses-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Handle BigInt values safely and consistently across JSON diagnostics and logger formats.
9 changes: 6 additions & 3 deletions packages/effect/src/Formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,9 @@ function safeToString(input: any): string {
* Uses `JSON.stringify` internally with a replacer that tracks the current
* object ancestry. Circular references are replaced with `undefined`, which
* omits them from object output. `Redactable` values are automatically redacted
* before serialization. Values not supported by JSON otherwise follow standard
* `JSON.stringify` behavior. The `space` parameter controls indentation and
* defaults to `0`.
* before serialization. `BigInt` values are stringified with an `n` suffix.
* Values not supported by JSON otherwise follow standard `JSON.stringify`
* behavior. The `space` parameter controls indentation and defaults to `0`.
*
* **Gotchas**
*
Expand Down Expand Up @@ -300,6 +300,9 @@ export function formatJson(input: unknown, options?: {
input,
function(this: unknown, _key: string, value: unknown) {
const redacted = redact(value)
if (typeof redacted === "bigint") {
return format(redacted)
}
if (typeof redacted !== "object" || redacted === null) {
return redacted
}
Expand Down
2 changes: 1 addition & 1 deletion packages/effect/src/Inspectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export const toStringUnknown = (u: unknown, whitespace: number | string | undefi
return u
}
try {
return typeof u === "object" ? formatJson(u, { space: whitespace }) : String(u)
return typeof u === "object" ? formatJson(u, { space: whitespace }) : format(u, { space: whitespace })
} catch {
return String(u)
}
Expand Down
1 change: 0 additions & 1 deletion packages/effect/src/internal/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6302,7 +6302,6 @@ export const formatLogSpan = (self: [label: string, timestamp: number], now: num
/** @internal */
export const structuredMessage = (u: unknown): unknown => {
switch (typeof u) {
case "bigint":
case "function":
case "symbol": {
return String(u)
Expand Down
19 changes: 19 additions & 0 deletions packages/effect/test/Formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it } from "@effect/vitest"
import {
Context,
Effect,
Inspectable,
Option,
Redactable,
Redacted,
Expand Down Expand Up @@ -256,12 +257,30 @@ describe("Formatter", () => {
strictEqual(formatJson({ left: shared, right: shared }), `{"left":{"a":1},"right":{"a":1}}`)
})

it("should stringify BigInt values", () => {
strictEqual(formatJson(123n), `"123n"`)
strictEqual(formatJson({ value: 123n }), `{"value":"123n"}`)
strictEqual(formatJson([1n, 2n]), `["1n","2n"]`)
})

it("should redact sensitive data", () => {
strictEqual(formatJson(data), `{"secret":"[REDACTED]"}`)
strictEqual(formatJson({ a: data }), `{"a":{"secret":"[REDACTED]"}}`)
})
})

describe("Inspectable.toStringUnknown", () => {
it("should stringify BigInt values", () => {
strictEqual(Inspectable.toStringUnknown(123n), `123n`)
strictEqual(
Inspectable.toStringUnknown({ value: 123n }),
`{
"value": "123n"
}`
)
})
})

describe("SchemaIssue reportInput", () => {
const formatIssue = SchemaIssue.makeFormatterDefault()

Expand Down
21 changes: 21 additions & 0 deletions packages/effect/test/Logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,27 @@ describe("Logger", () => {
assert.ok(!output.includes("annotation=\"\\\"value with spaces\\\"\""))
}))

it.effect("formats BigInt messages consistently", () =>
Effect.gen(function*() {
const simple: Array<string> = []
const logFmt: Array<string> = []
const structured: Array<unknown> = []
const json: Array<unknown> = []
const loggers = [
Logger.formatSimple.pipe(Logger.map((output) => void simple.push(output))),
Logger.formatLogFmt.pipe(Logger.map((output) => void logFmt.push(output))),
Logger.formatStructured.pipe(Logger.map((output) => void structured.push(output.message))),
Logger.formatJson.pipe(Logger.map((output) => void json.push(JSON.parse(output).message)))
]

yield* Effect.logInfo(123n, { value: 123n }).pipe(Effect.provide(Logger.layer(loggers)))

assert.include(simple[0], `message=123n message="{\\"value\\":123n}"`)
assert.include(logFmt[0], `message=123n message="{\\"value\\":123n}"`)
assert.deepStrictEqual(structured, [[123n, { value: 123n }]])
assert.deepStrictEqual(json, [["123n", { value: "123n" }]])
}))

it.effect("annotateLogsScoped applies annotations only while scoped", () =>
Effect.gen(function*() {
const annotations: Array<Record<string, unknown>> = []
Expand Down