amplitude-api 0.0.9 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
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
+ :options
13
+
14
+ def initialize
15
+ self.class.defaults.each { |k, v| send("#{k}=", v) }
16
+ end
17
+
18
+ class << self
19
+ def base_properties
20
+ %i[event_type event_properties user_properties user_id device_id]
21
+ end
22
+
23
+ def revenue_properties
24
+ %i[revenue_type product_id revenue price quantity]
25
+ end
26
+
27
+ def optional_properties
28
+ %i[
29
+ time
30
+ ip platform country insert_id
31
+ groups app_version os_name os_version
32
+ device_brand device_manufacturer device_model
33
+ carrier region city dma language
34
+ location_lat location_lng
35
+ idfa idfv adid android_id
36
+ event_id session_id
37
+ ]
38
+ end
39
+
40
+ def defaults
41
+ {
42
+ api_key: nil,
43
+ secret_key: nil,
44
+ whitelist: base_properties + revenue_properties + optional_properties,
45
+ time_formatter: ->(time) { time ? time.to_i * 1_000 : nil },
46
+ event_properties_formatter: ->(props) { props || {} },
47
+ user_properties_formatter: ->(props) { props || {} }
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,71 +1,56 @@
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 ] device_id
8
- # @return [ String ] the device_id to be sent to Amplitude
9
- attr_accessor :device_id
10
- # @!attribute [ rw ] event_type
11
- # @return [ String ] the event_type to be sent to Amplitude
12
- attr_accessor :event_type
13
- # @!attribute [ rw ] event_properties
14
- # @return [ String ] the event_properties to be attached to the Amplitude Event
15
- attr_accessor :event_properties
16
- # @!attribute [ rw ] user_properties
17
- # @return [ String ] the user_properties to be passed for the user
18
- attr_accessor :user_properties
19
- # @!attribute [ rw ] time
20
- # @return [ Time ] Time that the event occurred (defaults to now)
21
- attr_accessor :time
22
- # @!attribute [ rw ] ip
23
- # @return [ String ] IP address of the user
24
- attr_accessor :ip
25
-
26
- # @!attribute [ rw ] insert_id
27
- # @return [ String ] the unique identifier to be sent to Amplitude
28
- attr_accessor :insert_id
29
-
30
- # @!attribute [ rw ] price
31
- # @return [ String ] (required for revenue data) price of the item purchased
32
- attr_accessor :price
33
-
34
- # @!attribute [ rw ] quantity
35
- # @return [ String ] (required for revenue data, defaults to 1 if not specified) quantity of the item purchased
36
- attr_accessor :quantity
37
-
38
- # @!attribute [ rw ] product_id
39
- # @return [ String ] an identifier for the product. (Note: you must send a price and quantity with this field)
40
- attr_accessor :product_id
41
-
42
- # @!attribute [ rw ] revenue_type
43
- # @return [ String ] type of revenue. (Note: you must send a price and quantity with this field)
44
- attr_accessor :revenue_type
10
+ AmplitudeAPI::Config.instance.whitelist.each do |attribute|
11
+ instance_eval("attr_accessor :#{attribute}", __FILE__, __LINE__)
12
+ end
45
13
 
46
14
  # Create a new Event
47
15
  #
48
- # @param [ String ] user_id a user_id to associate with the event
49
- # @param [ String ] device_id a device_id to associate with the event
50
- # @param [ String ] event_type a name for the event
51
- # @param [ Hash ] event_properties various properties to attach to the event
52
- # @param [ Time ] Time that the event occurred (defaults to now)
53
- # @param [ Double ] price (optional, but required for revenue data) price of the item purchased
54
- # @param [ Integer ] quantity (optional, but required for revenue data) quantity of the item purchased
55
- # @param [ String ] product_id (optional) an identifier for the product.
56
- # @param [ String ] revenue_type (optional) type of revenue
57
- # @param [ String ] IP address of the user
58
- # @param [ String ] insert_id a unique identifier for the event
16
+ # See (Amplitude HTTP API Documentation)[https://developers.amplitude.com/docs/http-api-v2]
17
+ # for a list of valid parameters and their types.
59
18
  def initialize(attributes = {})
