customerio 2.2.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,17 +1,7 @@
1
1
  ---
2
- !binary "U0hBMjU2":
3
- metadata.gz: !binary |-
4
- NGJmMGRlMjNmNzZmNzFiZDI5NjM4YzRhNTczOWY1MzUzY2Y1MzVhMmMwZDg3
5
- YTcyNTM4NDY5MjM4OTBiOTNlOA==
6
- data.tar.gz: !binary |-
7
- NDE5YWFjYmM4NjQxYWU2YThhZDNhYzM0ZDdmOTU2OWExNWZmODM4YjEwY2Ri
8
- ZTZlOWIzNzU5ZDUyZmY4ZDg5Yg==
2
+ SHA256:
3
+ metadata.gz: bd807525a01387116c22f29d0ac98e9b783fe669fd092da9d97cac74a660316d
4
+ data.tar.gz: 0ff5f3b518f07d5e3432a69e4a5738fd5bdeabc359593960bf9ac8f46081a7fc
9
5
  SHA512:
10
- metadata.gz: !binary |-
11
- MDkxMTRkNGZjYWQyYjIzZjEwODM2YzQxN2Y4YzVmZmZiOWE5YmU3NjUwYzdi
12
- Zjg5NzE2ZDg1YWI0YTg2MDY5NTkzM2QwOWNlNTAwN2U3MThmODMwODU5YTVi
13
- YzA1ZmM2OTFmYzVkNWIxMjhhMGViNjhhODQ1ZWJkODIxODlhODg=
14
- data.tar.gz: !binary |-
15
- YjNiNTRlZTNkMjg2NGEwNjNiZmEyMTQyZWZhNTMyYjgxZDI1MDkxOGM4MDI1
16
- NjRlY2U4OTBmMTM0ZTM1ZDU5M2FkNjJjNjE2ZjhmZTRiMzE0ZmE1MWJkMTQx
17
- NDE1MDNhMWYzYmFmZTBlMTJlZDU5OGQzOGQwYjM5ZWNkMGZhOTU=
6
+ metadata.gz: 07a1b0e3472abe674a69413e81e6cf2b87c5002a3b074a26053efc0b8da2e7d91d380055c54409922d35ff2d876aeb22c956ce1ea246842ec1ce08ad3079f35b
7
+ data.tar.gz: eb291aab81e6e802a204d7c0b2d36890fa2ce26cf82c3af773a8cfddcacdc9b0d4fca7c684aee03a884bafdda4ac088c234294d197fe28041bae2573b186a1da
@@ -0,0 +1,61 @@
1
+ ---
2
+ version: 2
3
+
4
+ references:
5
+ restore_cache: &restore_cache
6
+ type: cache-restore
7
+ name: Restore bundle cache
8
+ key: customerio-ruby-{{ checksum "customerio.gemspec" }}
9
+
10
+ save_cache: &save_cache
11
+ type: cache-save
12
+ name: Store bundle cache
13
+ key: customerio-ruby-{{ checksum "customerio.gemspec" }}
14
+ paths:
15
+ - vendor/bundle
16
+
17
+ jobs:
18
+ ruby_27:
19
+ working_directory: ~/customerio-ruby
20
+ docker:
21
+ - image: circleci/ruby:2.7.2
22
+ environment:
23
+ RAILS_ENV: test
24
+ steps:
25
+ - checkout
26
+ - *restore_cache
27
+ - run: bundle install
28
+ - *save_cache
29
+ - run: bundle exec rspec
30
+ ruby_26:
31
+ working_directory: ~/customerio-ruby
32
+ docker:
33
+ - image: circleci/ruby:2.6.6
34
+ environment:
35
+ RAILS_ENV: test
36
+ steps:
37
+ - checkout
38
+ - *restore_cache
39
+ - run: bundle install
40
+ - *save_cache
41
+ - run: bundle exec rspec
42
+ ruby_25:
43
+ working_directory: ~/customerio-ruby
44
+ docker:
45
+ - image: circleci/ruby:2.5.8
46
+ environment:
47
+ RAILS_ENV: test
48
+ steps:
49
+ - checkout
50
+ - *restore_cache
51
+ - run: bundle install
52
+ - *save_cache
53
+ - run: bundle exec rspec
54
+
55
+ workflows:
56
+ version: 2
57
+ test:
58
+ jobs:
59
+ - ruby_27
60
+ - ruby_26
61
+ - ruby_25
@@ -1,3 +1,17 @@
1
+ ## Customerio 3.0.0 - Dec 2, 2020
2
+
3
+ ### Added
4
+ - Support for the Transactional API
5
+
6
+ ### Removed
7
+ - `add_to_segment` and `remove_from_segment` methods
8
+ - Support for non-JSON data
9
+
10
+ ### Changed
11
+ - IDs in the URLs are now escaped.
12
+ - Improved validations for data that's passed in.
13
+ - Earlier, if you passed in an event name without a customer ID to the `track` method, we would create an anonymous event. That is now removed. To create an anonymous event, use the `anonymous_track` method.
14
+
1
15
  ## Customerio 2.2.1 - Mar 23, 2020
