customerio 0.1.0 → 0.2.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.
@@ -0,0 +1,17 @@
1
+ ## Customerio 0.2.0 - Nov 21, 2012 ##
2
+
3
+ * Allow raw hashes to be passed into `identify` and `track` methods rather than a customer object.
4
+ * Passing a customer object has been depreciated.
5
+ * Customizing ids with `Customerio::Client.id` block is deprecated.
6
+
7
+ ## Customerio 0.1.0 - Nov 15, 2012 ##
8
+
9
+ * Allow tracking of anonymous events.
10
+
11
+ ## Customerio 0.0.3 - Nov 5, 2012 ##
12
+
13
+ * Bump httparty dependency to the latest version.
14
+
15
+ ## Customerio 0.0.2 - May 22, 2012 ##
16
+
17
+ * First release.
data/Gemfile.lock CHANGED
@@ -1,17 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- customerio (0.0.3)
4
+ customerio (0.2.0)
5
+ activesupport
5
6
  httparty (~> 0.9.0)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
11
+ activesupport (3.2.9)
12
+ i18n (~> 0.6)
13
+ multi_json (~> 1.0)
10
14
  diff-lcs (1.1.3)
11
15
  fakeweb (1.3.0)
12
16
  httparty (0.9.0)
13
17
  multi_json (~> 1.0)
14
18
  multi_xml
19
+ i18n (0.6.1)
15
20
  multi_json (1.3.7)
16
21
  multi_xml (0.5.1)
17
22
  rake (0.9.2.2)
data/customerio.gemspec CHANGED
@@ -16,6 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.version = Customerio::VERSION
17
17
 
18
18
  gem.add_dependency('httparty', '~> 0.9.0')
19
+ gem.add_dependency('activesupport')
19
20
 
20
21
  gem.add_development_dependency('rake')
21
22
  gem.add_development_dependency('rspec')
@@ -1,13 +1,20 @@
1
1
  require 'httparty'
2
+ require 'active_support/hash_with_indifferent_access'
2
3
 
3
4
  module Customerio
4
5
  class Client
5
6
  include HTTParty
6
7
  base_uri 'https://app.customer.io'
7
8
 
9
+ CustomerProxy = Struct.new("Customer", :id)
10
+
11
+ class MissingIdAttributeError < RuntimeError; end
12
+ class InvalidResponse < RuntimeError; end
13
+
8
14
  @@id_block = nil
9
15
 
10
16
  def self.id(&block)
17
+ warn "[DEPRECATION] Customerio::Client.id customization is deprecated."
11
18
  @@id_block = block
12
19
  end
13
20
 
@@ -19,40 +26,47 @@ module Customerio
19
26
  @auth = { :username => site_id, :password => secret_key }
20
27
  end
21
28
 
22
- def identify(customer, attributes = {})
23
- create_or_update(customer, attributes)
29
+ def identify(*args)
30
+ attributes = extract_attributes(args)
31
+
32
+ if args.any?
33
+ customer = args.first
34
+ attributes = attributes_from(customer).merge(attributes)
35
+ end
36
+
37
+ create_or_update(attributes)
24
38
  end
25
39
 
26
40
  def track(*args)
27
- hash = args.last.is_a?(Hash) ? args.pop : {}
41
+ attributes = extract_attributes(args)
28
42
 
29
43
  if args.length == 1
30
44
  # Only passed in an event name, create an anonymous event
31
- create_anonymous_event(args.first, hash)
45
+ event_name = args.first
46
+ create_anonymous_event(event_name, attributes)
32
47
  else
33
48
  # Passed in a customer and an event name.
34
49
  # Track the event for the given customer
35
50
  customer, event_name = args
36
51
 
37
- identify(customer)
38
- create_customer_event(customer, event_name, hash)
52
+ identify(attributes_from(customer))
53
+ create_customer_event(id_from(customer), event_name, attributes)
39
54
  end
40
55
  end
41
56
 
42
57
  private
43
58
 
44
- def create_or_update(customer, attributes = {})
45
- body = {
46
- :id => id(customer),
47
- :email => customer.email,
48
- :created_at => customer.created_at.to_i
49
- }.merge(attributes)
59
+ def create_or_update(attributes = {})
60
+ raise MissingIdAttributeError.new("Must provide an customer id") unless attributes[:id]
61
+
62
+ url = customer_path(attributes[:id])
63
+ attributes[:id] = custom_id(attributes[:id])
50
64
 
