Observability
Wardic is built on Effect, and speaks its observability: logs, failure reports, traces and metrics. It installs no logger or reporter of its own. The provider's env takes any Layer that gives Wardic's services, so an app merges Effect's own beside DomLive:
import type { JSX } from "@solidjs/web"
import { CheckFrames, DomLive } from "@wardic/dom"
import { domAnimation, WardicProvider } from "@wardic/solid/unstable/v2"
import { DevTools } from "effect/devtools"
import * as Effect from "effect/Effect"
import * as ErrorReporter from "effect/ErrorReporter"
import * as Layer from "effect/Layer"
import * as Logger from "effect/Logger"
import * as References from "effect/References"
import * as Scope from "effect/Scope"
/** Each failure Wardic stops, where it stops: to the console here, to your error tracker in an app. */
const reporter = ErrorReporter.make(({ error, attributes }) => console.error(error, attributes))
/**
* Wardic's Environment in development: the DOM's, with Effect's own logging, reporting and tracing.
* Made before the app renders: a provider builds its `env` at once, and `DevTools.layer()` waits for
* its connection as it builds, so its services go in built.
*/
const devEnv = async () =>
Layer.mergeAll(
DomLive,
// Wardic's Debug logs, pretty in the browser's console, and each one on its span in the devtools.
Logger.layer([Logger.consolePrettyBrowser(), Logger.tracerLogger]),
Layer.succeed(References.MinimumLogLevel, "Debug"),
ErrorReporter.layer([reporter]),
// Every animation's span, and the Frame loop's metrics, to the Effect devtools, connected while the page is open.
Layer.succeedContext(await Effect.runPromise(Layer.buildWithScope(DevTools.layer(), Scope.makeUnsafe()))),
// A frame with a NaN or an Infinity in it fails its animation, naming the key.
Layer.succeed(CheckFrames, true),
)
/** The Environment the app runs on: `devEnv` in development, the DOM's alone in production. */
const env = import.meta.env.DEV ? await devEnv() : DomLive
export default function App(props: { readonly children: JSX.Element }) {
return (
<WardicProvider env={env} features={[domAnimation]}>
{props.children}
</WardicProvider>
)
}import { CheckFrames, DomLive } from "@wardic/dom"
import { domAnimation, WardicProvider } from "@wardic/solid"
import { DevTools } from "effect/devtools"
import * as Effect from "effect/Effect"
import * as ErrorReporter from "effect/ErrorReporter"
import * as Layer from "effect/Layer"
import * as Logger from "effect/Logger"
import * as References from "effect/References"
import * as Scope from "effect/Scope"
import type { JSX } from "solid-js"
/** Each failure Wardic stops, where it stops: to the console here, to your error tracker in an app. */
const reporter = ErrorReporter.make(({ error, attributes }) => console.error(error, attributes))
/**
* Wardic's Environment in development: the DOM's, with Effect's own logging, reporting and tracing.
* Made before the app renders: a provider builds its `env` at once, and `DevTools.layer()` waits for
* its connection as it builds, so its services go in built.
*/
const devEnv = async () =>
Layer.mergeAll(
DomLive,
// Wardic's Debug logs, pretty in the browser's console, and each one on its span in the devtools.
Logger.layer([Logger.consolePrettyBrowser(), Logger.tracerLogger]),
Layer.succeed(References.MinimumLogLevel, "Debug"),
ErrorReporter.layer([reporter]),
// Every animation's span, and the Frame loop's metrics, to the Effect devtools, connected while the page is open.
Layer.succeedContext(await Effect.runPromise(Layer.buildWithScope(DevTools.layer(), Scope.makeUnsafe()))),
// A frame with a NaN or an Infinity in it fails its animation, naming the key.
Layer.succeed(CheckFrames, true),
)
/** The Environment the app runs on: `devEnv` in development, the DOM's alone in production. */
const env = import.meta.env.DEV ? await devEnv() : DomLive
export default function App(props: { readonly children: JSX.Element }) {
return (
<WardicProvider env={env} features={[domAnimation]}>
{props.children}
</WardicProvider>
)
}That's a development setup, so it's given under import.meta.env.DEV alone: in production, the app runs on plain DomLive, and the devtools never load.
A provider builds its env at once, as its first element renders, so each Layer in it must build without waiting. DevTools.layer() waits for its connection as it builds, for up to a second when the devtools aren't open, so the sample builds it before the app renders, with Layer.buildWithScope, and gives its services with Layer.succeedContext. Any Layer that waits as it builds goes in the same way: given as it is, it fails the first element with an error saying so.
Logs
Effect's defaults are production's: Wardic's Debug logs are dropped, and its warnings and errors print to the console. With the level at "Debug", you see:
- each animation as it starts, such as
wardic.springValues: start, with the keys it moves; - the layout system's nodes, as they're registered and disposed, and presence's exits as they're decided.
Its warnings point at a mistake, for example:
wardic.style: atransformstring is dropped whilestylesets a transform key, such asxorscale.wardic.interpolator: a pair of values that can't be blended, such as10pxand50%, switches at the first frame.- In development only, a prop whose Feature no provider or
LazyWardicabove it gives.
Failures
Every failure Wardic stops is logged at Error, and handed to the app's ErrorReporters: a failed animation (wardic.spine: a leg failed), a callback that threw (wardic.gesture: onTap failed), an exit, a drag's release, a layout pass, a lazy load of Features. An interruption, such as a new target or an unmount, is neither. A reporter made with ErrorReporter.make hears each failure once, even when it crosses two of Wardic's boundaries.
Traces and metrics
With a tracer installed, such as the devtools' or OpenTelemetry's, each animation runs in a span named for what it is: wardic.springValues, wardic.animateValues, wardic.keyframesValues, or wardic.reducedMotion.snap for a key that jumps. Each burst of frames is a wardic.frameloop span, and each layout pass a wardic.projection.measure. With no tracer, Wardic makes no spans at all.
The frame loop's metrics are wardic_frameloop_animations, the animations running now; wardic_frameloop_settled and wardic_frameloop_failed, counts of those that ended; and wardic_frameloop_tick_ms, each frame's work, as a histogram.
DevTools.layer() streams spans and metrics to Effect's devtools, on ws://localhost:34437 by default. To send them to OpenTelemetry instead, use Effect's OTLP exporter:
import { FetchHttpClient } from "effect/http"
import { OtlpSerialization, OtlpTracer } from "effect/observability"
// Wardic's spans, and the app's own, to an OpenTelemetry collector.
const tracing = OtlpTracer.layer({ url: "http://localhost:4318/v1/traces", resource: { serviceName: "shop" } }).pipe(
Layer.provide(OtlpSerialization.layerJson),
Layer.provide(FetchHttpClient.layer),
)
const env = Layer.mergeAll(DomLive, tracing)Checking frames
CheckFrames, from @wardic/dom, fails an animation whose frame holds a NaN or an Infinity, naming the key and the value (a non-finite Frame: x is NaN), where the page would quietly drop a NaNpx. It checks every value of every frame, which costs up to 40% of a frame's work, so it's for development.
effect/ErrorReporter, effect/devtools and effect/observability are marked unstable in Effect 4.1, so their names can change in a minor release.