Skip to content

Commit 9300e43

Browse files
solnicclaude
andcommitted
feat(test): add assert_sentry_metric/2 assertion helper
Add a dedicated assert_sentry_metric/2 helper that mirrors assert_sentry_log/3 for metrics. Uses find semantics so tests succeed even when multiple metrics are emitted together (a common pattern), and returns unmatched metrics to an inbox for subsequent assertions in the same test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ab9b0b9 commit 9300e43

2 files changed

Lines changed: 141 additions & 0 deletions

File tree

lib/sentry/test/assertions.ex

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ defmodule Sentry.Test.Assertions do
2424
assert_sentry_log(:info, "User session started")
2525
assert_sentry_log(:info, ~r/session started/, trace_id: "abc123")
2626
27+
Use the metric shorthand (find semantics — works with multiple co-emitted metrics):
28+
29+
assert_sentry_metric(:counter, name: "button.clicks")
30+
assert_sentry_metric(:distribution, name: "response.time")
31+
2732
Find a specific event among many:
2833
2934
events = Sentry.Test.pop_sentry_reports()
@@ -44,6 +49,7 @@ defmodule Sentry.Test.Assertions do
4449
4550
assert_sentry_report(:log, [level: :info, body: "hi"], timeout: 2000)
4651
assert_sentry_log(:info, "hi", timeout: 2000)
52+
assert_sentry_metric(:counter, name: "clicks", timeout: 2000)
4753
4854
"""
4955
@moduledoc since: "13.0.0"
@@ -179,6 +185,49 @@ defmodule Sentry.Test.Assertions do
179185
match || flunk(format_find_error(logs, criteria, "log"))
180186
end
181187

188+
@doc """
189+
Asserts that a metric was captured matching the given type and criteria.
190+
191+
Awaits asynchronously-captured metrics: the pipeline is flushed and the
192+
collector is polled until a metric matching the criteria is found or the
193+
timeout elapses (default `#{1000}ms`, overridable via the `:timeout`
194+
reserved key in `criteria`).
195+
196+
Uses find semantics (not assert-exactly-1), so this succeeds even when
197+
multiple metrics were emitted together — as is common when a single
198+
request records several measurements.
199+
200+
Unmatched metrics are returned to an inbox so that multiple successive
201+
`assert_sentry_metric/2` calls in the same test each see a clean slate.
202+
203+
Returns the matched metric.
204+
205+
## Examples
206+
207+
assert_sentry_metric(:counter, name: "button.clicks")
208+
assert_sentry_metric(:distribution, name: "response.time", value: 42.5)
209+
assert_sentry_metric(:gauge, name: "memory.usage", attributes: %{pool: "main"})
210+
assert_sentry_metric(:counter, name: "requests", timeout: 2000)
211+
212+
"""
213+
@doc since: "13.0.0"
214+
@spec assert_sentry_metric(:counter | :distribution | :gauge, keyword()) :: Sentry.Metric.t()
215+
def assert_sentry_metric(type, criteria \\ [])
216+
when type in [:counter, :distribution, :gauge] do
217+
{timeout, criteria} = Keyword.pop(criteria, :timeout, @default_timeout)
218+
criteria = [type: type] ++ criteria
219+
220+
metrics =
221+
await_items(:metric, timeout, fn items ->
222+
Enum.any?(items, &matches_criteria?(&1, criteria))
223+
end)
224+
225+
{match, remaining} = extract_first_match(metrics, criteria)
226+
put_inbox(:metric, remaining)
227+
228+
match || flunk(format_find_error(metrics, criteria, "metric"))
229+
end
230+
182231
@doc """
183232
Finds the first item in `items` that matches all `criteria`.
184233

test/sentry/test/assertions_test.exs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,4 +456,96 @@ defmodule Sentry.Test.AssertionsTest do
456456
:ets.insert(table, {System.unique_integer([:monotonic]), metric})
457457
metric
458458
end
459+
460+
describe "assert_sentry_metric/2" do
461+
setup do
462+
SentryTest.setup_sentry()
463+
end
464+
465+
test "finds metric by type when multiple metrics exist" do
466+
insert_metric(type: :counter, name: "clicks", value: 5)
467+
insert_metric(type: :distribution, name: "response.time", value: 42.5)
468+
469+
metric = assert_sentry_metric(:counter, name: "clicks")
470+
assert %Sentry.Metric{} = metric
471+
assert metric.type == :counter
472+
assert metric.name == "clicks"
473+
assert metric.value == 5
474+
end
475+
476+
test "uses find semantics - succeeds when target is among many" do
477+
insert_metric(type: :counter, name: "sync.read.entities", value: 1)
478+
insert_metric(type: :distribution, name: "sync.read.entities.count", value: 10)
479+
480+
metric = assert_sentry_metric(:distribution, name: "sync.read.entities.count")
481+
assert metric.value == 10
482+
end
483+
484+
test "returns unmatched metrics to inbox for subsequent assertions" do
485+
insert_metric(type: :counter, name: "first.metric", value: 1)
486+
insert_metric(type: :distribution, name: "second.metric", value: 99.9)
487+
488+
assert_sentry_metric(:counter, name: "first.metric")
489+
# Second assertion must still find the distribution metric
490+
assert_sentry_metric(:distribution, name: "second.metric")
491+
end
492+
493+
test "matches additional criteria beyond type" do
494+
insert_metric(type: :counter, name: "button.clicks", value: 1,
495+
attributes: %{button_id: "submit"})
496+
insert_metric(type: :counter, name: "button.clicks", value: 1,
497+
attributes: %{button_id: "cancel"})
498+
499+
metric = assert_sentry_metric(:counter,
500+
name: "button.clicks",
501+
attributes: %{button_id: "submit"}
502+
)
503+
assert metric.attributes[:button_id] == "submit"
504+
end
505+
506+
test "fails when no matching metric found" do
507+
insert_metric(type: :counter, name: "other.metric")
508+
509+
assert_raise ExUnit.AssertionError, ~r/No matching Sentry metric found/, fn ->
510+
assert_sentry_metric(:gauge, name: "nonexistent", timeout: 10)
511+
end
512+
end
513+
514+
test "fails when type doesn't match" do
515+
insert_metric(type: :counter, name: "my.metric")
516+
517+
assert_raise ExUnit.AssertionError, ~r/No matching Sentry metric found/, fn ->
518+
assert_sentry_metric(:gauge, name: "my.metric", timeout: 10)
519+
end
520+
end
521+
522+
test "respects :timeout option" do
523+
before = System.monotonic_time(:millisecond)
524+
525+
assert_raise ExUnit.AssertionError, ~r/No matching Sentry metric found/, fn ->
526+
assert_sentry_metric(:counter, name: "missing", timeout: 50)
527+
end
528+
529+
elapsed = System.monotonic_time(:millisecond) - before
530+
assert elapsed < 500, "expected fast failure, waited #{elapsed}ms"
531+
end
532+
533+
test "awaits async metrics" do
534+
table = Process.get(:sentry_test_collector)
535+
536+
Task.start(fn ->
537+
Process.sleep(30)
538+
metric = struct!(Sentry.Metric,
539+
type: :counter,
540+
name: "async.metric",
541+
value: 1,
542+
timestamp: System.system_time(:nanosecond) / 1_000_000_000
543+
)
544+
:ets.insert(table, {System.unique_integer([:monotonic]), metric})
545+
end)
546+
547+
metric = assert_sentry_metric(:counter, name: "async.metric", timeout: 500)
548+
assert metric.name == "async.metric"
549+
end
550+
end
459551
end

0 commit comments

Comments
 (0)