hackle-ruby-sdk 0.1.0 → 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/lib/hackle.rb +38 -1
- data/lib/hackle/client.rb +31 -18
- data/lib/hackle/decision/bucketer.rb +16 -2
- data/lib/hackle/decision/decider.rb +20 -5
- data/lib/hackle/events/event_dispatcher.rb +22 -15
- data/lib/hackle/events/event_processor.rb +11 -0
- data/lib/hackle/events/user_event.rb +61 -0
- data/lib/hackle/models/bucket.rb +11 -0
- data/lib/hackle/models/event.rb +26 -0
- data/lib/hackle/models/event_type.rb +8 -0
- data/lib/hackle/models/experiment.rb +42 -9
- data/lib/hackle/models/slot.rb +7 -0
- data/lib/hackle/models/user.rb +24 -0
- data/lib/hackle/models/variation.rb +10 -0
- data/lib/hackle/version.rb +1 -1
- data/lib/hackle/workspaces/polling_workspace_fetcher.rb +3 -0
- data/lib/hackle/workspaces/workspace.rb +14 -1
- metadata +5 -3
- data/lib/hackle/events/event.rb +0 -33
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/lib/hackle.rb
CHANGED
@@ -12,7 +12,7 @@ module Hackle
|
|
12
12
|
# @see Client#initialize
|
13
13
|
#
|
14
14
|
# @param sdk_key [String] The SDK key of your Hackle environment
|
15
|
-
# @param options
|
15
|
+
# @param options Optional parameters of configuration options
|
16
16
|
#
|
17
17
|
# @return [Client] The Hackle client instance.
|
18
18
|
#
|
@@ -36,4 +36,41 @@ module Hackle
|
|
36
36
|
decider: Decider.new
|
37
37
|
)
|
38
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
|
39
76
|
end
|
data/lib/hackle/client.rb
CHANGED
@@ -3,16 +3,18 @@
|
|
3
3
|
require 'hackle/decision/bucketer'
|
4
4
|
require 'hackle/decision/decider'
|
5
5
|
|
6
|
-
require 'hackle/events/
|
6
|
+
require 'hackle/events/user_event'
|
7
7
|
require 'hackle/events/event_dispatcher'
|
8
8
|
require 'hackle/events/event_processor'
|
9
9
|
|
10
10
|
require 'hackle/http/http'
|
11
11
|
|
12
12
|
require 'hackle/models/bucket'
|
13
|
+
require 'hackle/models/event'
|
13
14
|
require 'hackle/models/event_type'
|
14
15
|
require 'hackle/models/experiment'
|
15
16
|
require 'hackle/models/slot'
|
17
|
+
require 'hackle/models/user'
|
16
18
|
require 'hackle/models/variation'
|
17
19
|
|
18
20
|
require 'hackle/workspaces/http_workspace_fetcher'
|
@@ -36,8 +38,14 @@ module Hackle
|
|
36
38
|
#
|
37
39
|
def initialize(config:, workspace_fetcher:, event_processor:, decider:)
|
38
40
|
@logger = config.logger
|
41
|
+
|
42
|
+
# @type [PollingWorkspaceFetcher]
|
39
43
|
@workspace_fetcher = workspace_fetcher
|
44
|
+
|
45
|
+
# @type [EventProcessor]
|
40
46
|
@event_processor = event_processor
|
47
|
+
|
48
|
+
# @type [Decider]
|
41
49
|
@decider = decider
|
42
50
|
end
|
43
51
|
|
@@ -50,16 +58,16 @@ module Hackle
|
|
50
58
|
# - The user is not allocated to the experiment
|
51
59
|
# - The decided variation has been dropped
|
52
60
|
#
|
53
|
-
# @param experiment_key [Integer] The unique key of the experiment.
|
54
|
-
# @param
|
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.
|
55
63
|
# @param default_variation [String] The default variation of the experiment.
|
56
64
|
#
|
57
65
|
# @return [String] The decided variation for the user, or default variation
|
58
66
|
#
|
59
|
-
def variation(experiment_key:,
|
67
|
+
def variation(experiment_key:, user:, default_variation: 'A')
|
60
68
|
|
61
|
-
return default_variation if experiment_key.nil?
|
62
|
-
return default_variation if
|
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?
|
63
71
|
|
64
72
|
workspace = @workspace_fetcher.fetch
|
65
73
|
return default_variation if workspace.nil?
|
@@ -67,40 +75,45 @@ module Hackle
|
|
67
75
|
experiment = workspace.get_experiment(experiment_key: experiment_key)
|
68
76
|
return default_variation if experiment.nil?
|
69
77
|
|
70
|
-
decision = @decider.decide(experiment: experiment,
|
78
|
+
decision = @decider.decide(experiment: experiment, user: user)
|
71
79
|
case decision
|
72
80
|
when Decision::NotAllocated
|
73
81
|
default_variation
|
74
82
|
when Decision::ForcedAllocated
|
75
83
|
decision.variation_key
|
76
84
|
when Decision::NaturalAllocated
|
77
|
-
exposure_event =
|
85
|
+
exposure_event = UserEvent::Exposure.new(user: user, experiment: experiment, variation: decision.variation)
|
78
86
|
@event_processor.process(event: exposure_event)
|
79
87
|
decision.variation.key
|
80
88
|
else
|
81
89
|
default_variation
|
82
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
|
83
95
|
end
|
84
96
|
|
85
97
|
#
|
86
|
-
# Records the
|
98
|
+
# Records the event that occurred by the user.
|
87
99
|
#
|
88
|
-
# @param
|
89
|
-
# @param
|
90
|
-
# @param value [Float] Additional numeric value of the events (e.g. purchase_amount, api_latency, etc.)
|
100
|
+
# @param event [Event] the event that occurred.
|
101
|
+
# @param user [User] the user that occurred the event.
|
91
102
|
#
|
92
|
-
def track(
|
103
|
+
def track(event:, user:)
|
93
104
|
|
94
|
-
return if
|
95
|
-
return if
|
105
|
+
return if event.nil? || !event.is_a?(Event) || !event.valid?
|
106
|
+
return if user.nil? || !user.is_a?(User) || !user.valid?
|
96
107
|
|
97
108
|
workspace = @workspace_fetcher.fetch
|
98
109
|
return if workspace.nil?
|
99
110
|
|
100
|
-
event_type = workspace.get_event_type(event_type_key:
|
101
|
-
|
102
|
-
track_event = Event::Track.new(user_id: user_id, event_type: event_type, value: value)
|
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)
|
103
113
|
@event_processor.process(event: track_event)
|
114
|
+
|
115
|
+
rescue => e
|
116
|
+
@logger.error { "Unexpected error while tracking event: #{e.inspect}" }
|
104
117
|
end
|
105
118
|
|
106
119
|
#
|
@@ -4,20 +4,34 @@ require 'murmurhash3'
|
|
4
4
|
|
5
5
|
module Hackle
|
6
6
|
class Bucketer
|
7
|
-
|
7
|
+
|
8
|
+
# @param bucket [Bucket]
|
9
|
+
# @param user [User]
|
10
|
+
#
|
11
|
+
# @return [Slot, nil]
|
12
|
+
def bucketing(bucket:, user:)
|
8
13
|
slot_number = calculate_slot_number(
|
9
14
|
seed: bucket.seed,
|
10
15
|
slot_size: bucket.slot_size,
|
11
|
-
user_id:
|
16
|
+
user_id: user.id
|
12
17
|
)
|
13
18
|
bucket.get_slot(slot_number: slot_number)
|
14
19
|
end
|
15
20
|
|
21
|
+
# @param seed [Integer]
|
22
|
+
# @param slot_size [Integer]
|
23
|
+
# @param user_id [String]
|
24
|
+
#
|
25
|
+
# @return [Integer]
|
16
26
|
def calculate_slot_number(seed:, slot_size:, user_id:)
|
17
27
|
hash_value = hash(data: user_id, seed: seed)
|
18
28
|
hash_value.abs % slot_size
|
19
29
|
end
|
20
30
|
|
31
|
+
# @param data [String]
|
32
|
+
# @param seed [Integer]
|
33
|
+
#
|
34
|
+
# @return [Integer]
|
21
35
|
def hash(data:, seed:)
|
22
36
|
unsigned_value = MurmurHash3::V32.str_hash(data, seed)
|
23
37
|
if (unsigned_value & 0x80000000).zero?
|
@@ -2,20 +2,25 @@
|
|
2
2
|
|
3
3
|
module Hackle
|
4
4
|
class Decision
|
5
|
+
|
5
6
|
class NotAllocated < Decision
|
6
7
|
end
|
7
8
|
|
8
9
|
class ForcedAllocated < Decision
|
10
|
+
# @return [String]
|
9
11
|
attr_reader :variation_key
|
10
12
|
|
13
|
+
# @param variation_key [String]
|
11
14
|
def initialize(variation_key:)
|
12
15
|
@variation_key = variation_key
|
13
16
|
end
|
14
17
|
end
|
15
18
|
|
16
19
|
class NaturalAllocated < Decision
|
20
|
+
# @return [Variation]
|
17
21
|
attr_reader :variation
|
18
22
|
|
23
|
+
# @param variation [Variation]
|
19
24
|
def initialize(variation:)
|
20
25
|
@variation = variation
|
21
26
|
end
|
@@ -27,21 +32,31 @@ module Hackle
|
|
27
32
|
@bucketer = Bucketer.new
|
28
33
|
end
|
29
34
|
|
30
|
-
|
35
|
+
# @param experiment [Experiment]
|
36
|
+
# @param user [User]
|
37
|
+
#
|
38
|
+
# @return [Decision]
|
39
|
+
def decide(experiment:, user:)
|
31
40
|
case experiment
|
32
41
|
when Experiment::Completed
|
33
42
|
Decision::ForcedAllocated.new(variation_key: experiment.winner_variation_key)
|
34
43
|
when Experiment::Running
|
35
|
-
decide_running(running_experiment: experiment,
|
44
|
+
decide_running(running_experiment: experiment, user: user)
|
45
|
+
else
|
46
|
+
NotAllocated.new
|
36
47
|
end
|
37
48
|
end
|
38
49
|
|
39
|
-
|
50
|
+
# @param running_experiment [Experiment::Running]
|
51
|
+
# @param user [User]
|
52
|
+
#
|
53
|
+
# @return [Decision]
|
54
|
+
def decide_running(running_experiment:, user:)
|
40
55
|
|
41
|
-
overridden_variation = running_experiment.get_overridden_variation(
|
56
|
+
overridden_variation = running_experiment.get_overridden_variation(user: user)
|
42
57
|
return Decision::ForcedAllocated.new(variation_key: overridden_variation.key) unless overridden_variation.nil?
|
43
58
|
|
44
|
-
allocated_slot = @bucketer.bucketing(bucket: running_experiment.bucket,
|
59
|
+
allocated_slot = @bucketer.bucketing(bucket: running_experiment.bucket, user: user)
|
45
60
|
return Decision::NotAllocated.new if allocated_slot.nil?
|
46
61
|
|
47
62
|
allocated_variation = running_experiment.get_variation(variation_id: allocated_slot.variation_id)
|
@@ -53,9 +53,9 @@ module Hackle
|
|
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
|
@@ -6,6 +6,8 @@ module Hackle
|
|
6
6
|
|
7
7
|
DEFAULT_FLUSH_INTERVAL = 10
|
8
8
|
|
9
|
+
# @param config [Config]
|
10
|
+
# @param event_dispatcher [EventDispatcher]
|
9
11
|
def initialize(config:, event_dispatcher:)
|
10
12
|
@logger = config.logger
|
11
13
|
@event_dispatcher = event_dispatcher
|
@@ -26,6 +28,8 @@ module Hackle
|
|
26
28
|
def stop!
|
27
29
|
return unless @running
|
28
30
|
|
31
|
+
@logger.info { 'Shutting down Hackle event_processor' }
|
32
|
+
|
29
33
|
@message_processor.produce(message: Message::Shutdown.new, non_block: false)
|
30
34
|
@consume_task.join(10)
|
31
35
|
@flush_task.shutdown
|
@@ -34,6 +38,7 @@ module Hackle
|
|
34
38
|
@running = false
|
35
39
|
end
|
36
40
|
|
41
|
+
# @param event [UserEvent]
|
37
42
|
def process(event:)
|
38
43
|
@message_processor.produce(message: Message::Event.new(event))
|
39
44
|
end
|
@@ -44,8 +49,11 @@ module Hackle
|
|
44
49
|
|
45
50
|
class Message
|
46
51
|
class Event < Message
|
52
|
+
|
53
|
+
# @return [UserEvent]
|
47
54
|
attr_reader :event
|
48
55
|
|
56
|
+
# @param event [UserEvent]
|
49
57
|
def initialize(event)
|
50
58
|
@event = event
|
51
59
|
end
|
@@ -71,6 +79,8 @@ module Hackle
|
|
71
79
|
@consumed_events = []
|
72
80
|
end
|
73
81
|
|
82
|
+
# @param message [Message]
|
83
|
+
# @param non_block [boolean]
|
74
84
|
def produce(message:, non_block: true)
|
75
85
|
@message_queue.push(message, non_block)
|
76
86
|
rescue ThreadError
|
@@ -99,6 +109,7 @@ module Hackle
|
|
99
109
|
|
100
110
|
private
|
101
111
|
|
112
|
+
# @param event [UserEvent]
|
102
113
|
def consume_event(event:)
|
103
114
|
@consumed_events << event
|
104
115
|
dispatch_events if @consumed_events.length >= DEFAULT_MAX_EVENT_DISPATCH_SIZE
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackle
|
4
|
+
|
5
|
+
class UserEvent
|
6
|
+
|
7
|
+
# @!attribute [r] timestamp
|
8
|
+
# @return [Integer]
|
9
|
+
# @!attribute [r] user
|
10
|
+
# @return [User]
|
11
|
+
attr_reader :timestamp, :user
|
12
|
+
|
13
|
+
# @param user [User]
|
14
|
+
def initialize(user:)
|
15
|
+
@timestamp = UserEvent.generate_timestamp
|
16
|
+
@user = user
|
17
|
+
end
|
18
|
+
|
19
|
+
class Exposure < UserEvent
|
20
|
+
|
21
|
+
# @!attribute [r] experiment
|
22
|
+
# @return [Experiment]
|
23
|
+
# @!attribute [r] variation
|
24
|
+
# @return [Variation]
|
25
|
+
attr_reader :experiment, :variation
|
26
|
+
|
27
|
+
# @param user [User]
|
28
|
+
# @param experiment [Experiment]
|
29
|
+
# @param variation [Variation]
|
30
|
+
def initialize(user:, experiment:, variation:)
|
31
|
+
super(user: user)
|
32
|
+
@experiment = experiment
|
33
|
+
@variation = variation
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
class Track < UserEvent
|
39
|
+
|
40
|
+
# @!attribute [r] event_type
|
41
|
+
# @return [EventType]
|
42
|
+
# @!attribute [r] event
|
43
|
+
# @return [Event]
|
44
|
+
attr_reader :event_type, :event
|
45
|
+
|
46
|
+
# @param user [User]
|
47
|
+
# @param event_type [EventType]
|
48
|
+
# @param event [Event]
|
49
|
+
def initialize(user:, event_type:, event:)
|
50
|
+
super(user: user)
|
51
|
+
@event_type = event_type
|
52
|
+
@event = event
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Integer]
|
57
|
+
def self.generate_timestamp
|
58
|
+
(Time.now.to_f * 1000).to_i
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/hackle/models/bucket.rb
CHANGED
@@ -1,13 +1,24 @@
|
|
1
1
|
module Hackle
|
2
|
+
|
2
3
|
class Bucket
|
4
|
+
|
5
|
+
# @!attribute [r] seed
|
6
|
+
# @return [Integer]
|
7
|
+
# @!attribute [r] slot_size
|
8
|
+
# @return [Integer]
|
3
9
|
attr_reader :seed, :slot_size
|
4
10
|
|
11
|
+
# @param seed [Integer]
|
12
|
+
# @param slot_size [Integer]
|
13
|
+
# @param slots [Array]
|
5
14
|
def initialize(seed:, slot_size:, slots:)
|
6
15
|
@seed = seed
|
7
16
|
@slot_size = slot_size
|
8
17
|
@slots = slots
|
9
18
|
end
|
10
19
|
|
20
|
+
# @param slot_number [Integer]
|
21
|
+
# @return [Slot, nil]
|
11
22
|
def get_slot(slot_number:)
|
12
23
|
@slots.find { |slot| slot.contains?(slot_number: slot_number) }
|
13
24
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Hackle
|
2
|
+
class Event
|
3
|
+
|
4
|
+
# @!attribute [r] key
|
5
|
+
# @return [String]
|
6
|
+
# @!attribute [r] value
|
7
|
+
# @return [Float, nil]
|
8
|
+
# @!attribute [r] properties
|
9
|
+
# @return [Hash]
|
10
|
+
attr_reader :key, :value, :properties
|
11
|
+
|
12
|
+
|
13
|
+
# @param key [String]
|
14
|
+
# @param value [Float, nil]
|
15
|
+
# @param properties [Hash{Symbol => String, Number, boolean}]
|
16
|
+
def initialize(key:, value:, properties:)
|
17
|
+
@key = key
|
18
|
+
@value = value
|
19
|
+
@properties = properties
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
!key.nil? && key.is_a?(String)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,12 +1,20 @@
|
|
1
1
|
module Hackle
|
2
2
|
class EventType
|
3
|
+
|
4
|
+
# @!attribute [r] id
|
5
|
+
# @return [Integer]
|
6
|
+
# @!attribute [r] key
|
7
|
+
# @return [String]
|
3
8
|
attr_reader :id, :key
|
4
9
|
|
10
|
+
# @param id [Integer]
|
11
|
+
# @param key [String]
|
5
12
|
def initialize(id:, key:)
|
6
13
|
@id = id
|
7
14
|
@key = key
|
8
15
|
end
|
9
16
|
|
17
|
+
# @param key [String]
|
10
18
|
def self.undefined(key:)
|
11
19
|
EventType.new(id: 0, key: key)
|
12
20
|
end
|
@@ -1,34 +1,67 @@
|
|
1
1
|
module Hackle
|
2
2
|
class Experiment
|
3
|
+
|
4
|
+
# @!attribute [r] id
|
5
|
+
# @return [Integer]
|
6
|
+
# @!attribute [r] key
|
7
|
+
# @return [Integer]
|
3
8
|
attr_reader :id, :key
|
4
9
|
|
10
|
+
# @param id [Integer]
|
11
|
+
# @param key [Integer]
|
12
|
+
def initialize(id:, key:)
|
13
|
+
@id = id
|
14
|
+
@key = key
|
15
|
+
end
|
16
|
+
|
5
17
|
class Running < Experiment
|
18
|
+
|
19
|
+
# @!attribute [r] bucket
|
20
|
+
# @return [Bucket]
|
6
21
|
attr_reader :bucket
|
7
22
|
|
8
|
-
|
9
|
-
|
10
|
-
|
23
|
+
# @param id [Integer]
|
24
|
+
# @param key [Integer]
|
25
|
+
# @param bucket [Bucket]
|
26
|
+
# @param variations [Hash{String => Variation}]
|
27
|
+
# @param overrides [Hash{String => Integer}]
|
28
|
+
def initialize(id:, key:, bucket:, variations:, overrides:)
|
29
|
+
super(id: id, key: key)
|
11
30
|
@bucket = bucket
|
31
|
+
|
32
|
+
# @type [Hash{String => Variation}]
|
12
33
|
@variations = variations
|
13
|
-
|
34
|
+
|
35
|
+
# @type [Hash{String => Integer}]
|
36
|
+
@overrides = overrides
|
14
37
|
end
|
15
38
|
|
39
|
+
# @param variation_id [Integer]
|
40
|
+
# @return [Variation, nil]
|
16
41
|
def get_variation(variation_id:)
|
17
42
|
@variations[variation_id]
|
18
43
|
end
|
19
44
|
|
20
|
-
|
21
|
-
|
22
|
-
|
45
|
+
# @param user [User]
|
46
|
+
# @return [Variation, nil]
|
47
|
+
def get_overridden_variation(user:)
|
48
|
+
overridden_variation_id = @overrides[user.id]
|
49
|
+
return nil if overridden_variation_id.nil?
|
50
|
+
get_variation(variation_id: overridden_variation_id)
|
23
51
|
end
|
24
52
|
end
|
25
53
|
|
26
54
|
class Completed < Experiment
|
55
|
+
|
56
|
+
# @!attribute [r] winner_variation_key
|
57
|
+
# @return [String]
|
27
58
|
attr_reader :winner_variation_key
|
28
59
|
|
60
|
+
# @param id [Integer]
|
61
|
+
# @param key [Integer]
|
62
|
+
# @param winner_variation_key [String]
|
29
63
|
def initialize(id:, key:, winner_variation_key:)
|
30
|
-
|
31
|
-
@key = key
|
64
|
+
super(id: id, key: key)
|
32
65
|
@winner_variation_key = winner_variation_key
|
33
66
|
end
|
34
67
|
end
|
data/lib/hackle/models/slot.rb
CHANGED
@@ -1,13 +1,20 @@
|
|
1
1
|
module Hackle
|
2
2
|
class Slot
|
3
|
+
# @!attribute variation_id
|
4
|
+
# @return [Integer]
|
3
5
|
attr_reader :variation_id
|
4
6
|
|
7
|
+
# @param start_inclusive [Integer]
|
8
|
+
# @param end_exclusive [Integer]
|
9
|
+
# @param variation_id [Integer]
|
5
10
|
def initialize(start_inclusive:, end_exclusive:, variation_id:)
|
6
11
|
@start_inclusive = start_inclusive
|
7
12
|
@end_exclusive = end_exclusive
|
8
13
|
@variation_id = variation_id
|
9
14
|
end
|
10
15
|
|
16
|
+
# @param slot_number [Integer]
|
17
|
+
# @return [boolean]
|
11
18
|
def contains?(slot_number:)
|
12
19
|
@start_inclusive <= slot_number && slot_number < @end_exclusive
|
13
20
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hackle
|
2
|
+
|
3
|
+
class User
|
4
|
+
|
5
|
+
# @!attribute [r] id
|
6
|
+
# @return [String]
|
7
|
+
# @!attribute [r] properties
|
8
|
+
# @return [Hash]
|
9
|
+
attr_reader :id, :properties
|
10
|
+
|
11
|
+
#
|
12
|
+
# @param id [String]
|
13
|
+
# @param properties [Hash]
|
14
|
+
#
|
15
|
+
def initialize(id:, properties:)
|
16
|
+
@id = id
|
17
|
+
@properties = properties
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
!id.nil? && id.is_a?(String)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,7 +1,17 @@
|
|
1
1
|
module Hackle
|
2
2
|
class Variation
|
3
|
+
|
4
|
+
# @!attribute id
|
5
|
+
# @return [Integer]
|
6
|
+
# @!attribute key
|
7
|
+
# @return [String]
|
8
|
+
# @!attribute dropped
|
9
|
+
# @return [boolean]
|
3
10
|
attr_reader :id, :key, :dropped
|
4
11
|
|
12
|
+
# @param id [Integer]
|
13
|
+
# @param key [String]
|
14
|
+
# @param dropped [boolean]
|
5
15
|
def initialize(id:, key:, dropped:)
|
6
16
|
@id = id
|
7
17
|
@key = key
|
data/lib/hackle/version.rb
CHANGED
@@ -15,6 +15,7 @@ module Hackle
|
|
15
15
|
@running = false
|
16
16
|
end
|
17
17
|
|
18
|
+
# @return [Workspace, nil]
|
18
19
|
def fetch
|
19
20
|
@current_workspace.get
|
20
21
|
end
|
@@ -30,6 +31,8 @@ module Hackle
|
|
30
31
|
def stop!
|
31
32
|
return unless @running
|
32
33
|
|
34
|
+
@logger.info { 'Shutting down Hackle workspace_fetcher' }
|
35
|
+
|
33
36
|
@task.shutdown
|
34
37
|
@running = false
|
35
38
|
end
|
@@ -1,14 +1,27 @@
|
|
1
1
|
module Hackle
|
2
2
|
class Workspace
|
3
|
+
|
4
|
+
# @param experiments [Hash{Integer => Experiment}]
|
5
|
+
# @param event_types [Hash{String => EventType}]
|
3
6
|
def initialize(experiments:, event_types:)
|
7
|
+
|
8
|
+
# @type [Hash{Integer => Experiment}]
|
4
9
|
@experiments = experiments
|
10
|
+
|
11
|
+
# @type [Hash{String => EventType}]
|
5
12
|
@event_types = event_types
|
6
13
|
end
|
7
14
|
|
15
|
+
# @param experiment_key [Integer]
|
16
|
+
#
|
17
|
+
# @return [Experiment, nil]
|
8
18
|
def get_experiment(experiment_key:)
|
9
19
|
@experiments[experiment_key]
|
10
20
|
end
|
11
21
|
|
22
|
+
# @param event_type_key [String]
|
23
|
+
#
|
24
|
+
# @return [EventType]
|
12
25
|
def get_event_type(event_type_key:)
|
13
26
|
event_type = @event_types[event_type_key]
|
14
27
|
|
@@ -40,7 +53,7 @@ module Hackle
|
|
40
53
|
key: data[:key],
|
41
54
|
bucket: buckets[data[:bucketId]],
|
42
55
|
variations: Hash[data[:variations].map { |v| [v[:id], variation(v)] }],
|
43
|
-
|
56
|
+
overrides: Hash[data[:execution][:userOverrides].map { |u| [u[:userId], u[:variationId]] }]
|
44
57
|
)
|
45
58
|
end
|
46
59
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hackle-ruby-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hackle
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -142,14 +142,16 @@ files:
|
|
142
142
|
- lib/hackle/config.rb
|
143
143
|
- lib/hackle/decision/bucketer.rb
|
144
144
|
- lib/hackle/decision/decider.rb
|
145
|
-
- lib/hackle/events/event.rb
|
146
145
|
- lib/hackle/events/event_dispatcher.rb
|
147
146
|
- lib/hackle/events/event_processor.rb
|
147
|
+
- lib/hackle/events/user_event.rb
|
148
148
|
- lib/hackle/http/http.rb
|
149
149
|
- lib/hackle/models/bucket.rb
|
150
|
+
- lib/hackle/models/event.rb
|
150
151
|
- lib/hackle/models/event_type.rb
|
151
152
|
- lib/hackle/models/experiment.rb
|
152
153
|
- lib/hackle/models/slot.rb
|
154
|
+
- lib/hackle/models/user.rb
|
153
155
|
- lib/hackle/models/variation.rb
|
154
156
|
- lib/hackle/version.rb
|
155
157
|
- lib/hackle/workspaces/http_workspace_fetcher.rb
|
data/lib/hackle/events/event.rb
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Hackle
|
4
|
-
class Event
|
5
|
-
attr_reader :timestamp, :user_id
|
6
|
-
|
7
|
-
class Exposure < Event
|
8
|
-
attr_reader :experiment, :variation
|
9
|
-
|
10
|
-
def initialize(user_id:, experiment:, variation:)
|
11
|
-
@timestamp = Event.generate_timestamp
|
12
|
-
@user_id = user_id
|
13
|
-
@experiment = experiment
|
14
|
-
@variation = variation
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
class Track < Event
|
19
|
-
attr_reader :event_type, :value
|
20
|
-
|
21
|
-
def initialize(user_id:, event_type:, value: nil)
|
22
|
-
@timestamp = Event.generate_timestamp
|
23
|
-
@user_id = user_id
|
24
|
-
@event_type = event_type
|
25
|
-
@value = value
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.generate_timestamp
|
30
|
-
(Time.now.to_f * 1000).to_i
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|