---
url: https://jer-k.github.io/effect-prophet/guide/tracing.md
description: >-
  The spans and attributes Effect Prophet records for fits, forecasts and
  evaluations, and how to send them to a tracing backend.
---

# Tracing

Every fit, forecast and evaluation is traced automatically. You don't add any code to the
library calls: point your app at a tracing backend such as Jaeger, Honeycomb or Grafana Tempo,
and each step shows up there with how long it took and what kind of work it did.

If your app doesn't set up a tracer, nothing is sent anywhere.

## See your first trace

Start [Jaeger](https://www.jaegertracing.io/) locally:

```sh
docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:latest
```

Then send spans to it with Effect's built-in OpenTelemetry exporter. No extra packages are needed:

```ts
import { Effect, Layer } from "effect";
import { FetchHttpClient } from "effect/http";
import { OtlpSerialization, OtlpTracer } from "effect/observability";
import { fit, predict, prophetFittingBackendLayer } from "effect-prophet";

// Send spans to any OpenTelemetry collector that accepts OTLP over HTTP, such as Jaeger.
const tracing = OtlpTracer.layer({
  url: "http://localhost:4318/v1/traces",
  resource: { serviceName: "sales-forecast" },
}).pipe(Layer.provide([OtlpSerialization.layerJson, FetchHttpClient.layer]));

const history = Array.from({ length: 90 }, (_, day) => ({
  timestamp: new Date(Date.UTC(2025, 0, 1 + day)).toISOString(),
  value: 100 + day * 0.5 + 10 * Math.sin((2 * Math.PI * day) / 7),
}));

const nextWeek = Array.from({ length: 7 }, (_, offset) =>
  new Date(Date.UTC(2025, 0, 91 + offset)).toISOString(),
);

const forecast = await Effect.runPromise(
  Effect.gen(function* () {
    const model = yield* fit(history);

    return yield* predict(model, nextWeek);
  }).pipe(
    // Your own span. Effect Prophet's spans appear underneath it.
    Effect.withSpan("weekly-sales-forecast"),
    Effect.provide([prophetFittingBackendLayer, tracing]),
  ),
);

console.log(forecast.length);

```

Open `http://localhost:16686`, choose the `sales-forecast` service and open the trace:

```txt
weekly-sales-forecast                 your span
├─ Prophet.fit
│  ├─ Prophet.decodeObservations      checking your history
│  ├─ Prophet.decodeOptions           checking your settings
│  └─ effect-prophet.wasm.fit         the forecasting engine fitting the model
└─ Prophet.predict
   └─ effect-prophet.wasm.predict     the forecasting engine computing the forecast
```

Spans from Effect Prophet join whatever trace is already active. Wrap your own work in
`Effect.withSpan`, as above, and the forecast appears as part of your request, job or pipeline.
Any tracer you give Effect works the same way, including one your app already provides.

## Why was this fit slow?

The engine runs as one step, so a trace has no spans *inside* it. Instead, the
`effect-prophet.wasm.fit` span records what the engine did. For example:

| Attribute                   | Value              | What it tells you                                            |
| --------------------------- | ------------------ | ------------------------------------------------------------ |
| `observation.count`         | `90`               | Rows in the history                                          |
| `changepoint.count`         | `25`               | Places the trend was allowed to bend                         |
| `parameter.count`           | `33`               | Values the engine had to solve for                           |
| `optimizer.algorithm`       | `newton`           | The solving method that ran                                  |
| `optimizer.iteration.count` | `600`              | Solving steps taken                                          |
| `optimizer.termination`     | `objective-change` | Why it stopped. `iteration-limit` means it ran out of steps. |
| `wasm.cold_start`           | `true`             | This call also loaded the engine, which only happens once    |

(Every attribute name starts with `effect_prophet.`, left off here to keep the table short.)

Fitting time is roughly the number of steps times the cost of each step. The cost of a step grows
with `parameter.count`, and the 25 default changepoints are often a large share of the parameters.
With the default settings, histories under 100 rows use Newton's method, whose steps get expensive
as parameters grow.

The number of steps is usually a few dozen to a few hundred. A count much higher than that, as in
the example above, usually means the data has almost no noise: a perfectly smooth synthetic series
can be fitted exactly, so the solver keeps shrinking its noise estimate toward zero, one step at a
time. Python Prophet does the same on such data. Real data rarely triggers this. If yours does,
fewer changepoints (see [Trends and changepoints](../examples/trend)) make each step cheaper. Let
the trace tell you whether it helped.

The first engine call in a process also loads the engine. In a browser, that includes downloading
the binary. `wasm.cold_start` marks that call so the one-time cost isn't mistaken for a slow fit.
Calls that start while it's loading wait for the same load and aren't marked.

## What is recorded

The attributes Effect Prophet sets on its spans are only counts and fixed labels, such as
`growth: "linear"` or `optimizer.algorithm: "newton"`. They never contain:

* your values, timestamps or forecasts
* regressor, event or seasonality names
* fitted coefficients or other model internals
* error messages, stack traces or seeds

Failed spans are different. When an operation fails, its span ends with the error, as every
Effect span does, and your tracer decides what to do with it. The OTLP exporter above sends the
error's message and stack trace with the span, and error messages can name your data. For
example, a non-finite regressor value fails with a message that includes the regressor's name:

```txt
Expected a finite number
  at [0]["regressors"]["private_customer_health_index"]
```

If your field names or other inputs are sensitive, decide what your exporter may send before
pointing it at a shared backend: filter or redact exception events and status messages in your
tracer or in a collector processor. Effect Prophet leaves that policy to your application, and
doesn't change or hide its errors.

## Span reference

### Operations

Each of these functions creates a span named after it:

| Span                                                                                               | Created by                                                                                    |
| -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `Prophet.fit`                                                                                      | `fit`                                                                                         |
| `Prophet.predict`                                                                                  | `predict`                                                                                     |
| `Prophet.predictUncertainty`                                                                       | `predictUncertainty`                                                                          |
| `Prophet.predictWithIntervals`                                                                     | `predictWithIntervals`, around its `Prophet.predictUncertainty` and `Prophet.predict` spans   |
| `Prophet.detectAnomalies`                                                                          | `detectAnomalies`, around `Prophet.decodeObservations` and `Prophet.predictWithIntervals`     |
| `Prophet.residuals`                                                                                | `residuals`, around `Prophet.decodeObservations` and `Prophet.predict`                        |
| `Prophet.forecast`                                                                                 | `forecast`, around `Prophet.decodeObservations`, `Prophet.fit` and `Prophet.predict`          |
| `Prophet.anomalies`                                                                                | `anomalies`, around `Prophet.decodeObservations`, `Prophet.fit` and `Prophet.detectAnomalies` |
| `Prophet.crossValidate`                                                                            | `crossValidate`                                                                               |
| `Prophet.makeFutureTimestamps`                                                                     | `makeFutureTimestamps`                                                                        |
| `Prophet.planRollingOrigin`                                                                        | `planRollingOrigin`                                                                           |
| `Prophet.makeParameterGrid`                                                                        | `makeParameterGrid`, around one `Prophet.decodeOptions` for the base and one per candidate    |
| `Prophet.searchModels`                                                                             | `searchModels`                                                                                |
| `Prophet.evaluateHoldout`                                                                          | `evaluateHoldout`                                                                             |
| `Prophet.crossValidateBaseline`                                                                    | `crossValidateBaseline`                                                                       |
| `Prophet.comparePerformance`                                                                       | `comparePerformance`                                                                          |
| `Prophet.scaledErrorMetric`                                                                        | `scaledErrorMetric`, around `Prophet.decodeObservations`                                      |
| `Prophet.encodeFittedModel`, `Prophet.decodeFittedModel`                                           | Saving and loading models                                                                     |
| `Prophet.encodeEvaluationReport`, `Prophet.decodeEvaluationReport`                                 | Saving and loading evaluation reports                                                         |
| `Prophet.decodeObservations`, `Prophet.decodeOptions`                                              | Checking input, also called inside `fit`                                                      |
| `Prophet.inspectCountryHolidays`                                                                   | `inspectCountryHolidays`                                                                      |
| `Prophet.parseCountryHolidays`                                                                     | Checking a `countryHolidays` selection, inside `decodeOptions` and `inspectCountryHolidays`   |
| `Prophet.parseEventCalendar`, `Prophet.parseRegressorDefinitions`, `Prophet.decodeLinearOptimizer` | Checking settings, inside `decodeOptions`                                                     |

### Regressor models

A regressor [forecast by its own model](../examples/regressors#forecasting-a-regressor) adds a
span for that model's work:

| Span                             | Covers                                                                                                                         |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `Prophet.fitRegressorModel`      | Fitting one regressor's model inside `Prophet.fit`, with its own `effect-prophet.wasm.fit` span and `Prophet.fit` attributes   |
| `Prophet.predictRegressorModel`  | Forecasting one regressor inside `Prophet.predict`, with its own `effect-prophet.wasm.predict` span                            |
| `Prophet.simulateRegressorModel` | Simulating one regressor's future values inside `Prophet.predictUncertainty`, with its own `effect-prophet.wasm.simulate` span |

Each regressor model is fitted before the main model, so its `effect-prophet.wasm.fit` span comes
first. A prediction whose rows all fall inside the history needs no regressor forecast and creates
no `Prophet.predictRegressorModel` or `Prophet.simulateRegressorModel` span. Options that fail
validation create no `Prophet.fitRegressorModel` span.

### Engine and evaluation steps

| Span                                  | Covers                                                             |
| ------------------------------------- | ------------------------------------------------------------------ |
| `effect-prophet.wasm.fit`             | Fitting in the engine, including loading it and reading results    |
| `effect-prophet.wasm.predict`         | Computing a forecast in the engine                                 |
| `effect-prophet.wasm.simulate`        | Simulating uncertainty ranges in the engine                        |
| `effect-prophet.wasm.componentRanges` | Each component's range over a posterior, in `predictWithIntervals` |
| `effect-prophet.evaluation.fold`      | One cross-validation fold: a fit and a forecast                    |
| `effect-prophet.evaluation.candidate` | One candidate setting during `searchModels`                        |
| `effect-prophet.evaluation.holdout`   | The final check in `evaluateHoldout`                               |

Input that fails validation, or a request with nothing to do, such as an empty list of dates,
returns before reaching the engine and creates no engine span. `forecast` checks its history and
future rows before fitting, so malformed rows create no `Prophet.fit` span. `anomalies` also
checks the history's length before fitting, so a history too long to simulate creates no
`Prophet.fit` span.

### Country calendar steps

| Span                                    | Covers                                                                                                                                                |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Prophet.resolveCountryHolidays`        | Resolving a country calendar's dates and coverage for the requested years, inside `fit`, `predict`, `predictUncertainty` and `inspectCountryHolidays` |
| `effect-prophet.calendar.load-snapshot` | Loading and checking a packaged country calendar file, inside a resolution for `fit` or `inspectCountryHolidays`                                      |

Prediction resolves dates from the snapshot saved in the model, so it reads no file and creates no
`effect-prophet.calendar.load-snapshot` span. A malformed selection or year list fails before
resolution and creates no calendar step span; an empty prediction creates none either.

### Attributes

Every attribute name starts with `effect_prophet.`.

**`Prophet.fit`**

| Attribute                                              | Meaning                                                                                                                                         |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `seasonality.custom.count`                             | Custom seasonalities you added                                                                                                                  |
| `seasonality.builtin.enabled.count`                    | Built-in seasonalities (yearly, weekly, daily) in use                                                                                           |
| `seasonality.builtin.{yearly,weekly,daily}.resolution` | Why each was used or skipped: `explicitly-enabled`, `automatic-enabled`, `explicitly-disabled`, `insufficient-history` or `sampling-too-sparse` |
| `regressor.count`                                      | Extra factors (regressors). Only present when there are some.                                                                                   |
| `regressor_model.count`                                | Regressors forecast by their own models. Only present when there are some.                                                                      |
| `event.count`                                          | Holidays and events, including country holidays. Only present when there are some.                                                              |
| `seasonality.condition.count`                          | Conditions used by conditional seasonalities. Only present when there are some.                                                                 |

**`Prophet.makeFutureTimestamps`**

| Attribute         | Meaning                                                                          |
| ----------------- | -------------------------------------------------------------------------------- |
| `frequency`       | The `frequency` used, such as `D` or `MS`                                        |
| `timestamp.count` | Timestamps returned, history included. Neither is set when the input is invalid. |

**`Prophet.makeParameterGrid`**

| Attribute         | Meaning                                               |
| ----------------- | ----------------------------------------------------- |
| `axis.count`      | Settings varied. Set only when the grid is valid.     |
| `candidate.count` | Candidates returned. Set only when the grid is valid. |

A grid that is too large, has an empty or repeated setting, or isn't shaped like a grid fails
before any `Prophet.decodeOptions` span.

**`effect-prophet.wasm.fit`**

| Attribute                                                                                              | Meaning                                                                                                                                             |
| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `backend.type`, `operation`                                                                            | Always `rust-wasm` and `fit`                                                                                                                        |
| `model.type`, `growth`                                                                                 | `flat-map`, `linear-piecewise-map`, `logistic-piecewise-map`, `flat-mcmc`, `linear-piecewise-mcmc` or `logistic-piecewise-mcmc`, and the trend type |
| `observation.count`                                                                                    | Rows in the history                                                                                                                                 |
| `seasonality.count`, `coefficient.count`                                                               | Seasonal, event and regressor components, and the coefficients behind them                                                                          |
| `changepoint.count`                                                                                    | Places the trend was allowed to bend. Always `0` for flat trends                                                                                    |
| `parameter.count`                                                                                      | Total values solved for, including one per changepoint for flat trends, which never use them                                                        |
| `scaling.mode`                                                                                         | How values were scaled before fitting                                                                                                               |
| `component.mode`                                                                                       | `mixed` for multiplicative effects and capped trends; `additive` for flat trends with only additive effects                                         |
| `wasm.cold_start`                                                                                      | `true` only for the call that loaded the engine                                                                                                     |
| `optimizer.requested_algorithm`, `optimizer.max_iterations`, `optimizer.fallback`                      | The optimizer settings you passed in `map.optimizer`, or Prophet's defaults                                                                         |
| `optimizer.algorithm`, `optimizer.termination`, `optimizer.iteration.count`                            | What ran, why it stopped and how many steps it took                                                                                                 |
| `optimizer.attempt.count`, `optimizer.failed_attempt.iteration.count`, `optimizer.hessian_reset.count` | Retries and recoveries, when the first method fell back to the second                                                                               |
| `optimizer.initialization`                                                                             | `warm-start` when you passed `map.warmStart`. Only present then.                                                                                    |
| `optimizer.warm_start.delta`, `optimizer.warm_start.beta`                                              | `warm-start`, or `default` when the counts differed. Only present when a warm-started fit ran the optimizer                                         |
| `mcmc.chains`, `mcmc.samples`, `mcmc.warmup`                                                           | The [Bayesian fitting](../examples/bayesian) settings. Only present with `mcmc`, which has no `optimizer.*` attributes                              |
| `mcmc.initialization`                                                                                  | `warm-start` when you passed `map.warmStart`, otherwise `default`                                                                                   |
| `mcmc.termination`, `mcmc.draw.count`                                                                  | `sampled` or `constant-target-shortcut`, and the draws kept                                                                                         |
| `mcmc.divergence.count`, `mcmc.max_tree_depth.count`                                                   | Divergent draws and draws that hit the tree-depth limit. Absent for the constant-target shortcut                                                    |

**`effect-prophet.wasm.predict`, `effect-prophet.wasm.simulate` and `effect-prophet.wasm.componentRanges`**

| Attribute                           | Meaning                                                                                                                 |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `model.type`, `prediction.count`    | The model and the number of dates forecast                                                                              |
| `seasonality.count`, `scaling.mode` | As for fitting (`predict` only)                                                                                         |
| `output.kind`, `sample.count`       | `intervals` or `samples`, and simulations per date (`simulate` only); a posterior runs `ceil(samples / draws)` per draw |
| `mcmc.draw.count`                   | Posterior draws (`simulate` and `componentRanges`, posteriors only)                                                     |
| `wasm.cold_start`                   | `true` only for the call that loaded the engine                                                                         |

**`Prophet.forecast`**

| Attribute           | Meaning                                                   |
| ------------------- | --------------------------------------------------------- |
| `observation.count` | Rows in the history. Set only when the forecast succeeds. |
| `prediction.count`  | Rows forecast. Set only when the forecast succeeds.       |

**`Prophet.detectAnomalies` and `Prophet.anomalies`**

| Attribute           | Meaning                                                 |
| ------------------- | ------------------------------------------------------- |
| `observation.count` | Measurements checked. Set only when the check succeeds. |
| `anomaly.count`     | Measurements outside their range. Set only on success.  |

**`Prophet.scaledErrorMetric`**

| Attribute    | Meaning                                                            |
| ------------ | ------------------------------------------------------------------ |
| `metric.lag` | The naive rule: `previous-row` or `duration`. Set only on success. |
| `fold.count` | Rounds given a scale. Set only when every round has one.           |

**`Prophet.residuals`**

| Attribute           | Meaning                                                          |
| ------------------- | ---------------------------------------------------------------- |
| `observation.count` | Measurements compared. Set only when every residual is computed. |

**Country calendars**

| Span                                    | Attributes                                                                                                                                                                      |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Prophet.resolveCountryHolidays`        | `calendar.source` (`bundled` from the package, `embedded` from a fitted model), `calendar.coverage`, `calendar.year.count`, `calendar.occurrence.count`, `calendar.issue.count` |
| `effect-prophet.calendar.load-snapshot` | `calendar.source` (always `bundled`), `calendar.name.count`, `calendar.partition.count`                                                                                         |

`calendar.occurrence.count` and `calendar.issue.count` are set only when resolution succeeds.

**Evaluation**

| Span                                            | Attributes                                                                                                                                                              |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Prophet.crossValidate`, `Prophet.searchModels` | `evaluation.scaling.mode`                                                                                                                                               |
| `effect-prophet.evaluation.fold`                | `operation`, `fold.index`, `fold.count`, `training.count`, `assessment.count`, `growth`, `model.type`, `evaluation.scaling.mode`, `uncertainty.enabled`, `sample.count` |
| `effect-prophet.evaluation.candidate`           | `operation`, `candidate.index`, `candidate.count`, `fold.count`, `objective`, `uncertainty.enabled`, `evaluation.scaling.mode`                                          |
| `effect-prophet.evaluation.holdout`             | `operation`, `growth`, `model.type`, `training.count`, `assessment.count`, `baseline.count`, `uncertainty.enabled`                                                      |

`objective` is the built-in metric name, such as `mae`, or `custom` for a
[custom metric](../examples/cross-validation#your-own-metrics). A custom metric's own name is
never set as an attribute, because you choose it. An error your `score` function fails with can
still reach your exporter through the failed span, as described in
[What is recorded](#what-is-recorded).