60
- self.user_id = getopt(attributes, :user_id, '')
61
- self.device_id = getopt(attributes, :device_id, nil)
62
- self.event_type = getopt(attributes, :event_type, '')
63
- self.event_properties = getopt(attributes, :event_properties, {})
64
- self.user_properties = getopt(attributes, :user_properties, {})
65
- self.time = getopt(attributes, :time)
66
- self.ip = getopt(attributes, :ip, '')
67
- self.insert_id = getopt(attributes, :insert_id)
68
- validate_revenue_arguments(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
+ 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
69
54
  end
70
55
 
71
56
  def user_id=(value)
@@ -81,49 +66,97 @@ class AmplitudeAPI
81
66
  #
82
67
  # Used for serialization and comparison
83
68
  def to_hash
84
- serialized_event = {}
85
- serialized_event[:event_type] = event_type
86
- serialized_event[:user_id] = user_id
87
- serialized_event[:event_properties] = event_properties
88
- serialized_event[:user_properties] = user_properties
89
- serialized_event = add_optional_properties(serialized_event)
90
- serialized_event.merge(revenue_hash)
69
+ event = {
70
+ event_type: event_type,
71
+ event_properties: formatted_event_properties,
72
+ user_properties: formatted_user_properties
73
+ }
74
+ event[:user_id] = user_id if user_id
75
+ event[:device_id] = device_id if device_id
76
+ event.merge(optional_properties).merge(revenue_hash).merge(extra_properties)
91
77
  end
78
+ alias to_h to_hash
92
79
 
93
- # @return [ Hash ] A serialized Event with optional properties
94
- def add_optional_properties(serialized_event)
95
- serialized_event[:device_id] = device_id if device_id
96
- serialized_event[:time] = formatted_time if time
97
- serialized_event[:ip] = ip if ip
98
- serialized_event[:insert_id] = insert_id if insert_id
99
- serialized_event
80
+ # @return [ Hash ] Optional properties
81
+ #
82
+ # Returns optional properties (belong to the API but are optional)
83
+ def optional_properties
84
+ AmplitudeAPI::Config.optional_properties.map do |prop|
85
+ val = prop == :time ? formatted_time : send(prop)
86
+ val ? [prop, val] : nil
87
+ end.compact.to_h
88
+ end
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
+
102
+ # @return [ true, false ]
103
+ #
104
+ # Returns true if the event type matches one reserved by Amplitude API.
105
+ def reserved_event?(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)
100
112
  end
101
113
 
102
114
  # @return [ true, false ]
103
115
  #
104
116
  # Compares +to_hash+ for equality
105
117
  def ==(other)
106
- if other.respond_to?(:to_hash)
107
- to_hash == other.to_hash
108
- else
109
- false
110
- end
118
+ return false unless other.respond_to?(:to_h)
119
+
120
+ to_h == other.to_h
111
121
  end
112
122
 
113
123
  private
114
124
 
115
125
  def formatted_time
116
- time.to_i * 1_000
126
+ Config.instance.time_formatter.call(time)
117
127
  end
118
128
 
119
- def validate_revenue_arguments(options)
120
- self.price = getopt(options, :price)
121
- self.quantity = getopt(options, :quantity, 1) if price
122
- self.product_id = getopt(options, :product_id)
123
- self.revenue_type = getopt(options, :revenue_type)
124
- return if price
125
- raise ArgumentError, 'You must provide a price in order to use the product_id' if product_id
126
- raise ArgumentError, 'You must provide a price in order to use the revenue_type' if revenue_type
129
+ def formatted_event_properties
130
+ Config.instance.event_properties_formatter.call(event_properties)
131
+ end
132
+
133
+ def formatted_user_properties
134
+ Config.instance.user_properties_formatter.call(user_properties)
135
+ end
136
+
137
+ def validate_arguments
138
+ validate_required_arguments
139
+ validate_revenue_arguments
140
+ end
141
+
142
+ def validate_required_arguments
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)
146
+ end
147
+
148
+ def validate_revenue_arguments
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}"
127
160
  end
