chore: added ssr telemetry

This commit is contained in:
Denis Donici 2026-02-26 01:52:46 +02:00 committed by Wout De Puysseleir
parent 2a9c197c53
commit 079ed517b7
3 changed files with 129 additions and 1 deletions

View file

@ -11,6 +11,22 @@ defmodule LiveSvelte.SSR do
To define a custom renderer, change the application config in `config.exs`:
config :live_svelte, ssr_module: MyCustomSSRModule
## Telemetry
Exposes a telemetry span for each render under the key `[:live_svelte, :ssr]`.
The following events are emitted:
* `[:live_svelte, :ssr, :start]` fired when a render begins.
Metadata: `%{component: name, props: props, slots: slots}`.
* `[:live_svelte, :ssr, :stop]` fired when a render completes successfully.
Metadata: same. Measurements include `%{duration: duration}` in native time units
(convert with `:erlang.convert_time_unit(duration, :native, :millisecond)`).
* `[:live_svelte, :ssr, :exception]` fired when the renderer raises.
The exception is re-raised after the event is emitted.
"""
@type component_name :: String.t()
@ -41,8 +57,11 @@ defmodule LiveSvelte.SSR do
@spec render(component_name, props, slots) :: render_response | no_return
def render(name, props, slots) do
mod = Application.get_env(:live_svelte, :ssr_module, LiveSvelte.SSR.NodeJS)
meta = %{component: name, props: props, slots: slots}
mod.render(name, props, slots)
:telemetry.span([:live_svelte, :ssr], meta, fn ->
{mod.render(name, props, slots), meta}
end)
end
@deprecated "Use LiveSvelte.SSR.NodeJS.server_path/0 instead."

View file

@ -67,6 +67,7 @@ defmodule LiveSvelte.MixProject do
{:jason, "~> 1.2", optional: true},
{:lazy_html, ">= 0.1.0", only: :test},
{:nodejs, "~> 3.1"},
{:telemetry, "~> 0.4 or ~> 1.0"},
{:phoenix, ">= 1.7.0"},
{:phoenix_html, ">= 3.3.1"},
{:phoenix_live_view, ">= 0.18.0"}

108
test/ssr_telemetry_test.exs Normal file
View file

@ -0,0 +1,108 @@
defmodule LiveSvelte.SSRTelemetryTest do
# must be synchronous — modifies global config and attaches telemetry handlers
use ExUnit.Case, async: false
alias LiveSvelte.SSR
# Named handler to avoid telemetry "local function" performance penalty log warning.
# test_pid is threaded through the telemetry config argument.
def handle_telemetry(event, measurements, metadata, test_pid) do
send(test_pid, {:telemetry_event, event, measurements, metadata})
end
defmodule MockSSRRenderer do
@moduledoc false
@behaviour SSR
@impl true
def render("Success", _props, _slots) do
%{"head" => "", "html" => "<div>Hello</div>"}
end
def render("Failing", _props, _slots) do
raise RuntimeError, "SSR render failed"
end
# Catch-all: prevents unhelpful FunctionClauseError on typos in test component names
def render(name, _props, _slots) do
raise "MockSSRRenderer: no handler for #{inspect(name)}"
end
end
setup do
original_module = Application.get_env(:live_svelte, :ssr_module)
Application.put_env(:live_svelte, :ssr_module, MockSSRRenderer)
test_pid = self()
handler_id = {__MODULE__, :telemetry_test, make_ref()}
:telemetry.attach_many(
handler_id,
[
[:live_svelte, :ssr, :start],
[:live_svelte, :ssr, :stop],
[:live_svelte, :ssr, :exception]
],
&__MODULE__.handle_telemetry/4,
test_pid
)
on_exit(fn ->
:telemetry.detach(handler_id)
if original_module do
Application.put_env(:live_svelte, :ssr_module, original_module)
else
Application.delete_env(:live_svelte, :ssr_module)
end
end)
:ok
end
describe "SSR telemetry" do
test "emits :start event with component, props, and slots metadata" do
props = %{"count" => 42}
slots = %{"default" => "<span>slot content</span>"}
SSR.render("Success", props, slots)
assert_receive {:telemetry_event, [:live_svelte, :ssr, :start], _measurements, metadata}
assert metadata.component == "Success"
assert metadata.props == %{"count" => 42}
assert metadata.slots == %{"default" => "<span>slot content</span>"}
end
test "emits :stop event with duration measurement and passes through render result" do
result = SSR.render("Success", %{}, %{})
assert result == %{"head" => "", "html" => "<div>Hello</div>"}
assert_receive {:telemetry_event, [:live_svelte, :ssr, :stop], measurements, metadata}
assert metadata.component == "Success"
assert is_integer(measurements.duration)
assert measurements.duration >= 0
end
test "emits :exception event with metadata and duration when renderer raises" do
assert_raise RuntimeError, "SSR render failed", fn ->
SSR.render("Failing", %{}, %{})
end
assert_receive {:telemetry_event, [:live_svelte, :ssr, :exception], measurements, metadata}
assert metadata.component == "Failing"
assert is_integer(measurements.duration)
assert measurements.duration >= 0
end
test "emits independent telemetry events for each render call" do
SSR.render("Success", %{"call" => 1}, %{})
SSR.render("Success", %{"call" => 2}, %{})
assert_receive {:telemetry_event, [:live_svelte, :ssr, :start], _,
%{component: "Success", props: %{"call" => 1}}}
assert_receive {:telemetry_event, [:live_svelte, :ssr, :start], _,
%{component: "Success", props: %{"call" => 2}}}
end
end
end