testlab_sdk_ruby 0.2.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 +7 -0
- data/lib/testlab_sdk_ruby/testlab_client.rb +140 -0
- data/lib/testlab_sdk_ruby/testlab_feature_logic.rb +88 -0
- data/lib/testlab_sdk_ruby/version.rb +5 -0
- data/lib/testlab_sdk_ruby.rb +21 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b7e084eaecf8e5963ba3977a3b23e4618b0780a51f804b679f7663003a954f45
|
4
|
+
data.tar.gz: be0d11c361c5076c6f0fb24753a733003cd7cb08df2e5a5b1faca1627894f76d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6e1e42b99883f208f8a580fb58e709f067104f4646a59a8aba25492cf3a616b376c785ac4907f30cf171165564b24c161133a2ba8a4c5cb09146a181016fd141
|
7
|
+
data.tar.gz: 0c78c3018c22f8d81c0cd8818c74ba47806aabf5a59612816414c1e1667def427bb232afeea70b6cbd0cb59ad33517236cedc008a1c500812fe1a63c6caa6e03
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require "testlab_sdk_ruby/testlab_feature_logic"
|
2
|
+
require "securerandom" # uuid = SecureRandom.uuid
|
3
|
+
require "httparty"
|
4
|
+
require "rufus-scheduler"
|
5
|
+
|
6
|
+
class Client
|
7
|
+
attr_accessor :config, :context, :features
|
8
|
+
|
9
|
+
def initialize(config)
|
10
|
+
@config = config
|
11
|
+
@context = nil
|
12
|
+
@features = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_default_context
|
16
|
+
self.context = { user_id: SecureRandom.uuid, ip: get_ip }
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_context(new_context)
|
20
|
+
self.context = context.merge(new_context)
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_ip
|
24
|
+
response = HTTParty.get("https://ipapi.co/json/")
|
25
|
+
response.parsed_response["ip"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_feature_value(name)
|
29
|
+
feature = @features.find { |f| f["name"] == name }
|
30
|
+
return false unless feature
|
31
|
+
|
32
|
+
if feature["type_id"] != 3
|
33
|
+
return is_enabled(features, name, context[:user_id])
|
34
|
+
else
|
35
|
+
enabled = is_enabled(features, name, context[:user_id])
|
36
|
+
# return false unless enabled
|
37
|
+
variant = get_variant(features, name, context[:user_id])
|
38
|
+
users = get_users
|
39
|
+
existing_user =
|
40
|
+
users.find do |user|
|
41
|
+
user["id"] == context[:user_id] && user["variant_id"] == variant["id"]
|
42
|
+
end
|
43
|
+
if enabled && variant && !existing_user
|
44
|
+
create_user(context[:user_id], variant["id"], context[:ip])
|
45
|
+
end
|
46
|
+
enabled && variant
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_features
|
51
|
+
url = "#{config[:server_address]}/api/feature"
|
52
|
+
response = HTTParty.get(url)
|
53
|
+
self.features = response.parsed_response
|
54
|
+
end
|
55
|
+
|
56
|
+
def timed_fetch(interval)
|
57
|
+
if interval > 0
|
58
|
+
scheduler = Rufus::Scheduler.new
|
59
|
+
scheduler.every "#{interval}.s" do
|
60
|
+
fetch_features
|
61
|
+
end
|
62
|
+
Thread.new { scheduler.join }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def fetch_features
|
67
|
+
url = "#{config[:server_address]}/api/feature"
|
68
|
+
last_modified = Time.now - config[:interval]
|
69
|
+
|
70
|
+
response =
|
71
|
+
HTTParty.get(
|
72
|
+
url,
|
73
|
+
options: {
|
74
|
+
headers: {
|
75
|
+
"If-Modified-Since" => last_modified.rfc2822,
|
76
|
+
},
|
77
|
+
},
|
78
|
+
)
|
79
|
+
puts response.parsed_response
|
80
|
+
self.features = response.parsed_response if response.code == 200
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_users
|
84
|
+
url = "#{config[:server_address]}/api/users"
|
85
|
+
response = HTTParty.get(url)
|
86
|
+
response.parsed_response
|
87
|
+
end
|
88
|
+
|
89
|
+
def create_user(id, variant_id, ip_address)
|
90
|
+
url = "#{config[:server_address]}/api/users"
|
91
|
+
|
92
|
+
response =
|
93
|
+
HTTParty.post(
|
94
|
+
url,
|
95
|
+
{
|
96
|
+
body: {
|
97
|
+
id: id,
|
98
|
+
variant_id: variant_id,
|
99
|
+
ip_address: ip_address,
|
100
|
+
}.to_json,
|
101
|
+
headers: {
|
102
|
+
"Content-Type" => "application/json",
|
103
|
+
},
|
104
|
+
},
|
105
|
+
)
|
106
|
+
if response.code == 200
|
107
|
+
return response.parsed_response
|
108
|
+
else
|
109
|
+
puts "Error creating user: #{response.body}"
|
110
|
+
return response.parsed_response
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_event(variant_id, user_id)
|
115
|
+
url = "#{config[:server_address]}/api/events"
|
116
|
+
|
117
|
+
response =
|
118
|
+
HTTParty.post(
|
119
|
+
url,
|
120
|
+
{
|
121
|
+
body: { variant_id: variant_id, user_id: user_id }.to_json,
|
122
|
+
headers: {
|
123
|
+
"Content-Type" => "application/json",
|
124
|
+
},
|
125
|
+
},
|
126
|
+
)
|
127
|
+
if response.code == 200
|
128
|
+
return response.parsed_response
|
129
|
+
else
|
130
|
+
puts "Error creating user: #{response.body}"
|
131
|
+
return response.parsed_response
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# myClient = Client.new({ server_address: "http://localhost:3000", interval: 10 })
|
137
|
+
# myClient.add_default_context
|
138
|
+
# myClient.get_features
|
139
|
+
# puts myClient.context[:user_id]
|
140
|
+
# puts myClient.get_feature_value("new_experiment")
|
@@ -0,0 +1,88 @@
|
|
1
|
+
def hash_message(message)
|
2
|
+
hash = 0
|
3
|
+
message.each_char do |c|
|
4
|
+
hash = (hash << 5) - hash + c.ord
|
5
|
+
hash &= 0xFFFFFFFF # Convert to 32-bit integer
|
6
|
+
end
|
7
|
+
(hash.to_f / 2**32).abs # Scale to [0, 1]
|
8
|
+
end
|
9
|
+
|
10
|
+
def is_active?(start_date, end_date)
|
11
|
+
current_date = DateTime.now
|
12
|
+
current_date >= start_date && current_date <= end_date
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_enabled(features, name, user_id)
|
16
|
+
# Find target feature based on name
|
17
|
+
feature = features.find { |f| f["name"] == name }
|
18
|
+
raise TypeError, "Provided name does not match any feature." if feature.nil?
|
19
|
+
|
20
|
+
# Return false if current date is outside of date range for feature
|
21
|
+
start_date = DateTime.parse(feature["start_date"])
|
22
|
+
end_date = DateTime.parse(feature["end_date"])
|
23
|
+
|
24
|
+
return false unless is_active?(start_date, end_date)
|
25
|
+
|
26
|
+
# Return false if feature is not running (toggled off) or if the hashed ID is outside of the target user_percentage range
|
27
|
+
# For Type 3 (features), users can only be assigned to one feature (total percentage of users enrolled in features can not exceed 100%)
|
28
|
+
|
29
|
+
case feature["type_id"]
|
30
|
+
when 1
|
31
|
+
feature["is_running"]
|
32
|
+
when 2
|
33
|
+
hashed_id = hash_message(user_id + name)
|
34
|
+
feature["is_running"] && hashed_id < feature["user_percentage"]
|
35
|
+
when 3
|
36
|
+
hashed_id = hash_message(user_id)
|
37
|
+
type_3_features =
|
38
|
+
features.filter do |f|
|
39
|
+
f["type_id"] == 3 &&
|
40
|
+
is_active?(
|
41
|
+
DateTime.parse(f["start_date"]),
|
42
|
+
DateTime.parse(f["end_date"]),
|
43
|
+
)
|
44
|
+
end
|
45
|
+
segment_start = 0
|
46
|
+
segment_end = 0
|
47
|
+
|
48
|
+
type_3_features.each do |exp|
|
49
|
+
segment_end += exp["user_percentage"]
|
50
|
+
if hashed_id >= segment_start && hashed_id <= segment_end &&
|
51
|
+
exp["name"] == name
|
52
|
+
return true
|
53
|
+
else
|
54
|
+
segment_start = segment_end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_variant(features, name, user_id)
|
63
|
+
hashed_id = hash_message(user_id)
|
64
|
+
puts "uuid, hashed #{user_id}, #{hashed_id}"
|
65
|
+
|
66
|
+
feature = features.find { |f| f["name"] == name }
|
67
|
+
raise TypeError, "Provided name does not match any feature." unless feature
|
68
|
+
|
69
|
+
variants = feature["variant_arr"]
|
70
|
+
type3features = features.select { |f| f["type_id"] == 3 }
|
71
|
+
segment_start, segment_end = 0, 0
|
72
|
+
|
73
|
+
type3features.each do |exp|
|
74
|
+
segment_end += exp["user_percentage"]
|
75
|
+
if hashed_id >= segment_start && hashed_id <= segment_end &&
|
76
|
+
exp["name"] == name
|
77
|
+
running_total = segment_start
|
78
|
+
variants.each do |variant|
|
79
|
+
running_total += variant["weight"].to_f * variant["weight"].to_f
|
80
|
+
return variant if hashed_id <= running_total
|
81
|
+
end
|
82
|
+
else
|
83
|
+
segment_start = segment_end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
return false
|
88
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "testlab_sdk_ruby/version"
|
4
|
+
|
5
|
+
require "testlab_sdk_ruby/testlab_client"
|
6
|
+
|
7
|
+
class Config
|
8
|
+
def initialize(server_address, interval)
|
9
|
+
@server_address = server_address
|
10
|
+
@interval = interval
|
11
|
+
end
|
12
|
+
|
13
|
+
def connect
|
14
|
+
client =
|
15
|
+
Client.new({ server_address: @server_address, interval: @interval })
|
16
|
+
client.get_features
|
17
|
+
client.add_default_context
|
18
|
+
client.timed_fetch(@interval)
|
19
|
+
client
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: testlab_sdk_ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alison Martinez
|
8
|
+
- Chelsea Saunders
|
9
|
+
- Sarah Bunker
|
10
|
+
- Abbie Papka
|
11
|
+
autorequire:
|
12
|
+
bindir: exe
|
13
|
+
cert_chain: []
|
14
|
+
date: 2023-03-10 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: httparty
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rufus-scheduler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
description:
|
45
|
+
email:
|
46
|
+
- ''
|
47
|
+
executables: []
|
48
|
+
extensions: []
|
49
|
+
extra_rdoc_files: []
|
50
|
+
files:
|
51
|
+
- lib/testlab_sdk_ruby.rb
|
52
|
+
- lib/testlab_sdk_ruby/testlab_client.rb
|
53
|
+
- lib/testlab_sdk_ruby/testlab_feature_logic.rb
|
54
|
+
- lib/testlab_sdk_ruby/version.rb
|
55
|
+
homepage: https://github.com/TestL-ab
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata: {}
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 2.6.0
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubygems_version: 3.2.3
|
75
|
+
signing_key:
|
76
|
+
specification_version: 4
|
77
|
+
summary: Client SDK for accessing TestLab A/B testing platform
|
78
|
+
test_files: []
|