128
161
 
129
162
  def revenue_hash
@@ -132,6 +165,7 @@ class AmplitudeAPI
132
165
  revenue_hash[:revenueType] = revenue_type if revenue_type
133
166
  revenue_hash[:quantity] = quantity if quantity
134
167
  revenue_hash[:price] = price if price
168
+ revenue_hash[:revenue] = revenue if revenue
135
169
  revenue_hash
136
170
  end
137
171
 
@@ -140,3 +174,4 @@ class AmplitudeAPI
140
174
  end
141
175
  end
142
176
  end
177
+ # rubocop:enable Metrics/ClassLength
@@ -1,9 +1,11 @@
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
7
9
  # @!attribute [ rw ] device_id
8
10
  # @return [ String ] the device_id to be sent to Amplitude
9
11
  attr_accessor :device_id
@@ -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.0.9'.freeze
4
+ VERSION = "0.3.1"
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,7 +31,55 @@ 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
+ 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
66
+
67
+ The following code snippet will delete a user from amplitude
68
+
69
+ ```ruby
70
+ # Configure your Amplitude API key
71
+ AmplitudeAPI.config.api_key = "abcdef123456"
72
+
73
+ # Configure your Amplitude Secret Key
74
+ AmplitudeAPI.config.secret_key = "secretMcSecret"
75
+
76
+ AmplitudeAPI.delete(user_ids: ["12345"],
77
+ requester: "privacy@example.com"
78
+ )
79
+ ```
80
+
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.
34
83
 
35
84
  ## What's Next
36
85
 
@@ -38,7 +87,7 @@ Currently, we are using this in Rails and using ActiveJob to dispatch events asy
38
87
  * Configurable default account to use when no `user_id` present
39
88
 
40
89
  ## Other useful resources
41
- * [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)
42
91
  * [Segment.io Amplitude integration](https://segment.com/docs/integrations/amplitude/)
43
92
 
44
93
  ## Contributing
@@ -1,231 +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
45
- time = Time.parse('2016-01-01 00:00:00 -0000')
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)
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
- '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"
55
59
  )
56
60
 
57
- expect(event.to_hash).to eq(event_type: 'sausage',
61
+ expect(event.to_hash).to eq(event_type: "sausage",
58
62
  user_id: 123,
59
- device_id: 'abcd',
60
- event_properties: { 'a' => 'b' },
61
- user_properties: { 'c' => 'd' },
63
+ device_id: "abcd",
64
+ event_properties: { "a" => "b" },
65
+ user_properties: { "c" => "d" },
62
66
  time: 1_451_606_400_000,
63
- ip: '127.0.0.1',
64
- insert_id: 'bestId')
67
+ ip: "127.0.0.1",
68
+ platform: "Web",
69
+ country: "United States",
70
+ insert_id: "bestId")
65
71
  end
66
72
 
67
- it 'accepts symbol attributes' do
68
- time = Time.parse('2016-01-01 00:00:00 -0000')
73
+ it "accepts symbol attributes" do
74
+ time = Time.at(1_451_606_400_000 / 1_000)
69
75
  event = described_class.new(
70
76
  user_id: 123,
71
- device_id: 'abcd',
72
- event_type: 'sausage',
73
- event_properties: { 'a' => 'b' },
74
- user_properties: { 'c' => 'd' },
77
+ device_id: "abcd",
78
+ event_type: "sausage",
79
+ event_properties: { "a" => "b" },
80
+ user_properties: { "c" => "d" },
75
81
  time: time,
76
- ip: '127.0.0.1',
77
- insert_id: 'bestId'
82
+ ip: "127.0.0.1",
83
+ platform: "Web",
84
+ country: "United States",
85
+ insert_id: "bestId"
78
86
  )
