rox-rollout 5.1.2 → 6.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1d81cc04111645ddde18f02d6ae020199aa46082f7dcddd4b193910a5a5d7a3
4
- data.tar.gz: eee4b5026bc9b70d828e83eba5677c8d5e5a51c1a7e5926ad5eca12320e2f0f7
3
+ metadata.gz: 73d64c9e8123a69712c15cada3f6c9b2642c12953a8c25250eb9963d845dae41
4
+ data.tar.gz: 1e14bc76699a72e0f34dc3a5039025ab184049807b930a2c0fa407915c5380b1
5
5
  SHA512:
6
- metadata.gz: 522d9dbc4659d022d081c3f067ad0533d0adef2ee50b93e6869e8c5e751e3b67ca8db800917c8c2cee3069aa77b4f5f3ce430f51c195516339f71e42cf24b20f
7
- data.tar.gz: 60938d6f6c5b0f0a6934e88e9f4aa40b2f7240d60b2e2647cc71f540dc602f24de1afe9dc3eda5ff65634ad97eda176d5664d03022edd5e51669f6cd5d273f93
6
+ metadata.gz: a615f9162c302b51d2359d0bc645fa1965a0d28c556678ee7e8a471cc7d1e5cbfef1ca73b06aaaaa91335665c30309286747c31e3930ad4d09fb4547936f1e53
7
+ data.tar.gz: c92c4de2381d270ab77dfc543efe00eaa0182d217a0e1e792a5934c58d8cf1c9f0e8d22138839dfd3d63c0ca434835cb8eabe28fdc76a3335bb4e7867da09a44
data/.gitignore CHANGED
@@ -9,3 +9,13 @@
9
9
  /tmp/
10
10
  Gemfile.lock
11
11
  .ruby-version
