dav: tag a refusal with the card it names
The fingerprint still folds the id, so one issue holds the lot;
the card now rides along as a tag, to search which ones were caught.

Assisted-by: Claude Opus 5 via Claude Code
change rqrymuyxqtptolvsrnyymwlmxkvuqrzk
commit f727ba866dd0b08a24e67c6053da22bd03ae67c4
author Alpha Chen <alpha@kejadlen.dev>
date
parent qkmqnqvw
diff --git a/lib/pro_tacts/refusal_alerts.rb b/lib/pro_tacts/refusal_alerts.rb
index 33fb400..ff53ca7 100644
--- a/lib/pro_tacts/refusal_alerts.rb
+++ b/lib/pro_tacts/refusal_alerts.rb
@@ -34,16 +34,21 @@ module ProTacts
     private
 
     # Fingerprinted on the route with the card id replaced, so Sentry
-    # groups a refusal that repeats across cards as one issue.
+    # groups a refusal that repeats across cards as one issue. The id
+    # rides as the `card` tag instead, so an issue's events can be
+    # searched by card.
     #: (Rack::env env, Integer status) -> void
     def alert(env, status)
       method = env.fetch("REQUEST_METHOD")
-      route = env["PATH_INFO"].to_s.sub(%r{/[^/]+\.vcf\z}, "/{id}.vcf")
+      path = env["PATH_INFO"].to_s
+      card = path[%r{/([^/]+)\.vcf\z}, 1]
+      route = path.sub(%r{/[^/]+\.vcf\z}, "/{id}.vcf")
 
       Sentry.capture_message(
         "#{method} #{route} answered #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}",
         level: :warning,
         fingerprint: [method, route, status.to_s],
+        tags: card ? { card: } : {},
       )
     end
   end
diff --git a/sig/gems/sentry.rbs b/sig/gems/sentry.rbs
index f1b8de7..0618afc 100644
--- a/sig/gems/sentry.rbs
+++ b/sig/gems/sentry.rbs
@@ -4,7 +4,7 @@
 # and the photo decode refusal contact.rb captures. ExchangeLog tags the
 # request's scope and reads whether anything was reported under it.
 module Sentry
-  def self.capture_message: (String message, ?level: Symbol, ?fingerprint: Array[String]) -> untyped
+  def self.capture_message: (String message, ?level: Symbol, ?fingerprint: Array[String], ?tags: Hash[Symbol, String]) -> untyped
 
   def self.capture_exception: (Exception exception, ?level: Symbol) -> untyped
 
diff --git a/test/pro_tacts/test_refusal_alerts.rb b/test/pro_tacts/test_refusal_alerts.rb
index 1667c59..be421a2 100644
--- a/test/pro_tacts/test_refusal_alerts.rb
+++ b/test/pro_tacts/test_refusal_alerts.rb
@@ -56,6 +56,15 @@ class RefusalAlertsTest < Minitest::Test
     assert_equal "PUT /dav/addressbook/{id}.vcf answered 412 Precondition Failed", sentry_messages.first
   end
 
+  def test_the_card_a_refusal_names_is_a_tag
+    @status = 412
+
+    dav "/dav/addressbook/aiden.vcf"
+    dav "/dav/addressbook/"
+
+    assert_equal ["aiden", nil], sentry_events.map { it.tags[:card] }
+  end
+
   def test_the_method_and_status_split_groups
     @status = 412
     dav "/dav/addressbook/aiden.vcf", method: "PUT"