79
87
 
80
- expect(event.to_hash).to eq(event_type: 'sausage',
88
+ expect(event.to_hash).to eq(event_type: "sausage",
81
89
  user_id: 123,
82
- device_id: 'abcd',
83
- event_properties: { 'a' => 'b' },
84
- user_properties: { 'c' => 'd' },
90
+ device_id: "abcd",
91
+ event_properties: { "a" => "b" },
92
+ user_properties: { "c" => "d" },
85
93
  time: 1_451_606_400_000,
86
- ip: '127.0.0.1',
87
- insert_id: 'bestId')
94
+ ip: "127.0.0.1",
95
+ platform: "Web",
96
+ country: "United States",
97
+ insert_id: "bestId")
88
98
  end
89
99
  end
90
100
 
91
- context 'the user does not send in a price' do
92
- 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
+
93
130
  expect do
94
131
  described_class.new(
95
132
  user_id: 123,
96
- event_type: 'bad event',
97
- product_id: 'hopscotch.4lyfe'
133
+ event_type: "bad event",
134
+ revenue_type: "whatever",
135
+ price: 10.2
98
136
  )
99
- end.to raise_error(ArgumentError)
137
+ end.not_to raise_error
100
138
  end
101
139
 
102
- 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
103
141
  expect do
104
142
  described_class.new(
105
143
  user_id: 123,
106
- event_type: 'bad event',
107
- revenue_type: 'tax return'
144
+ event_type: "bad event",
145
+ product_id: "hopscotch.4lyfe",
146
+ revenue: 100.1
108
147
  )
109
- end.to raise_error(ArgumentError)
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
110
158
  end
111
159
  end
112
160
  end
113
161
 
114
- describe '#to_hash' do
115
- it 'includes the event type' do
162
+ describe "#to_hash" do
163
+ it "includes the event type" do
116
164
  event = described_class.new(
117
165
  user_id: 123,
118
- event_type: 'clicked on home'
166
+ event_type: "clicked on home"
119
167
  )
120
- expect(event.to_hash[:event_type]).to eq('clicked on home')
168
+ expect(event.to_hash[:event_type]).to eq("clicked on home")
121
169
  end
122
170
 
123
- it 'includes arbitrary properties' do
171
+ it "includes arbitrary properties" do
124
172
  event = described_class.new(
125
173
  user_id: 123,
126
- event_type: 'clicked on home',
174
+ event_type: "clicked on home",
127
175
  event_properties: { abc: :def }
128
176
  )
129
177
  expect(event.to_hash[:event_properties]).to eq(abc: :def)
130
178
  end
131
179
 
132
- describe 'time' do
133
- it 'includes a time for the event' do
134
- 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)
135
183
  event = described_class.new(
136
184
  user_id: 123,
137
- event_type: 'clicked on home',
185
+ event_type: "clicked on home",
138
186
  time: time
139
187
  )
140
188
  expect(event.to_hash[:time]).to eq(1_451_606_400_000)
141
189
  end
142
190
 
143
- it 'does not include time if it is not set' do
191
+ it "does not include time if it is not set" do
144
192
  event = described_class.new(
145
193
  user_id: 123,
146
- event_type: 'clicked on home'
194
+ event_type: "clicked on home"
147
195
  )
148
196
  expect(event.to_hash).not_to have_key(:time)
149
197
  end
150
198
  end
151
199
 
152
- describe 'insert_id' do
153
- it 'includes an insert_id for the event' do
200
+ describe "insert_id" do
201
+ it "includes an insert_id for the event" do
154
202
  event = described_class.new(
155
203
  user_id: 123,
156
- event_type: 'clicked on home',
157
- insert_id: 'foo-bar'
204
+ event_type: "clicked on home",
205
+ insert_id: "foo-bar"
158
206
  )
159
- expect(event.to_hash[:insert_id]).to eq('foo-bar')
207
+ expect(event.to_hash[:insert_id]).to eq("foo-bar")
160
208
  end
