amplitude-api 0.1.0 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class is 115 lines long. It's on the limit, it should be refactored before
4
+ # including more code.
5
+ #
6
+ # rubocop:disable Metrics/ClassLength
1
7
  class AmplitudeAPI
2
8
  # AmplitudeAPI::Event
3
9
  class Event
@@ -7,15 +13,46 @@ class AmplitudeAPI
7
13
 
8
14
  # Create a new Event
9
15
  #
10
- # See (Amplitude HTTP API Documentation)[https://amplitude.zendesk.com/hc/en-us/articles/204771828-HTTP-API]
16
+ # See (Amplitude HTTP API Documentation)[https://developers.amplitude.com/docs/http-api-v2]
11
17
  # for a list of valid parameters and their types.
12
18
  def initialize(attributes = {})
19
+ @extra_properties = []
13
20
  attributes.each do |k, v|
14
- send("#{k}=", v) if respond_to?("#{k}=")
21
+ send("#{k}=", v)
15
22
  end
16
23
  validate_arguments
17
24
  end
18
25
 
26
+ def method_missing(method_name, *args)
27
+ super if block_given?
28
+ super unless method_name.to_s.end_with? "="
29
+
30
+ property_name = method_name.to_s.delete_suffix("=")
31
+
32
+ @extra_properties << property_name
33
+
34
+ create_setter property_name
35
+ create_getter property_name
36
+
37
+ send("#{property_name}=".to_sym, *args)
38
+ end
39
+
40
+ def create_setter(attribute_name)
41
+ self.class.send(:define_method, "#{attribute_name}=".to_sym) do |value|
42
+ instance_variable_set("@" + attribute_name.to_s, value)
43
+ end
44
+ end
45
+
46
+ def create_getter(attribute_name)
47
+ self.class.send(:define_method, attribute_name.to_sym) do
48
+ instance_variable_get("@" + attribute_name.to_s)
49
+ end
50
+ end
51
+
52
+ def respond_to_missing?(method_name, *args)
53
+ @extra_properties.include?(method_name) || @extra_properties.include?("#{method_name}=") || super
54
+ end
55
+
19
56
  def user_id=(value)
20
57
  @user_id =
21
58
  if value.respond_to?(:id)
@@ -36,28 +73,42 @@ class AmplitudeAPI
36
73
  }
37
74
  event[:user_id] = user_id if user_id
38
75
  event[:device_id] = device_id if device_id
39
- event.merge(optional_properties).merge(revenue_hash)
76
+ event.merge(optional_properties).merge(revenue_hash).merge(extra_properties)
40
77
  end
41
78
  alias to_h to_hash
42
79
 
43
80
  # @return [ Hash ] Optional properties
81
+ #
82
+ # Returns optional properties (belong to the API but are optional)
44
83
  def optional_properties
45
- %i[device_id time ip platform country insert_id].map do |prop|
84
+ AmplitudeAPI::Config.optional_properties.map do |prop|
46
85
  val = prop == :time ? formatted_time : send(prop)
47
86
  val ? [prop, val] : nil
48
87
  end.compact.to_h
49
88
  end
50
89
 
90
+ # @return [ Hash ] Extra properties
91
+ #
92
+ # Returns optional properties (not belong to the API, are assigned by the user)
93
+ # This way, if the API is updated with new properties, the gem will be able
94
+ # to work with the new specification until the code is modified
95
+ def extra_properties
96
+ @extra_properties.map do |prop|
97
+ val = send(prop)
98
+ val ? [prop.to_sym, val] : nil
99
+ end.compact.to_h
100
+ end
101
+
51
102
  # @return [ true, false ]
52
103
  #
53
104
  # Returns true if the event type matches one reserved by Amplitude API.
54
105
  def reserved_event?(type)
55
- ['[Amplitude] Start Session',
56
- '[Amplitude] End Session',
57
- '[Amplitude] Revenue',
58
- '[Amplitude] Revenue (Verified)',
59
- '[Amplitude] Revenue (Unverified)',
60
- '[Amplitude] Merged User'].include?(type)
106
+ ["[Amplitude] Start Session",
107
+ "[Amplitude] End Session",
108
+ "[Amplitude] Revenue",
109
+ "[Amplitude] Revenue (Verified)",
110
+ "[Amplitude] Revenue (Unverified)",
111
+ "[Amplitude] Merged User"].include?(type)
61
112
  end
62
113
 
