-
-
Notifications
You must be signed in to change notification settings - Fork 8
error reporter migration
Effect v4 has a first-class ErrorReporter module. We can use it to replace the duplicated custom implementations in packages/infra/src/errorReporter.ts and packages/vue/src/errorReporter.ts.
The target shape is:
- register platform reporters once with
ErrorReporter.layer - report failed effects with
Effect.withErrorReporting - report already-captured causes with
ErrorReporter.report - move per-error filtering, severity, and metadata onto error classes with
ErrorReporter.ignore,ErrorReporter.severity, andErrorReporter.attributes
Our current reporters do several things that ErrorReporter.make already owns:
- skips interruption-only causes
- turns cause reasons into reportable
Errorvalues - deduplicates repeated cause/error objects
- supports severity
- supports structured attributes
The custom code should keep only app-specific behavior:
- Sentry scope setup
- request context attached to Sentry
- log annotations we still want in addition to Sentry
- the old category/name field, currently
__error_name__
Create one reporter per runtime environment. Keep the callback synchronous: ErrorReporter.make is invoked from the failing fiber and returns void.
import * as Sentry from "@sentry/node"
import * as Cause from "effect/Cause"
import * as ErrorReporter from "effect/ErrorReporter"
import { LocaleRef } from "effect-app/RequestContext"
import { storeId } from "effect-app/Store"
import { LogLevelToSentry } from "effect-app/utils"
export const SentryErrorReporter = ErrorReporter.make(({ attributes, cause, error, fiber, severity }) => {
const scope = new Sentry.Scope()
scope.setLevel(LogLevelToSentry(severity))
scope.setContext("context", {
locale: fiber.getRef(LocaleRef),
namespace: fiber.getRef(storeId)
})
scope.setContext("attributes", attributes)
scope.setContext("cause", { pretty: Cause.pretty(cause) })
Sentry.captureException(error, scope)
})
export const ErrorReporterLive = ErrorReporter.layer([SentryErrorReporter])For the browser package use the same shape with @sentry/browser and without server-only request context.
Provide the layer at runtime/bootstrap, not at every call site:
const RuntimeLive = AppLayer.pipe(
Layer.provideMerge(ErrorReporterLive)
)Use { mergeWithExisting: true } only when adding a reporter without replacing already configured reporters.
Current manual reporting:
self.pipe(Effect.tapCause(reportError("Request")))Becomes:
self.pipe(Effect.withErrorReporting)For streams, keep the stream boundary and swap only the reporter:
stream.pipe(Stream.tapCause(ErrorReporter.report))Current "unexpected only" reporting:
self.pipe(reportUnknownRequestError)Becomes:
self.pipe(Effect.withErrorReporting({ defectsOnly: true }))Current places that already have a cause and should keep going:
yield* reportRuntimeError(cause, extra)Become:
yield* ErrorReporter.report(cause)If the extra data is important, attach it at the source error with ErrorReporter.attributes. If the data is call-site-only, keep a small helper that logs/sends a message separately; do not rebuild a parallel cause reporter.
Move severity decisions from helper functions into the error values.
import * as ErrorReporter from "effect/ErrorReporter"
class FetchAborted extends Error {
readonly [ErrorReporter.severity] = "Info" as const
}
class ExpectedNotFound extends Error {
readonly [ErrorReporter.ignore] = true
}For existing tagged errors, add the same fields to the class.
class RateLimited extends Data.TaggedError("RateLimited")<{
readonly retryAfter: number
}> {
readonly [ErrorReporter.severity] = "Warn" as const
readonly [ErrorReporter.attributes] = {
retryAfter: this.retryAfter
}
}This replaces custom logic like determineLevel(cause) and ad hoc "silenced" symbols.
The old API accepts reportError("Request"), reportError("Runtime"), and reportError("Queue"). Effect's reporter does not need a name parameter.
Preserve category only when it is still useful for dashboards:
- Prefer span/log context when the category is already the current operation.
- Add
ErrorReporter.attributesto domain errors when category is intrinsic to the error. - For legacy dashboard compatibility, temporarily wrap reporting boundaries with log annotations or add a small reporter-specific mapping from fiber span/request data to Sentry tags.
Avoid keeping reportError(name) as a compatibility layer. It preserves the old API and hides the intended migration.
- Add
ErrorReporterLivefor infra and vue. - Provide it once at runtime/bootstrap.
- Convert effect boundaries from
tapCause(reportError(...))toEffect.withErrorReporting. - Convert stream boundaries from
Stream.tapCause(reportError(...))toStream.tapCause(ErrorReporter.report). - Convert "unknown only" boundaries to
Effect.withErrorReporting({ defectsOnly: true }). - Move call-site extras/severity onto error classes with
ErrorReporter.attributesandErrorReporter.severity. - Delete
packages/infra/src/reportError.tsafter its call sites are gone. - Delete
packages/infra/src/errorReporter.tsandpackages/vue/src/errorReporter.tsonce onlyreportMessage/ non-cause helpers remain, or split those helpers into a separate Sentry utility.
-
ErrorReporter.makecallbacks are synchronous. Read request data fromfiber.getRef(...), not by yielding effects likegetRC. -
ErrorReporter.reportreports to reporters registered on the current fiber. MissingErrorReporter.layermeans no reports. - Default severity is
"Info"unless an object error hasErrorReporter.severity. - The Effect reporter already skips interruption-only causes; do not duplicate that check at call sites.
- For streaming boundaries, keep the existing request-scope layer rules. The reporter layer is a
Layer, so provide it at runtime or with the same per-request scope discipline used elsewhere.