Skip to content

Commit fc41dcf

Browse files
jfet97claude
andauthored
Include trace and span IDs in error toasts for faster debugging (#624)
* feat: add trace and span IDs to toast messages * test: enhance toast message assertions to include trace and span IDs * feat: add showSpanInfo option to ToastOptions and update span info handling * feat: set default value of showSpanInfo to false in ToastOptions and update span info handling * fix: correct showSpanInfo handling in error toast options * fix: silence no-unnecessary-type-assertion on load-bearing Literals[0] cast the cast is required because Object.assign widens the Default field type without it, breaking the expectTypeOf(l.Default).toEqualTypeOf<"a">() test assertion. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 80b2261 commit fc41dcf

5 files changed

Lines changed: 45 additions & 7 deletions

File tree

.changeset/funny-webs-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect-app/vue": patch
3+
---
4+
5+
add trace id and span id to toasts

packages/effect-app/src/Schema/ext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export const Literals = <const Literals extends NonEmptyReadonlyArray<AST.Litera
100100
withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(a)))
101101
}) // todo: copy annotations from original?
102102
},
103+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- load-bearing: Object.assign widens the field type without it, breaking `expectTypeOf(l.Default).toEqualTypeOf<"a">()` in tests
103104
Default: literals[0] as Literals[0],
104105
withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(literals[0]))),
105106
withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(literals[0])))

packages/vue/src/commander.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1806,6 +1806,7 @@ export const CommanderStatic = {
18061806
| string
18071807
| ((id: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => true | string | undefined)
18081808
errorRenderer?: (e: E, action: string, arg: NoInfer<Args>[0], ctx: NoInfer<Args>[1]) => string | undefined
1809+
showSpanInfo?: false
18091810
onWaiting?:
18101811
| null
18111812
| undefined
@@ -1870,7 +1871,8 @@ export const CommanderStatic = {
18701871
hasCustomFailure ? intl.formatMessage({ id: customFailure }, cc.state) : cc.action,
18711872
options?.errorRenderer as ErrorRenderer<E, Args> | undefined
18721873
),
1873-
stableToastId
1874+
stableToastId,
1875+
...options?.showSpanInfo === false ? { showSpanInfo: options.showSpanInfo } : {}
18741876
})(_, ...args)
18751877
)
18761878
}),

packages/vue/src/withToast.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CurrentToastId, Toast } from "./toast.js"
55
export interface ToastOptions<A, E, Args extends ReadonlyArray<unknown>, WaiR, SucR, ErrR> {
66
stableToastId?: undefined | string | ((...args: Args) => string | undefined)
77
timeout?: number
8+
showSpanInfo?: false
89
onWaiting:
910
| string
1011
| ((...args: Args) => string | null)
@@ -74,15 +75,23 @@ export class WithToast extends Context.Service<WithToast>()("WithToast", {
7475
return
7576
}
7677

78+
const spanInfo = options.showSpanInfo !== false
79+
? yield* Effect.currentSpan.pipe(
80+
Effect.map((span) => `\nTrace: ${span.traceId}\nSpan: ${span.spanId}`),
81+
Effect.orElseSucceed(() => "")
82+
)
83+
: ""
84+
7785
const t = yield* wrapEffect(options.onFailure)(Cause.findErrorOption(cause), ...args)
7886
const opts = { timeout: baseTimeout * 2 }
7987

8088
if (typeof t === "object") {
89+
const message = t.message + spanInfo
8190
return t.level === "warn"
82-
? yield* toast.warning(t.message, toastId !== undefined ? { ...opts, id: toastId } : opts)
83-
: yield* toast.error(t.message, toastId !== undefined ? { ...opts, id: toastId } : opts)
91+
? yield* toast.warning(message, toastId !== undefined ? { ...opts, id: toastId } : opts)
92+
: yield* toast.error(message, toastId !== undefined ? { ...opts, id: toastId } : opts)
8493
}
85-
yield* toast.error(t, toastId !== undefined ? { ...opts, id: toastId } : opts)
94+
yield* toast.error(t + spanInfo, toastId !== undefined ? { ...opts, id: toastId } : opts)
8695
}, Effect.uninterruptible)),
8796
toastId !== undefined ? Effect.provideService(CurrentToastId, CurrentToastId.of({ toastId })) : (_) => _
8897
)

packages/vue/test/Mutation.test.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,27 @@ it.live("fail", () =>
400400
expect(command.waiting).toBe(false)
401401
expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
402402
expect(toasts.length).toBe(1) // toast should show error
403+
expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
404+
expect(toasts[0].message).toMatch(/Trace: [a-f0-9]{32}/)
405+
expect(toasts[0].message).toMatch(/Span: [a-f0-9]{16}/)
406+
}))
407+
408+
it.live("fail with showSpanInfo disabled", () =>
409+
Effect
410+
.gen(function*() {
411+
const toasts: any[] = []
412+
const Command = useExperimental({ toasts, messages: DefaultIntl.en })
413+
414+
const command = Command.fn("Test Action")(
415+
function*() {
416+
return yield* Effect.fail({ message: "Boom!" })
417+
},
418+
Command.withDefaultToast({ showSpanInfo: false })
419+
)
420+
421+
yield* Fiber.join(command.handle())
422+
423+
expect(toasts.length).toBe(1)
403424
expect(toasts[0].message).toBe("Test Action Failed:\nBoom!")
404425
}))
405426

@@ -456,7 +477,7 @@ it.live("defect", () =>
456477
expect(command.waiting).toBe(false)
457478
expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
458479
expect(toasts.length).toBe(1) // toast should show error
459-
expect(toasts[0].message).toBe("Test Action unexpected error, please try again shortly.")
480+
expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
460481
}))
461482

462483
it.live("works with alt", () =>
@@ -714,7 +735,7 @@ it.live("fail with alt", () =>
714735
expect(command.waiting).toBe(false)
715736
expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
716737
expect(toasts.length).toBe(1) // toast should show error
717-
expect(toasts[0].message).toBe("Test Action Failed:\nBoom!")
738+
expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
718739
}))
719740

720741
it.live("fail and recover with alt", () =>
@@ -774,7 +795,7 @@ it.live("defect with alt", () =>
774795
expect(command.waiting).toBe(false)
775796
expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
776797
expect(toasts.length).toBe(1) // toast should show error
777-
expect(toasts[0].message).toBe("Test Action unexpected error, please try again shortly.")
798+
expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
778799
}))
779800

780801
describe("state-in-toast", () => {

0 commit comments

Comments
 (0)