rox-rollout 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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