hackle-ruby-sdk 0.0.1 → 1.0.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 +4 -4
- data/README.md +1 -3
- data/hackle-ruby-sdk.gemspec +1 -1
- data/lib/hackle-ruby-sdk.rb +2 -21
- data/lib/hackle.rb +76 -0
- data/lib/hackle/client.rb +127 -0
- data/lib/{hackle-ruby-sdk → hackle}/config.rb +1 -1
- data/lib/hackle/decision/bucketer.rb +44 -0
- data/lib/hackle/decision/decider.rb +69 -0
- data/lib/{hackle-ruby-sdk → hackle}/events/event_dispatcher.rb +31 -24
- data/lib/{hackle-ruby-sdk → hackle}/events/event_processor.rb +22 -11
- data/lib/hackle/events/user_event.rb +61 -0
- data/lib/{hackle-ruby-sdk → hackle}/http/http.rb +7 -9
- data/lib/hackle/models/bucket.rb +26 -0
- data/lib/hackle/models/event.rb +26 -0
- data/lib/hackle/models/event_type.rb +22 -0
- data/lib/hackle/models/experiment.rb +69 -0
- data/lib/hackle/models/slot.rb +22 -0
- data/lib/hackle/models/user.rb +24 -0
- data/lib/hackle/models/variation.rb +21 -0
- data/lib/{hackle-ruby-sdk → hackle}/version.rb +2 -2
- data/lib/hackle/workspaces/http_workspace_fetcher.rb +24 -0
- data/lib/{hackle-ruby-sdk → hackle}/workspaces/polling_workspace_fetcher.rb +4 -1
- data/lib/hackle/workspaces/workspace.rb +100 -0
- metadata +22 -19
- data/lib/hackle-ruby-sdk/client.rb +0 -108
- data/lib/hackle-ruby-sdk/decision/bucketer.rb +0 -26
- data/lib/hackle-ruby-sdk/decision/decider.rb +0 -54
- data/lib/hackle-ruby-sdk/events/event.rb +0 -33
- data/lib/hackle-ruby-sdk/models/bucket.rb +0 -15
- data/lib/hackle-ruby-sdk/models/event_type.rb +0 -10
- data/lib/hackle-ruby-sdk/models/experiment.rb +0 -39
- data/lib/hackle-ruby-sdk/models/slot.rb +0 -15
- data/lib/hackle-ruby-sdk/models/variation.rb +0 -11
- data/lib/hackle-ruby-sdk/workspaces/http_workspace_fetcher.rb +0 -24
- data/lib/hackle-ruby-sdk/workspaces/workspace.rb +0 -78
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2eb9518d11c4980c9a6abf036b8400fad5de381db6c60d2fc98b68e37cda8851
|
4
|
+
data.tar.gz: faf6536122c01e9a55f95eef4e6497313a7e09e2ff16ea1123ca6c7fdc56470a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbf193163d0b5926f052e76b7b01e0ca472297ef5da32b2317e90a1f35450cefa6496a72bcd0e9d6c7faf83947b6a9379e07be7111f5fa0a5030b891013c6c1c
|
7
|
+
data.tar.gz: e9d8ed3a4439684fb0b3308edb0a2e48c36ba041fa8f081e33ddad237ee8a5b546f0c98a1ec05b5c4123f76fe9289bc2ea3e84d9b02b068a3833472eec77691e
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Hackle::Ruby::Sdk
|
2
2
|
|
3
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hackle
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hackle`.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
data/hackle-ruby-sdk.gemspec
CHANGED
data/lib/hackle-ruby-sdk.rb
CHANGED
@@ -1,22 +1,3 @@
|
|
1
|
-
|
2
|
-
require 'hackle-ruby-sdk/decision/decider'
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
require 'hackle
|
5
|
-
require 'hackle-ruby-sdk/events/event_dispatcher'
|
6
|
-
require 'hackle-ruby-sdk/events/event_processor'
|
7
|
-
|
8
|
-
require 'hackle-ruby-sdk/http/http'
|
9
|
-
|
10
|
-
require 'hackle-ruby-sdk/models/bucket'
|
11
|
-
require 'hackle-ruby-sdk/models/event_type'
|
12
|
-
require 'hackle-ruby-sdk/models/experiment'
|
13
|
-
require 'hackle-ruby-sdk/models/slot'
|
14
|
-
require 'hackle-ruby-sdk/models/variation'
|
15
|
-
|
16
|
-
require 'hackle-ruby-sdk/workspaces/http_workspace_fetcher'
|
17
|
-
require 'hackle-ruby-sdk/workspaces/polling_workspace_fetcher'
|
18
|
-
require 'hackle-ruby-sdk/workspaces/workspace'
|
19
|
-
|
20
|
-
require 'hackle-ruby-sdk/client'
|
21
|
-
require 'hackle-ruby-sdk/config'
|
22
|
-
require 'hackle-ruby-sdk/version'
|
3
|
+
require 'hackle'
|
data/lib/hackle.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/client'
|
4
|
+
require 'hackle/config'
|
5
|
+
require 'hackle/version'
|
6
|
+
|
7
|
+
module Hackle
|
8
|
+
|
9
|
+
#
|
10
|
+
# Instantiates a Hackle client.
|
11
|
+
#
|
12
|
+
# @see Client#initialize
|
13
|
+
#
|
14
|
+
# @param sdk_key [String] The SDK key of your Hackle environment
|
15
|
+
# @param options Optional parameters of configuration options
|
16
|
+
#
|
17
|
+
# @return [Client] The Hackle client instance.
|
18
|
+
#
|
19
|
+
def self.client(sdk_key:, **options)
|
20
|
+
config = Config.new(options)
|
21
|
+
sdk_info = SdkInfo.new(key: sdk_key)
|
22
|
+
|
23
|
+
http_workspace_fetcher = HttpWorkspaceFetcher.new(config: config, sdk_info: sdk_info)
|
24
|
+
polling_workspace_fetcher = PollingWorkspaceFetcher.new(config: config, http_fetcher: http_workspace_fetcher)
|
25
|
+
|
26
|
+
event_dispatcher = EventDispatcher.new(config: config, sdk_info: sdk_info)
|
27
|
+
event_processor = EventProcessor.new(config: config, event_dispatcher: event_dispatcher)
|
28
|
+
|
29
|
+
polling_workspace_fetcher.start!
|
30
|
+
event_processor.start!
|
31
|
+
|
32
|
+
Client.new(
|
33
|
+
config: config,
|
34
|
+
workspace_fetcher: polling_workspace_fetcher,
|
35
|
+
event_processor: event_processor,
|
36
|
+
decider: Decider.new
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Instantiate a user to be used for the hackle sdk.
|
42
|
+
#
|
43
|
+
# The only required parameter is `id`, which must uniquely identify each user.
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# Hackle.user(id: 'ae2182e0')
|
47
|
+
# Hackle.user(id: 'ae2182e0', app_version: '1.0.1', paying_customer: false)
|
48
|
+
#
|
49
|
+
# @param id [String] The identifier of the user. (e.g. device_id, account_id etc.)
|
50
|
+
# @param properties Additional properties of the user. (e.g. app_version, membership_grade, etc.)
|
51
|
+
#
|
52
|
+
# @return [User] The configured user object.
|
53
|
+
#
|
54
|
+
def self.user(id:, **properties)
|
55
|
+
User.new(id: id, properties: properties)
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Instantiate an event to be used for the hackle sdk.
|
60
|
+
#
|
61
|
+
# The only required parameter is `key`, which must uniquely identify each event.
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# Hackle.event(key: 'purchase')
|
65
|
+
# Hackle.event(key: 'purchase', value: 42000.0, app_version: '1.0.1', payment_method: 'CARD' )
|
66
|
+
#
|
67
|
+
# @param key [String] The unique key of the events.
|
68
|
+
# @param value [Float] Optional numeric value of the events (e.g. purchase_amount, quantity, etc.)
|
69
|
+
# @param properties Additional properties of the events (e.g. app_version, os_type, etc.)
|
70
|
+
#
|
71
|
+
# @return [Event] The configured event object.
|
72
|
+
#
|
73
|
+
def self.event(key:, value: nil, **properties)
|
74
|
+
Event.new(key: key, value: value, properties: properties)
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/decision/bucketer'
|
4
|
+
require 'hackle/decision/decider'
|
5
|
+
|
6
|
+
require 'hackle/events/user_event'
|
7
|
+
require 'hackle/events/event_dispatcher'
|
8
|
+
require 'hackle/events/event_processor'
|
9
|
+
|
10
|
+
require 'hackle/http/http'
|
11
|
+
|
12
|
+
require 'hackle/models/bucket'
|
13
|
+
require 'hackle/models/event'
|
14
|
+
require 'hackle/models/event_type'
|
15
|
+
require 'hackle/models/experiment'
|
16
|
+
require 'hackle/models/slot'
|
17
|
+
require 'hackle/models/user'
|
18
|
+
require 'hackle/models/variation'
|
19
|
+
|
20
|
+
require 'hackle/workspaces/http_workspace_fetcher'
|
21
|
+
require 'hackle/workspaces/polling_workspace_fetcher'
|
22
|
+
require 'hackle/workspaces/workspace'
|
23
|
+
|
24
|
+
module Hackle
|
25
|
+
|
26
|
+
#
|
27
|
+
# A client for Hackle API.
|
28
|
+
#
|
29
|
+
class Client
|
30
|
+
|
31
|
+
#
|
32
|
+
# Initializes a Hackle client.
|
33
|
+
#
|
34
|
+
# @param config [Config]
|
35
|
+
# @param workspace_fetcher [PollingWorkspaceFetcher]
|
36
|
+
# @param event_processor [EventProcessor]
|
37
|
+
# @param decider [Decider]
|
38
|
+
#
|
39
|
+
def initialize(config:, workspace_fetcher:, event_processor:, decider:)
|
40
|
+
@logger = config.logger
|
41
|
+
|
42
|
+
# @type [PollingWorkspaceFetcher]
|
43
|
+
@workspace_fetcher = workspace_fetcher
|
44
|
+
|
45
|
+
# @type [EventProcessor]
|
46
|
+
@event_processor = event_processor
|
47
|
+
|
48
|
+
# @type [Decider]
|
49
|
+
@decider = decider
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Decide the variation to expose to the user for experiment.
|
54
|
+
#
|
55
|
+
# This method return the control variation 'A' if:
|
56
|
+
# - The experiment key is invalid
|
57
|
+
# - The experiment has not started yet
|
58
|
+
# - The user is not allocated to the experiment
|
59
|
+
# - The decided variation has been dropped
|
60
|
+
#
|
61
|
+
# @param experiment_key [Integer] The unique key of the experiment. MUST NOT be nil.
|
62
|
+
# @param user [User] the user to participate in the experiment. MUST NOT be nil.
|
63
|
+
# @param default_variation [String] The default variation of the experiment.
|
64
|
+
#
|
65
|
+
# @return [String] The decided variation for the user, or default variation
|
66
|
+
#
|
67
|
+
def variation(experiment_key:, user:, default_variation: 'A')
|
68
|
+
|
69
|
+
return default_variation if experiment_key.nil? || !experiment_key.is_a?(Integer)
|
70
|
+
return default_variation if user.nil? || !user.is_a?(User) || !user.valid?
|
71
|
+
|
72
|
+
workspace = @workspace_fetcher.fetch
|
73
|
+
return default_variation if workspace.nil?
|
74
|
+
|
75
|
+
experiment = workspace.get_experiment(experiment_key: experiment_key)
|
76
|
+
return default_variation if experiment.nil?
|
77
|
+
|
78
|
+
decision = @decider.decide(experiment: experiment, user: user)
|
79
|
+
case decision
|
80
|
+
when Decision::NotAllocated
|
81
|
+
default_variation
|
82
|
+
when Decision::ForcedAllocated
|
83
|
+
decision.variation_key
|
84
|
+
when Decision::NaturalAllocated
|
85
|
+
exposure_event = UserEvent::Exposure.new(user: user, experiment: experiment, variation: decision.variation)
|
86
|
+
@event_processor.process(event: exposure_event)
|
87
|
+
decision.variation.key
|
88
|
+
else
|
89
|
+
default_variation
|
90
|
+
end
|
91
|
+
|
92
|
+
rescue => e
|
93
|
+
@logger.error { "Unexpected error while deciding variation for experiment[#{experiment_key}]. Returning default variation[#{default_variation}]: #{e.inspect}" }
|
94
|
+
default_variation
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Records the event that occurred by the user.
|
99
|
+
#
|
100
|
+
# @param event [Event] the event that occurred.
|
101
|
+
# @param user [User] the user that occurred the event.
|
102
|
+
#
|
103
|
+
def track(event:, user:)
|
104
|
+
|
105
|
+
return if event.nil? || !event.is_a?(Event) || !event.valid?
|
106
|
+
return if user.nil? || !user.is_a?(User) || !user.valid?
|
107
|
+
|
108
|
+
workspace = @workspace_fetcher.fetch
|
109
|
+
return if workspace.nil?
|
110
|
+
|
111
|
+
event_type = workspace.get_event_type(event_type_key: event.key)
|
112
|
+
track_event = UserEvent::Track.new(user: user, event_type: event_type, event: event)
|
113
|
+
@event_processor.process(event: track_event)
|
114
|
+
|
115
|
+
rescue => e
|
116
|
+
@logger.error { "Unexpected error while tracking event: #{e.inspect}" }
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Shutdown the background task and release the resources used for the background task.
|
121
|
+
#
|
122
|
+
def close
|
123
|
+
@workspace_fetcher.stop!
|
124
|
+
@event_processor.stop!
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'murmurhash3'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
class Bucketer
|
7
|
+
|
8
|
+
# @param bucket [Bucket]
|
9
|
+
# @param user [User]
|
10
|
+
#
|
11
|
+
# @return [Slot, nil]
|
12
|
+
def bucketing(bucket:, user:)
|
13
|
+
slot_number = calculate_slot_number(
|
14
|
+
seed: bucket.seed,
|
15
|
+
slot_size: bucket.slot_size,
|
16
|
+
user_id: user.id
|
17
|
+
)
|
18
|
+
bucket.get_slot(slot_number: slot_number)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param seed [Integer]
|
22
|
+
# @param slot_size [Integer]
|
23
|
+
# @param user_id [String]
|
24
|
+
#
|
25
|
+
# @return [Integer]
|
26
|
+
def calculate_slot_number(seed:, slot_size:, user_id:)
|
27
|
+
hash_value = hash(data: user_id, seed: seed)
|
28
|
+
hash_value.abs % slot_size
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param data [String]
|
32
|
+
# @param seed [Integer]
|
33
|
+
#
|
34
|
+
# @return [Integer]
|
35
|
+
def hash(data:, seed:)
|
36
|
+
unsigned_value = MurmurHash3::V32.str_hash(data, seed)
|
37
|
+
if (unsigned_value & 0x80000000).zero?
|
38
|
+
unsigned_value
|
39
|
+
else
|
40
|
+
-((unsigned_value ^ 0xFFFFFFFF) + 1)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackle
|
4
|
+
class Decision
|
5
|
+
|
6
|
+
class NotAllocated < Decision
|
7
|
+
end
|
8
|
+
|
9
|
+
class ForcedAllocated < Decision
|
10
|
+
# @return [String]
|
11
|
+
attr_reader :variation_key
|
12
|
+
|
13
|
+
# @param variation_key [String]
|
14
|
+
def initialize(variation_key:)
|
15
|
+
@variation_key = variation_key
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class NaturalAllocated < Decision
|
20
|
+
# @return [Variation]
|
21
|
+
attr_reader :variation
|
22
|
+
|
23
|
+
# @param variation [Variation]
|
24
|
+
def initialize(variation:)
|
25
|
+
@variation = variation
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Decider
|
31
|
+
def initialize
|
32
|
+
@bucketer = Bucketer.new
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param experiment [Experiment]
|
36
|
+
# @param user [User]
|
37
|
+
#
|
38
|
+
# @return [Decision]
|
39
|
+
def decide(experiment:, user:)
|
40
|
+
case experiment
|
41
|
+
when Experiment::Completed
|
42
|
+
Decision::ForcedAllocated.new(variation_key: experiment.winner_variation_key)
|
43
|
+
when Experiment::Running
|
44
|
+
decide_running(running_experiment: experiment, user: user)
|
45
|
+
else
|
46
|
+
NotAllocated.new
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param running_experiment [Experiment::Running]
|
51
|
+
# @param user [User]
|
52
|
+
#
|
53
|
+
# @return [Decision]
|
54
|
+
def decide_running(running_experiment:, user:)
|
55
|
+
|
56
|
+
overridden_variation = running_experiment.get_overridden_variation(user: user)
|
57
|
+
return Decision::ForcedAllocated.new(variation_key: overridden_variation.key) unless overridden_variation.nil?
|
58
|
+
|
59
|
+
allocated_slot = @bucketer.bucketing(bucket: running_experiment.bucket, user: user)
|
60
|
+
return Decision::NotAllocated.new if allocated_slot.nil?
|
61
|
+
|
62
|
+
allocated_variation = running_experiment.get_variation(variation_id: allocated_slot.variation_id)
|
63
|
+
return Decision::NotAllocated.new if allocated_variation.nil?
|
64
|
+
return Decision::NotAllocated.new if allocated_variation.dropped
|
65
|
+
|
66
|
+
Decision::NaturalAllocated.new(variation: allocated_variation)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -6,10 +6,10 @@ module Hackle
|
|
6
6
|
DEFAULT_DISPATCH_WORKER_SIZE = 2
|
7
7
|
DEFAULT_DISPATCH_QUEUE_CAPACITY = 50
|
8
8
|
|
9
|
-
def initialize(config
|
9
|
+
def initialize(config:, sdk_info:)
|
10
10
|
@logger = config.logger
|
11
|
-
@client = HTTP.client(config.event_uri)
|
12
|
-
@headers = HTTP.sdk_headers(sdk_info)
|
11
|
+
@client = HTTP.client(base_uri: config.event_uri)
|
12
|
+
@headers = HTTP.sdk_headers(sdk_info: sdk_info)
|
13
13
|
@dispatcher_executor = Concurrent::ThreadPoolExecutor.new(
|
14
14
|
min_threads: DEFAULT_DISPATCH_WORKER_SIZE,
|
15
15
|
max_threads: DEFAULT_DISPATCH_WORKER_SIZE,
|
@@ -17,10 +17,10 @@ module Hackle
|
|
17
17
|
)
|
18
18
|
end
|
19
19
|
|
20
|
-
def dispatch(events)
|
21
|
-
payload = create_payload(events)
|
20
|
+
def dispatch(events:)
|
21
|
+
payload = create_payload(events: events)
|
22
22
|
begin
|
23
|
-
@dispatcher_executor.post { dispatch_payload(payload) }
|
23
|
+
@dispatcher_executor.post { dispatch_payload(payload: payload) }
|
24
24
|
rescue Concurrent::RejectedExecutionError
|
25
25
|
@logger.warn { 'Dispatcher executor queue is full. Event dispatch rejected' }
|
26
26
|
end
|
@@ -35,7 +35,7 @@ module Hackle
|
|
35
35
|
|
36
36
|
private
|
37
37
|
|
38
|
-
def dispatch_payload(payload)
|
38
|
+
def dispatch_payload(payload:)
|
39
39
|
request = Net::HTTP::Post.new('/api/v1/events', @headers)
|
40
40
|
request.content_type = 'application/json'
|
41
41
|
request.body = payload.to_json
|
@@ -43,19 +43,19 @@ module Hackle
|
|
43
43
|
response = @client.request(request)
|
44
44
|
|
45
45
|
status_code = response.code.to_i
|
46
|
-
HTTP.check_successful(status_code)
|
46
|
+
HTTP.check_successful(status_code: status_code)
|
47
47
|
rescue => e
|
48
48
|
@logger.error { "Failed to dispatch events: #{e.inspect}" }
|
49
49
|
end
|
50
50
|
|
51
|
-
def create_payload(events)
|
51
|
+
def create_payload(events:)
|
52
52
|
exposure_events = []
|
53
53
|
track_events = []
|
54
54
|
events.each do |event|
|
55
55
|
case event
|
56
|
-
when
|
56
|
+
when UserEvent::Exposure
|
57
57
|
exposure_events << create_exposure_event(event)
|
58
|
-
when
|
58
|
+
when UserEvent::Track
|
59
59
|
track_events << create_track_event(event)
|
60
60
|
end
|
61
61
|
end
|
@@ -65,24 +65,31 @@ module Hackle
|
|
65
65
|
}
|
66
66
|
end
|
67
67
|
|
68
|
-
|
68
|
+
#
|
69
|
+
# @param exposure [UserEvent::Exposure]
|
70
|
+
#
|
71
|
+
def create_exposure_event(exposure)
|
69
72
|
{
|
70
|
-
timestamp:
|
71
|
-
userId:
|
72
|
-
experimentId:
|
73
|
-
experimentKey:
|
74
|
-
variationId:
|
75
|
-
variationKey:
|
73
|
+
timestamp: exposure.timestamp,
|
74
|
+
userId: exposure.user.id,
|
75
|
+
experimentId: exposure.experiment.id,
|
76
|
+
experimentKey: exposure.experiment.key,
|
77
|
+
variationId: exposure.variation.id,
|
78
|
+
variationKey: exposure.variation.key
|
76
79
|
}
|
77
80
|
end
|
78
81
|
|
79
|
-
|
82
|
+
#
|
83
|
+
# @param track [UserEvent::Track]
|
84
|
+
#
|
85
|
+
def create_track_event(track)
|
80
86
|
{
|
81
|
-
timestamp:
|
82
|
-
userId:
|
83
|
-
eventTypeId:
|
84
|
-
eventTypeKey:
|
85
|
-
value: event.value
|
87
|
+
timestamp: track.timestamp,
|
88
|
+
userId: track.user.id,
|
89
|
+
eventTypeId: track.event_type.id,
|
90
|
+
eventTypeKey: track.event_type.key,
|
91
|
+
value: track.event.value,
|
92
|
+
properties: track.event.properties
|
86
93
|
}
|
87
94
|
end
|
88
95
|
end
|