customerio 2.2.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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