12
+
13
+ # E2E test artifacts
14
+ /e2e-logs/
15
+ /sdk-end-2-end-tests/
16
+ /scripts/
17
+ *.gem
18
+
19
+ # Project memory
20
+ claude.md
21
+ CLAUDE.md
data/Jenkinsfile ADDED
@@ -0,0 +1,212 @@
1
+ pipeline {
2
+ agent any
3
+
4
+ libraries {
5
+ lib('fm-shared-library@main')
6
+ }//end libraries. Github Repo: https://github.com/rollout/fm-cbci-shared-library
7
+
8
+ options {
9
+ timeout(time: 45, unit: 'MINUTES')
10
+ }
11
+
12
+ stages {
13
+ stage('Checkout') {
14
+ steps {
15
+ checkout scm
16
+ }
17
+ }
18
+
19
+ stage("Run Unit tests"){
20
+ parallel {
21
+ stage("ruby-2.6") {
22
+ agent {
23
+ kubernetes {
24
+ inheritFrom 'ruby'
25
+ yamlFile './cbci-templates/sdkci-unit.yml'
26
+ }
27
+ }
28
+
29
+ steps {
30
+ container(name: "ruby-2-6", shell: "sh") {
31
+ withCredentials([
32
+ sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
33
+ ]) {
34
+ echo "Executing Run tests"
35
+ sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
36
+ label: "Running unit tests"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ stage("ruby-2.7") {
42
+ agent {
43
+ kubernetes {
44
+ inheritFrom 'ruby'
45
+ yamlFile './cbci-templates/sdkci-unit.yml'
46
+ }
47
+ }
48
+
49
+ steps {
50
+ container(name: "ruby-2-7", shell: "sh") {
51
+ withCredentials([
52
+ sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
53
+ ]) {
54
+ echo "Executing Run tests"
55
+ sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
56
+ label: "Running unit tests"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ stage("ruby-3.0") {
62
+ agent {
63
+ kubernetes {
64
+ inheritFrom 'ruby'
65
+ yamlFile './cbci-templates/sdkci-unit.yml'
66
+ }
67
+ }
68
+
69
+ steps {
70
+ container(name: "ruby-3-0", shell: "sh") {
71
+ withCredentials([
72
+ sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
73
+ ]) {
74
+ echo "Executing Run tests"
75
+ sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
76
+ label: "Running unit tests"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ stage("ruby-3.1") {
82
+ agent {
83
+ kubernetes {
84
+ inheritFrom 'ruby'
85
+ yamlFile './cbci-templates/sdkci-unit.yml'
86
+ }
87
+ }
88
+
89
+ steps {
90
+ container(name: "ruby-3-1", shell: "sh") {
91
+ withCredentials([
92
+ sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
93
+ ]) {
94
+ echo "Executing Run tests"
95
+ sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
96
+ label: "Running unit tests"
97
+ }
98
+ }
99
+ }
100
+ }
101
+ stage("ruby-3.2") {
102
+ agent {
103
+ kubernetes {
104
+ inheritFrom 'ruby'
105
+ yamlFile './cbci-templates/sdkci-unit.yml'
106
+ }
107
+ }
108
+
109
+ steps {
110
+ container(name: "ruby-3-2", shell: "sh") {
111
+ withCredentials([
112
+ sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
113
+ ]) {
114
+ echo "Executing Run tests"
115
+ sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
116
+ label: "Running unit tests"
117
+ }
118
+ }
119
+ }
120
+ }
121
+ stage("ruby-3.3") {
122
+ agent {
123
+ kubernetes {
124
+ inheritFrom 'ruby'
125
+ yamlFile './cbci-templates/sdkci-unit.yml'
126
+ }
127
+ }
128
+
129
+ steps {
130
+ container(name: "ruby-3-3", shell: "sh") {
131
+ withCredentials([
132
+ sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
133
+ ]) {
134
+ echo "Executing Run tests"
135
+ sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
136
+ label: "Running unit tests"
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ post{
143
+ success{
144
+ script {
145
+ echo 'Unit Tests OK; posting results'
146
+ currentBuild.result = 'SUCCESS'
147
+ }
148
+ }
149
+ failure{
150
+ echo 'Unit Tests Failed;'
151
+ }
152
+ }
153
+ }
154
+ stage("Run E2E tests"){
155
+ agent {
156
+ kubernetes {
157
+ inheritFrom 'default'
158
+ yamlFile './cbci-templates/sdkci-e2e.yml'
159
+ }
160
+ }
161
+
162
+ steps {
163
+ container("rox-proxy") {
164
+ waitForRoxProxy()
165
+ }
166
+
167
+ container(name: "server", shell: 'sh') {
168
+ withCredentials([
169
+ string(credentialsId: 'TEST_E2E_BEARER', variable: 'TEST_E2E_BEARER'),
170
+ sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
171
+ sshUserPrivateKey(credentialsId: 'SDK_E2E_TESTS_DEPLOY_KEY', keyFileVariable: 'SDK_E2E_TESTS_DEPLOY_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
172
+ ]) {
173
+ script {
174
+ addGitHubFingerprint()
175
+ TESTENVPARAMS = "QA_E2E_BEARER=$TEST_E2E_BEARER API_HOST=https://api.test.rollout.io CD_API_ENDPOINT=https://api.test.rollout.io/device/get_configuration CD_S3_ENDPOINT=https://rox-conf.test.rollout.io/ SS_API_ENDPOINT=https://api.test.rollout.io/device/update_state_store/ SS_S3_ENDPOINT=https://rox-state.test.rollout.io/ CLIENT_DATA_CACHE_KEY=client_data ANALYTICS_ENDPOINT=https://analytic.test.rollout.io/ NOTIFICATIONS_ENDPOINT=https://push.test.rollout.io/sse"
176
+ ROOT_DIR = pwd()
177
+
178
+ withEnv(["GIT_SSH_COMMAND=ssh -i ${SDK_E2E_SSH_KEY}"]) {
179
+ echo "Executing E2E tests"
180
+ sh script: """
181
+ apt-get update && apt-get install -y curl gnupg
182
+ curl -sL https://deb.nodesource.com/setup_lts.x | bash -
183
+ apt-get install -y nodejs && npm install -g yarn
184
+
185
+ git clone git@github.com:rollout/sdk-end-2-end-tests.git
186
+ ln -s ${ROOT_DIR}/e2e-server/ ${ROOT_DIR}/sdk-end-2-end-tests/drivers/ruby
187
+
188
+ cd ${ROOT_DIR}/sdk-end-2-end-tests/drivers/nodejs && yarn install --frozen-lockfile
189
+ cd ${ROOT_DIR}/sdk-end-2-end-tests && yarn install --frozen-lockfile
190
+
191
+ ${TESTENVPARAMS} SDK_LANG=ruby NODE_ENV=container yarn test:env
192
+ """, label: "Pull SDK end2 tests repository"
193
+ }// end withEnv
194
+ }
195
+ }
196
+ }
197
+ }
198
+ post{
199
+ success{
200
+ script {
201
+ echo 'E2E Tests OK; posting results'
202
+ currentBuild.result = 'SUCCESS'
203
+ }
204
+ }
205
+ failure{
206
+ echo 'E2E Tests Failed;'
207
+ }
208
+
209
+ }
210
+ }
211
+ }
212
+ }
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rox-rollout.svg)](https://badge.fury.io/rb/rox-rollout)
4
4
 
5
- Ruby supported versions: 2.3.x, 2.4.x, 2.5.x, 2.6.x
5
+ Ruby supported versions: 2.6, 2.7, 3.0, 3.1, 3.2, 3.3
6
6
 
7
7
  ## Run tests
8
8
 
@@ -0,0 +1,29 @@
1
+ apiVersion: v1
2
+ kind: Pod
3
+ metadata:
4
+ name: rox-ruby-sdk-e2e
5
+ spec:
6
+ serviceAccountName: ops-gcr-rw
7
+ containers:
8
+ - name: server
9
+ image: ruby:3.2
10
+ tty: true
11
+ resources:
12
+ requests:
13
+ memory: "1Gi"
14
+ cpu: "1000m"
15
+ limits:
16
+ memory: "2Gi"
17
+ cpu: "1000m"
18
+ - name: rox-proxy
19
+ image: rollout/simple-proxy
20
+ tty: true
21
+ ports:
22
+ - containerPort: 8080
23
+ resources:
24
+ requests:
25
+ memory: "1Gi"
26
+ cpu: "1000m"
27
+ limits:
28
+ memory: "1Gi"
29
+ cpu: "1000m"
@@ -0,0 +1,67 @@
1
+ apiVersion: v1
2
+ kind: Pod
3
+ metadata:
4
+ name: rox-ruby-sdk-unit
5
+ spec:
6
+ serviceAccountName: ops-gcr-rw
7
+ containers:
8
+ - name: ruby-2-6
9
+ image: ruby:2.6
10
+ tty: true
11
+ resources:
12
+ requests:
13
+ memory: "1Gi"
14
+ cpu: "1000m"
15
+ limits:
16
+ memory: "2Gi"
17
+ cpu: "1000m"
18
+ - name: ruby-2-7
19
+ image: ruby:2.7
20
+ tty: true
21
+ resources:
22
+ requests:
23
+ memory: "1Gi"
24
+ cpu: "1000m"
25
+ limits:
26
+ memory: "2Gi"
27
+ cpu: "1000m"
28
+ - name: ruby-3-0
29
+ image: ruby:3.0
30
+ tty: true
31
+ resources:
32
+ requests:
33
+ memory: "1Gi"
34
+ cpu: "1000m"
35
+ limits:
36
+ memory: "2Gi"
37
+ cpu: "1000m"
38
+ - name: ruby-3-1
39
+ image: ruby:3.1
40
+ tty: true
41
+ resources:
42
+ requests:
43
+ memory: "1Gi"
44
+ cpu: "1000m"
45
+ limits:
46
+ memory: "2Gi"
47
+ cpu: "1000m"
48
+ - name: ruby-3-2
49
+ image: ruby:3.2
50
+ tty: true
51
+ resources:
52
+ requests:
53
+ memory: "1Gi"
54
+ cpu: "1000m"
55
+ limits:
56
+ memory: "2Gi"
57
+ cpu: "1000m"
58
+ - name: ruby-3-3
59
+ image: ruby:3.3
60
+ tty: true
61
+ resources:
62
+ requests:
63
+ memory: "1Gi"
64
+ cpu: "1000m"
65
+ limits:
66
+ memory: "2Gi"
67
+ cpu: "1000m"
@@ -8,9 +8,11 @@ gem install sinatra --conservative
8
8
  gem install sinatra-contrib --conservative
9
9
  gem install json --conservative
10
10
  gem install em-eventsource --conservative
11
+ gem install rackup
12
+ gem install webrick
11
13
 
12
14
  nohup ruby ./server.rb $1 1>"$DIR"/log_"$1".out 2>&1 &
13
15
  while true ; do
14
16
  curl -p http://127.0.0.1:$1/status-check && exit
15
17
  sleep 1
16
- done
18
+ done
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Simple example to test Rox SDK with Ruby 3.3
5
+ require 'bundler/setup'
6
+ require 'rox/server/rox_server'
7
+ require 'rox/server/flags/rox_flag'
8
+ require 'rox/server/flags/rox_string'
9
+
10
+ # Define a container class for flags
11
+ class FeatureFlags
12
+ attr_reader :enable_tutorial, :color_variant, :timeout_value
13
+
14
+ def initialize
15
+ @enable_tutorial = Rox::Server::RoxFlag.new(false)
16
+ @color_variant = Rox::Server::RoxString.new('blue', ['red', 'green', 'blue'])
17
+ @timeout_value = Rox::Server::RoxInt.new(30, [10, 30, 60])
18
+ end
19
+ end
20
+
21
+ # Initialize flags
22
+ flags = FeatureFlags.new
23
+
24
+ # Register flags
25
+ puts "Registering feature flags..."
26
+ Rox::Server::RoxServer.register(flags)
27
+
28
+ # Set custom properties
29
+ puts "Setting custom properties..."
30
+ Rox::Server::RoxServer.set_custom_string_property('tier', 'premium')
31
+ Rox::Server::RoxServer.set_custom_boolean_property('is_beta') { true }
32
+
33
+ # Setup SDK with test API key
34
+ puts "Setting up Rox SDK..."
35
+ puts "Ruby version: #{RUBY_VERSION}"
36
+ puts "Rox SDK version: #{Rox::VERSION}"
37
+
38
+ begin
39
+ # Use a test API key in valid format (24-char hex for MongoDB ObjectId)
40
+ # This will fail to connect but will test initialization
41
+ api_key = '507f1f77bcf86cd799439011'
42
+
43
+ options = Rox::Server::RoxOptions.new(
44
+ version: '1.0.0',
45
+ fetch_interval: 60
46
+ )
47
+
48
+ Rox::Server::RoxServer.setup(api_key, options)
49
+
50
+ # Give it a moment to initialize
51
+ sleep 2
52
+
53
+ # Test flag evaluation (will use defaults since we can't fetch)
54
+ puts "\nTesting flag evaluation:"
55
+ puts "enable_tutorial: #{flags.enable_tutorial.enabled?(nil)}"
56
+ puts "color_variant: #{flags.color_variant.value(nil)}"
57
+ puts "timeout_value: #{flags.timeout_value.value(nil)}"
58
+
59
+ # Test dynamic API
60
+ puts "\nTesting dynamic API:"
61
+ dynamic_api = Rox::Server::RoxServer.dynamic_api
62
+ puts "Dynamic flag (default false): #{dynamic_api.enabled?('dynamic.test', false, nil)}"
63
+ puts "Dynamic value (default 'test'): #{dynamic_api.value('dynamic.string', 'test', nil)}"
64
+
65
+ puts "\n✅ Ruby #{RUBY_VERSION} compatibility test PASSED!"
66
+ puts "SDK initialized and flags evaluated successfully."
67
+
68
+ rescue StandardError => e
69
+ puts "\n❌ Error: #{e.message}"
70
+ puts e.backtrace.first(5)
71
+ exit 1
72
+ ensure
73
+ # Shutdown SDK
74
+ puts "\nShutting down SDK..."
75
+ Rox::Server::RoxServer.shutdown
76
+ end
77
+
78
+ puts "\nSample app completed successfully!"
@@ -10,10 +10,11 @@ require 'rox/core/logging/logging'
10
10
  module Rox
11
11
  module Core
12
12
  class ConfigurationParser
13
- def initialize(signature_verifier, error_reporter, configuration_fetched_invoker)
13
+ def initialize(signature_verifier, error_reporter, configuration_fetched_invoker, rox_options = nil)
14
14
  @signature_verifier = signature_verifier
15
15
  @error_reporter = error_reporter
16
16
  @configuration_fetched_invoker = configuration_fetched_invoker
17
+ @rox_options = rox_options
17
18
  end
18
19
 
19
20
  def parse(fetch_result, sdk_settings)
@@ -31,8 +32,8 @@ module Rox
31
32
  return nil
32
33
  end
33
34
 
34
- if fetch_result.source != ConfigurationSource::ROXY && !@signature_verifier.verify(json_obj['data'],
35
- json_obj['signature_v0'])
35
+ if signature_verification_enabled? && fetch_result.source != ConfigurationSource::ROXY && !@signature_verifier.verify(json_obj['data'],
36
+ json_obj['signature_v0'])
36
37
  @configuration_fetched_invoker.invoke_error(FetcherError::SIGNATURE_VERIFICATION_ERROR)
37
38
  @error_reporter.report('Failed to validate signature',
38
39
  StandardError.new("Data : #{json_obj['data']} Signature : #{json_obj['signature_v0']}"))
@@ -81,6 +82,14 @@ module Rox
81
82
  def parse_target_group(data)
82
83
  TargetGroupModel.new(data['_id'], data['condition'])
83
84
  end
85
+
86
+ private
87
+
88
+ def signature_verification_enabled?
89
+ return true if @rox_options.nil?
90
+
91
+ !@rox_options.disable_signature_verification
92
+ end
84
93
  end
85
94
  end
86
95
  end
@@ -1,3 +1,5 @@
1
+ require 'rox/server/self_managed_options'
2
+
1
3
  module Rox
2
4
  module Core
3
5
  module Environment
@@ -50,6 +52,16 @@ module Rox
50
52
  @notifications_path
51
53
  end
52
54
 
55
+ def self.set_platform(rox_options)
56
+ @cdn_path = 'https://rox-conf.cloudbees.io'
57
+ @state_cdn_path = 'https://rox-state.cloudbees.io'
58
+ alternative_api_url = rox_options&.self_managed_options&.server_url || 'https://api.cloudbees.io'
59
+ @api_path = "#{alternative_api_url}/device/get_configuration"
60
+ @state_api_path = "#{alternative_api_url}/device/update_state_store"
61
+ @analytics_path = rox_options&.self_managed_options&.analytics_url || 'https://fm-analytics.cloudbees.io'
62
+ @notifications_path = 'https://sdk-notification-service.cloudbees.io/sse'
63
+ end
64
+
53
65
  private
54
66
  def self.setQA()
55
67
  @cdn_path = 'https://qa-conf.rollout.io'
@@ -75,11 +87,11 @@ module Rox
75
87
  alternative_api_url = rox_options&.self_managed_options&.server_url || 'https://x-api.rollout.io'
76
88
  @api_path = "#{alternative_api_url}/device/get_configuration"
77
89
  @state_api_path = "#{alternative_api_url}/device/update_state_store"
78
- @analytics_path = rox_options&.self_managed_options&.analytics_url ||'https://analytic.rollout.io'
90
+ @analytics_path = rox_options&.self_managed_options&.analytics_url || 'https://analytic.rollout.io'
79
91
  @notifications_path = 'https://push.rollout.io/sse'
80
92
  end
81
93
  end
82
94
  end
83
95
  end
84
96
 
85
- Rox::Core::Environment.reset
97
+ Rox::Core::Environment.reset
data/lib/rox/core/core.rb CHANGED
@@ -25,11 +25,14 @@ require 'rox/core/security/signature_verifier'
25
25
  require 'rox/core/security/signature_verifier_mock'
26
26
  require 'rox/core/utils/periodic_task'
27
27
  require 'rox/core/client/dynamic_api'
28
+ require 'rox/core/helpers/api_key'
28
29
  require 'rox/core/error_handling/userspace_unhandled_error_invoker'
29
30
 
30
31
  module Rox
31
32
  module Core
32
33
  class Core
34
+ include Rox::Core::Helpers::ApiKey
35
+
33
36
  def initialize
34
37
  @flag_repository = FlagRepository.new
35
38
  @custom_property_repository = CustomPropertyRepository.new
@@ -59,7 +62,7 @@ module Rox
59
62
  def setup(sdk_settings, device_properties)
60
63
  @sdk_settings = sdk_settings
61
64
  @rox_options = device_properties.rox_options
62
- Rox::Core::Environment.reset(@rox_options)
65
+ reset_environment!(sdk_settings&.api_key, @rox_options)
63
66
 
64
67
  experiments_extensions = ExperimentsExtensions.new(@parser, @target_group_repository, @flag_repository,
65
68
  @experiment_repository)
@@ -156,7 +159,7 @@ module Rox
156
159
  SignatureVerifier.new
157
160
  end
158
161
  configuration_parser = ConfigurationParser.new(signature_verifier, @error_reporter,
159
- @configuration_fetched_invoker)
162
+ @configuration_fetched_invoker, @rox_options)
160
163
  result = @configuration_fetcher.fetch
161
164
  return if result.nil?
162
165
 
@@ -220,12 +223,13 @@ module Rox
220
223
  @state_sender.dump_state
221
224
  end
222
225
 
223
- def validate_api_key(api_key)
224
- valid_api_key_pattern = /^[a-f\d]{24}$/
225
- if api_key&.strip&.empty?
226
- raise ArgumentError, 'Blank Rollout api key - must be specified'
227
- elsif !valid_api_key_pattern.match(api_key)
228
- raise ArgumentError, 'Illegal Rollout api key'
226
+ def reset_environment!(api_key, rox_options)
227
+ raise ArgumentError, 'Blank Feature Flag API key - must be specified' unless api_key
228
+
229
+ if is_cbp?(api_key)
230
+ Rox::Core::Environment.set_platform(rox_options)
231
+ else
232
+ Rox::Core::Environment.reset(rox_options)
229
233
  end
230
234
  end
231
235
 
@@ -43,6 +43,10 @@ module Rox
43
43
  def disabled(context)
44
44
  yield unless enabled?(context)
45
45
  end
46
+
47
+ def external_type
48
+ 'Boolean'
49
+ end
46
50
  end
47
51
  end
48
52
  end
@@ -8,6 +8,10 @@ module Rox
8
8
  send_impressions(return_value, context)
9
9
  return_value
10
10
  end
11
+
12
+ def external_type
13
+ 'Number'
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -8,6 +8,10 @@ module Rox
8
8
  send_impressions(return_value, context)
9
9
  return_value
10
10
  end
11
+
12
+ def external_type
13
+ 'Number'
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -63,6 +63,10 @@ module Rox
63
63
 
64
64
  return_value
65
65
  end
66
+
67
+ def external_type
68
+ 'String'
69
+ end
66
70
  end
67
71
  end
68
72
  end
@@ -0,0 +1,28 @@
1
+
2
+ module Rox
3
+ module Core
4
+ module Helpers
5
+ module ApiKey
6
+ MONGO_API_KEY_PATTERN = /^[a-f\d]{24}$/
7
+ UUID_API_KEY_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
8
+
9
+ def is_cbp?(api_key)
10
+ validate_api_key(api_key) && UUID_API_KEY_PATTERN.match?(api_key)
11
+ rescue ArgumentError
12
+ false
13
+ end
14
+
15
+ def validate_api_key(api_key)
16
+ if api_key&.strip&.empty?
17
+ raise ArgumentError, 'Blank Rollout api key - must be specified'
18
+ elsif !MONGO_API_KEY_PATTERN.match(api_key) && !UUID_API_KEY_PATTERN.match(api_key)
19
+ raise ArgumentError, 'Illegal Rollout api key'
20
+ else
21
+ true
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -8,10 +8,13 @@ require 'rox/core/network/request'
8
8
  require 'rox/core/network/request_data'
9
9
  require 'rox/core/network/response'
10
10
  require 'rox/core/utils/debouncer'
11
+ require 'rox/core/helpers/api_key'
11
12
 
12
13
  module Rox
13
14
  module Core
14
15
  class StateSender
16
+ include Rox::Core::Helpers::ApiKey
17
+
15
18
  def initialize(sdk_settings, device_properties, flag_repository, custom_property_repository)
16
19
  @sdk_settings = sdk_settings
17
20
  @device_properties = device_properties
@@ -27,8 +30,8 @@ module Rox
27
30
 
28
31
  def send
29
32
  rollout_key = @device_properties.rollout_key
30
- serialized_feature_flags = StateSender.seralize_flag_repository(@flag_repository)
31
- serialized_custom_properties = StateSender.serialize_custom_properties_repository(@custom_property_repository)
33
+ serialized_feature_flags = seralize_flag_repository
34
+ serialized_custom_properties = serialize_custom_properties_repository
32
35
 
33
36
  state_payload = StateSender.state_payload(serialized_feature_flags, serialized_custom_properties,
34
37
  @device_properties, @sdk_settings.dev_mode_secret)
@@ -39,7 +42,7 @@ module Rox
39
42
  response = get_state_from_CDN(rollout_key, md5_signature)
40
43
  if response.success?
41
44
  Rox::Core::Logging.logger.debug('Successfully fetched state from CDN')
42
- return
45
+ return response, nil
43
46
  else
44
47
  Rox::Core::Logging.logger.debug('Failed to fetch state from CDN. Sending state to API...')
45
48
  end
@@ -52,6 +55,7 @@ module Rox
52
55
  'Failed to send state to API.'
53
56
  end
54
57
  Rox::Core::Logging.logger.debug(message)
58
+ return response, state_payload
55
59
  end
56
60
 
57
61
  def dump_state
@@ -108,7 +112,7 @@ module Rox
108
112
  def self.seralize_flag_repository_with_values(flag_repository)
109
113
  flags = []
110
114
  flag_repository.all_flags.sort_by(&:name).each do |f|
111
- flags << {
115
+ flags << {
112
116
  name: f.name,
113
117
  enabled: f.enabled?,
114
118
  defaultValue: f.default_value,
@@ -142,6 +146,45 @@ module Rox
142
146
  end
143
147
  properties
144
148
  end
149
+
150
+ private
151
+
152
+ def cbp?
153
+ # Memoize assuming that @sdk_settings are not mutable during state sender lifetime
154
+ @cbp ||= is_cbp?(@sdk_settings&.api_key)
155
+ end
156
+
157
+ def seralize_flag_repository
158
+ flags = []
159
+ @flag_repository.all_flags.sort_by(&:name).each do |f|
160
+ flag = {
161
+ name: f.name,
162
+ defaultValue: f.default_value,
163
+ options: f.options
164
+ }
165
+
166
+ flag[:externalType] = f.external_type if cbp?
167
+
168
+ flags << flag
169
+ end
170
+ flags
171
+ end
172
+
173
+ def serialize_custom_properties_repository
174
+ properties = []
175
+ @custom_property_repository.all_custom_properties.sort_by(&:name).each do |p|
176
+ # Check not to send date custom properties to rollout
177
+ return if !is_cbp?(@sdk_settings&.api_key) && p.type.external_type == DateTime
178
+
179
+ properties << {
180
+ name: p.name,
181
+ type: p.type.type,
182
+ externalType: p.type.external_type
183
+ }
184
+ end
185
+ properties
186
+ end
187
+
145
188
  end
146
189
  end
147
190
  end
@@ -13,6 +13,7 @@ module Rox
13
13
  INT = CustomPropertyType.new('int', 'Number')
14
14
  FLOAT = CustomPropertyType.new('double', 'Number')
15
15
  SEMVER = CustomPropertyType.new('semver', 'Semver')
16
+ DATETIME = CustomPropertyType.new("time", "DateTime")
16
17
  end
17
18
  end
18
19
  end
@@ -113,6 +113,17 @@ module Rox
113
113
  )
114
114
  end
115
115
 
116
+ def internal_datetime
117
+ type = Rox::Core::CustomPropertyType::DATETIME
118
+
119
+ Rox::Core::DeviceProperty.new(
120
+ "now",
121
+ type
122
+ ) do
123
+ Time.now
124
+ end
125
+ end
126
+
116
127
  # rubocop:disable Metrics/MethodLength
117
128
  def all_properties
118
129
  [
@@ -124,7 +135,8 @@ module Rox
124
135
  internal_app_key,
125
136
  internal_distinct_id,
126
137
  internal_lib_version,
127
- internal_api_version
138
+ internal_api_version,
139
+ internal_datetime
128
140
  ]
129
141
  end
130
142
  end
@@ -169,6 +169,15 @@ module Rox
169
169
  stack.push(TokenType::UNDEFINED)
170
170
  end
171
171
  end
172
+
173
+ add_operator('tsToNum') do |_parser, stack, _context|
174
+ datetime = stack.pop
175
+ if datetime.is_a?(DateTime)
176
+ stack.push(datetime.to_time.to_i)
177
+ else
178
+ stack.push(TokenType::UNDEFINED)
179
+ end
180
+ end
172
181
  end
173
182
  end
174
183
  end
@@ -1,15 +1,19 @@
1
1
  require 'rox/core/logging/logging'
2
2
  require 'rox/server/logging/server_logger'
3
+ require 'rox/core/helpers/api_key'
3
4
 
4
5
  module Rox
5
6
  module Server
6
7
  class RoxOptions
8
+ include Rox::Core::Helpers::ApiKey
9
+
7
10
  attr_reader :dev_mode_key, :version, :fetch_interval,
8
11
  :logger, :impression_handler,
9
12
  :configuration_fetched_handler,
10
13
  :roxy_url, :self_managed_options,
11
14
  :network_configurations_options,
12
- :dynamic_property_rule_handler
15
+ :dynamic_property_rule_handler,
16
+ :disable_signature_verification
13
17
 
14
18
  def initialize(
15
19
  dev_mode_key: nil,
@@ -21,7 +25,8 @@ module Rox
21
25
  roxy_url: nil,
22
26
  self_managed_options: nil,
23
27
  network_configurations_options: nil,
24
- dynamic_property_rule_handler: nil
28
+ dynamic_property_rule_handler: nil,
29
+ disable_signature_verification: nil
25
30
  )
26
31
  @dev_mode_key = dev_mode_key || 'stam'
27
32
  @version = version || '0.0'
@@ -42,11 +47,43 @@ module Rox
42
47
  @dynamic_property_rule_handler = dynamic_property_rule_handler || proc do |prop_name, context|
43
48
  context ? context[prop_name] : nil
44
49
  end
50
+ @disable_signature_verification = disable_signature_verification
45
51
  end
46
52
 
47
53
  def self_managed?
48
54
  !@self_managed_options.nil?
49
55
  end
56
+
57
+ def clone_with_options(options = {})
58
+ # Check if any keys that are not present as attributes were given as params
59
+ redundant_keys = options.keys - valid_attributes
60
+
61
+ # Filter options if there are such keys that are not present
62
+ if !redundant_keys.empty?
63
+ options = options.reduce({}) do |acc, (key, value)|
64
+ acc[key] = value unless redundant_keys.include?(key)
65
+ acc
66
+ end
67
+ end
68
+
69
+ valid_attributes.each do |va|
70
+ unless options.key?(va)
71
+ options[va] = self.instance_variable_get("@#{va}".to_sym)
72
+ end
73
+ end
74
+
75
+ self.class.new(**options)
76
+ end
77
+
78
+ private
79
+
80
+ def valid_attributes
81
+ return @valid_attributes if @valid_attributes
82
+
83
+ cons = self.class.instance_method(:initialize)
84
+ key_parameters = cons.parameters.select { |type, name| type == :keyreq || type == :key }
85
+ @valid_attributes = key_parameters.map(&:last)
86
+ end
50
87
  end
51
88
  end
52
89
  end
@@ -14,9 +14,20 @@ module Rox
14
14
 
15
15
  class << self
16
16
  def setup(api_key, rox_options = nil)
17
- validate_api_key(api_key)
18
-
19
- rox_options = Rox::Server::RoxOptions.new if rox_options.nil?
17
+ # @core.setup calls validate_api_key internally anyway, but it will happen in another thread
18
+ # because of the report_on_exception code below (which might effect older [maybe not supported Ruby versions])
19
+ # decided to call it explicity here too (to keep the current behavor)
20
+ # anyway, this also short circuit the initialization (no need for the initialization and Thread.new)
21
+ # and only costs us calling the validation twice instead of once
22
+ @core.validate_api_key(api_key)
23
+
24
+ rox_options = if rox_options.nil?
25
+ Rox::Server::RoxOptions.new(disable_signature_verification: @core.is_cbp?(api_key))
26
+ else
27
+ rox_options = rox_options.clone_with_options({
28
+ disable_signature_verification: @core.is_cbp?(api_key) || rox_options.disable_signature_verification
29
+ })
30
+ end
20
31
  sdk_settings = Rox::Server::SdkSettings.new(api_key, rox_options.dev_mode_key)
21
32
  server_properties = Rox::Server::ServerProperties.new(sdk_settings, rox_options)
22
33
 
@@ -93,6 +104,12 @@ module Rox
93
104
  &block))
94
105
  end
95
106
 
107
+ def set_custom_datetime_property(name, value = nil, &block)
108
+ @core.add_custom_property(
109
+ Rox::Core::CustomProperty.new(name, Rox::Core::CustomPropertyType::DATETIME, value, &block)
110
+ )
111
+ end
112
+
96
113
  def dynamic_api
97
114
  @core.dynamic_api(Rox::Server::ServerEntitiesProvider.new)
98
115
  end
@@ -100,14 +117,6 @@ module Rox
100
117
  def dump_state
101
118
  @core.dump_state
102
119
  end
103
-
104
- private
105
-
106
- def validate_api_key(api_key)
107
- raise ArgumentError, 'api_key is not valid' if (api_key =~ /^[a-f\d]{24}$/).nil?
108
-
109
- true
110
- end
111
120
  end
112
121
  end
113
122
  end
data/lib/rox/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rox
2
- VERSION = '5.1.2'.freeze
2
+ VERSION = '6.0.1'.freeze
3
3
  end
data/rox.gemspec CHANGED
@@ -24,10 +24,10 @@ Gem::Specification.new do |spec|
24
24
 
25
25
  spec.add_runtime_dependency 'em-eventsource', '~> 0.3.2'
26
26
 
27
- spec.add_development_dependency 'bundler', '~> 2.4.6'
28
- spec.add_development_dependency 'minitest', '~> 5.11'
29
- spec.add_development_dependency 'pry-byebug', '~> 3.7.0'
30
- spec.add_development_dependency 'rake', '~> 12.3'
31
- spec.add_development_dependency 'rubocop', '~> 1.10'
32
- spec.add_development_dependency 'webmock', '~> 3.7.5'
27
+ spec.add_development_dependency 'bundler', '~> 2.4'
28
+ spec.add_development_dependency 'minitest', '~> 5.20'
29
+ spec.add_development_dependency 'pry-byebug', '~> 3.9'
30
+ spec.add_development_dependency 'rake', '~> 13.0'
31
+ spec.add_development_dependency 'rubocop', '~> 1.50'
32
+ spec.add_development_dependency 'webmock', '~> 3.20'
33
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rox-rollout
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.2
4
+ version: 6.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - CloudBees
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-18 00:00:00.000000000 Z
11
+ date: 2026-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-eventsource
@@ -30,85 +30,85 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 2.4.6
33
+ version: '2.4'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 2.4.6
40
+ version: '2.4'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '5.11'
47
+ version: '5.20'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '5.11'
54
+ version: '5.20'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: pry-byebug
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 3.7.0
61
+ version: '3.9'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 3.7.0
68
+ version: '3.9'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rake
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '12.3'
75
+ version: '13.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '12.3'
82
+ version: '13.0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rubocop
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '1.10'
89
+ version: '1.50'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '1.10'
96
+ version: '1.50'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: webmock
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 3.7.5
103
+ version: '3.20'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 3.7.5
111
- description:
110
+ version: '3.20'
111
+ description:
112
112
  email:
113
113
  - support@rollout.io
114
114
  executables: []
@@ -116,20 +116,22 @@ extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
118
  - ".editorconfig"
119
- - ".github/workflows/e2e_tests.yaml"
120
- - ".github/workflows/unit_tests.yaml"
121
119
  - ".gitignore"
122
120
  - ".rubocop.yml"
123
121
  - Gemfile
122
+ - Jenkinsfile
124
123
  - LICENSE
125
124
  - README.md
126
125
  - Rakefile
127
126
  - bin/console
128
127
  - bin/setup
128
+ - cbci-templates/sdkci-e2e.yml
129
+ - cbci-templates/sdkci-unit.yml
129
130
  - e2e-server/.gitignore
130
131
  - e2e-server/run_server.sh
131
132
  - e2e-server/server.rb
132
133
  - example/local.rb
134
+ - examples/simple_example.rb
133
135
  - lib/rox.rb
134
136
  - lib/rox/core/analytics/backoff_policy.rb
135
137
  - lib/rox/core/analytics/client.rb
@@ -168,6 +170,7 @@ files:
168
170
  - lib/rox/core/error_handling/exception_trigger.rb
169
171
  - lib/rox/core/error_handling/userspace_handler_exception.rb
170
172
  - lib/rox/core/error_handling/userspace_unhandled_error_invoker.rb
173
+ - lib/rox/core/helpers/api_key.rb
171
174
  - lib/rox/core/impression/impression_args.rb
172
175
  - lib/rox/core/impression/impression_invoker.rb
173
176
  - lib/rox/core/impression/models/experiment.rb
@@ -232,7 +235,7 @@ homepage: https://github.com/rollout/rox-ruby
232
235
  licenses:
233
236
  - Nonstandard
234
237
  metadata: {}
235
- post_install_message:
238
+ post_install_message:
236
239
  rdoc_options: []
237
240
  require_paths:
238
241
  - lib
@@ -247,8 +250,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
247
250
  - !ruby/object:Gem::Version
248
251
  version: '0'
249
252
  requirements: []
250
- rubygems_version: 3.0.3.1
251
- signing_key:
253
+ rubygems_version: 3.5.9
254
+ signing_key:
252
255
  specification_version: 4
253
256
  summary: Feature Management ROX Ruby SDK
254
257
  test_files: []
@@ -1,52 +0,0 @@
1
- name: E2E tests
2
-
3
- on:
4
- push:
5
- branches:
6
- - master
7
- pull_request:
8
- branches:
9
- - master
10
-
11
- jobs:
12
- build:
13
-
14
- runs-on: macos-latest
15
-
16
- steps:
17
- - uses: actions/checkout@v3
18
- with:
19
- path: rox-ruby
20
- - name: Checkout e2e tests
21
- uses: actions/checkout@v3
22
- with:
23
- repository: rollout/sdk-end-2-end-tests
24
- ref: master
25
- token: ${{ secrets.E2E_PAT }}
26
- path: sdk-end-2-end-tests
27
- - name: link driver
28
- working-directory: ./sdk-end-2-end-tests/drivers
29
- run: ln -s $GITHUB_WORKSPACE/rox-ruby/e2e-server ruby
30
- - name: build e2e node driver
31
- working-directory: ./sdk-end-2-end-tests/drivers/nodejs
32
- run: |
33
- yarn install --frozen-lockfile
34
- - name: build and run e2e
35
- working-directory: ./sdk-end-2-end-tests
36
- run: |
37
- yarn install --frozen-lockfile
38
- QA_E2E_BEARER=$QA_E2E_BEARER API_HOST=https://api.test.rollout.io CD_API_ENDPOINT=https://api.test.rollout.io/device/get_configuration CD_S3_ENDPOINT=https://rox-conf.test.rollout.io/ SS_API_ENDPOINT=https://api.test.rollout.io/device/update_state_store/ SS_S3_ENDPOINT=https://rox-state.test.rollout.io/ CLIENT_DATA_CACHE_KEY=client_data ANALYTICS_ENDPOINT=https://analytic.test.rollout.io/ NOTIFICATIONS_ENDPOINT=https://push.test.rollout.io/sse SDK_LANG=ruby NODE_ENV=container yarn test:env
39
- env:
40
- QA_E2E_BEARER: ${{ secrets.QA_E2E_BEARER }}
41
- - name: Show e2e server driver logs
42
- if: ${{ always() }}
43
- run: cat ./sdk-end-2-end-tests/drivers/ruby/log_1234.out || echo "no log file"
44
- - name: Show e2e server sync driver logs
45
- if: ${{ always() }}
46
- run: cat ./sdk-end-2-end-tests/drivers/nodejs/log_1233.out || echo "no log file"
47
- - name: Show e2e server init ClientData driver logs
48
- if: ${{ always() }}
49
- run: cat ./sdk-end-2-end-tests/drivers/ruby/log_2234.out || echo "no log file"
50
- - name: Show e2e server init ClientData sync driver logs
51
- if: ${{ always() }}
52
- run: cat ./sdk-end-2-end-tests/drivers/nodejs/log_2233.out || echo "no log file"
@@ -1,20 +0,0 @@
1
- name: Unit Tests
2
-
3
- on: [push]
4
-
5
- jobs:
6
- test:
7
- strategy:
8
- fail-fast: false
9
- matrix:
10
- os: [ubuntu-latest]
11
- ruby: [2.6, 2.7, '3.0', 3.1, 3.2]
12
- runs-on: ${{ matrix.os }}
13
- steps:
14
- - uses: actions/checkout@v3
15
- - uses: ruby/setup-ruby@v1
16
- with:
17
- ruby-version: ${{ matrix.ruby }}
18
- bundler: 2.4.6
19
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
20
- - run: bundle exec rake test