amplitude-api 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AmplitudeAPI
2
- VERSION = '0.1.0'.freeze
4
+ VERSION = "0.3.0"
3
5
  end
data/lib/amplitude_api.rb CHANGED
@@ -1,18 +1,20 @@
1
- require 'json'
2
- require 'typhoeus'
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "faraday"
3
5
 
4
6
  # AmplitudeAPI
5
7
  class AmplitudeAPI
6
- require_relative 'amplitude_api/config'
7
- require_relative 'amplitude_api/event'
8
- require_relative 'amplitude_api/identification'
8
+ require_relative "amplitude_api/config"
9
+ require_relative "amplitude_api/event"
10
+ require_relative "amplitude_api/identification"
9
11
 
10
- TRACK_URI_STRING = 'https://api.amplitude.com/httpapi'.freeze
11
- IDENTIFY_URI_STRING = 'https://api.amplitude.com/identify'.freeze
12
- SEGMENTATION_URI_STRING = 'https://amplitude.com/api/2/events/segmentation'.freeze
13
- DELETION_URI_STRING = 'https://amplitude.com/api/2/deletions/users'.freeze
12
+ TRACK_URI_STRING = "https://api.amplitude.com/2/httpapi"
13
+ IDENTIFY_URI_STRING = "https://api.amplitude.com/identify"
14
+ SEGMENTATION_URI_STRING = "https://amplitude.com/api/2/events/segmentation"
15
+ DELETION_URI_STRING = "https://amplitude.com/api/2/deletions/users"
14
16
 
15
- USER_WITH_NO_ACCOUNT = "user who doesn't have an account".freeze
17
+ USER_WITH_NO_ACCOUNT = "user who doesn't have an account"
16
18
 
17
19
  class << self
18
20
  def config
@@ -38,12 +40,12 @@ class AmplitudeAPI
38
40
  # @param [ String ] event_name a string that describes the event, e.g. "clicked on Home"
39
41
  # @param [ String ] user a string or integer that uniquely identifies a user.
40
42
  # @param [ String ] device a string that uniquely identifies the device.
41
- # @param [ Hash ] event_properties a hash that is serialized to JSON,
43
+ # @option options [ Hash ] event_properties a hash that is serialized to JSON,
42
44
  # and can contain any other property to be stored on the Event
43
- # @param [ Hash ] user_properties a hash that is serialized to JSON,
45
+ # @option options [ Hash ] user_properties a hash that is serialized to JSON,
44
46
  # and contains user properties to be associated with the user
45
47
  #
46
- # @return [ Typhoeus::Response ]
48
+ # @return [ Faraday::Response ]
47
49
  def send_event(event_name, user, device, options = {})
