amplitude-api 0.0.10 → 0.3.2

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,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
+ @extra_properties = []
20
+ attributes.each do |k, v|
21
+ send("#{k}=", v)
22
+ end
23
+ validate_arguments
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.10'.freeze
4
+ VERSION = "0.3.2"
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
93
121
  expect do
94
122
  described_class.new(
95
123
  user_id: 123,
96
- event_type: 'bad event',
97
- product_id: 'hopscotch.4lyfe'
124
+ event_type: "bad event",
125
+ product_id: "hopscotch.4lyfe",
126
+ price: 10.2
98
127
  )
99
- 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
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
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
+
103
150
  expect do
104
151
  described_class.new(
105
152
  user_id: 123,
106
- event_type: 'bad event',
107
- revenue_type: 'tax return'
153
+ event_type: "bad event",
154
+ revenue_type: "whatever",
155
+ revenue: 100.1
108
156
  )
109
- end.to raise_error(ArgumentError)
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")
227
+ end
228
+
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")
180
246
  end
181
247
 
182
- it 'sets the quantity to 1 if the price is set and the quantity is not' do
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
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"
205
292
  event = described_class.new(
206
293
  user_id: 123,
207
- event_type: 'clicked on home',
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,63 @@ 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 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
237
383
  end