51
- self.class.put(customer_path(customer), options.merge(:body => body))
65
+ verify_response(self.class.put(url, options.merge(:body => attributes)))
52
66
  end
53
67
 
54
- def create_customer_event(customer, event_name, attributes = {})
55
- create_event("#{customer_path(customer)}/events", event_name, attributes)
68
+ def create_customer_event(customer_id, event_name, attributes = {})
69
+ create_event("#{customer_path(customer_id)}/events", event_name, attributes)
56
70
  end
57
71
 
58
72
  def create_anonymous_event(event_name, attributes = {})
@@ -61,17 +75,54 @@ module Customerio
61
75
 
62
76
  def create_event(url, event_name, attributes = {})
63
77
  body = { :name => event_name, :data => attributes }
64
- self.class.post(url, options.merge(:body => body))
78
+ verify_response(self.class.post(url, options.merge(:body => body)))
65
79
  end
66
80
 
67
- def customer_path(customer)
68
- "/api/v1/customers/#{id(customer)}"
81
+ def customer_path(id)
82
+ "/api/v1/customers/#{custom_id(id)}"
69
83
  end
70
84
 
71
- def id(customer)
72
- @@id_block ? @@id_block.call(customer) : customer.id
85
+ def verify_response(response)
86
+ if response.code >= 200 && response.code < 300
87
+ response
88
+ else
89
+ raise InvalidResponse.new("Customer.io API returned an invalid response: #{response.code}")
90
+ end
91
+ end
92
+
93
+ def extract_attributes(args)
94
+ HashWithIndifferentAccess.new(args.last.is_a?(Hash) ? args.pop : {})
95
+ end
96
+
97
+ def attributes_from(customer)
98
+ if id?(customer)
99
+ HashWithIndifferentAccess.new(:id => customer)
100
+ else
101
+ HashWithIndifferentAccess.new(
102
+ :id => id_from(customer),
103
+ :email => customer.email,
104
+ :created_at => customer.created_at.to_i
105
+ )
106
+ end
107
+ end
108
+
109
+ def id_from(customer)
110
+ if id?(customer)
111
+ customer
112
+ else
113
+ warn "[DEPRECATION] Passing a customer object to Customerio::Client is deprecated. Just pass a hash with an id key."
114
+ customer.id
115
+ end
116
+ end
117
+
118
+ def custom_id(id)
119
+ @@id_block ? @@id_block.call(id) : id
73
120
  end
74
121
 
122
+ def id?(object)
123
+ object.is_a?(Integer) || object.is_a?(String)
124
+ end
125
+
75
126
  def options
76
127
  { :basic_auth => @auth }
77
128
  end
@@ -1,3 +1,3 @@
1
1
  module Customerio
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/readme.markdown CHANGED
@@ -49,12 +49,6 @@ If you're using Rails, create an initializer `config/initializers/customerio.rb`
49
49
 
50
50
  $customerio = Customerio::Client.new("YOUR SITE ID", "YOUR API SECRET KEY")
51
51
 
52
- By default, this gem identifies customers by just their `id`. However, a common approach is to use `production_2342` as the id attribute for the javascript snippet. You'll want to use the same format by customizing the id in `config/initializers/customerio.rb`:
53
-
54
- Customerio::Client.id do |customer|
55
- "#{Rails.env}_#{customer.id}"
56
- end
57
-
58
52
  ### Identify logged in customers
59
53
 
