From ffa633407eba7c6982d3e5fb0f992ab824868bfd Mon Sep 17 00:00:00 2001 From: Jeremy Gottfried Date: Thu, 4 May 2023 18:13:29 -0400 Subject: [PATCH 1/2] add trigger broadcast method --- README.md | 53 +++++++++ lib/customerio.rb | 1 + lib/customerio/api.rb | 22 ++++ .../requests/trigger_broadcast_request.rb | 41 +++++++ spec/api_client_spec.rb | 109 ++++++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 lib/customerio/requests/trigger_broadcast_request.rb diff --git a/README.md b/README.md index 0a15fb2..f919f60 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,59 @@ 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 +) + +file = File.open('', 'r') +request.attach("filename", file.read) + +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 95f33f6..1f5f3d7 100644 --- a/lib/customerio.rb +++ b/lib/customerio.rb @@ -5,6 +5,7 @@ module Customerio require "customerio/base_client" require "customerio/client" require "customerio/requests/send_email_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 2530f76..9563002 100644 --- a/lib/customerio/api.rb +++ b/lib/customerio/api.rb @@ -26,10 +26,32 @@ def send_email(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 "/v1/send/email" 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 a7a0db6..02afd0c 100644 --- a/spec/api_client_spec.rb +++ b/spec/api_client_spec.rb @@ -169,4 +169,113 @@ def json(data) req.message[:attachments].should eq({ "test" => Base64.strict_encode64("test-content") }) 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 From 31d041f43378e77ac117bd2855aa5061b9dd817a Mon Sep 17 00:00:00 2001 From: Jeremy Gottfried Date: Wed, 17 May 2023 14:23:44 -0400 Subject: [PATCH 2/2] no attachment on broadcast --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 7c80a07..248ee0a 100644 --- a/README.md +++ b/README.md @@ -344,9 +344,6 @@ request = Customerio::TriggerBroadcastRequest.new( payload: payload ) -file = File.open('', 'r') -request.attach("filename", file.read) - begin response = client.trigger_broadcast(request) puts response