2
16
 
3
17
  - Add license to gemspec [#55](https://github.com/customerio/customerio-ruby/pull/55)
data/README.md CHANGED
@@ -51,12 +51,6 @@ If you're using Rails, create an initializer `config/initializers/customerio.rb`
51
51
  $customerio = Customerio::Client.new("YOUR SITE ID", "YOUR API SECRET KEY")
52
52
  ```
53
53
 
54
- If you'd like to send complex data to associate to a user as json, pass a json option:
55
-
56
- ```ruby
57
- customerio = Customerio::Client.new("YOUR SITE ID", "YOUR API SECRET KEY", :json => true)
58
- ```
59
-
60
54
  ### Identify logged in customers
61
55
 
62
56
  Tracking data of logged in customers is a key part of [Customer.io](http://customer.io). In order to
@@ -183,20 +177,51 @@ Start tracking events and identifies again for a previously suppressed customer.
183
177
  $customerio.unsuppress(5)
184
178
  ```
185
179
 
186
- ### Add customers to a manual segment
180
+ ### Send Transactional Messages
187
181
 
188
- Add the list of customer ids to the specified manual segment. If you send customer ids that don't exist yet in an add_to_segment request, we will automatically create customer profiles for the new customer ids.
182
+ To use the Customer.io [Transactional API](https://customer.io/docs/transactional-api), create an instance of the API client using an [app key](https://customer.io/docs/managing-credentials#app-api-keys).
189
183
 
190
- ```ruby
191
- $customerio.add_to_segment(segment_id=1,customer_ids=['1','2','3'])
192
- ```
184
+ Create a new `SendEmailRequest` object containing:
185
+
186
+ * `transactional_message_id`: the ID of the transactional message you want to send, or the `body`, `from`, and `subject` of a new message.
187
+ * `to`: the email address of your recipients
188
+ * an `identifiers` object containing the `id` of your recipient. If the `id` does not exist, Customer.io creates it.
189
+ * a `message_data` object containing properties that you want reference in your message using liquid.
190
+ * You can also send attachments with your message. Use `attach` to encode attachments.
193
191
 
194
- ### Remove customers from a manual segment
192
+ Use `send_email` referencing your request to send a transactional message. [Learn more about transactional messages and `SendEmailRequest` properties](https://customer.io/docs/transactional-api).
195
193
 
196
- Remove the list of customer ids from the specified manual segment.
197
194
 
198
195
  ```ruby
199
- $customerio.remove_from_segment(segment_id=1,customer_ids=['1','2','3'])
196
+ require "customerio"
197
+
198
+ client = Customerio::APIClient.new("your API key")
199
+
200
+ request = Customerio::SendEmailRequest.new(
201
+ to: "person@example.com",
202
+ transactional_message_id: "3",
203
+ message_data: {
204
+ name: "Person",
205
+ items: {
206
+ name: "shoes",
207
+ price: "59.99",
208
+ },
209
+ products: [],
210
+ },
211
+ identifiers: {
212
+ id: "2",
213
+ },
214
+ )
215
+
216
+ file = File.open('<file-path>', 'r')
217
+ request.attach("filename", file.read)
218
+
219
+ begin
220
+ response = client.send_email(request)
221
+ puts response
222
+ rescue Customerio::InvalidResponse => e
223
+ puts e.code, e.message
224
+ end
200
225
  ```
201
226
 
202
227
  ## Contributing
@@ -17,10 +17,10 @@ Gem::Specification.new do |gem|
17
17
  gem.version = Customerio::VERSION
18
18
 
19
19
  gem.add_dependency('multi_json', "~> 1.0")
20
+ gem.add_dependency('addressable', '~> 2.7.0')
20
21
 
21
22
  gem.add_development_dependency('rake', '~> 10.5')
22
23
  gem.add_development_dependency('rspec', '3.3.0')
23
- gem.add_development_dependency('webmock', '1.24.2')
24
- gem.add_development_dependency('addressable', '~> 2.3.6')
24
+ gem.add_development_dependency('webmock', '3.6.0')
25
25
  gem.add_development_dependency('json')
26
26
  end
@@ -1,6 +1,9 @@
1
1
  require "customerio/version"
2
2
 
3
3
  module Customerio
4
+ require "customerio/base_client"
4
5
  require "customerio/client"
6
+ require "customerio/requests/send_email_request"
7
+ require "customerio/api"
5
8
  require "customerio/param_encoder"
6
9
  end
@@ -0,0 +1,34 @@
1
+ require 'net/http'
2
+ require 'multi_json'
3
+
4
+ module Customerio
5
+ class APIClient
6
+ DEFAULT_API_URL = 'https://api.customer.io'
7
+
8
+ def initialize(app_key, options = {})
9
+ options[:url] = DEFAULT_API_URL if options[:url].nil? || options[:url].empty?
10
+ @client = Customerio::BaseClient.new({ app_key: app_key }, options)
11
+ end
12
+
13
+ def send_email(req)
14
+ raise "request must be an instance of Customerio::SendEmailRequest" unless req.is_a?(Customerio::SendEmailRequest)
15
+ response = @client.request(:post, send_email_path, req.message)
16
+
17
+ case response
18
+ when Net::HTTPSuccess then
19
+ JSON.parse(response.body)
20
+ when Net::HTTPBadRequest then
21
+ json = JSON.parse(response.body)
22
+ raise Customerio::InvalidResponse.new(response.code, json['meta']['error'], response)
23
+ else
24
+ raise InvalidResponse.new(response.code, response.body)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def send_email_path
31
+ "/v1/send/email"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,87 @@
1
+ require 'net/http'
2
+ require 'multi_json'
3
+
4
+ module Customerio
5
+ DEFAULT_TIMEOUT = 10
6
+
7
+ class InvalidRequest < RuntimeError; end
8
+ class InvalidResponse < RuntimeError
9
+ attr_reader :code, :response
10
+
11
+ def initialize(code, body, response=nil)
12
+ @message = body
13
+ @code = code
14
+ @response = response
15
+
16
+ super(@message)
17
+ end
18
+ end
19
+
20
+ class BaseClient
21
+ def initialize(auth, options = {})
22
+ @auth = auth
23
+ @timeout = options[:timeout] || DEFAULT_TIMEOUT
24
+ @base_uri = options[:url]
25
+ end
26
+
27
+ def request(method, path, body = nil, headers = {})
28
+ execute(method, path, body, headers)
29
+ end
30
+
31
+ def request_and_verify_response(method, path, body = nil, headers = {})
32
+ verify_response(request(method, path, body, headers))
33
+ end
34
+
35
+ private
36
+
37
+ def execute(method, path, body = nil, headers = {})
38
+ uri = URI.join(@base_uri, path)
39
+
40
+ session = Net::HTTP.new(uri.host, uri.port)
41
+ session.use_ssl = (uri.scheme == 'https')
42
+ session.open_timeout = @timeout
43
+ session.read_timeout = @timeout
44
+
45
+ req = request_class(method).new(uri.path)
46
+
47
+ if @auth.has_key?(:site_id) && @auth.has_key?(:api_key)
48
+ req.initialize_http_header(headers)
49
+ req.basic_auth @auth[:site_id], @auth[:api_key]
50
+ else
51
+ headers['Authorization'] = "Bearer #{@auth[:app_key]}"
52
+ req.initialize_http_header(headers)
53
+ end
54
+
55
+ if !body.nil?
56
+ req.add_field('Content-Type', 'application/json')
57
+ req.body = MultiJson.dump(body)
58
+ end
59
+
60
+ session.start do |http|
61
+ http.request(req)
62
+ end
63
+ end
64
+
65
+ def request_class(method)
66
+ case method
67
+ when :post
68
+ Net::HTTP::Post
69
+ when :put
70
+ Net::HTTP::Put
71
+ when :delete
72
+ Net::HTTP::Delete
73
+ else
74
+ raise InvalidRequest.new("Invalid request method #{method.inspect}")
75
+ end
76
+ end
77
+
78
+ def verify_response(response)
79
+ case response
80
+ when Net::HTTPSuccess then
81
+ response
82
+ else
83
+ raise InvalidResponse.new(response.code, response.body, response)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,29 +1,15 @@
1
- require 'net/http'
2
- require 'multi_json'
1
+ require "addressable/uri"
3
2
 
4
3
  module Customerio
5
- DEFAULT_BASE_URI = 'https://track.customer.io'
6
- DEFAULT_TIMEOUT = 10
7
-
8
4
  class Client
5
+ DEFAULT_TRACK_URL = 'https://track.customer.io'
6
+
9
7
  class MissingIdAttributeError < RuntimeError; end
10
8
  class ParamError < RuntimeError; end
11
- class InvalidRequest < RuntimeError; end
12
- class InvalidResponse < RuntimeError
13
- attr_reader :response
14
-
15
- def initialize(message, response)
16
- super(message)
17
- @response = response
18
- end
19
- end
20
9
 
21
- def initialize(site_id, secret_key, options = {})
22
- @username = site_id
23
- @password = secret_key
24
- @json = options.has_key?(:json) ? options[:json] : true
25
- @base_uri = options[:base_uri] || DEFAULT_BASE_URI
26
- @timeout = options[:timeout] || DEFAULT_TIMEOUT
10
+ def initialize(site_id, api_key, options = {})
11
+ options[:url] = DEFAULT_TRACK_URL if options[:url].nil? || options[:url].empty?
12
+ @client = Customerio::BaseClient.new({ site_id: site_id, api_key: api_key }, options)
27
13
  end
28
14
 
29
15
  def identify(attributes)
@@ -31,41 +17,36 @@ module Customerio
31
17
  end
32
18
 
33
19
  def delete(customer_id)
34
- verify_response(request(:delete, customer_path(customer_id)))
20
+ raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
21
+ @client.request_and_verify_response(:delete, customer_path(customer_id))
35
22
  end
36
23
 
37
24
  def suppress(customer_id)
38
- verify_response(request(:post, suppress_path(customer_id)))
25
+ raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
26
+ @client.request_and_verify_response(:post, suppress_path(customer_id))
39
27
  end
40
28
 
41
29
  def unsuppress(customer_id)
42
- verify_response(request(:post, unsuppress_path(customer_id)))
30
+ raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
31
+ @client.request_and_verify_response(:post, unsuppress_path(customer_id))
43
32
  end
44
33
 
45
- def track(*args)
46
- attributes = extract_attributes(args)
47
-
48
- if args.length == 1
49
- # Only passed in an event name, create an anonymous event
50
- event_name = args.first
51
- create_anonymous_event(event_name, attributes)
52
- else
53
- # Passed in a customer id and an event name.
54
- # Track the event for the given customer
55
- customer_id, event_name = args
34
+ def track(customer_id, event_name, attributes = {})
35
+ raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
36
+ raise ParamError.new("event_name must be a non-empty string") if is_empty?(event_name)
56
37
 
57
- create_customer_event(customer_id, event_name, attributes)
58
- end
38
+ create_customer_event(customer_id, event_name, attributes)
59
39
  end
60
40
 
61
41
  def anonymous_track(event_name, attributes = {})
42
+ raise ParamError.new("event_name must be a non-empty string") if is_empty?(event_name)
62
43
  create_anonymous_event(event_name, attributes)
63
44
  end
64
45
 
65
46
  def add_device(customer_id, device_id, platform, data={})
66
- raise ParamError.new("customer_id must be a non-empty string") unless customer_id != "" and !customer_id.nil?
67
- raise ParamError.new("device_id must be a non-empty string") unless device_id != "" and !device_id.nil?
68
- raise ParamError.new("platform must be a non-empty string") unless platform != "" and !platform.nil?
47
+ raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
48
+ raise ParamError.new("device_id must be a non-empty string") if is_empty?(device_id)
49
+ raise ParamError.new("platform must be a non-empty string") if is_empty?(platform)
69
50
 
70
51
  if data.nil?
71
52
  data = {}
@@ -73,69 +54,54 @@ module Customerio
73
54
 
74
55
  raise ParamError.new("data parameter must be a hash") unless data.is_a?(Hash)
75
56
 
76
- verify_response(request(:put, device_path(customer_id), {
57
+ @client.request_and_verify_response(:put, device_path(customer_id), {
77
58
  :device => data.update({
78
59
  :id => device_id,
79
60
  :platform => platform,
80
61
  })
81
- }))
62
+ })
82
63
  end
83
64
 
84
65
  def delete_device(customer_id, device_id)
85
- raise ParamError.new("customer_id must be a non-empty string") unless customer_id != "" and !customer_id.nil?
86
- raise ParamError.new("device_id must be a non-empty string") unless device_id != "" and !device_id.nil?
87
-
88
- verify_response(request(:delete, device_id_path(customer_id, device_id)))
89
- end
90
-
91
- def add_to_segment(segment_id, customer_ids)
92
- raise ParamError.new("segment_id must be an integer") unless segment_id.is_a? Integer
93
- raise ParamError.new("customer_ids must be a list of values") unless customer_ids.is_a? Array
66
+ raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
67
+ raise ParamError.new("device_id must be a non-empty string") if is_empty?(device_id)
94
68
 
95
- customer_ids = customer_ids.map{ |id| id.to_s }
96
-
97
- verify_response(request(:post, add_to_segment_path(segment_id), {
98
- :ids => customer_ids,
99
- }))
69
+ @client.request_and_verify_response(:delete, device_id_path(customer_id, device_id))
100
70
  end
101
71
 
102
- def remove_from_segment(segment_id, customer_ids)
103
- raise ParamError.new("segment_id must be an integer") unless segment_id.is_a? Integer
104
- raise ParamError.new("customer_ids must be a list of values") unless customer_ids.is_a? Array
72
+ private
105
73
 
106
- customer_ids = customer_ids.map{ |id| id.to_s }
107
-
108
- verify_response(request(:post, remove_from_segment_path(segment_id), {
109
- :ids => customer_ids,
110
- }))
74
+ def escape(val)
75
+ # CGI.escape is recommended for escaping, however, it doesn't correctly escape spaces.
76
+ Addressable::URI.encode_component(val.to_s, Addressable::URI::CharacterClasses::UNRESERVED)
111
77
  end
112
78
 
113
- private
79
+ def device_path(customer_id)
80
+ "/api/v1/customers/#{escape(customer_id)}/devices"
81
+ end
114
82
 
115
- def add_to_segment_path(segment_id)
116
- "/api/v1/segments/#{segment_id}/add_customers"
83
+ def device_id_path(customer_id, device_id)
84
+ "/api/v1/customers/#{escape(customer_id)}/devices/#{escape(device_id)}"
117
85
  end
118
86
 
119
- def remove_from_segment_path(segment_id)
120
- "/api/v1/segments/#{segment_id}/remove_customers"
87
+ def customer_path(id)
88
+ "/api/v1/customers/#{escape(id)}"
121
89
  end
122
90
 
123
- def device_path(customer_id)
124
- "/api/v1/customers/#{customer_id}/devices"
91
+ def suppress_path(customer_id)
92
+ "/api/v1/customers/#{escape(customer_id)}/suppress"
125
93
  end
126
94
 
127
- def device_id_path(customer_id, device_id)
128
- "/api/v1/customers/#{customer_id}/devices/#{device_id}"
95
+ def unsuppress_path(customer_id)
96
+ "/api/v1/customers/#{escape(customer_id)}/unsuppress"
129
97
  end
130
98
 
131
99
  def create_or_update(attributes = {})
132
100
  attributes = Hash[attributes.map { |(k,v)| [ k.to_sym, v ] }]
133
-
134
- raise MissingIdAttributeError.new("Must provide a customer id") unless attributes[:id]
101
+ raise MissingIdAttributeError.new("Must provide a customer id") if is_empty?(attributes[:id])
135
102
 
136
103
  url = customer_path(attributes[:id])
137
-
138
- verify_response(request(:put, url, attributes))
104
+ @client.request_and_verify_response(:put, url, attributes)
139
105
  end
140
106
 
141
107
  def create_customer_event(customer_id, event_name, attributes = {})
@@ -149,79 +115,15 @@ module Customerio
149
115
  def create_event(url, event_name, attributes = {})
150
116
  body = { :name => event_name, :data => attributes }
151
117
  body[:timestamp] = attributes[:timestamp] if valid_timestamp?(attributes[:timestamp])
152
- verify_response(request(:post, url, body))
153
- end
154
-
155
- def customer_path(id)
156
- "/api/v1/customers/#{id}"
157
- end
158
-
159
- def suppress_path(customer_id)
160
- "/api/v1/customers/#{customer_id}/suppress"
161
- end
162
-
163
- def unsuppress_path(customer_id)
164
- "/api/v1/customers/#{customer_id}/unsuppress"
118
+ @client.request_and_verify_response(:post, url, body)
165
119
  end
166
120
 
167
121
  def valid_timestamp?(timestamp)
168
122
  timestamp && timestamp.is_a?(Integer) && timestamp > 999999999 && timestamp < 100000000000
169
123
  end
170
124
 
171
-
172
- def verify_response(response)
173
- if response.code.to_i >= 200 && response.code.to_i < 300
174
- response
175
- else
176
- raise InvalidResponse.new("Customer.io API returned an invalid response: #{response.code}", response)
177
- end
178
- end
179
-
180
- def extract_attributes(args)
181
- hash = args.last.is_a?(Hash) ? args.pop : {}
182
- hash.inject({}){ |hash, (k,v)| hash[k.to_sym] = v; hash }
183
- end
184
-
185
- def request(method, path, body = nil, headers = {})
186
- uri = URI.join(@base_uri, path)
187
-
188
- session = Net::HTTP.new(uri.host, uri.port)
189
- session.use_ssl = (uri.scheme == 'https')
190
- session.open_timeout = @timeout
191
- session.read_timeout = @timeout
192
-
193
- req = request_class(method).new(uri.path)
194
- req.initialize_http_header(headers)
195
- req.basic_auth @username, @password
196
-
197
- add_request_body(req, body) unless body.nil?
198
-
199
- session.start do |http|
200
- http.request(req)
201
- end
202
- end
203
-
204
- def request_class(method)
205
- case method
206
- when :post
207
- Net::HTTP::Post
208
- when :put
209
- Net::HTTP::Put
210
- when :delete
211
- Net::HTTP::Delete
212
- else
213
- raise InvalidRequest.new("Invalid request method #{method.inspect}")
214
- end
215
- end
216
-
217
- def add_request_body(req, body)
218
- if @json
219
- req.add_field('Content-Type', 'application/json')
220
- req.body = MultiJson.dump(body)
221
- else
222
- req.add_field('Content-Type', 'application/x-www-form-urlencoded')
223
- req.body = ParamEncoder.to_params(body)
224
- end
125
+ def is_empty?(val)
126
+ val.nil? || (val.is_a?(String) && val.strip == "")
225
127
  end
226
128
  end
227
129
  end