60
54
  Tracking data of logged in customers is a key part of [Customer.io](http://customer.io). In order to
@@ -77,16 +71,17 @@ You'll want to indentify your customers when they sign up for your app and any t
77
71
  key information changes. This keeps [Customer.io](http://customer.io) up to date with your customer information.
78
72
 
79
73
  # Arguments
80
- # customer (required) - a customer object which responds to a few key methods:
81
- # id - a unique identifier for the customer
82
- # email - the customer's current email address
83
- # created_at - a timestamp which represents when the
84
- # customer was first created.
85
- #
86
- # attributes (optional) - a hash of information about the customer. You can pass any
87
- # information that would be useful in your triggers.
74
+ # attributes (required) - a hash of information about the customer. You can pass any
75
+ # information that would be useful in your triggers. You
76
+ # must at least pass in an id, email, and created_at timestamp.
88
77
 
89
- $customerio.identify(customer, first_name: "Bob", plan: "basic")
78
+ $customerio.identify(
79
+ id: 5,
80
+ email: "bob@example.com,
81
+ created_at: customer.created_at.to_i,
82
+ first_name: "Bob",
83
+ plan: "basic"
84
+ )
90
85
 
91
86
 
92
87
  ### Tracking a custom event
@@ -97,13 +92,13 @@ with automated emails, and track conversions when you're sending automated email
97
92
  encourage your customers to perform an action.
98
93
 
99
94
  # Arguments
100
- # customer (required) - the customer who you want to associate with the event.
101
- # name (required) - the name of the event you want to track.
102
- # attributes (optional) - any related information you'd like to attach to this
103
- # event. These attributes can be used in your triggers to control who should
104
- # receive the triggered email. You can set any number of data values.
95
+ # customer_id (required) - the id of the customer who you want to associate with the event.
96
+ # name (required) - the name of the event you want to track.
97
+ # attributes (optional) - any related information you'd like to attach to this
98
+ # event. These attributes can be used in your triggers to control who should
99
+ # receive the triggered email. You can set any number of data values.
105
100
 
106
- $customerio.track(user, "purchase", type: "socks", price: "13.99")
101
+ $customerio.track(5, "purchase", type: "socks", price: "13.99")
107
102
 
108
103
  ## Contributing
109
104
 
data/spec/client_spec.rb CHANGED
@@ -3,6 +3,13 @@ require 'spec_helper'
3
3
  describe Customerio::Client do
4
4
  let(:client) { Customerio::Client.new("SITE_ID", "API_KEY") }
5
5
  let(:customer) { mock("Customer", :id => 5, :email => "customer@example.com", :created_at => Time.now) }
6
+ let(:response) { mock("Response", code: 200) }
7
+
8
+ before do
9
+ # Dont call out to customer.io
10
+ Customerio::Client.stub(:post).and_return(response)
11
+ Customerio::Client.stub(:put).and_return(response)
12
+ end
6
13
 
7
14
  describe ".base_uri" do
8
15
  it "should be set to customer.io's api" do
@@ -11,52 +18,78 @@ describe Customerio::Client do
11
18
  end
12
19
 
13
20
  describe "#identify" do
14
- it "sends a PUT request to customer.io's customer API" do
15
- Customerio::Client.should_receive(:put).with("/api/v1/customers/5", anything())
16
- client.identify(customer)
17
- end
21
+ it "sends a PUT request to customer.io's customer API" do
22
+ Customerio::Client.should_receive(:put).with("/api/v1/customers/5", anything()).and_return(response)
23
+ client.identify(:id => 5)
24
+ end
18
25
 
19
- it "uses the site_id and api key for basic auth" do
20
- Customerio::Client.should_receive(:put).with("/api/v1/customers/5", {
21
- :basic_auth => { :username => "SITE_ID", :password => "API_KEY" },
22
- :body => anything()
23
- })
26
+ it "raises an error if PUT doesn't return a 2xx response code" do
27
+ Customerio::Client.should_receive(:put).and_return(mock("Response", code: 500))
28
+ lambda { client.identify(:id => 5) }.should raise_error(Customerio::Client::InvalidResponse)
29
+ end
24
30
 
25
- client.identify(customer)
26
- end
31
+ it "uses the site_id and api key for basic auth" do
32
+ Customerio::Client.should_receive(:put).with("/api/v1/customers/5", {
33
+ :basic_auth => { :username => "SITE_ID", :password => "API_KEY" },
34
+ :body => anything()
35
+ }).and_return(response)
27
36
 
28
- it "sends the customer's id, email, and created_at timestamp" do
29
- Customerio::Client.should_receive(:put).with("/api/v1/customers/5", {
30
- :basic_auth => anything(),
31
- :body => {
32
- :id => 5,
33
- :email => "customer@example.com",
34
- :created_at => Time.now.to_i
35
- }
36
- })
37
+ client.identify(:id => 5)
38
+ end
37
39
 
38
- client.identify(customer)
39
- end
40
+ it "sends along all attributes" do
41
+ Customerio::Client.should_receive(:put).with("/api/v1/customers/5", {
42
+ :basic_auth => anything(),
43
+ :body => {
44
+ :id => 5,
45
+ :email => "customer@example.com",
46
+ :created_at => Time.now.to_i,
47
+ :first_name => "Bob",
48
+ :plan => "basic"
49
+ }.stringify_keys
50
+ }).and_return(response)
51
+
52
+ client.identify(:id => 5, :email => "customer@example.com", :created_at => Time.now.to_i, :first_name => "Bob", :plan => "basic")
53
+ end
40
54
 
41
- it "sends any optional attributes" do
42
- Customerio::Client.should_receive(:put).with("/api/v1/customers/5", {
43
- :basic_auth => anything(),
44
- :body => {
45
- :id => 5,
46
- :email => "customer@example.com",
47
- :created_at => Time.now.to_i,
48
- :first_name => "Bob",
49
- :plan => "basic"
50
- }
51
- })
55
+ it "requires an id attribute" do
56
+ lambda { client.identify(:email => "customer@example.com") }.should raise_error(Customerio::Client::MissingIdAttributeError)
57
+ end
52
58
 
53
- client.identify(customer, :first_name => "Bob", :plan => "basic")
54
- end
59
+ context "customer object passed in" do
60
+ it "sends the customer's id, email, and created_at timestamp" do
61
+ Customerio::Client.should_receive(:put).with("/api/v1/customers/5", {
62
+ :basic_auth => anything(),
63
+ :body => {
64
+ :id => 5,
65
+ :email => "customer@example.com",
66
+ :created_at => Time.now.to_i
67
+ }.stringify_keys
68
+ }).and_return(response)
69
+
70
+ client.identify(customer)
71
+ end
72
+
73
+ it "sends any optional attributes" do
74
+ Customerio::Client.should_receive(:put).with("/api/v1/customers/5", {
75
+ :basic_auth => anything(),
76
+ :body => {
77
+ :id => 5,
78
+ :email => "customer@example.com",
79
+ :created_at => Time.now.to_i,
80
+ :first_name => "Bob",
81
+ :plan => "basic"
82
+ }.stringify_keys
83
+ }).and_return(response)
84
+
85
+ client.identify(customer, :first_name => "Bob", :plan => "basic")
86
+ end
87
+ end
55
88
 
56
89
  context "client has customized identities" do
57
90
  before do
58
- Customerio::Client.id do |customer|
59
- "production_#{customer.id}"
91
+ Customerio::Client.id do |customer_id|
92
+ "production_#{customer_id}"
60
93
  end
61
94
  end
62
95
 
@@ -67,28 +100,40 @@ describe Customerio::Client do
67
100
  :id => "production_5",
68
101
  :email => "customer@example.com",
69
102
  :created_at => Time.now.to_i
70
- }
71
- })
103
+ }.stringify_keys
104
+ }).and_return(response)
72
105
 