161
209
 
162
- 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
163
211
  event = described_class.new(
164
212
  user_id: 123,
165
- event_type: 'clicked on home'
213
+ event_type: "clicked on home"
166
214
  )
167
215
  expect(event.to_hash).not_to have_key(:insert_id)
168
216
  end
169
217
  end
170
218
 
171
- describe 'revenue params' do
172
- it 'includes the price if it is set' do
173
- price = 100_000.99
219
+ describe "platform" do
220
+ it "includes the platform for the event" do
174
221
  event = described_class.new(
175
222
  user_id: 123,
176
- event_type: 'clicked on home',
177
- price: price
223
+ event_type: "clicked on home",
224
+ platform: "Web"
178
225
  )
179
- expect(event.to_hash[:price]).to eq(price)
226
+ expect(event.to_hash[:platform]).to eq("Web")
180
227
  end
181
228
 
182
- 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
183
259
  price = 100_000.99
184
260
  event = described_class.new(
185
261
  user_id: 123,
186
- event_type: 'clicked on home',
262
+ event_type: "clicked on home",
187
263
  price: price
188
264
  )
189
- expect(event.to_hash[:quantity]).to eq(1)
265
+ expect(event.to_hash[:price]).to eq(price)
190
266
  end
191
267
 
192
- it 'includes the quantity if it is set' do
268
+ it "includes the quantity if it is set" do
193
269
  quantity = 100
194
270
  event = described_class.new(
195
271
  user_id: 123,
196
- event_type: 'clicked on home',
272
+ event_type: "clicked on home",
197
273
  quantity: quantity,
198
274
  price: 10.99
199
275
  )
200
276
  expect(event.to_hash[:quantity]).to eq(quantity)
201
277
  end
202
278
 
203
- it 'includes the productID if set' do
204
- product_id = 'hopscotch.subscriptions.rule'
279
+ it "includes the revenue if it is set" do
280
+ revenue = 100
205
281
  event = described_class.new(
206
282
  user_id: 123,
207
- event_type: 'clicked on home',
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"
292
+ event = described_class.new(
293
+ user_id: 123,
294
+ event_type: "clicked on home",
208
295
  price: 199.99,
209
296
  product_id: product_id
210
297
  )
211
298
  expect(event.to_hash[:productId]).to eq(product_id)
212
299
  end
213
300
 
214
- it 'includes the revenueType if set' do
215
- revenue_type = 'income'
301
+ it "includes the revenueType if set" do
302
+ revenue_type = "income"
216
303
  event = described_class.new(
217
304
  user_id: 123,
218
- event_type: 'clicked on home',
305
+ event_type: "clicked on home",
219
306
  price: 199.99,
220
307
  revenue_type: revenue_type
221
308
  )
222
309
  expect(event.to_hash[:revenueType]).to eq(revenue_type)
223
310
  end
224
311
 
225
- 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
226
313
  event = described_class.new(
227
314
  user_id: 123,
228
- event_type: 'clicked on home'
315
+ event_type: "clicked on home"
229
316
  )
230
317
  expect(event.to_hash).not_to have_key(:quantity)
231
318
  expect(event.to_hash).not_to have_key(:revenueType)
@@ -234,4 +321,50 @@ describe AmplitudeAPI::Event do
234
321
  end
235
322
  end
236
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 "does not define property until assigned" do
351
+ expect {
352
+ event.undefined_property
353
+ }.to raise_error NoMethodError, /undefined_property/
354
+ end
355
+
356
+ it "do not accepts blocks when assigning values to create properties" do
357
+ expect do
358
+ event.arbitrary_property { puts "whatever" }
359
+ end.to raise_error NoMethodError
360
+ end
361
+
362
+ it "includes arbitrary properties in the generated hash" do
363
+ event.arbitrary_property = "arbitrary value"
364
+
365
+ hash = event.to_hash
366
+
367
+ expect(hash).to include(arbitrary_property: "arbitrary value")
368
+ end
369
+ end
237
370
  end