48
50
  event = AmplitudeAPI::Event.new(
49
51
  user_id: user,
@@ -68,10 +70,10 @@ class AmplitudeAPI
68
70
  def track_body(*events)
69
71
  event_body = events.flatten.map(&:to_hash)
70
72
 
71
- {
73
+ JSON.generate(
72
74
  api_key: api_key,
73
- event: JSON.generate(event_body)
74
- }
75
+ events: event_body
76
+ )
75
77
  end
76
78
 
77
79
  # @overload track(event)
@@ -80,11 +82,11 @@ class AmplitudeAPI
80
82
  # @overload track([events])
81
83
  # @param [ Array<AmplitudeAPI::Event> ] Send an array of events in a single request to Amplitude
82
84
  #
83
- # @return [ Typhoeus::Response ]
85
+ # @return [ Faraday::Response ]
84
86
  #
85
87
  # Send one or more Events to the Amplitude API
86
88
  def track(*events)
87
- Typhoeus.post(TRACK_URI_STRING, body: track_body(events))
89
+ Faraday.post(TRACK_URI_STRING, track_body(events), "Content-Type" => "application/json")
88
90
  end
89
91
 
90
92
  # ==== Identification related methods
@@ -123,40 +125,40 @@ class AmplitudeAPI
123
125
  # @overload identify([identifications])
124
126
  # @param [ Array<AmplitudeAPI::Identify> ] Send an array of identifications in a single request to Amplitude
125
127
  #
126
- # @return [ Typhoeus::Response ]
128
+ # @return [ Faraday::Response ]
127
129
  #
128
130
  # Send one or more Identifications to the Amplitude Identify API
129
131
  def identify(*identifications)
130
- Typhoeus.post(IDENTIFY_URI_STRING, body: identify_body(identifications))
132
+ Faraday.post(IDENTIFY_URI_STRING, identify_body(identifications))
131
133
  end
132
134
 
133
135
  # ==== Event Segmentation related methods
134
136
 
135
137
  # Get metrics for an event with segmentation.
136
138
  #
137
- # @param [ Hash ] e a hash that defines event.
139
+ # @param [ Hash ] event a hash that defines event.
138
140
  # @param [ Time ] start_time a start time.
139
141
  # @param [ Time ] end_time a end time.
140
- # @param [ String ] m a string that defines aggregate function.
142
+ # @option options [ String ] m a string that defines aggregate function.
141
143
  # For non-property metrics: "uniques", "totals", "pct_dau", or "average" (default: "uniques").
142
144
  # For property metrics: "histogram", "sums", or "value_avg"
143
145
  # (note: a valid "group_by" value is required in parameter e).
144
- # @param [ Integer ] i an integer that defines segmentation interval.
146
+ # @option options [ Integer ] i an integer that defines segmentation interval.
145
147
  # Set to -300000, -3600000, 1, 7, or 30 for realtime, hourly, daily, weekly,
146
148
  # and monthly counts, respectively (default: 1). Realtime segmentation is capped at 2 days,
147
149
  # hourly segmentation is capped at 7 days, and daily at 365 days.
148
- # @param [ Array ] s an array that defines segment definitions.
149
- # @param [ String ] g a string that defines property to group by.
150
- # @param [ Integer ] limit an integer that defines number of Group By values
150
+ # @option options [ Array ] s an array that defines segment definitions.
151
+ # @option options [ String ] g a string that defines property to group by.
152
+ # @option options [ Integer ] limit an integer that defines number of Group By values
151
153
  # returned (default: 100). The maximum limit is 1000.
152
154
  #
153
- # @return [ Typhoeus::Response ]
155
+ # @return [ Faraday::Response ]
154
156
  def segmentation(event, start_time, end_time, **options)
155
- Typhoeus.get SEGMENTATION_URI_STRING, userpwd: "#{api_key}:#{secret_key}", params: {
157
+ Faraday.get SEGMENTATION_URI_STRING, userpwd: "#{api_key}:#{secret_key}", params: {
156
158
  e: event.to_json,
157
159
  m: options[:m],
158
- start: start_time.strftime('%Y%m%d'),
159
- end: end_time.strftime('%Y%m%d'),
160
+ start: start_time.strftime("%Y%m%d"),
161
+ end: end_time.strftime("%Y%m%d"),
160
162
  i: options[:i],
161
163
  s: (options[:s] || []).map(&:to_json),
162
164
  g: options[:g],
@@ -164,27 +166,48 @@ class AmplitudeAPI
164
166
  }.delete_if { |_, value| value.nil? }
165
167
  end
166
168
 
167
- # ==== GDPR compliance methods
168
-
169
- # Delete a user from amplitude when they request it to comply with GDPR
169
+ # Delete a user from amplitude
170
+ #
170
171
  # You must pass in either an array of user_ids or an array of amplitude_ids
171
172
  #
172
- # @param [ user_ids ] (optional) the user_ids to delete
173
+ # @param [ Array<String> ] (optional) the user_ids to delete
173
174
  # based on your database
174
- # @param [ amplitude_ids ] (optional) the amplitude_ids to delete
175
+ # @param [ Array<Integer> ] (optional) the amplitude_ids to delete
175
176
  # based on the amplitude database
176
- # @param [ requester ] the email address of the person who
177
+ # @param [ String ] requester the email address of the person who
177
178
  # is requesting the deletion, optional but useful for reporting
178
- #
179
- # @return [ Typhoeus::Response ]
180
- def delete(user_ids: nil, amplitude_ids: nil, requester: nil)
181
- Typhoeus.post DELETION_URI_STRING,
182
- userpwd: "#{api_key}:#{config.secret_key}",
183
- body: {
184
- amplitude_ids: amplitude_ids,
185
- user_ids: user_ids,
186
- requester: requester
187
- }.delete_if { |_, value| value.nil? }
179
+ # @param [ Boolean ] (optional) ignore any invalid user IDs(users that do no
180
+ # exist in the project) that were passed in
181
+ # @param [ Boolean ] (optional) delete from the entire org rather than just
182
+ # this project.
183
+ # @return [ Faraday::Response ]
184
+ def delete(user_ids: nil, amplitude_ids: nil, requester: nil, ignore_invalid_id: nil, delete_from_org: nil)
185
+ user_ids = Array(user_ids)
186
+ amplitude_ids = Array(amplitude_ids)
187
+
188
+ faraday = Faraday.new do |conn|
189
+ conn.basic_auth config.api_key, config.secret_key
190
+ end
191
+
192
+ faraday.post(
193
+ DELETION_URI_STRING,
194
+ delete_body(user_ids, amplitude_ids, requester, ignore_invalid_id, delete_from_org),
195
+ "Content-Type" => "application/json"
196
+ )
197
+ end
198
+
199
+ private
200
+
201
+ def delete_body(user_ids, amplitude_ids, requester, ignore_invalid_id, delete_from_org)
202
+ body = {
203
+ amplitude_ids: amplitude_ids,
204
+ user_ids: user_ids,
205
+ requester: requester
206
+ }.delete_if { |_, value| value.nil? || value.empty? }
207
+
208
+ body[:ignore_invalid_id] = ignore_invalid_id.to_s if ignore_invalid_id
209
+ body[:delete_from_org] = delete_from_org.to_s if delete_from_org
210
+ JSON.generate(body)
188
211
  end
189
212
  end
190
213
  end
data/readme.md CHANGED
@@ -19,7 +19,7 @@ AmplitudeAPI.config.api_key = "abcdef123456"
19
19
 
20
20
 
21
21
  event = AmplitudeAPI::Event.new({
22
- user_id: "123",
22
+ user_id: "12345",
23
23
  event_type: "clicked on home",
24
24
  time: Time.now,
25
25
  insert_id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
@@ -31,7 +31,7 @@ event = AmplitudeAPI::Event.new({
31
31
  AmplitudeAPI.track(event)
32
32
  ```
33
33
 
34
- ## GDPR Compliance
34
+ ## User Privacy APIs
35
35
 
36
36
  The following code snippet will delete a user from amplitude
37
37
 
@@ -42,12 +42,13 @@ AmplitudeAPI.config.api_key = "abcdef123456"
42
42
  # Configure your Amplitude Secret Key
43
43
  AmplitudeAPI.config.secret_key = "secretMcSecret"
44
44
 
45
- AmplitudeAPI.delete(user_ids: [233],
46
- requester: "privacy@mycompany.com"
45
+ AmplitudeAPI.delete(user_ids: ["12345"],
46
+ requester: "privacy@example.com"
47
47
  )
48
48
  ```
49
49
 
50
- Currently, we are using this in Rails and using ActiveJob to dispatch events asynchronously. I plan on moving background/asynchronous support into this gem.
50
+ Currently, we are using this in Rails and using ActiveJob to dispatch events asynchronously. I plan on moving
51
+ background/asynchronous support into this gem.
51
52
 
52
53
  ## What's Next
53
54
 
@@ -55,7 +56,7 @@ Currently, we are using this in Rails and using ActiveJob to dispatch events asy
55
56
  * Configurable default account to use when no `user_id` present
56
57
 
57
58
  ## Other useful resources
58
- * [Amplitude HTTP Api Documentation](https://amplitude.zendesk.com/hc/en-us/articles/204771828)
59
+ * [Amplitude HTTP API V2 Api Documentation](https://developers.amplitude.com/docs/http-api-v2)
59
60
  * [Segment.io Amplitude integration](https://segment.com/docs/integrations/amplitude/)
60
61
 
61
62
  ## Contributing
@@ -1,277 +1,318 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
2
4
 
3
5
  describe AmplitudeAPI::Event do
4
6
  user = Struct.new(:id)
5
7
 
6
- context 'with a user object' do
7
- describe '#body' do
8
+ context "with a user object" do
9
+ describe "#body" do
8
10
  it "populates with the user's id" do
9
11
  event = described_class.new(
10
12
  user_id: user.new(123),
11
- event_type: 'clicked on home'
13
+ event_type: "clicked on home"
12
14
  )
13
15
  expect(event.to_hash[:user_id]).to eq(123)
14
16
  end
15
17
  end
16
18
  end
17
19
 
18
- context 'with a user id' do
19
- describe '#body' do
20
+ context "with a user id" do
21
+ describe "#body" do
20
22
  it "populates with the user's id" do
21
23
  event = described_class.new(
22
24
  user_id: 123,
23
- event_type: 'clicked on home'
25
+ event_type: "clicked on home"
24
26
  )
25
27
  expect(event.to_hash[:user_id]).to eq(123)
26
28
  end
27
29
  end
28
30
  end
29
31
 
30
- context 'without a user' do
31
- describe '#body' do
32
- it 'populates with the unknown user' do
32
+ context "without a user" do
33
+ describe "#body" do
34
+ it "populates with the unknown user" do
33
35
  event = described_class.new(
34
36
  user_id: nil,
35
- event_type: 'clicked on home'
37
+ event_type: "clicked on home"
36
38
  )
37
39
  expect(event.to_hash[:user_id]).to eq(AmplitudeAPI::USER_WITH_NO_ACCOUNT)
38
40
  end
39
41
  end
40
42
  end
41
43
 
42
- describe 'init' do
43
- context 'attributes' do
44
- it 'accepts string attributes' do
44
+ describe "init" do
45
+ context "attributes" do
46
+ it "accepts string attributes" do
45
47
  time = Time.at(1_451_606_400_000 / 1_000)
46
48
  event = described_class.new(
47
- 'user_id' => 123,
48
- 'device_id' => 'abcd',
49
- 'event_type' => 'sausage',
50
- 'event_properties' => { 'a' => 'b' },
51
- 'user_properties' => { 'c' => 'd' },
52
- 'time' => time,
53
- 'ip' => '127.0.0.1',
54
- 'platform' => 'Web',
55
- 'country' => 'United States',
56
- 'insert_id' => 'bestId'
49
+ "user_id" => 123,
50
+ "device_id" => "abcd",
51
+ "event_type" => "sausage",
52
+ "event_properties" => { "a" => "b" },
53
+ "user_properties" => { "c" => "d" },
54
+ "time" => time,
55
+ "ip" => "127.0.0.1",
56
+ "platform" => "Web",
57
+ "country" => "United States",
58
+ "insert_id" => "bestId"
57
59
  )
58
60
 
59
- expect(event.to_hash).to eq(event_type: 'sausage',
61
+ expect(event.to_hash).to eq(event_type: "sausage",
60
62
  user_id: 123,
61
- device_id: 'abcd',
62
- event_properties: { 'a' => 'b' },
63
- user_properties: { 'c' => 'd' },
63
+ device_id: "abcd",
64
+ event_properties: { "a" => "b" },
65
+ user_properties: { "c" => "d" },
64
66
  time: 1_451_606_400_000,
65
- ip: '127.0.0.1',
66
- platform: 'Web',
67
- country: 'United States',
68
- insert_id: 'bestId')
67
+ ip: "127.0.0.1",
68
+ platform: "Web",
69
+ country: "United States",
70
+ insert_id: "bestId")
69
71
  end
70
72
 
71
- it 'accepts symbol attributes' do
73
+ it "accepts symbol attributes" do
72
74
  time = Time.at(1_451_606_400_000 / 1_000)
73
75
  event = described_class.new(
74
76
  user_id: 123,
75
- device_id: 'abcd',
76
- event_type: 'sausage',
77
- event_properties: { 'a' => 'b' },
78
- user_properties: { 'c' => 'd' },
77
+ device_id: "abcd",
78
+ event_type: "sausage",
79
+ event_properties: { "a" => "b" },
80
+ user_properties: { "c" => "d" },
79
81
  time: time,
80
- ip: '127.0.0.1',
81
- platform: 'Web',
82
- country: 'United States',
83
- insert_id: 'bestId'
82
+ ip: "127.0.0.1",
83
+ platform: "Web",
84
+ country: "United States",
85
+ insert_id: "bestId"
84
86
  )
85
87
 
86
- expect(event.to_hash).to eq(event_type: 'sausage',
88
+ expect(event.to_hash).to eq(event_type: "sausage",
87
89
  user_id: 123,
88
- device_id: 'abcd',
89
- event_properties: { 'a' => 'b' },
90
- user_properties: { 'c' => 'd' },
90
+ device_id: "abcd",
91
+ event_properties: { "a" => "b" },
92
+ user_properties: { "c" => "d" },
91
93
  time: 1_451_606_400_000,
92
- ip: '127.0.0.1',
93
- platform: 'Web',
94
- country: 'United States',
95
- insert_id: 'bestId')
94
+ ip: "127.0.0.1",
95
+ platform: "Web",
96
+ country: "United States",
97
+ insert_id: "bestId")
96
98
  end
97
99
  end
98
100
 
99
- context 'the user does not send in a price' do
100
- it 'raises an error if the user sends in a product_id' do
101
+ context "the user sends a revenue_type or a product_id" do
102
+ it "raises an error if there is not a price neither a revenue" do
101
103
  expect do
102
104
  described_class.new(
103
105
  user_id: 123,
104
- event_type: 'bad event',
105
- product_id: 'hopscotch.4lyfe'
106
+ event_type: "bad event",
107
+ product_id: "hopscotch.4lyfe"
106
108
  )
107
- end.to raise_error(ArgumentError)
109
+ end.to raise_error ArgumentError, /You must provide a price or a revenue/
110
+
111
+ expect do
112
+ described_class.new(
113
+ user_id: 123,
114
+ event_type: "bad event",
115
+ revenue_type: "whatever"
116
+ )
117
+ end.to raise_error ArgumentError, /You must provide a price or a revenue/
108
118
  end
109
119
 
110
- it 'raises an error if the user sends in a revenue_type' do
120
+ it "does not raise an error if there is a price" do
111
121
  expect do
112
122
  described_class.new(
113
123
  user_id: 123,
114
- event_type: 'bad event',
115
- revenue_type: 'tax return'
124
+ event_type: "bad event",
125
+ product_id: "hopscotch.4lyfe",
126
+ price: 10.2
116
127
  )
117
- end.to raise_error(ArgumentError)
128
+ end.not_to raise_error
129
+
130
+ expect do
131
+ described_class.new(
132
+ user_id: 123,
133
+ event_type: "bad event",
134
+ revenue_type: "whatever",
135
+ price: 10.2
136
+ )
137
+ end.not_to raise_error
138
+ end
139
+
140
+ it "does not raise an error if there is a revenue" do
141
+ expect do
142
+ described_class.new(
143
+ user_id: 123,
144
+ event_type: "bad event",
145
+ product_id: "hopscotch.4lyfe",
146
+ revenue: 100.1
147
+ )
148
+ end.not_to raise_error
149
+
150
+ expect do
151
+ described_class.new(
152
+ user_id: 123,
153
+ event_type: "bad event",
154
+ revenue_type: "whatever",
155
+ revenue: 100.1
156
+ )
157
+ end.not_to raise_error
118
158
  end
119
159
  end
120
160
  end
121
161
 
122
- describe '#to_hash' do
123
- it 'includes the event type' do
162
+ describe "#to_hash" do
163
+ it "includes the event type" do
124
164
  event = described_class.new(
125
165
  user_id: 123,
126
- event_type: 'clicked on home'
166
+ event_type: "clicked on home"
127
167
  )
128
- expect(event.to_hash[:event_type]).to eq('clicked on home')
168
+ expect(event.to_hash[:event_type]).to eq("clicked on home")
129
169
  end
130
170
 
131
- it 'includes arbitrary properties' do
171
+ it "includes arbitrary properties" do
132
172
  event = described_class.new(
133
173
  user_id: 123,
134
- event_type: 'clicked on home',
174
+ event_type: "clicked on home",
135
175
  event_properties: { abc: :def }
136
176
  )
137
177
  expect(event.to_hash[:event_properties]).to eq(abc: :def)
138
178
  end
139
179
 
140
- describe 'time' do
141
- it 'includes a time for the event' do
180
+ describe "time" do
181
+ it "includes a time for the event" do
142
182
  time = Time.at(1_451_606_400_000 / 1_000)
143
183
  event = described_class.new(
144
184
  user_id: 123,
145
- event_type: 'clicked on home',
185
+ event_type: "clicked on home",
146
186
  time: time
147
187
  )
148
188
  expect(event.to_hash[:time]).to eq(1_451_606_400_000)
149
189
  end
150
190
 
151
- it 'does not include time if it is not set' do
191
+ it "does not include time if it is not set" do
152
192
  event = described_class.new(
153
193
  user_id: 123,
154
- event_type: 'clicked on home'
194
+ event_type: "clicked on home"
155
195
  )
156
196
  expect(event.to_hash).not_to have_key(:time)
157
197
  end
158
198
  end
159
199
 
160
- describe 'insert_id' do
161
- it 'includes an insert_id for the event' do
200
+ describe "insert_id" do
201
+ it "includes an insert_id for the event" do
162
202
  event = described_class.new(
163
203
  user_id: 123,
164
- event_type: 'clicked on home',
165
- insert_id: 'foo-bar'
204
+ event_type: "clicked on home",
205
+ insert_id: "foo-bar"
166
206
  )
167
- expect(event.to_hash[:insert_id]).to eq('foo-bar')
207
+ expect(event.to_hash[:insert_id]).to eq("foo-bar")
168
208
  end
169
209
 
170
- it 'does not include insert_id if it is not set' do
210
+ it "does not include insert_id if it is not set" do
171
211
  event = described_class.new(
172
212
  user_id: 123,
173
- event_type: 'clicked on home'
213
+ event_type: "clicked on home"
174
214
  )
175
215
  expect(event.to_hash).not_to have_key(:insert_id)
176
216
  end
177
217
  end
178
218
 
179
- describe 'platform' do
180
- it 'includes the platform for the event' do
219
+ describe "platform" do
220
+ it "includes the platform for the event" do
181
221
  event = described_class.new(
182
222
  user_id: 123,
183
- event_type: 'clicked on home',
184
- platform: 'Web'
223
+ event_type: "clicked on home",
224
+ platform: "Web"
185
225
  )
186
- expect(event.to_hash[:platform]).to eq('Web')
226
+ expect(event.to_hash[:platform]).to eq("Web")
187
227
  end
188
228
 
189
- it 'does not include the platform if it is not set' do
229
+ it "does not include the platform if it is not set" do
190
230
  event = described_class.new(
191
231
  user_id: 123,
192
- event_type: 'clicked on home'
232
+ event_type: "clicked on home"
193
233
  )
194
234
  expect(event.to_hash).not_to have_key(:platform)
195
235
  end
196
236
  end
197
237
 
198
- describe 'country' do
199
- it 'includes the country for the event' do
238
+ describe "country" do
239
+ it "includes the country for the event" do
200
240
  event = described_class.new(
201
241
  user_id: 123,
202
- event_type: 'clicked on home',
203
- country: 'United States'
242
+ event_type: "clicked on home",
243
+ country: "United States"
204
244
  )
205
- expect(event.to_hash[:country]).to eq('United States')
245
+ expect(event.to_hash[:country]).to eq("United States")
206
246
  end
207
247
 
208
- it 'does not include the country if it is not set' do
248
+ it "does not include the country if it is not set" do
209
249
  event = described_class.new(
210
250
  user_id: 123,
211
- event_type: 'clicked on home'
251
+ event_type: "clicked on home"
212
252
  )
213
253
  expect(event.to_hash).not_to have_key(:country)
214
254
  end
215
255
  end
216
256
 
217
- describe 'revenue params' do
218
- it 'includes the price if it is set' do
257
+ describe "revenue params" do
258
+ it "includes the price if it is set" do
219
259
  price = 100_000.99
220
260
  event = described_class.new(
221
261
  user_id: 123,
222
- event_type: 'clicked on home',
262
+ event_type: "clicked on home",
223
263
  price: price
224
264
  )
225
265
  expect(event.to_hash[:price]).to eq(price)
226
266
  end
227
267
 
228
- it 'sets the quantity to 1 if the price is set and the quantity is not' do
229
- price = 100_000.99
268
+ it "includes the quantity if it is set" do
269
+ quantity = 100
230
270
  event = described_class.new(
231
271
  user_id: 123,
232
- event_type: 'clicked on home',
233
- price: price
272
+ event_type: "clicked on home",
273
+ quantity: quantity,
274
+ price: 10.99
234
275
  )
235
- expect(event.to_hash[:quantity]).to eq(1)
276
+ expect(event.to_hash[:quantity]).to eq(quantity)
236
277
  end
237
278
 
238
- it 'includes the quantity if it is set' do
239
- quantity = 100
279
+ it "includes the revenue if it is set" do
280
+ revenue = 100
240
281
  event = described_class.new(
241
282
  user_id: 123,
242
- event_type: 'clicked on home',
243
- quantity: quantity,
244
- price: 10.99
283
+ event_type: "clicked on home",
284
+ quantity: 456,
285
+ revenue: revenue
245
286
  )
246
- expect(event.to_hash[:quantity]).to eq(quantity)
287
+ expect(event.to_hash[:revenue]).to eq(revenue)
247
288
  end
248
289
 
249
- it 'includes the productID if set' do
250
- product_id = 'hopscotch.subscriptions.rule'
290
+ it "includes the productID if set" do
291
+ product_id = "hopscotch.subscriptions.rule"
251
292
  event = described_class.new(
252
293
  user_id: 123,
253
- event_type: 'clicked on home',
294
+ event_type: "clicked on home",
254
295
  price: 199.99,
255
296
  product_id: product_id
256
297
  )
257
298
  expect(event.to_hash[:productId]).to eq(product_id)
258
299
  end
259
300
 
260
- it 'includes the revenueType if set' do
261
- revenue_type = 'income'
301
+ it "includes the revenueType if set" do
302
+ revenue_type = "income"
262
303
  event = described_class.new(
263
304
  user_id: 123,
264
- event_type: 'clicked on home',
305
+ event_type: "clicked on home",
265
306
  price: 199.99,
266
307
  revenue_type: revenue_type
267
308
  )
268
309
  expect(event.to_hash[:revenueType]).to eq(revenue_type)
269
310
  end
270
311
 
271
- it 'does not include revenue params if they are not set' do
312
+ it "does not include revenue params if they are not set" do
272
313
  event = described_class.new(
273
314
  user_id: 123,
274
- event_type: 'clicked on home'
315
+ event_type: "clicked on home"
275
316
  )
276
317
  expect(event.to_hash).not_to have_key(:quantity)
277
318
  expect(event.to_hash).not_to have_key(:revenueType)
@@ -280,4 +321,44 @@ describe AmplitudeAPI::Event do
280
321
  end
281
322
  end
282
323
  end
324
+
325
+ describe "arbitrary properties" do
326
+ # We need to create a class for each test because the methods we are calling
327
+ # in this test group are modifying the class
328
+ let(:klass) { Class.new described_class }
329
+
330
+ let(:event) {
331
+ klass.new(
332
+ user_id: 123,
333
+ event_type: "bad event"
334
+ )
335
+ }
336
+
337
+ it "creates arbitrary properties when assigning values" do
338
+ event.arbitrary_property = "arbitrary value"
339
+
340
+ expect(event.arbitrary_property).to eq "arbitrary value"
341
+ end
342
+
343
+ it "responds_to? arbitrary properties" do
344
+ event.arbitrary_property = "arbitrary value"
345
+
346
+ expect(event.respond_to?(:arbitrary_property)).to be true
347
+ expect(event.respond_to?(:arbitrary_property=)).to be true
348
+ end
349
+
350
+ it "do not accepts blocks when assigning values to create properties" do
351
+ expect do
352
+ event.arbitrary_property { puts "whatever" }
353
+ end.to raise_error NoMethodError
354
+ end
355
+
356
+ it "includes arbitrary properties in the generated hash" do
357
+ event.arbitrary_property = "arbitrary value"
358
+
359
+ hash = event.to_hash
360
+
361
+ expect(hash).to include(arbitrary_property: "arbitrary value")
362
+ end
363
+ end
283
364
  end