73
106
  client.identify(customer)
74
107
  end
108
+
109
+ it "uses custom identity when using a pure hash" do
110
+ Customerio::Client.should_receive(:put).with("/api/v1/customers/production_5", {
111
+ :basic_auth => anything(),
112
+ :body => {
113
+ :id => "production_5",
114
+ :email => "customer@example.com",
115
+ :created_at => Time.now.to_i
116
+ }.stringify_keys
117
+ }).and_return(response)
118
+
119
+ client.identify(:id => 5, :email => "customer@example.com", :created_at => Time.now.to_i)
120
+ end
75
121
  end
76
122
  end
77
123
 
78
124
  describe "#track" do
79
- before do
80
- # Don't actually send identify requests
81
- Customerio::Client.stub(:put)
82
- end
83
-
84
125
  it "sends a POST request to the customer.io's event API" do
85
- Customerio::Client.should_receive(:post).with("/api/v1/customers/5/events", anything())
126
+ Customerio::Client.should_receive(:post).with("/api/v1/customers/5/events", anything()).and_return(response)
86
127
  client.track(customer, "purchase")
87
128
  end
88
129
 
89
- it "calls identify with the user to ensure they've been properly identified" do
90
- Customerio::Client.stub(:post) # don't send the request
91
- client.should_receive(:identify).with(customer)
130
+ it "raises an error if POST doesn't return a 2xx response code" do
131
+ Customerio::Client.should_receive(:post).and_return(mock("Response", code: 500))
132
+ lambda { client.track(customer, "purchase") }.should raise_error(Customerio::Client::InvalidResponse)
133
+ end
134
+
135
+ it "calls identify with the user's attributes to ensure they've been properly identified" do
136
+ client.should_receive(:identify).with({ :id => 5, :email => "customer@example.com", :created_at => Time.now.to_i }.stringify_keys).and_return(response)
92
137
  client.track(customer, "purchase")
93
138
  end
94
139
 
@@ -105,7 +150,7 @@ describe Customerio::Client do
105
150
  Customerio::Client.should_receive(:post).with("/api/v1/customers/5/events", {
106
151
  :basic_auth => anything(),
107
152
  :body => { :name => "purchase", :data => {} }
108
- })
153
+ }).and_return(response)
109
154
 
