Skip to content

PATTERN Cited by 1 source

Framework-exposed tracing API for renderer developers

Problem

Custom client-side operations (button clicks, filter applies, lazy-load AJAX calls, form submits) are the most interesting things to trace on the web — they represent actual user intent and are the load-bearing events for CBOs at browser altitude. But:

  • Frontend developers are often not OTel experts; asking them to import OpenTelemetry, create tracers, manage spans, and remember to close them is friction that leads to skipped instrumentation.
  • The browser has no async-context propagation primitive the way Node.js has AsyncLocalStorage, so manual span passing through function parameters is the default (see concepts/async-context-propagation).
  • Fleet-wide conventions (span naming, attribute tagging, error-tag semantics) drift if each frontend team rolls their own.

Solution

The web framework itself exposes a tracing API as a first-class developer tool, scoped to the unit of composition the framework already has (a renderer, component, view, handler — whatever the framework's atomic unit is). The API returns a span object with addTags() / finish() methods that developers can use to annotate their own operations without knowing OTel.

Zalando's Rendering Engine instantiation (Source: sources/2024-07-28-zalando-opentelemetry-for-javascript-observability-at-zalando):

export default view()
  .withQueries(/* ... */)
  .withProcessDependencies(/* ... */)
  .withRender(props => {
    const traceAs = props.tools.observability.traceAs;
    // props.tools.observability has tools related to client-
    // side observability

    const fetchFilteredProducts = (filter: string) => {
      const span = traceAs("fetch_filtered_products");
      span.addTags({ href: window.href });
      serviceClient.get(`/search?q=${filter}`)
        .then(res => {
          // process response
        })
        .catch(err => {
          span.addTags({ error: true });
        })
        .finally(() => {
          span.finish();
        });
    };

    return (
      <div>
        <button onClick={() => fetchFilteredProducts("shoes")}>
          Fetch Shoes
        </button>
      </div>
    );
  })

Key properties:

  • traceAs("op_name") is a one-function entry point; the developer doesn't import OTel or know what a tracer is.
  • The returned span object has a minimal surface: addTags() and finish() (and error-tagging implicitly via .catch block) — a subset of OTel's API chosen for frontend ergonomics.
  • The span is created in the framework's context so it gets automatic parent linkage to the page-load span.
  • The framework controls the exporter config, the consent gate (concepts/gdpr-consent-gated-telemetry), the propagation format — all invisible to the developer.

When to use this pattern

  • You own a web framework (internal or public) and want to drive adoption of client-side tracing without training every frontend engineer.
  • You have fleet-wide conventions (span naming, attributes, error semantics) you want to enforce centrally.
  • You already have a composition unit (renderer, component, handler, resolver) the API can hang off of.

When not to use this pattern

  • You don't own a framework — there's no natural hook point to expose the API.
  • Your application's tracing is already set up end-to-end via OTel directly and developers are comfortable with it — the wrapper adds a layer for no gain.
  • Your scope is a small number of handcrafted pages where direct OTel use is simpler than building an abstraction.

Failure modes

  • Span leaks from forgotten finish() calls — developers use traceAs but forget to call finish() in some error paths. Unbounded span accumulation can memory-leak the page. Framework should ideally wire in AbortController / cleanup on component unmount.
  • Attribute naming drift — without an enforced vocabulary the addTags({ foo: bar }) calls produce undifferentiated attribute soup across the fleet. Pair this pattern with a published list of standard attribute names.
  • Over-instrumentation — because the API is easy, developers may instrument trivial operations that add noise without signal. A naming review during code review is the only forcing function.

Sibling patterns

Seen in

Last updated · 550 distilled / 1,221 read