63
114
  # @return [ true, false ]
@@ -89,15 +140,23 @@ class AmplitudeAPI
89
140
  end
90
141
 
91
142
  def validate_required_arguments
92
- raise ArgumentError, 'You must provide user_id or device_id (or both)' unless user_id || device_id
93
- raise ArgumentError, 'You must provide event_type' unless event_type
94
- raise ArgumentError, 'Invalid event_type - cannot match a reserved event name' if reserved_event?(event_type)
143
+ raise ArgumentError, "You must provide user_id or device_id (or both)" unless user_id || device_id
144
+ raise ArgumentError, "You must provide event_type" unless event_type
145
+ raise ArgumentError, "Invalid event_type - cannot match a reserved event name" if reserved_event?(event_type)
95
146
  end
96
147
 
97
148
  def validate_revenue_arguments
98
- return self.quantity ||= 1 if price
99
- raise ArgumentError, 'You must provide a price in order to use the product_id' if product_id
100
- raise ArgumentError, 'You must provide a price in order to use the revenue_type' if revenue_type
149
+ return true if !revenue_type && !product_id
150
+ return true if revenue || price
151
+
152
+ raise ArgumentError, revenue_error_message
153
+ end
154
+
155
+ def revenue_error_message
156
+ error_field = "product_id" if product_id
157
+ error_field = "revenue_type" if revenue_type
158
+
159
+ "You must provide a price or a revenue in order to use the field #{error_field}"
101
160
  end
102
161
 
103
162
  def revenue_hash
@@ -106,6 +165,7 @@ class AmplitudeAPI
106
165
  revenue_hash[:revenueType] = revenue_type if revenue_type
107
166
  revenue_hash[:quantity] = quantity if quantity
108
167
  revenue_hash[:price] = price if price
168
+ revenue_hash[:revenue] = revenue if revenue
109
169
  revenue_hash
110
170
  end
111
171
 
@@ -114,3 +174,4 @@ class AmplitudeAPI
114
174
  end
115
175
  end
116
176
  end
177
+ # rubocop:enable Metrics/ClassLength
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AmplitudeAPI
2
4
  # AmplitudeAPI::Identification
3
5
  class Identification
@@ -16,7 +18,7 @@ class AmplitudeAPI
16
18
  # @param [ String ] user_id a user_id to associate with the identification
17
19
  # @param [ String ] device_id a device_id to associate with the identification
18
20
  # @param [ Hash ] user_properties various properties to attach to the user identification
19
- def initialize(user_id: '', device_id: nil, user_properties: {})
21
+ def initialize(user_id: "", device_id: nil, user_properties: {})
20
22
  self.user_id = user_id
21
23
  self.device_id = device_id if device_id
