Current section

Files

Jump to
excessibility lib telemetry_capture timeline.ex
Raw

lib/telemetry_capture/timeline.ex

defmodule Excessibility.TelemetryCapture.Timeline do
@moduledoc """
Generates timeline data from telemetry snapshots.
Extracts key state, computes diffs, and formats timeline entries
for human and AI consumption.
"""
alias Excessibility.TelemetryCapture.Diff
alias Excessibility.TelemetryCapture.Filter
@default_highlight_fields [:current_user, :live_action, :errors, :form]
@small_value_threshold 100
@doc """
Extracts key state from assigns for timeline display.
Includes:
- Highlighted fields (from config)
- Small primitive values (< #{@small_value_threshold} chars)
- List counts (products: [3 items] -> products_count: 3)
- Auto-detected important fields (status, action, etc.)
"""
def extract_key_state(assigns, highlight_fields \\ @default_highlight_fields) do
using_defaults? = highlight_fields == @default_highlight_fields
Enum.reduce(assigns, %{}, fn {key, value}, acc ->
cond do
key in highlight_fields -> Map.put(acc, key, value)
is_list(value) -> Map.put(acc, :"#{key}_count", length(value))
using_defaults? and small_value?(value) -> Map.put(acc, key, value)
true -> acc
end
end)
# Always include highlighted fields as-is
# Convert non-highlighted lists to counts
# Include small primitives only when using default fields
# Skip everything else
end
defp small_value?(value) when is_integer(value), do: true
defp small_value?(value) when is_atom(value), do: true
defp small_value?(value) when is_boolean(value), do: true
defp small_value?(value) when is_binary(value) do
byte_size(value) <= @small_value_threshold
end
defp small_value?(_), do: false
@doc """
Builds a complete timeline from snapshots.
Returns a map with:
- :test - test name
- :duration_ms - total test duration
- :timeline - list of timeline entries
"""
def build_timeline([], test_name) do
%{
test: test_name,
timeline: [],
duration_ms: 0
}
end
def build_timeline(snapshots, test_name, opts \\ []) do
first_timestamp = List.first(snapshots).timestamp
last_timestamp = List.last(snapshots).timestamp
duration_ms = DateTime.diff(last_timestamp, first_timestamp, :millisecond)
timeline =
snapshots
|> Enum.with_index(1)
|> Enum.map(fn {snapshot, index} ->
previous = if index > 1, do: Enum.at(snapshots, index - 2)
build_timeline_entry(snapshot, previous, index, opts)
end)
%{
test: test_name,
duration_ms: duration_ms,
timeline: timeline
}
end
@doc """
Builds a single timeline entry from a snapshot and its predecessor.
"""
def build_timeline_entry(snapshot, previous, sequence, opts \\ []) do
filtered_assigns = Filter.filter_assigns(snapshot.assigns, opts)
key_state =
extract_key_state(filtered_assigns, opts[:highlight_fields] || @default_highlight_fields)
previous_assigns =
if previous do
Filter.filter_assigns(previous.assigns, opts)
end
diff = Diff.compute_diff(filtered_assigns, previous_assigns)
changes = Diff.extract_changes(diff)
duration_since_previous =
if previous do
DateTime.diff(snapshot.timestamp, previous.timestamp, :millisecond)
end
%{
sequence: sequence,
event: snapshot.event_type,
timestamp: snapshot.timestamp,
view_module: snapshot.view_module,
key_state: key_state,
changes: changes,
duration_since_previous_ms: duration_since_previous
}
end
end