diff --git a/README.md b/README.md index b438fda..248ee0a 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,56 @@ rescue Customerio::InvalidResponse => e end ``` +### Trigger Broadcasts + +To use the Customer.io [Trigger Broadcast API](https://www.customer.io/docs/api/app/#operation/triggerBroadcast), create an instance of the API client using an [app key](https://customer.io/docs/managing-credentials#app-api-keys). + +Create a new `TriggerBroadcastRequest` object containing: + +* `broadcast_id`: the ID of the broadcast you want to trigger. + +Optionally, add a `payload` object with any of the following: +* one of the audience options to override the default audience: `recipients | emails | ids | per_user_data | data_file_url`. +* `data`: an object containing information you want to use to populate your broadcast. +* `email_add_duplicates`: a boolean indicating if an email address associated with more than one profile id is an error (default: false). +* `email_ignore_missing`: if false, a missing email address is an error (default: false). +* `id_ignore_missing`: if false, a missing customer ID is an error (default: false). + +Use `trigger_broadcast` referencing your request to trigger the broadcast. [Learn more about triggering broadcasts and `TriggerBroadcastRequest` properties](https://customer.io/docs/api-triggered-broadcasts/). + +```ruby +require "customerio" + +client = Customerio::APIClient.new("your API key", region: Customerio::Regions::US) + +payload = { + emails: [ + "recipient1@example.com", + "anotherRecipient@example.com" + ], + data: { + headline: "Roadrunner spotted in Albuquerque!", + date: 1511315635, + text: "We received reports of a roadrunner in your immediate area! Head to your dashboard to view more information!" + }, + email_add_duplicates: false, + email_ignore_missing: false, + id_ignore_missing: false +} + +request = Customerio::TriggerBroadcastRequest.new( + broadcast_id: 12345, + payload: payload +) + +begin + response = client.trigger_broadcast(request) + puts response +rescue Customerio::InvalidResponse => e + puts e.code, e.message +end +``` + ## Contributing 1. Fork it diff --git a/lib/customerio.rb b/lib/customerio.rb index e824c5b..0982bd9 100644 --- a/lib/customerio.rb +++ b/lib/customerio.rb @@ -6,6 +6,7 @@ module Customerio require "customerio/client" require "customerio/requests/send_email_request" require "customerio/requests/send_push_request" + require "customerio/requests/trigger_broadcast_request" require "customerio/api" require "customerio/param_encoder" end diff --git a/lib/customerio/api.rb b/lib/customerio/api.rb index 97394b1..6f7bcd6 100644 --- a/lib/customerio/api.rb +++ b/lib/customerio/api.rb @@ -41,6 +41,24 @@ def send_push(req) end end + def trigger_broadcast(req) + unless req.is_a?(Customerio::TriggerBroadcastRequest) + raise 'request must be an instance of Customerio::TriggerBroadcastRequest' + end + + response = @client.request(:post, trigger_broadcast_path(req.broadcast_id), req.payload) + + case response + when Net::HTTPSuccess + JSON.parse(response.body) + when Net::HTTPBadRequest + json = JSON.parse(response.body) + raise Customerio::InvalidResponse.new(response.code, json['meta']['error'], response) + else + raise InvalidResponse.new(response.code, response.body) + end + end + private def send_email_path @@ -50,5 +68,9 @@ def send_email_path def send_push_path "/v1/send/push" end + + def trigger_broadcast_path(broadcast_id) + "/v1/campaigns/#{broadcast_id}/triggers" + end end end diff --git a/lib/customerio/requests/trigger_broadcast_request.rb b/lib/customerio/requests/trigger_broadcast_request.rb new file mode 100644 index 0000000..c7699f4 --- /dev/null +++ b/lib/customerio/requests/trigger_broadcast_request.rb @@ -0,0 +1,41 @@ +module Customerio + class TriggerBroadcastRequest + attr_reader :broadcast_id, :payload + + def initialize(broadcast_id:, payload:{}) + @broadcast_id = broadcast_id + @payload = payload.delete_if { |field| invalid_field?(field) } + + validate_broadcast_id + validate_xor_recipients + end + + private + + OPTIONAL_FIELDS = [:data, :email_add_duplicates, :email_ignore_missing, :id_ignore_missing].freeze + + # we're not validating the structure, just that only one is present + ONLY_ONE_ALLOWED = [ + :recipients, + :emails, + :ids, + :per_user_data, + :data_file_url + ].freeze + + def invalid_field?(field) + !OPTIONAL_FIELDS.include?(field) && !ONLY_ONE_ALLOWED.include?(field) + end + + def validate_broadcast_id + raise 'broadcast id is required' unless broadcast_id + raise 'broadcast id must be an integer' unless broadcast_id.is_a?(Integer) + end + + def validate_xor_recipients + present = ONLY_ONE_ALLOWED.select { |field| payload.key?(field) } + + raise "Only one of #{arr.join(', ')} can be present" if present.length > 1 + end + end +end diff --git a/spec/api_client_spec.rb b/spec/api_client_spec.rb index fac44ab..6b78538 100644 --- a/spec/api_client_spec.rb +++ b/spec/api_client_spec.rb @@ -252,4 +252,113 @@ def json(data) client.send_push(req).should eq({ "delivery_id" => 2 }) end end + + describe '#trigger_broadcast' do + it "sends a POST request to the customer.io's broadcast API" do + payload = { + data: { name: 'foo' }, + recipients: { + segment: { id: 7 } + } + } + req = Customerio::TriggerBroadcastRequest.new(broadcast_id: 1, payload: payload) + + stub_request(:post, api_uri('/v1/campaigns/1/triggers')) + .with(headers: request_headers, body: req.payload) + .to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {}) + + expect(client.trigger_broadcast(req)).to eq({ 'delivery_id' => 1 }) + end + + it "handles validation failures (400)" do + payload = { + data: { name: 'foo' }, + emails: ['foo', 'bar'], + email_ignore_missing: true, + email_add_duplicates: true + } + req = Customerio::TriggerBroadcastRequest.new(broadcast_id: 1, payload: payload) + + err_json = { meta: { error: "example error" } }.to_json + + stub_request(:post, api_uri('/v1/campaigns/1/triggers')) + .with(headers: request_headers, body: req.payload) + .to_return(status: 400, body: err_json, headers: {}) + + lambda { client.trigger_broadcast(req) }.should( + raise_error(Customerio::InvalidResponse) { |error| + error.message.should eq "example error" + error.code.should eq "400" + } + ) + end + + it "handles other failures (5xx)" do + payload = { + data: { name: 'foo' }, + emails: ['foo', 'bar'], + email_ignore_missing: true, + email_add_duplicates: true + } + req = Customerio::TriggerBroadcastRequest.new(broadcast_id: 1, payload: payload) + + stub_request(:post, api_uri('/v1/campaigns/1/triggers')) + .with(headers: request_headers, body: req.payload) + .to_return(status: 500, body: "Server unavailable", headers: {}) + + lambda { client.trigger_broadcast(req) }.should( + raise_error(Customerio::InvalidResponse) { |error| + error.message.should eq "Server unavailable" + error.code.should eq "500" + } + ) + end + + it 'supports campaign triggers based on email fields' do + payload = { + data: { name: 'foo' }, + emails: ['foo', 'bar'], + email_ignore_missing: true, + email_add_duplicates: true + } + req = Customerio::TriggerBroadcastRequest.new(broadcast_id: 1, payload: payload) + + stub_request(:post, api_uri('/v1/campaigns/1/triggers')) + .with(headers: request_headers, body: req.payload) + .to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {}) + + + expect(client.trigger_broadcast(req)).to eq({ 'delivery_id' => 1 }) + end + + it 'supports campaign triggers based on id fields' do + payload = { + data: { name: 'foo' }, + ids: [1, 2, 3], + id_ignore_missing: true + } + req = Customerio::TriggerBroadcastRequest.new(broadcast_id: 1, payload: payload) + + stub_request(:post, api_uri('/v1/campaigns/1/triggers')) + .with(headers: request_headers, body: req.payload) + .to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {}) + + expect(client.trigger_broadcast(req)).to eq({ 'delivery_id' => 1 }) + end + + it 'supports campaign triggers based on per user data' do + user_data = { id: 1, data: { name: 'foo' } } + payload = { + data: { name: 'foo' }, + per_user_data: [user_data] + } + req = Customerio::TriggerBroadcastRequest.new(broadcast_id: 1, payload: payload) + + stub_request(:post, api_uri('/v1/campaigns/1/triggers')) + .with(headers: request_headers, body: req.payload) + .to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {}) + + expect(client.trigger_broadcast(req)).to eq({ 'delivery_id' => 1 }) + end + end end