Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions lib/sentry/test/assertions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ defmodule Sentry.Test.Assertions do
assert_sentry_log(:info, "User session started")
assert_sentry_log(:info, ~r/session started/, trace_id: "abc123")

Use the metric shorthand (find semantics — works with multiple co-emitted metrics):

assert_sentry_metric(:counter, name: "button.clicks")
assert_sentry_metric(:distribution, name: "response.time")

Find a specific event among many:

events = Sentry.Test.pop_sentry_reports()
Expand All @@ -44,6 +49,7 @@ defmodule Sentry.Test.Assertions do

assert_sentry_report(:log, [level: :info, body: "hi"], timeout: 2000)
assert_sentry_log(:info, "hi", timeout: 2000)
assert_sentry_metric(:counter, name: "clicks", timeout: 2000)

"""
@moduledoc since: "13.0.0"
Expand Down Expand Up @@ -179,6 +185,49 @@ defmodule Sentry.Test.Assertions do
match || flunk(format_find_error(logs, criteria, "log"))
end

@doc """
Asserts that a metric was captured matching the given type and criteria.

Awaits asynchronously-captured metrics: the pipeline is flushed and the
collector is polled until a metric matching the criteria is found or the
timeout elapses (default `#{1000}ms`, overridable via the `:timeout`
reserved key in `criteria`).

Uses find semantics (not assert-exactly-1), so this succeeds even when
multiple metrics were emitted together — as is common when a single
request records several measurements.

Unmatched metrics are returned to an inbox so that multiple successive
`assert_sentry_metric/2` calls in the same test each see a clean slate.

Returns the matched metric.

## Examples

assert_sentry_metric(:counter, name: "button.clicks")
assert_sentry_metric(:distribution, name: "response.time", value: 42.5)
assert_sentry_metric(:gauge, name: "memory.usage", attributes: %{pool: "main"})
assert_sentry_metric(:counter, name: "requests", timeout: 2000)

"""
@doc since: "13.0.0"
@spec assert_sentry_metric(:counter | :distribution | :gauge, keyword()) :: Sentry.Metric.t()
def assert_sentry_metric(type, criteria \\ [])
when type in [:counter, :distribution, :gauge] do
{timeout, criteria} = Keyword.pop(criteria, :timeout, @default_timeout)
criteria = [type: type] ++ criteria

metrics =
await_items(:metric, timeout, fn items ->
Enum.any?(items, &matches_criteria?(&1, criteria))
end)

{match, remaining} = extract_first_match(metrics, criteria)
put_inbox(:metric, remaining)

match || flunk(format_find_error(metrics, criteria, "metric"))
end

@doc """
Finds the first item in `items` that matches all `criteria`.

Expand Down
106 changes: 106 additions & 0 deletions test/sentry/test/assertions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -456,4 +456,110 @@ defmodule Sentry.Test.AssertionsTest do
:ets.insert(table, {System.unique_integer([:monotonic]), metric})
metric
end

describe "assert_sentry_metric/2" do
setup do
SentryTest.setup_sentry()
end

test "finds metric by type when multiple metrics exist" do
insert_metric(type: :counter, name: "clicks", value: 5)
insert_metric(type: :distribution, name: "response.time", value: 42.5)

metric = assert_sentry_metric(:counter, name: "clicks")
assert %Sentry.Metric{} = metric
assert metric.type == :counter
assert metric.name == "clicks"
assert metric.value == 5
end

test "uses find semantics - succeeds when target is among many" do
insert_metric(type: :counter, name: "sync.read.entities", value: 1)
insert_metric(type: :distribution, name: "sync.read.entities.count", value: 10)

metric = assert_sentry_metric(:distribution, name: "sync.read.entities.count")
assert metric.value == 10
end

test "returns unmatched metrics to inbox for subsequent assertions" do
insert_metric(type: :counter, name: "first.metric", value: 1)
insert_metric(type: :distribution, name: "second.metric", value: 99.9)

assert_sentry_metric(:counter, name: "first.metric")
# Second assertion must still find the distribution metric
assert_sentry_metric(:distribution, name: "second.metric")
end

test "matches additional criteria beyond type" do
insert_metric(
type: :counter,
name: "button.clicks",
value: 1,
attributes: %{button_id: "submit"}
)

insert_metric(
type: :counter,
name: "button.clicks",
value: 1,
attributes: %{button_id: "cancel"}
)

metric =
assert_sentry_metric(:counter,
name: "button.clicks",
attributes: %{button_id: "submit"}
)

assert metric.attributes[:button_id] == "submit"
end

test "fails when no matching metric found" do
insert_metric(type: :counter, name: "other.metric")

assert_raise ExUnit.AssertionError, ~r/No matching Sentry metric found/, fn ->
assert_sentry_metric(:gauge, name: "nonexistent", timeout: 10)
end
end

test "fails when type doesn't match" do
insert_metric(type: :counter, name: "my.metric")

assert_raise ExUnit.AssertionError, ~r/No matching Sentry metric found/, fn ->
assert_sentry_metric(:gauge, name: "my.metric", timeout: 10)
end
end

test "respects :timeout option" do
before = System.monotonic_time(:millisecond)

assert_raise ExUnit.AssertionError, ~r/No matching Sentry metric found/, fn ->
assert_sentry_metric(:counter, name: "missing", timeout: 50)
end

elapsed = System.monotonic_time(:millisecond) - before
assert elapsed < 500, "expected fast failure, waited #{elapsed}ms"
end

test "awaits async metrics" do
table = Process.get(:sentry_test_collector)

Task.start(fn ->
Process.sleep(30)

metric =
struct!(Sentry.Metric,
type: :counter,
name: "async.metric",
value: 1,
timestamp: System.system_time(:nanosecond) / 1_000_000_000
)

:ets.insert(table, {System.unique_integer([:monotonic]), metric})
end)

metric = assert_sentry_metric(:counter, name: "async.metric", timeout: 500)
assert metric.name == "async.metric"
end
end
end
Loading