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.
- checksums.yaml +5 -5
- data/.gitignore +2 -1
- data/.rubocop.yml +32 -0
- data/.travis.yml +2 -3
- data/Changelog.md +34 -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 +125 -24
- data/lib/amplitude_api/config.rb +51 -0
- data/lib/amplitude_api/event.rb +129 -80
- data/lib/amplitude_api/identification.rb +10 -3
- data/lib/amplitude_api/version.rb +3 -1
- data/readme.md +22 -5
- data/spec/lib/amplitude_api/event_spec.rb +234 -59
- data/spec/lib/amplitude_api/identification_spec.rb +21 -14
- data/spec/lib/amplitude_api_spec.rb +475 -161
- data/spec/spec_helper.rb +7 -4
- metadata +30 -29
|
@@ -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
|
data/lib/amplitude_api/event.rb
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
raise ArgumentError,
|
|
115
|
-
raise ArgumentError,
|
|
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
|
-
|
|
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:
|
|
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 ]
|
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,16 +31,32 @@ event = AmplitudeAPI::Event.new({
|
|
|
30
31
|
AmplitudeAPI.track(event)
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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:
|
|
49
|
-
|
|
114
|
+
event_type: "bad event",
|
|
115
|
+
revenue_type: "whatever"
|
|
50
116
|
)
|
|
51
|
-
end.to raise_error
|
|
117
|
+
end.to raise_error ArgumentError, /You must provide a price or a revenue/
|
|
52
118
|
end
|
|
53
119
|
|
|
54
|
-
it
|
|
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:
|
|
59
|
-
|
|
124
|
+
event_type: "bad event",
|
|
125
|
+
product_id: "hopscotch.4lyfe",
|
|
126
|
+
price: 10.2
|
|
60
127
|
)
|
|
61
|
-
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
|
|
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
|
|
67
|
-
it
|
|
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:
|
|
166
|
+
event_type: "clicked on home"
|
|
71
167
|
)
|
|
72
|
-
expect(event.to_hash[:event_type]).to eq(
|
|
168
|
+
expect(event.to_hash[:event_type]).to eq("clicked on home")
|
|
73
169
|
end
|
|
74
170
|
|
|
75
|
-
it
|
|
171
|
+
it "includes arbitrary properties" do
|
|
76
172
|
event = described_class.new(
|
|
77
173
|
user_id: 123,
|
|
78
|
-
event_type:
|
|
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
|
|
85
|
-
it
|
|
86
|
-
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)
|
|
87
183
|
event = described_class.new(
|
|
88
184
|
user_id: 123,
|
|
89
|
-
event_type:
|
|
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
|
|
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:
|
|
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
|
|
105
|
-
it
|
|
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:
|
|
109
|
-
insert_id:
|
|
204
|
+
event_type: "clicked on home",
|
|
205
|
+
insert_id: "foo-bar"
|
|
110
206
|
)
|
|
111
|
-
expect(event.to_hash[:insert_id]).to eq(
|
|
207
|
+
expect(event.to_hash[:insert_id]).to eq("foo-bar")
|
|
112
208
|
end
|
|
113
209
|
|
|
114
|
-
it
|
|
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:
|
|
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
|
|
124
|
-
it
|
|
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:
|
|
129
|
-
|
|
223
|
+
event_type: "clicked on home",
|
|
224
|
+
platform: "Web"
|
|
130
225
|
)
|
|
131
|
-
expect(event.to_hash[:
|
|
226
|
+
expect(event.to_hash[:platform]).to eq("Web")
|
|
132
227
|
end
|
|
133
228
|
|
|
134
|
-
it
|
|
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:
|
|
262
|
+
event_type: "clicked on home",
|
|
139
263
|
price: price
|
|
140
264
|
)
|
|
141
|
-
expect(event.to_hash[:
|
|
265
|
+
expect(event.to_hash[:price]).to eq(price)
|
|
142
266
|
end
|
|
143
267
|
|
|
144
|
-
it
|
|
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:
|
|
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
|
|
156
|
-
|
|
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:
|
|
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
|
|
167
|
-
revenue_type =
|
|
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:
|
|
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
|
|
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:
|
|
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
|