diff --git a/lib/customerio/client.rb b/lib/customerio/client.rb index 41fb315..44fc3d7 100644 --- a/lib/customerio/client.rb +++ b/lib/customerio/client.rb @@ -8,11 +8,21 @@ class IdentifierType end class Client - PUSH_OPENED = 'opened' - PUSH_CONVERTED = 'converted' - PUSH_DELIVERED = 'delivered' - - VALID_PUSH_EVENTS = [PUSH_OPENED, PUSH_CONVERTED, PUSH_DELIVERED] + DELIVERY_OPENED = 'opened' + DELIVERY_CONVERTED = 'converted' + DELIVERY_DELIVERED = 'delivered' + DELIVERY_BOUNCED = 'bounced' + DELIVERY_CLICKED = 'clicked' + DELIVERY_DEFERRED = 'deferred' + DELIVERY_DROPPED = 'dropped' + DELIVERY_SPAMMED = 'spammed' + + VALID_PUSH_EVENTS = [DELIVERY_OPENED, DELIVERY_CONVERTED, DELIVERY_DELIVERED] + + # The valid delivery events depend on the channel + # However, there is no way to validate the channel prior the API request + # https://customer.io/docs/api/track/#operation/metrics + VALID_DELIVERY_METRICS = VALID_PUSH_EVENTS + [DELIVERY_BOUNCED, DELIVERY_CLICKED, DELIVERY_DEFERRED, DELIVERY_DROPPED, DELIVERY_SPAMMED] class MissingIdAttributeError < RuntimeError; end class ParamError < RuntimeError; end @@ -90,6 +100,7 @@ def delete_device(customer_id, device_id) @client.request_and_verify_response(:delete, device_id_path(customer_id, device_id)) end + # Customer.io deprecated per https://customer.io/docs/api/track/#operation/pushMetrics def track_push_notification_event(event_name, attributes = {}) keys = [:delivery_id, :device_id, :timestamp] attributes = Hash[attributes.map { |(k,v)| [ k.to_sym, v ] }]. @@ -103,6 +114,19 @@ def track_push_notification_event(event_name, attributes = {}) @client.request_and_verify_response(:post, track_push_notification_event_path, attributes.merge(event: event_name)) end + def track_delivery_metric(metric_name, attributes = {}) + keys = [:delivery_id, :timestamp, :recipient, :reason, :href] + attributes = Hash[attributes.map { |(k,v)| [ k.to_sym, v ] }]. + select { |k, v| keys.include?(k) } + + raise ParamError.new('metric_name must be one of bounced, clicked, converted, deferred, delivered, dropped, opened, and spammed') unless VALID_DELIVERY_METRICS.include?(metric_name) + raise ParamError.new('delivery_id must be a non-empty string') unless attributes[:delivery_id] != "" and !attributes[:delivery_id].nil? + raise ParamError.new('timestamp must be a valid timestamp') if attributes[:timestamp] && !valid_timestamp?(attributes[:timestamp]) + raise ParamError.new('href must be a valid url') if attributes[:href] && !valid_url?(attributes[:href].present?) + + @client.request_and_verify_response(:post, track_delivery_metric_path, attributes.merge(metric: metric_name)) + end + def merge_customers(primary_id_type, primary_id, secondary_id_type, secondary_id) raise ParamError.new("invalid primary_id_type") if !is_valid_id_type?(primary_id_type) raise ParamError.new("primary_id must be a non-empty string") if is_empty?(primary_id) @@ -142,7 +166,11 @@ def unsuppress_path(customer_id) end def track_push_notification_event_path - "/push/events" + "/push/events" + end + + def track_delivery_metric_path + "/api/v1/metrics" end def merge_customers_path @@ -221,6 +249,12 @@ def valid_timestamp?(timestamp) timestamp && timestamp.is_a?(Integer) && timestamp > 999999999 && timestamp < 100000000000 end + def valid_url?(url) + %w[http https].include?(Addressable::URI.parse(url)&.scheme) + rescue Addressable::URI::InvalidURIError + false + end + def is_empty?(val) val.nil? || (val.is_a?(String) && val.strip == "") end diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 27ba3db..a35e6f4 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -685,6 +685,72 @@ def json(data) end end + describe "#track_delivery_metric" do + attr_accessor :client, :attributes + + before(:each) do + @client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true) + @attributes = { + :delivery_id => 'foo', + :timestamp => Time.now.to_i, + :href => nil, + :recipient => nil, + :reason => nil, + } + end + + it "sends a POST request to customer.io's /api/v1/metrics endpoint" do + stub_request(:post, api_uri('/api/v1/metrics')). + with( + :body => json(attributes.merge({ + :metric => 'opened' + })), + :headers => { + 'Content-Type' => 'application/json' + }). + to_return(:status => 200, :body => "", :headers => {}) + + client.track_delivery_metric('opened', attributes) + end + + it "should raise if event is invalid" do + stub_request(:post, api_uri('/api/v1/metrics')). + to_return(:status => 200, :body => "", :headers => {}) + + expect { + client.track_delivery_metric('closed', attributes.merge({ :delivery_id => nil })) + }.to raise_error(Customerio::Client::ParamError, 'metric_name must be one of bounced, clicked, converted, deferred, delivered, dropped, opened, and spammed') + end + + it "should raise if delivery_id is invalid" do + stub_request(:post, api_uri('/api/v1/metrics')). + to_return(:status => 200, :body => "", :headers => {}) + + expect { + client.track_delivery_metric('opened', attributes.merge({ :delivery_id => nil })) + }.to raise_error(Customerio::Client::ParamError, 'delivery_id must be a non-empty string') + + expect { + client.track_delivery_metric('opened', attributes.merge({ :delivery_id => '' })) + }.to raise_error(Customerio::Client::ParamError, 'delivery_id must be a non-empty string') + end + + it "should raise if timestamp is invalid" do + stub_request(:post, api_uri('/api/v1/metrics')). + to_return(:status => 200, :body => "", :headers => {}) + + client.track_delivery_metric('opened', attributes.merge({ :timestamp => nil })) + + expect { + client.track_delivery_metric('opened', attributes.merge({ :timestamp => 999999999 })) + }.to raise_error(Customerio::Client::ParamError, 'timestamp must be a valid timestamp') + + expect { + client.track_delivery_metric('opened', attributes.merge({ :timestamp => 100000000000 })) + }.to raise_error(Customerio::Client::ParamError, 'timestamp must be a valid timestamp') + end + end + describe "#merge_customers" do before(:each) do @client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true)