amplitude-api 0.0.8 → 0.3.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,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ class AmplitudeAPI
6
+ # AmplitudeAPI::Config
7
+ class Config
8
+ include Singleton
9
+
10
+ attr_accessor :api_key, :secret_key, :whitelist, :time_formatter,
11
+ :event_properties_formatter, :user_properties_formatter
12
+
13
+ def initialize
14
+ self.class.defaults.each { |k, v| send("#{k}=", v) }
15
+ end
16
+
17
+ class << self
18
+ def base_properties
19
+ %i[event_type event_properties user_properties user_id device_id]
20
+ end
21
+
22
+ def revenue_properties
23
+ %i[revenue_type product_id revenue price quantity]
24
+ end
25
+
26
+ def optional_properties
27
+ %i[
28
+ time
29
+ ip platform country insert_id
30
+ groups app_version os_name os_version
31
+ device_brand device_manufacturer device_model
32
+ carrier region city dma language
33
+ location_lat location_lng
34
+ idfa idfv adid android_id
35
+ event_id session_id
36
+ ]
37
+ end
38
+
39
+ def defaults
40
+ {
41
+ api_key: nil,
42
+ secret_key: nil,
43
+ whitelist: base_properties + revenue_properties + optional_properties,
44
+ time_formatter: ->(time) { time ? time.to_i * 1_000 : nil },
45
+ event_properties_formatter: ->(props) { props || {} },
46
+ user_properties_formatter: ->(props) { props || {} }
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,66 +1,55 @@
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
4
- # @!attribute [ rw ] user_id
5
- # @return [ String ] the user_id to be sent to Amplitude
6
- attr_accessor :user_id
7
- # @!attribute [ rw ] event_type
8
- # @return [ String ] the event_type to be sent to Amplitude
9
- attr_accessor :event_type
10
- # @!attribute [ rw ] event_properties
11
- # @return [ String ] the event_properties to be attached to the Amplitude Event
12
- attr_accessor :event_properties
13
- # @!attribute [ rw ] user_properties
14
- # @return [ String ] the user_properties to be passed for the user
15
- attr_accessor :user_properties
16
- # @!attribute [ rw ] time
17
- # @return [ Time ] Time that the event occurred (defaults to now)
18
- attr_accessor :time
19
- # @!attribute [ rw ] ip
20
- # @return [ String ] IP address of the user
21
- attr_accessor :ip
22
-
23
- # @!attribute [ rw ] insert_id
24
- # @return [ String ] the unique identifier to be sent to Amplitude
25
- attr_accessor :insert_id
26
-
27
- # @!attribute [ rw ] price
28
- # @return [ String ] (required for revenue data) price of the item purchased
29
- attr_accessor :price
30
-
31
- # @!attribute [ rw ] quantity
32
- # @return [ String ] (required for revenue data, defaults to 1 if not specified) quantity of the item purchased
33
- attr_accessor :quantity
34
-
35
- # @!attribute [ rw ] product_id
36
- # @return [ String ] an identifier for the product. (Note: you must send a price and quantity with this field)
37
- attr_accessor :product_id
38
-
39
- # @!attribute [ rw ] revenue_type
40
- # @return [ String ] type of revenue. (Note: you must send a price and quantity with this field)
41
- attr_accessor :revenue_type
10
+ AmplitudeAPI::Config.instance.whitelist.each do |attribute|
11
+ instance_eval("attr_accessor :#{attribute}", __FILE__, __LINE__)
12
+ end
42
13
 
43
14
  # Create a new Event
44
15
  #
45
- # @param [ String ] user_id a user_id to associate with the event
46
- # @param [ String ] event_type a name for the event
47
- # @param [ Hash ] event_properties various properties to attach to the event
48
- # @param [ Time ] Time that the event occurred (defaults to now)
49
- # @param [ Double ] price (optional, but required for revenue data) price of the item purchased
50
- # @param [ Integer ] quantity (optional, but required for revenue data) quantity of the item purchased
51
- # @param [ String ] product_id (optional) an identifier for the product.
52
- # @param [ String ] revenue_type (optional) type of revenue
53
- # @param [ String ] IP address of the user
54
- # @param [ String ] insert_id a unique identifier for the event
55
- def initialize(options = {})
56
- self.user_id = options.fetch(:user_id, '')
57
- self.event_type = options.fetch(:event_type, '')
58
- self.event_properties = options.fetch(:event_properties, {})
59
- self.user_properties = options.fetch(:user_properties, {})
60
- self.time = options[:time]
61
- self.ip = options.fetch(:ip, '')
62
- self.insert_id = options[:insert_id]
63
- validate_revenue_arguments(options)
16
+ # See (Amplitude HTTP API Documentation)[https://developers.amplitude.com/docs/http-api-v2]
17
+ # for a list of valid parameters and their types.
18
+ def initialize(attributes = {})
19
+ attributes.each do |k, v|
20
+ send("#{k}=", v) if respond_to?("#{k}=")
21
+ end
22
+ validate_arguments
23
+ @extra_properties = []
24
+ end
25
+
26
+ def method_missing(method_name, *args)
27
+ super if block_given?
28
+
29
+ property_name = method_name.to_s.delete_suffix("=")
30
+
31
+ @extra_properties << property_name
32
+
33
+ create_setter property_name
34
+ create_getter property_name
35
+
36
+ send("#{property_name}=".to_sym, *args)
37
+ end
38
+
39
+ def create_setter(attribute_name)
40
+ self.class.send(:define_method, "#{attribute_name}=".to_sym) do |value|
41
+ instance_variable_set("@" + attribute_name.to_s, value)
42
+ end
43
+ end
44
+
45
+ def create_getter(attribute_name)
46
+ self.class.send(:define_method, attribute_name.to_sym) do
47
+ instance_variable_get("@" + attribute_name.to_s)
48
+ end
49
+ end
50
+
51
+ def respond_to_missing?(method_name, *args)
52
+ @extra_properties.include?(method_name) || @extra_properties.include?("#{method_name}=") || super
64
53
  end
65
54
 
66
55
  def user_id=(value)
@@ -76,43 +65,97 @@ class AmplitudeAPI
76
65
  #
77
66
  # Used for serialization and comparison
78
67
  def to_hash
79
- serialized_event = {}
80
- serialized_event[:event_type] = event_type
81
- serialized_event[:user_id] = user_id
82
- serialized_event[:event_properties] = event_properties
83
- serialized_event[:user_properties] = user_properties
84
- serialized_event[:time] = formatted_time if time
85
- serialized_event[:ip] = ip if ip
86
- serialized_event[:insert_id] = insert_id if insert_id
68
+ event = {
69
+ event_type: event_type,
70
+ event_properties: formatted_event_properties,
71
+ user_properties: formatted_user_properties
72
+ }
73
+ event[:user_id] = user_id if user_id
74
+ event[:device_id] = device_id if device_id
75
+ event.merge(optional_properties).merge(revenue_hash).merge(extra_properties)
76
+ end
77
+ alias to_h to_hash
78
+
79
+ # @return [ Hash ] Optional properties
80
+ #
81
+ # Returns optional properties (belong to the API but are optional)
82
+ def optional_properties
83
+ AmplitudeAPI::Config.optional_properties.map do |prop|
84
+ val = prop == :time ? formatted_time : send(prop)
85
+ val ? [prop, val] : nil
86
+ end.compact.to_h
87
+ end
87
88
 
88
- serialized_event.merge(revenue_hash)
89
+ # @return [ Hash ] Extra properties
90
+ #
91
+ # Returns optional properties (not belong to the API, are assigned by the user)
92
+ # This way, if the API is updated with new properties, the gem will be able
93
+ # to work with the new specification until the code is modified
94
+ def extra_properties
95
+ @extra_properties.map do |prop|
96
+ val = send(prop)
97
+ val ? [prop.to_sym, val] : nil
98
+ end.compact.to_h
99
+ end
100
+
101
+ # @return [ true, false ]
102
+ #
103
+ # Returns true if the event type matches one reserved by Amplitude API.
104
+ def reserved_event?(type)
105
+ ["[Amplitude] Start Session",
106
+ "[Amplitude] End Session",
107
+ "[Amplitude] Revenue",
108
+ "[Amplitude] Revenue (Verified)",
109
+ "[Amplitude] Revenue (Unverified)",
110
+ "[Amplitude] Merged User"].include?(type)
89
111
  end
90
112
 
91
113
  # @return [ true, false ]
92
114
  #
93
115
  # Compares +to_hash+ for equality
94
116
  def ==(other)
95
- if other.respond_to?(:to_hash)
96
- to_hash == other.to_hash
97
- else
98
- false
99
- end
117
+ return false unless other.respond_to?(:to_h)
118
+
119
+ to_h == other.to_h
100
120
  end
101
121
 
102
122
  private
103
123
 
104
124
  def formatted_time
105
- time.to_i * 1_000
125
+ Config.instance.time_formatter.call(time)
126
+ end
127
+
128
+ def formatted_event_properties
129
+ Config.instance.event_properties_formatter.call(event_properties)
130
+ end
131
+
132
+ def formatted_user_properties
133
+ Config.instance.user_properties_formatter.call(user_properties)
106
134
  end
107
135
 
108
- def validate_revenue_arguments(options)
109
- self.price = options[:price]
110
- self.quantity = options[:quantity] || 1 if price
111
- self.product_id = options[:product_id]
112
- self.revenue_type = options[:revenue_type]
113
- return if price
114
- raise ArgumentError, 'You must provide a price in order to use the product_id' if product_id
115
- raise ArgumentError, 'You must provide a price in order to use the revenue_type' if revenue_type
136
+ def validate_arguments
137
+ validate_required_arguments
138
+ validate_revenue_arguments
139
+ end
140
+
141
+ def validate_required_arguments
142
+ raise ArgumentError, "You must provide user_id or device_id (or both)" unless user_id || device_id
143
+ raise ArgumentError, "You must provide event_type" unless event_type
144
+ raise ArgumentError, "Invalid event_type - cannot match a reserved event name" if reserved_event?(event_type)
145
+ end
146
+
147
+ def validate_revenue_arguments
148
+ return true if !revenue_type && !product_id
149
+ return true if revenue || price
150
+
151
+ raise ArgumentError, revenue_error_message
152
+ end
153
+
154
+ def revenue_error_message
155
+ error_field = "product_id" if product_id
156
+ error_field = "revenue_type" if revenue_type
157
+
158
+ "You must provide a price or a revenue in order to use the field #{error_field}"
116
159
  end
117
160
 
118
161
  def revenue_hash
@@ -121,7 +164,13 @@ class AmplitudeAPI
121
164
  revenue_hash[:revenueType] = revenue_type if revenue_type
122
165
  revenue_hash[:quantity] = quantity if quantity
123
166
  revenue_hash[:price] = price if price
167
+ revenue_hash[:revenue] = revenue if revenue
124
168
  revenue_hash
125
169
  end
170
+
171
+ def getopt(options, key, default = nil)
172
+ options.fetch(key.to_sym, options.fetch(key.to_s, default))
173
+ end
126
174
  end
127
175
  end
176
+ # rubocop:enable Metrics/ClassLength
@@ -1,9 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AmplitudeAPI
2
4
  # AmplitudeAPI::Identification
3
5
  class Identification
4
6
  # @!attribute [ rw ] user_id
5
7
  # @return [ String ] the user_id to be sent to Amplitude
6
- attr_accessor :user_id
8
+ attr_reader :user_id
9
+ # @!attribute [ rw ] device_id
10
+ # @return [ String ] the device_id to be sent to Amplitude
11
+ attr_accessor :device_id
7
12
  # @!attribute [ rw ] user_properties
8
13
  # @return [ String ] the user_properties to be attached to the Amplitude Identify
9
14
  attr_accessor :user_properties
@@ -11,9 +16,11 @@ class AmplitudeAPI
11
16
  # Create a new Identification
12
17
  #
13
18
  # @param [ String ] user_id a user_id to associate with the identification
19
+ # @param [ String ] device_id a device_id to associate with the identification
14
20
  # @param [ Hash ] user_properties various properties to attach to the user identification
15
- def initialize(user_id: '', user_properties: {})
21
+ def initialize(user_id: "", device_id: nil, user_properties: {})
16
22
  self.user_id = user_id
23
+ self.device_id = device_id if device_id
17
24
  self.user_properties = user_properties
18
25
  end
19
26
 
@@ -33,7 +40,7 @@ class AmplitudeAPI
33
40
  {
34
41
  user_id: user_id,
35
42
  user_properties: user_properties
36
- }
43
+ }.tap { |hsh| hsh[:device_id] = device_id if device_id }
37
44
  end
