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.
- checksums.yaml +5 -5
- data/.gitignore +2 -1
- data/.rubocop.yml +32 -0
- data/.travis.yml +2 -3
- data/Changelog.md +41 -0
- data/Gemfile +9 -7
- data/Gemfile.lock +48 -43
- data/Rakefile +7 -5
- data/amplitude-api.gemspec +17 -16
- data/lib/amplitude-api.rb +3 -4
- data/lib/amplitude_api.rb +99 -40
- data/lib/amplitude_api/config.rb +52 -0
- data/lib/amplitude_api/event.rb +124 -89
- data/lib/amplitude_api/identification.rb +4 -2
- data/lib/amplitude_api/version.rb +3 -1
- data/readme.md +53 -4
- data/spec/lib/amplitude_api/event_spec.rb +236 -90
- data/spec/lib/amplitude_api/identification_spec.rb +19 -17
- data/spec/lib/amplitude_api_spec.rb +388 -189
- data/spec/spec_helper.rb +7 -4
- metadata +30 -29
@@ -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
|
data/lib/amplitude_api/event.rb
CHANGED
@@ -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
|
-
|
5
|
-
|
6
|
-
|
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
|
-
#
|
49
|
-
#
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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 ]
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
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
|
126
|
+
Config.instance.time_formatter.call(time)
|
117
127
|
end
|
118
128
|
|
119
|
-
def
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
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:
|
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
|
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: "
|
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
|
-
|
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.
|
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
|
-
|
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
|
7
|
-
describe
|
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:
|
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
|
19
|
-
describe
|
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:
|
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
|
31
|
-
describe
|
32
|
-
it
|
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:
|
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
|
43
|
-
context
|
44
|
-
it
|
45
|
-
time = Time.
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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:
|
61
|
+
expect(event.to_hash).to eq(event_type: "sausage",
|
58
62
|
user_id: 123,
|
59
|
-
device_id:
|
60
|
-
event_properties: {
|
61
|
-
user_properties: {
|
63
|
+
device_id: "abcd",
|
64
|
+
event_properties: { "a" => "b" },
|
65
|
+
user_properties: { "c" => "d" },
|
62
66
|
time: 1_451_606_400_000,
|
63
|
-
ip:
|
64
|
-
|
67
|
+
ip: "127.0.0.1",
|
68
|
+
platform: "Web",
|
69
|
+
country: "United States",
|
70
|
+
insert_id: "bestId")
|
65
71
|
end
|
66
72
|
|
67
|
-
it
|
68
|
-
time = Time.
|
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:
|
72
|
-
event_type:
|
73
|
-
event_properties: {
|
74
|
-
user_properties: {
|
77
|
+
device_id: "abcd",
|
78
|
+
event_type: "sausage",
|
79
|
+
event_properties: { "a" => "b" },
|
80
|
+
user_properties: { "c" => "d" },
|
75
81
|
time: time,
|
76
|
-
ip:
|
77
|
-
|
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:
|
88
|
+
expect(event.to_hash).to eq(event_type: "sausage",
|
81
89
|
user_id: 123,
|
82
|
-
device_id:
|
83
|
-
event_properties: {
|
84
|
-
user_properties: {
|
90
|
+
device_id: "abcd",
|
91
|
+
event_properties: { "a" => "b" },
|
92
|
+
user_properties: { "c" => "d" },
|
85
93
|
time: 1_451_606_400_000,
|
86
|
-
ip:
|
87
|
-
|
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
|
92
|
-
it
|
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:
|
97
|
-
product_id:
|
124
|
+
event_type: "bad event",
|
125
|
+
product_id: "hopscotch.4lyfe",
|
126
|
+
price: 10.2
|
98
127
|
)
|
99
|
-
end.
|
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
|
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:
|
107
|
-
revenue_type:
|
153
|
+
event_type: "bad event",
|
154
|
+
revenue_type: "whatever",
|
155
|
+
revenue: 100.1
|
108
156
|
)
|
109
|
-
end.
|
157
|
+
end.not_to raise_error
|
110
158
|
end
|
111
159
|
end
|
112
160
|
end
|
113
161
|
|
114
|
-
describe
|
115
|
-
it
|
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:
|
166
|
+
event_type: "clicked on home"
|
119
167
|
)
|
120
|
-
expect(event.to_hash[:event_type]).to eq(
|
168
|
+
expect(event.to_hash[:event_type]).to eq("clicked on home")
|
121
169
|
end
|
122
170
|
|
123
|
-
it
|
171
|
+
it "includes arbitrary properties" do
|
124
172
|
event = described_class.new(
|
125
173
|
user_id: 123,
|
126
|
-
event_type:
|
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
|
133
|
-
it
|
134
|
-
time = Time.
|
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:
|
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
|
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:
|
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
|
153
|
-
it
|
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:
|
157
|
-
insert_id:
|
204
|
+
event_type: "clicked on home",
|
205
|
+
insert_id: "foo-bar"
|
158
206
|
)
|
159
|
-
expect(event.to_hash[:insert_id]).to eq(
|
207
|
+
expect(event.to_hash[:insert_id]).to eq("foo-bar")
|
160
208
|
end
|
161
209
|
|
162
|
-
it
|
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:
|
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
|
172
|
-
it
|
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:
|
177
|
-
|
223
|
+
event_type: "clicked on home",
|
224
|
+
platform: "Web"
|
178
225
|
)
|
179
|
-
expect(event.to_hash[:
|
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
|
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:
|
262
|
+
event_type: "clicked on home",
|
187
263
|
price: price
|
188
264
|
)
|
189
|
-
expect(event.to_hash[:
|
265
|
+
expect(event.to_hash[:price]).to eq(price)
|
190
266
|
end
|
191
267
|
|
192
|
-
it
|
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:
|
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
|
204
|
-
|
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:
|
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
|
215
|
-
revenue_type =
|
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:
|
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
|
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:
|
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
|