110
155
  client.track(customer, "purchase")
111
156
  end
@@ -115,33 +160,54 @@ describe Customerio::Client do
115
160
  :basic_auth => anything(),
116
161
  :body => {
117
162
  :name => "purchase",
118
- :data => { :type => "socks", :price => "13.99" }
163
+ :data => { :type => "socks", :price => "13.99" }.stringify_keys
119
164
  }
120
- })
165
+ }).and_return(response)
121
166
 
122
167
  client.track(customer, "purchase", :type => "socks", :price => "13.99")
123
168
  end
124
169
 
170
+ it "allows tracking by customer id as well" do
171
+ Customerio::Client.should_receive(:post).with("/api/v1/customers/5/events", {
172
+ :basic_auth => anything(),
173
+ :body => {
174
+ :name => "purchase",
175
+ :data => { :type => "socks", :price => "13.99" }.stringify_keys
176
+ }
177
+ }).and_return(response)
178
+
179
+ client.track(5, "purchase", :type => "socks", :price => "13.99")
180
+ end
181
+
125
182
  context "client has customized identities" do
126
183
  before do
127
- Customerio::Client.id do |customer|
128
- "production_#{customer.id}"
184
+ Customerio::Client.id do |customer_id|
185
+ "production_#{customer_id}"
129
186
  end
130
187
  end
131
188
 
132
189
  it "identifies the customer with the identification method" do
133
190
  Customerio::Client.should_receive(:post).with("/api/v1/customers/production_5/events", {
134
- :basic_auth => anything(),
135
- :body => anything()
136
- })
191
+ :basic_auth => anything(),
192
+ :body => anything()
193
+ }).and_return(response)
137
194
 
138
- client.track(customer, "purchase")
195
+ client.track(customer, "purchase")
196
+ end
197
+
198
+ it "uses the identification method when tracking by id" do
199
+ Customerio::Client.should_receive(:post).with("/api/v1/customers/production_5/events", {
200
+ :basic_auth => anything(),
201
+ :body => anything()
202
+ }).and_return(response)
203
+
204
+ client.track(5, "purchase")
139
205
  end
140
206
  end
141
207
 
142
208
  context "tracking an anonymous event" do
143
209
  it "sends a POST request to the customer.io's anonymous event API" do
144
- Customerio::Client.should_receive(:post).with("/api/v1/events", anything())
210
+ Customerio::Client.should_receive(:post).with("/api/v1/events", anything()).and_return(response)
145
211
  client.track("purchase")
146
212
  end
147
213
 
@@ -149,7 +215,7 @@ describe Customerio::Client do
149
215
  Customerio::Client.should_receive(:post).with("/api/v1/events", {
150
216
  :basic_auth => { :username => "SITE_ID", :password => "API_KEY" },
151
217
  :body => anything()
152
- })
218
+ }).and_return(response)
153
219
 
154
220
  client.track("purchase")
155
221
  end
@@ -158,7 +224,7 @@ describe Customerio::Client do
158
224
  Customerio::Client.should_receive(:post).with("/api/v1/events", {
159
225
  :basic_auth => anything(),
160
226
  :body => { :name => "purchase", :data => {} }
161
- })
227
+ }).and_return(response)
162
228
 
163
229
  client.track("purchase")
164
230
  end
@@ -168,9 +234,9 @@ describe Customerio::Client do
168
234
  :basic_auth => anything(),
169
235
  :body => {
170
236
  :name => "purchase",
171
- :data => { :type => "socks", :price => "13.99" }
237
+ :data => { :type => "socks", :price => "13.99" }.stringify_keys
172
238
  }
173
- })
239
+ }).and_return(response)
174
240
 
175
241
  client.track("purchase", :type => "socks", :price => "13.99")
176
242
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: customerio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-15 00:00:00.000000000 Z
12
+ date: 2012-11-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: httparty
@@ -27,6 +27,22 @@ dependencies:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
29
  version: 0.9.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
30
46
  - !ruby/object:Gem::Dependency
31
47
  name: rake
32
48
  requirement: !ruby/object:Gem::Requirement
@@ -84,6 +100,7 @@ extra_rdoc_files: []
84
100
  files:
85
101
  - .gitignore
86
102
  - .travis.yml
103
+ - CHANGELOG.markdown
87
104
  - Gemfile
88
105
  - Gemfile.lock
89
106
  - LICENSE