38
45
 
39
46
  # @return [ true, false ]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AmplitudeAPI
2
- VERSION = '0.0.8'.freeze
4
+ VERSION = "0.3.0"
3
5
  end
data/readme.md CHANGED
@@ -15,10 +15,11 @@ The following code snippet will immediately track an event to the Amplitude API.
15
15
 
16
16
  ```ruby
17
17
  # Configure your Amplitude API key
18
- AmplitudeAPI.api_key = "abcdef123456"
18
+ AmplitudeAPI.config.api_key = "abcdef123456"
19
+
19
20
 
20
21
  event = AmplitudeAPI::Event.new({
21
- user_id: "123",
22
+ user_id: "12345",
22
23
  event_type: "clicked on home",
23
24
  time: Time.now,
24
25
  insert_id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
@@ -30,16 +31,32 @@ event = AmplitudeAPI::Event.new({
30
31
  AmplitudeAPI.track(event)
31
32
  ```
32
33
 
33
- Currently, we are using this in Rails and using ActiveJob to dispatch events asynchronously. I plan on moving background/asynchronous support into this gem.
34
+ ## User Privacy APIs
35
+
36
+ The following code snippet will delete a user from amplitude
37
+
38
+ ```ruby
39
+ # Configure your Amplitude API key
40
+ AmplitudeAPI.config.api_key = "abcdef123456"
41
+
42
+ # Configure your Amplitude Secret Key
43
+ AmplitudeAPI.config.secret_key = "secretMcSecret"
44
+
45
+ AmplitudeAPI.delete(user_ids: ["12345"],
46
+ requester: "privacy@example.com"
47
+ )
48
+ ```
49
+
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.
34
52
 
