Repository: Shopify/statsd-instrument (Ruby)

Issue:
Prefix cannot be removed once set
The documentation for the `prefix` option states

> The `prefix` can be overridden by any metric call by setting the
>   `no_prefix` keyword argument to `true`. We recommend against doing this,
>   but this behavior is retained for backwards compatibility.
>   Rather, when you feel the need to do this, we recommend instantiating
>   a new client without prefix (using `clone_with_options`), and using it
>   to emit the metric.

However, this does not work due to the way `clone_with_options` works:

```ruby
def clone_with_options(prefix: nil, ...)
  self.class.new(prefix: prefix || @prefix, ...)
end
```

So calling `clone_with_options(prefix: nil)` will maintain the same prefix as before.


Discussion:

Patch:
diff --git a/lib/statsd/instrument/client.rb b/lib/statsd/instrument/client.rb
index 8d7e1deb..b9207b50 100644
--- a/lib/statsd/instrument/client.rb
+++ b/lib/statsd/instrument/client.rb
@@ -378,6 +378,8 @@ def event(title, text, timestamp: nil, hostname: nil, aggregation_key: nil, prio
         ))
       end
 
+      NO_CHANGE = Object.new
+
       # Instantiates a new StatsD client that uses the settings of the current client,
       # except for the provided overrides.
       #
@@ -386,11 +388,11 @@ def event(title, text, timestamp: nil, hostname: nil, aggregation_key: nil, prio
       #   will be disposed after the block returns
       # @return The return value of the block will be passed on as return value.
       def with_options(
-        sink: nil,
-        prefix: nil,
-        default_sample_rate: nil,
-        default_tags: nil,
-        datagram_builder_class: nil
+        sink: NO_CHANGE,
+        prefix: NO_CHANGE,
+        default_sample_rate: NO_CHANGE,
+        default_tags: NO_CHANGE,
+        datagram_builder_class: NO_CHANGE
       )
         client = clone_with_options(
           sink: sink,
@@ -404,18 +406,19 @@ def with_options(
       end
 
       def clone_with_options(
-        sink: nil,
-        prefix: nil,
-        default_sample_rate: nil,
-        default_tags: nil,
-        datagram_builder_class: nil
+        sink: NO_CHANGE,
+        prefix: NO_CHANGE,
+        default_sample_rate: NO_CHANGE,
+        default_tags: NO_CHANGE,
+        datagram_builder_class: NO_CHANGE
       )
         self.class.new(
-          sink: sink || @sink,
-          prefix: prefix || @prefix,
-          default_sample_rate: default_sample_rate || @default_sample_rate,
-          default_tags: default_tags || @default_tags,
-          datagram_builder_class: datagram_builder_class || @datagram_builder_class,
+          sink: sink == NO_CHANGE ? @sink : sink,
+          prefix: prefix == NO_CHANGE ? @prefix : prefix,
+          default_sample_rate: default_sample_rate == NO_CHANGE ? @default_sample_rate : default_sample_rate,
+          default_tags: default_tags == NO_CHANGE ? @default_tags : default_tags,
+          datagram_builder_class:
+            datagram_builder_class == NO_CHANGE ? @datagram_builder_class : datagram_builder_class,
         )
       end
 
diff --git a/lib/statsd/instrument/rubocop.rb b/lib/statsd/instrument/rubocop.rb
index d644fdf4..66ca862d 100644
--- a/lib/statsd/instrument/rubocop.rb
+++ b/lib/statsd/instrument/rubocop.rb
@@ -51,7 +51,7 @@ def has_keyword_argument?(node, sym)
       end
 
       def keyword_arguments(node)
-        return nil if node.arguments.empty?
+        return if node.arguments.empty?
 
         last_argument = if node.arguments.last&.type == :block_pass
           node.arguments[node.arguments.length - 2]


Test patch:
diff --git a/test/client_test.rb b/test/client_test.rb
index ac516e73..86cf3986 100644
--- a/test/client_test.rb
+++ b/test/client_test.rb
@@ -223,4 +223,18 @@ def test_clone_with_prefix_option
     original_client.increment("metric")
     client_with_other_options.increment("metric")
   end
+
+  def test_clone_can_remove_prefix
+    # Both clients will use the same sink.
+    mock_sink = mock("sink")
+    mock_sink.stubs(:sample?).returns(true)
+    mock_sink.expects(:<<).with("foo.metric:1|c").returns(mock_sink)
+    mock_sink.expects(:<<).with("metric:1|c").returns(mock_sink)
+
+    original_client = StatsD::Instrument::Client.new(sink: mock_sink, prefix: "foo")
+    client_with_other_options = original_client.clone_with_options(prefix: nil)
+
+    original_client.increment("metric")
+    client_with_other_options.increment("metric")
+  end
 end