22
24
  self.user_properties = user_properties
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AmplitudeAPI
2
- VERSION = '0.1.0'.freeze
4
+ VERSION = "0.3.3"
3
5
  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,38 @@ event = AmplitudeAPI::Event.new({
31
31
  AmplitudeAPI.track(event)
32
32
  ```
33
33
 
34
- ## GDPR Compliance
34
+ You can track multiple events with a single call, with the only limit of the payload
35
+ size imposed by Amplitude:
36
+
37
+ ```ruby
38
+ event_1 = AmplitudeAPI::Event.new(...)
39
+ event_2 = AmplitudeAPI::Event.new(...)
40
+
41
+ AmplitudeAPI.track(event_1, event_2)
42
+ ```
43
+
44
+ ```ruby
45
+ events = [event_1, event_2]
46
+ AmplitudeAPI.track(*events)
47
+ ```
48
+
49
+ In case you use an integer as the time, it is expected to be in seconds. Values in
50
+ the time field will be converted to milliseconds using `->(time) { time ? time.to_i * 1_000 : nil }`
51
+ You can change this behaviour and use your custom formatter. For example, in case
52
+ you wanted to use milliseconds instead of seconds you could do this:
53
+ ```ruby
54
+ AmplitudeAPI.config.time_formatter = ->(time) { time ? time.to_i : nil },
55
+ ```
56
+
57
+ You can speficy track options in the config. The options will be applied to all subsequent requests:
58
+
59
+ ```ruby
60
+ AmplitudeAPI.config.options = { min_id_length: 10 }
61
+ AmplitudeAPI.track(event)
62
+ ```
63
+
64
+
65
+ ## User Privacy APIs
35
66
 
36
67
  The following code snippet will delete a user from amplitude
37
68
 
@@ -42,12 +73,13 @@ AmplitudeAPI.config.api_key = "abcdef123456"
42
73
  # Configure your Amplitude Secret Key
43
74
  AmplitudeAPI.config.secret_key = "secretMcSecret"
44
75
 
45
- AmplitudeAPI.delete(user_ids: [233],
46
- requester: "privacy@mycompany.com"
76
+ AmplitudeAPI.delete(user_ids: ["12345"],
77
+ requester: "privacy@example.com"
47
78
  )
48
79
  ```
49
80
 
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.
81
+ Currently, we are using this in Rails and using ActiveJob to dispatch events asynchronously. I plan on moving
82
+ background/asynchronous support into this gem.
51
83
 
52
84
  ## What's Next
53
85
 
@@ -55,7 +87,7 @@ Currently, we are using this in Rails and using ActiveJob to dispatch events asy
55
87
  * Configurable default account to use when no `user_id` present
56
88
 
57
89
  ## Other useful resources
58
- * [Amplitude HTTP Api Documentation](https://amplitude.zendesk.com/hc/en-us/articles/204771828)
90
+ * [Amplitude HTTP API V2 Api Documentation](https://developers.amplitude.com/docs/http-api-v2)
59
91
  * [Segment.io Amplitude integration](https://segment.com/docs/integrations/amplitude/)
60
92
 
61
93
  ## 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
103
+ expect do
104
+ described_class.new(
105
+ user_id: 123,
106
+ event_type: "bad event",
107
+ product_id: "hopscotch.4lyfe"
108
+ )
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/
118
+ end
119
+
120
+ it "does not raise an error if there is a price" do
121
+ expect do
122
+ described_class.new(
123
+ user_id: 123,
124
+ event_type: "bad event",
125
+ product_id: "hopscotch.4lyfe",
126
+ price: 10.2
127
+ )
128
+ end.not_to raise_error
129
+
101
130
  expect do
102
131
  described_class.new(
103
132
  user_id: 123,
104
- event_type: 'bad event',
105
- product_id: 'hopscotch.4lyfe'
133
+ event_type: "bad event",
134
+ revenue_type: "whatever",
135
+ price: 10.2
106
136
  )
107
- end.to raise_error(ArgumentError)
137
+ end.not_to raise_error
108
138
  end
109
139
 
110
- it 'raises an error if the user sends in a revenue_type' do
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
+
111
150
  expect do
112
151
  described_class.new(
113
152
  user_id: 123,
114
- event_type: 'bad event',
115
- revenue_type: 'tax return'
153
+ event_type: "bad event",
154
+ revenue_type: "whatever",
155
+ revenue: 100.1
116
156
  )
117
- end.to raise_error(ArgumentError)
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,63 @@ 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 properties on initialization" do
338
+ property_value = "an arbitrary value"
339
+ creation_data = {
340
+ user_id: "whatever",
341
+ event_type: "something happened",
342
+ arbitrary_property: property_value
343
+ }
344
+
345
+ initialized_event = klass.new(creation_data)
346
+
347
+ expect(initialized_event.arbitrary_property).to eq property_value
348
+ end
349
+
350
+ it "creates arbitrary properties when assigning values" do
351
+ event.arbitrary_property = "arbitrary value"
352
+
353
+ expect(event.arbitrary_property).to eq "arbitrary value"
354
+ end
355
+
356
+ it "responds_to? arbitrary properties" do
357
+ event.arbitrary_property = "arbitrary value"
358
+
359
+ expect(event.respond_to?(:arbitrary_property)).to be true
360
+ expect(event.respond_to?(:arbitrary_property=)).to be true
361
+ end
362
+
363
+ it "does not define property until assigned" do
364
+ expect {
365
+ event.undefined_property
366
+ }.to raise_error NoMethodError, /undefined_property/
367
+ end
368
+
369
+ it "do not accepts blocks when assigning values to create properties" do
370
+ expect do
371
+ event.arbitrary_property { puts "whatever" }
372
+ end.to raise_error NoMethodError
373
+ end
374
+
375
+ it "includes arbitrary properties in the generated hash" do
376
+ event.arbitrary_property = "arbitrary value"
377
+
378
+ hash = event.to_hash
379
+
380
+ expect(hash).to include(arbitrary_property: "arbitrary value")
381
+ end
382
+ end
283
383
  end