rox-rollout 0.1.3

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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +45 -0
  3. data/.gitignore +8 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE +50 -0
  6. data/README_DEVELOP.md +19 -0
  7. data/Rakefile +16 -0
  8. data/_archive/.document +5 -0
  9. data/_archive/.rspec +1 -0
  10. data/_archive/Gemfile +15 -0
  11. data/_archive/Gemfile.lock +87 -0
  12. data/_archive/README.md +32 -0
  13. data/_archive/README.rdoc +19 -0
  14. data/_archive/Rakefile +50 -0
  15. data/_archive/lib/expr_function_definition.rb +52 -0
  16. data/_archive/lib/function_definition.rb +48 -0
  17. data/_archive/lib/function_token.rb +12 -0
  18. data/_archive/lib/object_extends.rb +12 -0
  19. data/_archive/lib/ruby_interpreter.rb +292 -0
  20. data/_archive/lib/stack.rb +48 -0
  21. data/_archive/lib/string_extends.rb +14 -0
  22. data/_archive/spec/ruby_interpreter_spec.rb +203 -0
  23. data/_archive/spec/spec_helper.rb +30 -0
  24. data/_archive/spec/stack_spec.rb +77 -0
  25. data/bin/console +14 -0
  26. data/bin/setup +8 -0
  27. data/e2e/container.rb +38 -0
  28. data/e2e/custom_props.rb +55 -0
  29. data/e2e/rox_e2e_test.rb +159 -0
  30. data/e2e/test_vars.rb +24 -0
  31. data/lib/rox.rb +5 -0
  32. data/lib/rox/core/client/buid.rb +82 -0
  33. data/lib/rox/core/client/device_properties.rb +45 -0
  34. data/lib/rox/core/client/internal_flags.rb +20 -0
  35. data/lib/rox/core/client/sdk_settings.rb +5 -0
  36. data/lib/rox/core/configuration/configuration.rb +5 -0
  37. data/lib/rox/core/configuration/configuration_fetched_args.rb +23 -0
  38. data/lib/rox/core/configuration/configuration_fetched_invoker.rb +37 -0
  39. data/lib/rox/core/configuration/configuration_parser.rb +85 -0
  40. data/lib/rox/core/configuration/fetcher_error.rb +13 -0
  41. data/lib/rox/core/configuration/fetcher_status.rb +10 -0
  42. data/lib/rox/core/configuration/models/experiment_model.rb +5 -0
  43. data/lib/rox/core/configuration/models/target_group_model.rb +5 -0
  44. data/lib/rox/core/consts/build.rb +8 -0
  45. data/lib/rox/core/consts/environment.rb +42 -0
  46. data/lib/rox/core/consts/property_type.rb +29 -0
  47. data/lib/rox/core/context/merged_context.rb +16 -0
  48. data/lib/rox/core/core.rb +131 -0
  49. data/lib/rox/core/entities/flag.rb +26 -0
  50. data/lib/rox/core/entities/flag_setter.rb +39 -0
  51. data/lib/rox/core/entities/variant.rb +56 -0
  52. data/lib/rox/core/impression/impression_args.rb +5 -0
  53. data/lib/rox/core/impression/impression_invoker.rb +41 -0
  54. data/lib/rox/core/impression/models/experiment.rb +14 -0
  55. data/lib/rox/core/impression/models/reporting_value.rb +5 -0
  56. data/lib/rox/core/logging/logging.rb +17 -0
  57. data/lib/rox/core/logging/no_op_logger.rb +11 -0
  58. data/lib/rox/core/network/configuration_fetch_result.rb +5 -0
  59. data/lib/rox/core/network/configuration_fetcher.rb +38 -0
  60. data/lib/rox/core/network/configuration_fetcher_base.rb +25 -0
  61. data/lib/rox/core/network/configuration_fetcher_roxy.rb +29 -0
  62. data/lib/rox/core/network/configuration_source.rb +9 -0
  63. data/lib/rox/core/network/request.rb +46 -0
  64. data/lib/rox/core/network/request_configuration_builder.rb +48 -0
  65. data/lib/rox/core/network/request_data.rb +5 -0
  66. data/lib/rox/core/network/response.rb +16 -0
  67. data/lib/rox/core/properties/custom_property.rb +18 -0
  68. data/lib/rox/core/properties/custom_property_type.rb +18 -0
  69. data/lib/rox/core/properties/device_property.rb +11 -0
  70. data/lib/rox/core/register/registerer.rb +35 -0
  71. data/lib/rox/core/reporting/error_reporter.rb +152 -0
  72. data/lib/rox/core/repositories/custom_property_repository.rb +61 -0
  73. data/lib/rox/core/repositories/experiment_repository.rb +21 -0
  74. data/lib/rox/core/repositories/flag_repository.rb +49 -0
  75. data/lib/rox/core/repositories/roxx/experiments_extensions.rb +82 -0
  76. data/lib/rox/core/repositories/roxx/properties_extensions.rb +26 -0
  77. data/lib/rox/core/repositories/target_group_repository.rb +17 -0
  78. data/lib/rox/core/roxx/core_stack.rb +22 -0
  79. data/lib/rox/core/roxx/evaluation_result.rb +28 -0
  80. data/lib/rox/core/roxx/node.rb +11 -0
  81. data/lib/rox/core/roxx/parser.rb +143 -0
  82. data/lib/rox/core/roxx/regular_expression_extensions.rb +33 -0
  83. data/lib/rox/core/roxx/string_tokenizer.rb +68 -0
  84. data/lib/rox/core/roxx/symbols.rb +14 -0
  85. data/lib/rox/core/roxx/token_type.rb +30 -0
  86. data/lib/rox/core/roxx/tokenized_expression.rb +119 -0
  87. data/lib/rox/core/roxx/value_compare_extensions.rb +137 -0
  88. data/lib/rox/core/security/signature_verifier.rb +18 -0
  89. data/lib/rox/core/utils/periodic_task.rb +12 -0
  90. data/lib/rox/core/utils/type_utils.rb +9 -0
  91. data/lib/rox/server/client/sdk_settings.rb +5 -0
  92. data/lib/rox/server/client/server_properties.rb +20 -0
  93. data/lib/rox/server/flags/rox_flag.rb +19 -0
  94. data/lib/rox/server/flags/rox_variant.rb +8 -0
  95. data/lib/rox/server/logging/server_logger.rb +35 -0
  96. data/lib/rox/server/rox_options.rb +27 -0
  97. data/lib/rox/server/rox_server.rb +83 -0
  98. data/lib/rox/version.rb +3 -0
  99. data/rox.gemspec +29 -0
  100. metadata +184 -0
