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 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestlabSdkRuby
4
+ VERSION = "0.2.0"
5
+ 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: []