# `StatifierUI.Trace.DeepLink`
[🔗](https://github.com/riddler/statifier-ui/blob/v0.11.0/lib/statifier_ui/trace/deep_link.ex#L1)

Builds a URL into a host's APM backend from the wire format's `otel`
correlation key (ADR-0013), so a rendered step can be followed to the
trace that covers it.

This module is the consuming half of the correlation story
(`StatifierUI.Trace.Otel` is the producing half). It calls no
OpenTelemetry API, knows no backend, and holds no default URL: **the
template is host configuration**, because only the host knows which
backend its spans went to and what that backend's trace URLs look like.
A package-supplied default would be a guess that silently sends operators
to a URL that does not exist.

## The template

A template is a string with `{...}` variables substituted from the message
being rendered:

    "https://apm.example.com/trace/{trace_id}?span={span_id}"

| Variable | From |
|---|---|
| `trace_id` | the message's `otel.trace_id` - 32 lowercase hex digits |
| `span_id` | the message's `otel.span_id` - 16 lowercase hex digits |
| `session` | the message's `session` |
| `macrostep` | the message's `macrostep` |

A template naming neither `trace_id` nor `span_id` is rejected: it would
render the same URL for every step, which is a configuration mistake and
not a deep link. Any other `{name}` is rejected too, rather than being
passed through as a literal - a typo (`{traceid}`, `{trace-id}`) that
survived into production would produce a plausible-looking URL that never
resolves.

Substituted values are percent-encoded to the unreserved set, so a
template is safe to write with the variable in a path segment or in a
query value. The two ids are hex and pass through unchanged; a session id
is arbitrary host text and does not.

## Compile once, at configuration time

`new/1` returns `{:error, reason}` for a bad template and `new!/1` raises,
matching `StatifierUI.Trace.Subscriber`'s treatment of its `:otel_context`
option: a host misconfiguration is loud, immediately, at the point the
option is read. What is never loud is a *message* with no correlation on
it - see below.

## Absence is not an error

`url/2` returns `nil` when the message carries no `otel` key, when the key
is present but malformed, or when the template asks for a value this
context cannot supply. Absence is the documented normal case: the wire
format omits `otel` whenever no bridge is attached (ADR-0013), so most
streams a consumer renders carry none, and a renderer asks for a link and
gets `nil` rather than branching on the stream's provenance first.

Malformed is folded into absent deliberately. The wire format is
language-neutral (ADR-0005), so an `otel` object may have been written by
a producer this repository never saw; ids that are not W3C Trace Context
hex cannot be looked up in any backend, and linking to them would send an
operator somewhere worse than nowhere.

## Examples

    iex> {:ok, template} = StatifierUI.Trace.DeepLink.new("https://apm.example.com/trace/{trace_id}?span={span_id}")
    iex> message = %StatifierUI.Trace.Message{
    ...>   type: "trace.entry_set", session: "sess_1", seq: 7, macrostep: 2,
    ...>   otel: %{"trace_id" => "4bf92f3577b34da6a3ce929d0e0e4736", "span_id" => "00f067aa0ba902b7"}
    ...> }
    iex> StatifierUI.Trace.DeepLink.url(template, message)
    "https://apm.example.com/trace/4bf92f3577b34da6a3ce929d0e0e4736?span=00f067aa0ba902b7"

    iex> {:ok, template} = StatifierUI.Trace.DeepLink.new("https://apm.example.com/trace/{trace_id}")
    iex> message = %StatifierUI.Trace.Message{type: "trace.entry_set", session: "sess_1", seq: 7, macrostep: 2}
    iex> StatifierUI.Trace.DeepLink.url(template, message)
    nil

# `context`

```elixir
@type context() ::
  StatifierUI.Trace.Message.t()
  | %{
      optional(:otel) =&gt; %{optional(String.t()) =&gt; String.t()} | nil,
      optional(:session) =&gt; String.t() | nil,
      optional(:macrostep) =&gt; non_neg_integer() | nil
    }
```

What a URL is built from: a wire message, or the same three values pulled
out of one. `:otel` is the message's `otel` object, string-keyed as the
wire format writes it.

# `error`

```elixir
@type error() ::
  {:not_a_string, term()}
  | {:unknown_variable, String.t()}
  | {:unbalanced_braces, String.t()}
  | :no_correlation_variable
```

# `t`

```elixir
@type t() :: %StatifierUI.Trace.DeepLink{
  segments: [String.t() | atom()],
  template: String.t()
}
```

A compiled template, built by `new/1` and reused per message.

# `correlation`

```elixir
@spec correlation(context()) :: %{optional(String.t()) =&gt; String.t()} | nil
```

The `otel` object of `context` when it is well-formed, `nil` otherwise.

Exposed because "does this step have a trace at all" is a question a
renderer asks (to decide whether to draw an affordance) separately from
"what is its URL".

## Examples

    iex> message = %StatifierUI.Trace.Message{
    ...>   type: "trace.entry_set", session: "s", seq: 1,
    ...>   otel: %{"trace_id" => "0123456789abcdef0123456789abcdef", "span_id" => "0123456789abcdef"}
    ...> }
    iex> StatifierUI.Trace.DeepLink.correlation(message)
    %{"trace_id" => "0123456789abcdef0123456789abcdef", "span_id" => "0123456789abcdef"}

    iex> message = %StatifierUI.Trace.Message{
    ...>   type: "trace.entry_set", session: "s", seq: 1,
    ...>   otel: %{"trace_id" => "TOO-SHORT", "span_id" => "0123456789abcdef"}
    ...> }
    iex> StatifierUI.Trace.DeepLink.correlation(message)
    nil

# `new`

```elixir
@spec new(term()) :: {:ok, t()} | {:error, error()}
```

Compiles `template` into a `t:t/0`.

Returns `{:error, reason}` rather than a template that renders wrongly:

  * `{:not_a_string, term}` - the option was not a string
  * `{:unknown_variable, name}` - `{name}` is not one of `trace_id`,
    `span_id`, `session`, `macrostep`
  * `{:unbalanced_braces, template}` - a `{` or `}` outside a variable
  * `:no_correlation_variable` - neither `trace_id` nor `span_id` appears

## Examples

    iex> {:ok, template} = StatifierUI.Trace.DeepLink.new("https://apm.example.com/t/{trace_id}")
    iex> template.template
    "https://apm.example.com/t/{trace_id}"

    iex> StatifierUI.Trace.DeepLink.new("https://apm.example.com/t/{traceid}")
    {:error, {:unknown_variable, "traceid"}}

    iex> StatifierUI.Trace.DeepLink.new("https://apm.example.com/session/{session}")
    {:error, :no_correlation_variable}

# `new!`

```elixir
@spec new!(term()) :: t()
```

Compiles `template`, raising `ArgumentError` on a bad one.

For the option-reading path, where a misconfigured host should hear about
it at configuration time rather than by getting no links at render time.

# `url`

```elixir
@spec url(t() | nil, context()) :: String.t() | nil
```

Builds the URL for `context`, or `nil` when there is no link to build.

`nil` in place of a template is itself a valid argument and returns `nil`,
so a renderer holding an unconfigured option calls this unconditionally
instead of branching around it.

## Examples

    iex> message = %StatifierUI.Trace.Message{type: "trace.entry_set", session: "s", seq: 1}
    iex> StatifierUI.Trace.DeepLink.url(nil, message)
    nil

---

*Consult [api-reference.md](api-reference.md) for complete listing*