@@ -0,0 +1,16 @@
1
+ module Rox
2
+ module Core
3
+ class Response
4
+ attr_accessor :status_code, :text
5
+
6
+ def initialize(status_code, text)
7
+ @status_code = status_code
8
+ @text = text
9
+ end
10
+
11
+ def success_status_code?
12
+ @status_code >= 200 && @status_code < 300
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ module Rox
2
+ module Core
3
+ class CustomProperty
4
+ attr_accessor :name, :type
5
+
6
+ def initialize(name, type, value = nil, &block)
7
+ @name = name
8
+ @type = type
9
+ @value = value
10
+ @block = block
11
+ end
12
+
13
+ def value(context)
14
+ @block.nil? ? @value : @block.call(context)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Rox
2
+ module Core
3
+ class CustomPropertyType
4
+ attr_accessor :type, :external_type
5
+
6
+ def initialize(type, external_type)
7
+ @type = type
8
+ @external_type = external_type
9
+ end
10
+
11
+ STRING = CustomPropertyType.new('string', 'String')
12
+ BOOL = CustomPropertyType.new('bool', 'Boolean')
13
+ INT = CustomPropertyType.new('int', 'Number')
14
+ FLOAT = CustomPropertyType.new('double', 'Number')
15
+ SEMVER = CustomPropertyType.new('semver', 'Semver')
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ require 'rox/core/properties/custom_property'
2
+
3
+ module Rox
4
+ module Core
5
+ class DeviceProperty < CustomProperty
6
+ def initialize(name, type, value = nil)
7
+ super('rox.' + name, type, value)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ module Rox
2
+ module Core
3
+ class Registerer
4
+ def initialize(flag_repository)
5
+ @flag_repository = flag_repository
6
+ @namespaces = []
7
+ @mutex = Mutex.new
8
+ end
9
+
10
+ def register_instance(container, ns)
11
+ raise ArgumentError, 'A namespace cannot be null' if ns.nil?
12
+
13
+ @mutex.synchronize do
14
+ raise ArgumentError, "A container with the given namespace (#{ns}) has already been registered" if @namespaces.include?(ns)
15
+ end
16
+
17
+ @namespaces << ns
18
+
19
+ container.methods.each do |method_name|
20
+ method = container.method(method_name)
21
+ next unless method.arity.zero?
22
+
23
+ begin
24
+ value = method.call
25
+ if value.is_a?(Variant)
26
+ @flag_repository.add_flag(value, ns == '' ? method_name.to_s : "#{ns}.#{method_name}")
27
+ end
28
+ rescue StandardError
29
+ next
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,152 @@
1
+ require 'rox/core/logging/logging'
2
+
3
+ module Rox
4
+ module Core
5
+ class ErrorReporter
6
+ BUGSNAG_NOTIFY_URL = 'https://notify.bugsnag.com'.freeze
7
+
8
+ def initialize(request, device_properties, buid)
9
+ @request = request
10
+ @device_properties = device_properties
11
+ @buid = buid
12
+ end
13
+
14
+ def report(message, ex)
15
+ return if @device_properties.rollout_environment == 'LOCAL'
16
+
17
+ Logging.logger.error("Error report: #{message}", ex)
18
+
19
+ begin
20
+ stack_trace = ex.nil? ? caller : ex.backtrace
21
+ stack = parse_stack_trace(stack_trace)
22
+ payload = create_payload(message, ex, stack)
23
+ rescue StandardError => e
24
+ Logging.logger.error('failed to create bugsnag json payload of the error', e)
25
+ else
26
+ Thread.new { send_error(payload) }
27
+ end
28
+ end
29
+
30
+ def send_error(payload)
31
+ Logging.logger.debug('Sending bugsnag error report...')
32
+
33
+ begin
34
+ @request.send_post(ErrorReporter::BUGSNAG_NOTIFY_URL, payload)
35
+ Logging.logger.debug('Bugsnag error report was sent')
36
+ rescue StandardError => ex
37
+ Logging.logger.error('Failed to send bugsnag error ', ex)
38
+ end
39
+ end
40
+
41
+ def create_payload(message, ex, stack)
42
+ payload = {}
43
+ add_api_key(payload)
44
+ add_notifier(payload)
45
+ add_events(message, ex, stack, payload)
46
+
47
+ payload
48
+ end
49
+
50
+ def add_metadata(message, ev)
51
+ inner_data = {
52
+ 'message' => message,
53
+ 'deviceId' => @device_properties.distinct_id,
54
+ 'buid' => @buid.to_s
55
+ }
56
+
57
+ metadata = {
58
+ 'data' => inner_data
59
+ }
60
+
61
+ ev['metaData'] = metadata
62
+ end
63
+
64
+ def add_api_key(payload)
65
+ payload['apiKey'] = '9569ec14f61546c6aa2a97856492bf4d'
66
+ end
67
+
68
+ def add_events(message, ex, stack, payload)
69
+ evs = []
70
+ add_event(message, ex, stack, evs)
71
+ payload['events'] = evs
72
+ end
73
+
74
+ def add_event(message, ex, stack, events)
75
+ ev = {}
76
+ add_payload_version(ev)
77
+ add_exceptions(message, ex, stack, ev)
78
+ add_user('id', @device_properties.rollout_key, ev)
79
+ add_metadata(message, ev)
80
+ add_app(ev)
81
+
82
+ events << ev
83
+ end
84
+
85
+ def add_payload_version(ev)
86
+ ev['payloadVersion'] = 2
87
+ end
88
+
89
+ def add_notifier(payload)
90
+ notifier = {
91
+ 'name' => 'Rollout Ruby SDK',
92
+ 'version' => @device_properties.lib_version
93
+ }
94
+ payload['notifier'] = notifier
95
+ end
96
+
97
+ def add_user(id, rollout_key, ev)
98
+ user = {
99
+ id => rollout_key
100
+ }
101
+ ev['user'] = user
102
+ end
103
+
104
+ def add_exceptions(message, ex, stack, ev)
105
+ exceptions = []
106
+ exception = {}
107
+
108
+ if ex.nil?
109
+ exception['errorClass'] = message
110
+ exception['message'] = message
111
+ exception['stacktrace'] = []
112
+ else
113
+ exception['errorClass'] = ex.message
114
+ exception['message'] = ex.message
115
+ exception['stacktrace'] = stack
116
+ end
117
+
118
+ exceptions.append(exception)
119
+ ev['exceptions'] = exceptions
120
+ end
121
+
122
+ def add_app(ev)
123
+ app = {
124
+ 'releaseStage' => @device_properties.rollout_environment,
125
+ 'version' => @device_properties.lib_version
126
+ }
127
+ ev['app'] = app
128
+ end
129
+
130
+ STACK_TRACE_LINE_REGEX = /^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$/
131
+
132
+ def parse_stack_trace(stack_trace)
133
+ stack = []
134
+ stack_trace.each do |line|
135
+ match = line.match(STACK_TRACE_LINE_REGEX)
136
+ next if match.nil?
137
+
138
+ file = match[1]
139
+ line_str = match[2]
140
+ func = match[3]
141
+ stack << {
142
+ 'file' => file,
143
+ 'method' => func,
144
+ 'lineNumber' => line_str.to_i,
145
+ 'columnNumber' => 0
146
+ }
147
+ end
148
+ stack
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,61 @@
1
+ module Rox
2
+ module Core
3
+ class CustomPropertyRepository
4
+ def initialize
5
+ @custom_properties = {}
6
+ @property_added_handlers = []
7
+ @mutex = Mutex.new
8
+ @handlers_mutex = Mutex.new
9
+ end
10
+
11
+ def add_custom_property(custom_property)
12
+ return if custom_property.name.nil? || custom_property.name.empty?
13
+
14
+ @mutex.synchronize do
15
+ @custom_properties[custom_property.name] = custom_property
16
+ end
17
+
18
+ raise_property_added_event(custom_property)
19
+ end
20
+
21
+ def add_custom_property_if_not_exists(custom_property)
22
+ return if custom_property.name.nil? || custom_property.name.empty?
23
+
24
+ @mutex.synchronize do
25
+ return if @custom_properties.include?(custom_property.name)
26
+ end
27
+
28
+ add_custom_property(custom_property)
29
+ end
30
+
31
+ def custom_property(name)
32
+ @mutex.synchronize do
33
+ return @custom_properties[name]
34
+ end
35
+ end
36
+
37
+ def all_custom_properties
38
+ @mutex.synchronize do
39
+ return @custom_properties.values
40
+ end
41
+ end
42
+
43
+ def register_property_added_handler(&block)
44
+ @handlers_mutex.synchronize do
45
+ @property_added_handlers << block
46
+ end
47
+ end
48
+
49
+ def raise_property_added_event(custom_property)
50
+ handlers = []
51
+ @handlers_mutex.synchronize do
52
+ handlers = @property_added_handlers.clone
53
+ end
54
+
55
+ handlers.each do |handler|
56
+ handler.call(custom_property)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ module Rox
2
+ module Core
3
+ class ExperimentRepository
4
+ def initialize
5
+ @experiments = []
6
+ end
7
+
8
+ def experiments=(experiments)
9
+ @experiments = experiments
10
+ end
11
+
12
+ def experiment_by_flag(flag_name)
13
+ @experiments.detect { |e| e.flags.include?(flag_name) }
14
+ end
15
+
16
+ def all_experiments
17
+ @experiments
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,49 @@
1
+ module Rox
2
+ module Core
3
+ class FlagRepository
4
+ def initialize
5
+ @variants = {}
6
+ @flag_added_handlers = []
7
+ @mutex = Mutex.new
8
+ @handlers_mutex = Mutex.new
9
+ end
10
+
11
+ def add_flag(variant, name)
12
+ variant.name = name if variant.name.nil? || variant.name.empty?
13
+ @mutex.synchronize do
14
+ @variants[name] = variant
15
+ end
16
+ raise_flag_added_event(variant)
17
+ end
18
+
19
+ def flag(name)
20
+ @mutex.synchronize do
21
+ return @variants[name]
22
+ end
23
+ end
24
+
25
+ def all_flags
26
+ @mutex.synchronize do
27
+ return @variants.values
28
+ end
29
+ end
30
+
31
+ def register_flag_added_handler(&block)
32
+ @handlers_mutex.synchronize do
33
+ @flag_added_handlers << block
34
+ end
35
+ end
36
+
37
+ def raise_flag_added_event(flag)
38
+ handlers = []
39
+ @handlers_mutex.synchronize do
40
+ handlers = @flag_added_handlers.clone
41
+ end
42
+
43
+ handlers.each do |handler|
44
+ handler.call(flag)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,82 @@
1
+ require 'digest'
2
+
3
+ module Rox
4
+ module Core
5
+ class ExperimentsExtensions
6
+ def initialize(parser, target_groups_repository, flags_repository, experiment_repository)
7
+ @parser = parser
8
+ @target_groups_repository = target_groups_repository
9
+ @flags_repository = flags_repository
10
+ @experiment_repository = experiment_repository
11
+ end
12
+
13
+ def extend
14
+ @parser.add_operator('mergeSeed') do |parser, stack, context|
15
+ seed1 = stack.pop
16
+ seed2 = stack.pop
17
+ stack.push("#{seed1}.#{seed2}")
18
+ end
19
+
20
+ @parser.add_operator('isInPercentage') do |parser, stack, context|
21
+ percentage = stack.pop
22
+ seed = stack.pop
23
+
24
+ percentage = percentage.is_a?(Numeric) ? percentage : percentage.to_s.to_f
25
+
26
+ bucket = bucket(seed)
27
+ stack.push(bucket <= percentage)
28
+ end
29
+
30
+ @parser.add_operator('isInPercentageRange') do |parser, stack, context|
31
+ percentage_low = stack.pop
32
+ percentage_high = stack.pop
33
+ seed = stack.pop
34
+
35
+ percentage_low = percentage_low.is_a?(Numeric) ? percentage_low : percentage_low.to_s.to_f
36
+ percentage_high = percentage_high.is_a?(Numeric) ? percentage_high : percentage_high.to_s.to_f
37
+
38
+ bucket = bucket(seed)
39
+ stack.push(percentage_low <= bucket && bucket < percentage_high)
40
+ end
41
+
42
+ @parser.add_operator('flagValue') do |parser, stack, context|
43
+ feature_flag_identifier = stack.pop
44
+ result = Flag::FLAG_FALSE_VALUE
45
+ variant = @flags_repository.flag(feature_flag_identifier)
46
+
47
+ if !variant.nil?
48
+ result = variant.value(context)
49
+ else
50
+ flags_experiment = @experiment_repository.experiment_by_flag(feature_flag_identifier)
51
+
52
+ if !flags_experiment.nil? && !flags_experiment.condition.nil? && !flags_experiment.condition.empty?
53
+ experiment_eval_result = parser.evaluate_expression(flags_experiment.condition, context).string_value
54
+ result = experiment_eval_result if !experiment_eval_result.nil? && !experiment_eval_result.empty?
55
+ end
56
+ end
57
+
58
+ stack.push(result)
59
+ end
60
+
61
+ @parser.add_operator('isInTargetGroup') do |parser, stack, context|
62
+ target_group_identifier = stack.pop
63
+ target_group = @target_groups_repository.target_group(target_group_identifier)
64
+ if target_group.nil?
65
+ stack.push(false)
66
+ else
67
+ stack.push(parser.evaluate_expression(target_group.condition, context).bool_value)
68
+ end
69
+ end
70
+ end
71
+
72
+ def bucket(seed)
73
+ bytes = Digest::MD5.digest(seed).bytes
74
+ hash = (bytes[0] & 0xFF) | ((bytes[1] & 0xFF) << 8) | ((bytes[2] & 0xFF) << 16) | ((bytes[3] & 0xFF) << 24)
75
+ hash &= 0xFFFFFFFF
76
+ bucket = hash * 1.0 / (2**32 - 1)
77
+
78
+ bucket == 1 ? 0 : bucket
79
+ end
80
+ end
81
+ end
82
+ end