Repository: Shopify/statsd-instrument (Ruby)

Issue:
trigger_statsd_increment does not support multiple calls with different tags
👋 , thansk for the great library

# Issue

My use-case is that I have a method which issues a bunch of `statsD` calls to the same metric with different tags. I then want to verify that this metric has correctly received the expected amount of calls, each with their respective tags. Here is a test I would expect to pass: 

```ruby
require 'statsd-instrument'

RSpec.configure do |config|
  config.include StatsD::Instrument::Matchers
end

RSpec.describe 'Matchers' do
  context 'trigger_statsd_increment' do
    it 'will pass if every statsD call matches its calls' do
      expect do
        StatsD.increment('counter', tags: ['variation:a'])
        StatsD.increment('counter', tags: ['variation:b'])
      end.to trigger_statsd_increment('counter', times: 2, tags: [['variation:a'], ['variation:b']])
    end
  end
end
```

However, today this test fails. 

https://github.com/Shopify/statsd-instrument/blob/5518d8de15c838b24a987871c7b54ccb90f3a7c3/lib/statsd/instrument/matchers.rb#L59
will test that for each call to `increment`, all the tags expected are present. We are not allowed to do different tags per calls. 

Thus, likewise 
```ruby
      end.to trigger_statsd_increment('counter', times: 2, tags: ['variation:a', 'variation:b'])
```
would not work either

Rewriting the test as 
```ruby
RSpec.describe 'Matchers' do
  context 'trigger_statsd_increment' do
    it 'will pass if every statsD call matches its calls' do
      expect do
        StatsD.increment('counter', tags: ['variation:a'])
        StatsD.increment('counter', tags: ['variation:b'])
      end.to trigger_statsd_increment('counter', times: 1, tags: ["variation:a"]).and trigger_statsd_increment('counter', times: 1, tags: ["variation:b"])
    end
  end
end
```
Does not pass either and fails with 
```
The numbers of StatsD calls for metric counter was unexpected. Expected 1, got 2
```
Indeed
https://github.com/Shopify/statsd-instrument/blob/5518d8de15c838b24a987871c7b54ccb90f3a7c3/lib/statsd/instrument/matchers.rb#L45-L53 
will first test for the metric name and return the error above as the error has not been matched enough times. The tags are not looked at for this to fail.

# Propositions

##  A: support `trigger_statsd_increment('counter', times: 2, tags: [['variation:a'], ['variation:b']])`

Here, each `trigger_statsd_increment` is linked by the metric name.

The `tags` would have 2 behaviors when present:
+ it is a list of lists, and then each element is supposed to match the call to `metric_name` **in the order those calls happened**
+ it is a list, and then we keep the behavior of today: all the calls for `metric_name` need to have tags match the unique list of expected tags. This would make the change retrocompatible (I don't expect anyone to use lists of list of tags)

##  B: support `trigger_statsd_increment('counter', times: 1, tags: ["variation:a"]).and trigger_statsd_increment('counter', times: 1, tags: ["variation:b"])`

When matching the `times`, we make sure that **both** the metric_name and the tags match.

The advantage I find to this is that it matches the API for `metric_name` matching where each `metric_name` is supposed to be assessed in different calls : 
```ruby
    it 'will pass if every statsD call matches its calls' do
        expect do
          StatsD.increment('counter')
          StatsD.increment('counter2')
        end.to trigger_statsd_increment('counter', times: 1).and trigger_statsd_increment('counter2', times: 1)
      end  
    end
```
That change would make it that different ((`metric_name`,`tags`) should be assessed in different calls).

# Implementation 

**I'm willing to implement those changes**, and favor proposition A. WDYT ?


Discussion:

Patch:
diff --git a/README.md b/README.md
index 6600ec88..62bfe2d1 100644
--- a/README.md
+++ b/README.md
@@ -355,6 +355,13 @@ RSpec.describe 'Matchers' do
     it 'will pass if there is no matching StatsD call on negative expectation' do
       expect { StatsD.increment('other_counter') }.not_to trigger_statsd_increment('counter')
     end
+
+    it 'will pass if every statsD call matches its call tag variations' do
+      expect do
+        StatsD.increment('counter', tags: ['variation:a'])
+        StatsD.increment('counter', tags: ['variation:b'])
+      end.to trigger_statsd_increment('counter', times: 1, tags: ["variation:a"]).and trigger_statsd_increment('counter', times: 1, tags: ["variation:b"])
+    end
   end
 end
 ```
diff --git a/lib/statsd/instrument/matchers.rb b/lib/statsd/instrument/matchers.rb
index ccc7e0e1..fd2fd900 100644
--- a/lib/statsd/instrument/matchers.rb
+++ b/lib/statsd/instrument/matchers.rb
@@ -43,7 +43,11 @@ def description
 
         def expect_statsd_call(metric_type, metric_name, options, &block)
           metrics = capture_statsd_calls(&block)
-          metrics = metrics.select { |m| m.type == metric_type && m.name == metric_name }
+          metrics = metrics.select do |m|
+            options_tags = options[:tags] || []
+            metric_tags = m.tags || []
+            m.type == metric_type && m.name == metric_name && metric_tags.all? { |t| options_tags.include?(t) }
+          end
 
           if metrics.empty?
             raise RSpec::Expectations::ExpectationNotMetError, "No StatsD calls for metric #{metric_name} were made."


Test patch:
diff --git a/test/matchers_test.rb b/test/matchers_test.rb
index 9e9a9bff..45317b72 100644
--- a/test/matchers_test.rb
+++ b/test/matchers_test.rb
@@ -34,6 +34,26 @@ def test_statsd_increment_compound_not_matched
     }))
   end
 
+  def test_statsd_increment_compound_using_and_matched
+    matcher_1 = StatsD::Instrument::Matchers::Increment.new(:c, "counter", times: 1, tags: ["a"])
+    matcher_2 = StatsD::Instrument::Matchers::Increment.new(:c, "counter", times: 1, tags: ["b"])
+
+    assert(matcher_1.and(matcher_2).matches?(lambda {
+      StatsD.increment("counter", tags: ["a"])
+      StatsD.increment("counter", tags: ["b"])
+    }))
+  end
+
+  def test_statsd_increment_compound_using_and_not_matched
+    matcher_1 = StatsD::Instrument::Matchers::Increment.new(:c, "counter", times: 1, tags: ["a"])
+    matcher_2 = StatsD::Instrument::Matchers::Increment.new(:c, "counter", times: 1, tags: ["b"])
+
+    refute(matcher_1.and(matcher_2).matches?(lambda {
+      StatsD.increment("counter", tags: ["a"])
+      StatsD.increment("counter", tags: ["c"])
+    }))
+  end
+
   def test_statsd_increment_with_times_matched
     assert(StatsD::Instrument::Matchers::Increment.new(:c, "counter", times: 1)
       .matches?(lambda { StatsD.increment("counter") }))