35
53
  ## What's Next
36
54
 
37
55
  * Thread support for background dispatching in bulk
38
- * `device_id` support as an alternative to `user_id`
39
56
  * Configurable default account to use when no `user_id` present
40
57
 
41
58
  ## Other useful resources
42
- * [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)
43
60
  * [Segment.io Amplitude integration](https://segment.com/docs/integrations/amplitude/)
44
61
 
45
62
  ## Contributing
@@ -1,183 +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 'the user does not send in a price' do
44
- it 'raises an error if the user sends in a product_id' do
44
+ describe "init" do
45
+ context "attributes" do
46
+ it "accepts string attributes" do
47
+ time = Time.at(1_451_606_400_000 / 1_000)
48
+ event = described_class.new(
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"
59
+ )
60
+
61
+ expect(event.to_hash).to eq(event_type: "sausage",
62
+ user_id: 123,
63
+ device_id: "abcd",
64
+ event_properties: { "a" => "b" },
65
+ user_properties: { "c" => "d" },
66
+ time: 1_451_606_400_000,
67
+ ip: "127.0.0.1",
68
+ platform: "Web",
69
+ country: "United States",
70
+ insert_id: "bestId")
71
+ end
72
+
73
+ it "accepts symbol attributes" do
74
+ time = Time.at(1_451_606_400_000 / 1_000)
75
+ event = described_class.new(
76
+ user_id: 123,
77
+ device_id: "abcd",
78
+ event_type: "sausage",
79
+ event_properties: { "a" => "b" },
80
+ user_properties: { "c" => "d" },
81
+ time: time,
82
+ ip: "127.0.0.1",
83
+ platform: "Web",
84
+ country: "United States",
85
+ insert_id: "bestId"
86
+ )
87
+
88
+ expect(event.to_hash).to eq(event_type: "sausage",
89
+ user_id: 123,
90
+ device_id: "abcd",
91
+ event_properties: { "a" => "b" },
92
+ user_properties: { "c" => "d" },
93
+ time: 1_451_606_400_000,
94
+ ip: "127.0.0.1",
95
+ platform: "Web",
96
+ country: "United States",
97
+ insert_id: "bestId")
98
+ end
99
+ end
100
+
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
+
45
111
  expect do
46
112
  described_class.new(
47
113
  user_id: 123,
48
- event_type: 'bad event',
49
- product_id: 'hopscotch.4lyfe'
114
+ event_type: "bad event",
115
+ revenue_type: "whatever"
50
116
  )
51
- end.to raise_error(ArgumentError)
117
+ end.to raise_error ArgumentError, /You must provide a price or a revenue/
52
118
  end
53
119
 
54
- 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
55
121
  expect do
56
122
  described_class.new(
57
123
  user_id: 123,
58
- event_type: 'bad event',
59
- revenue_type: 'tax return'
124
+ event_type: "bad event",
125
+ product_id: "hopscotch.4lyfe",
126
+ price: 10.2
60
127
  )
61
- 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
62
158
  end
63
159
  end
64
160
  end
65
161
 
66
- describe '#body' do
67
- it 'includes the event type' do
162
+ describe "#to_hash" do
163
+ it "includes the event type" do
68
164
  event = described_class.new(
69
165
  user_id: 123,
70
- event_type: 'clicked on home'
166
+ event_type: "clicked on home"
71
167
  )
72
- expect(event.to_hash[:event_type]).to eq('clicked on home')
168
+ expect(event.to_hash[:event_type]).to eq("clicked on home")
73
169
  end
74
170
 
75
- it 'includes arbitrary properties' do
171
+ it "includes arbitrary properties" do
76
172
  event = described_class.new(
77
173
  user_id: 123,
78
- event_type: 'clicked on home',
174
+ event_type: "clicked on home",
79
175
  event_properties: { abc: :def }
80
176
  )
81
177
  expect(event.to_hash[:event_properties]).to eq(abc: :def)
82
178
  end
83
179
 
84
- describe 'time' do
85
- it 'includes a time for the event' do
86
- time = Time.parse('2016-01-01 00:00:00 -0000')
180
+ describe "time" do
181
+ it "includes a time for the event" do
182
+ time = Time.at(1_451_606_400_000 / 1_000)
87
183
  event = described_class.new(
88
184
  user_id: 123,
89
- event_type: 'clicked on home',
185
+ event_type: "clicked on home",
90
186
  time: time
91
187
  )
92
188
  expect(event.to_hash[:time]).to eq(1_451_606_400_000)
93
189
  end
94
190
 
95
- it 'does not include time if it is not set' do
191
+ it "does not include time if it is not set" do
96
192
  event = described_class.new(
97
193
  user_id: 123,
98
- event_type: 'clicked on home'
194
+ event_type: "clicked on home"
99
195
  )
100
196
  expect(event.to_hash).not_to have_key(:time)
101
197
  end
102
198
  end
103
199
 
104
- describe 'insert_id' do
105
- it 'includes an insert_id for the event' do
200
+ describe "insert_id" do
201
+ it "includes an insert_id for the event" do
106
202
  event = described_class.new(
107
203
  user_id: 123,
108
- event_type: 'clicked on home',
109
- insert_id: 'foo-bar'
204
+ event_type: "clicked on home",
205
+ insert_id: "foo-bar"
110
206
  )
111
- expect(event.to_hash[:insert_id]).to eq('foo-bar')
207
+ expect(event.to_hash[:insert_id]).to eq("foo-bar")
112
208
  end
113
209
 
114
- 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
115
211
  event = described_class.new(
116
212
  user_id: 123,
117
- event_type: 'clicked on home'
213
+ event_type: "clicked on home"
118
214
  )
119
215
  expect(event.to_hash).not_to have_key(:insert_id)
120
216
  end
121
217
  end
122
218
 
123
- describe 'revenue params' do
124
- it 'includes the price if it is set' do
125
- price = 100_000.99
219
+ describe "platform" do
220
+ it "includes the platform for the event" do
126
221
  event = described_class.new(
127
222
  user_id: 123,
128
- event_type: 'clicked on home',
129
- price: price
223
+ event_type: "clicked on home",
224
+ platform: "Web"
130
225
  )
131
- expect(event.to_hash[:price]).to eq(price)
226
+ expect(event.to_hash[:platform]).to eq("Web")
132
227
  end
133
228
 
134
- it 'sets the quantity to 1 if the price is set and the quantity is not' do
229
+ it "does not include the platform if it is not set" do
230
+ event = described_class.new(
231
+ user_id: 123,
232
+ event_type: "clicked on home"
233
+ )
234
+ expect(event.to_hash).not_to have_key(:platform)
235
+ end
236
+ end
237
+
238
+ describe "country" do
239
+ it "includes the country for the event" do
240
+ event = described_class.new(
241
+ user_id: 123,
242
+ event_type: "clicked on home",
243
+ country: "United States"
244
+ )
245
+ expect(event.to_hash[:country]).to eq("United States")
246
+ end
247
+
248
+ it "does not include the country if it is not set" do
249
+ event = described_class.new(
250
+ user_id: 123,
251
+ event_type: "clicked on home"
252
+ )
253
+ expect(event.to_hash).not_to have_key(:country)
254
+ end
255
+ end
256
+
257
+ describe "revenue params" do
258
+ it "includes the price if it is set" do
135
259
  price = 100_000.99
136
260
  event = described_class.new(
137
261
  user_id: 123,
138
- event_type: 'clicked on home',
262
+ event_type: "clicked on home",
139
263
  price: price
140
264
  )
141
- expect(event.to_hash[:quantity]).to eq(1)
265
+ expect(event.to_hash[:price]).to eq(price)
142
266
  end
143
267
 
144
- it 'includes the quantity if it is set' do
268
+ it "includes the quantity if it is set" do
145
269
  quantity = 100
146
270
  event = described_class.new(
147
271
  user_id: 123,
148
- event_type: 'clicked on home',
272
+ event_type: "clicked on home",
149
273
  quantity: quantity,
150
274
  price: 10.99
151
275
  )
152
276
  expect(event.to_hash[:quantity]).to eq(quantity)
153
277
  end
154
278
 
155
- it 'includes the productID if set' do
156
- product_id = 'hopscotch.subscriptions.rule'
279
+ it "includes the revenue if it is set" do
280
+ revenue = 100
281
+ event = described_class.new(
282
+ user_id: 123,
283
+ event_type: "clicked on home",
284
+ quantity: 456,
285
+ revenue: revenue
286
+ )
287
+ expect(event.to_hash[:revenue]).to eq(revenue)
288
+ end
289
+
290
+ it "includes the productID if set" do
291
+ product_id = "hopscotch.subscriptions.rule"
157
292
  event = described_class.new(
158
293
  user_id: 123,
159
- event_type: 'clicked on home',
294
+ event_type: "clicked on home",
160
295
  price: 199.99,
161
296
  product_id: product_id
162
297
  )
163
298
  expect(event.to_hash[:productId]).to eq(product_id)
164
299
  end
165
300
 
166
- it 'includes the revenueType if set' do
167
- revenue_type = 'income'
301
+ it "includes the revenueType if set" do
302
+ revenue_type = "income"
168
303
  event = described_class.new(
169
304
  user_id: 123,
170
- event_type: 'clicked on home',
305
+ event_type: "clicked on home",
171
306
  price: 199.99,
172
307
  revenue_type: revenue_type
173
308
  )
174
309
  expect(event.to_hash[:revenueType]).to eq(revenue_type)
175
310
  end
176
311
 
177
- 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
178
313
  event = described_class.new(
179
314
  user_id: 123,
180
- event_type: 'clicked on home'
315
+ event_type: "clicked on home"
181
316
  )
182
317
  expect(event.to_hash).not_to have_key(:quantity)
183
318
  expect(event.to_hash).not_to have_key(:revenueType)
@@ -186,4 +321,44 @@ describe AmplitudeAPI::Event do
186
321
  end
187
322
  end
188
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
189
364
  end