launchdarkly-server-sdk 7.0.2 → 8.4.2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -4
  3. data/lib/ldclient-rb/config.rb +50 -70
  4. data/lib/ldclient-rb/context.rb +65 -50
  5. data/lib/ldclient-rb/evaluation_detail.rb +5 -1
  6. data/lib/ldclient-rb/events.rb +81 -8
  7. data/lib/ldclient-rb/impl/big_segments.rb +1 -1
  8. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  9. data/lib/ldclient-rb/impl/context.rb +3 -3
  10. data/lib/ldclient-rb/impl/context_filter.rb +30 -9
  11. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  12. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  13. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  14. data/lib/ldclient-rb/impl/evaluation_with_hook_result.rb +34 -0
  15. data/lib/ldclient-rb/impl/event_sender.rb +1 -0
  16. data/lib/ldclient-rb/impl/event_types.rb +61 -3
  17. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +12 -0
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +8 -0
  20. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +16 -3
  21. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +19 -2
  22. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  23. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  24. data/lib/ldclient-rb/impl/model/feature_flag.rb +25 -3
  25. data/lib/ldclient-rb/impl/repeating_task.rb +2 -3
  26. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  27. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  28. data/lib/ldclient-rb/in_memory_store.rb +7 -0
  29. data/lib/ldclient-rb/integrations/file_data.rb +1 -1
  30. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +84 -15
  31. data/lib/ldclient-rb/integrations/test_data.rb +3 -3
  32. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +11 -0
  33. data/lib/ldclient-rb/interfaces.rb +671 -0
  34. data/lib/ldclient-rb/ldclient.rb +313 -22
  35. data/lib/ldclient-rb/migrations.rb +230 -0
  36. data/lib/ldclient-rb/polling.rb +51 -5
  37. data/lib/ldclient-rb/reference.rb +11 -0
  38. data/lib/ldclient-rb/requestor.rb +5 -5
  39. data/lib/ldclient-rb/stream.rb +91 -29
  40. data/lib/ldclient-rb/util.rb +89 -0
  41. data/lib/ldclient-rb/version.rb +1 -1
  42. data/lib/ldclient-rb.rb +1 -0
  43. metadata +44 -6
@@ -0,0 +1,230 @@
1
+ require 'ldclient-rb/impl/migrations/migrator'
2
+
3
+ module LaunchDarkly
4
+ #
5
+ # Namespace for feature-flag based technology migration support.
6
+ #
7
+ module Migrations
8
+ # Symbol representing the old origin, or the old technology source you are migrating away from.
9
+ ORIGIN_OLD = :old
10
+ # Symbol representing the new origin, or the new technology source you are migrating towards.
11
+ ORIGIN_NEW = :new
12
+
13
+ # Symbol defining a read-related operation
14
+ OP_READ = :read
15
+ # Symbol defining a write-related operation
16
+ OP_WRITE = :write
17
+
18
+ STAGE_OFF = :off
19
+ STAGE_DUALWRITE = :dualwrite
20
+ STAGE_SHADOW = :shadow
21
+ STAGE_LIVE = :live
22
+ STAGE_RAMPDOWN = :rampdown
23
+ STAGE_COMPLETE = :complete
24
+
25
+ VALID_OPERATIONS = [
26
+ OP_READ,
27
+ OP_WRITE,
28
+ ]
29
+
30
+ VALID_ORIGINS = [
31
+ ORIGIN_OLD,
32
+ ORIGIN_NEW,
33
+ ]
34
+
35
+ VALID_STAGES = [
36
+ STAGE_OFF,
37
+ STAGE_DUALWRITE,
38
+ STAGE_SHADOW,
39
+ STAGE_LIVE,
40
+ STAGE_RAMPDOWN,
41
+ STAGE_COMPLETE,
42
+ ]
43
+
44
+ #
45
+ # The OperationResult wraps the {LaunchDarkly::Result} class to tie an operation origin to a result.
46
+ #
47
+ class OperationResult
48
+ extend Forwardable
49
+ def_delegators :@result, :value, :error, :exception, :success?
50
+
51
+ #
52
+ # @param origin [Symbol]
53
+ # @param result [LaunchDarkly::Result]
54
+ #
55
+ def initialize(origin, result)
56
+ @origin = origin
57
+ @result = result
58
+ end
59
+
60
+ #
61
+ # @return [Symbol] The origin this result is associated with.
62
+ #
63
+ attr_reader :origin
64
+ end
65
+
66
+ #
67
+ # A write result contains the operation results against both the authoritative and non-authoritative origins.
68
+ #
69
+ # Authoritative writes are always executed first. In the event of a failure, the non-authoritative write will not
70
+ # be executed, resulting in a nil value in the final WriteResult.
71
+ #
72
+ class WriteResult
73
+ #
74
+ # @param authoritative [OperationResult]
75
+ # @param nonauthoritative [OperationResult, nil]
76
+ #
77
+ def initialize(authoritative, nonauthoritative = nil)
78
+ @authoritative = authoritative
79
+ @nonauthoritative = nonauthoritative
80
+ end
81
+
82
+ #
83
+ # Returns the operation result for the authoritative origin.
84
+ #
85
+ # @return [OperationResult]
86
+ #
87
+ attr_reader :authoritative
88
+
89
+ #
90
+ # Returns the operation result for the non-authoritative origin.
91
+ #
92
+ # This result might be nil as the non-authoritative write does not execute in every stage, and will not execute
93
+ # if the authoritative write failed.
94
+ #
95
+ # @return [OperationResult, nil]
96
+ #
97
+ attr_reader :nonauthoritative
98
+ end
99
+
100
+
101
+ #
102
+ # The migration builder is used to configure and construct an instance of a
103
+ # {LaunchDarkly::Interfaces::Migrations::Migrator}. This migrator can be used to perform LaunchDarkly assisted
104
+ # technology migrations through the use of migration-based feature flags.
105
+ #
106
+ class MigratorBuilder
107
+ EXECUTION_SERIAL = :serial
108
+ EXECUTION_RANDOM = :random
109
+ EXECUTION_PARALLEL = :parallel
110
+
111
+ VALID_EXECUTION_ORDERS = [EXECUTION_SERIAL, EXECUTION_RANDOM, EXECUTION_PARALLEL]
112
+ private_constant :VALID_EXECUTION_ORDERS
113
+
114
+ #
115
+ # @param client [LaunchDarkly::LDClient]
116
+ #
117
+ def initialize(client)
118
+ @client = client
119
+
120
+ # Default settings as required by the spec
121
+ @read_execution_order = EXECUTION_PARALLEL
122
+ @measure_latency = true
123
+ @measure_errors = true
124
+
125
+ @read_config = nil # @type [LaunchDarkly::Impl::Migrations::MigrationConfig, nil]
126
+ @write_config = nil # @type [LaunchDarkly::Impl::Migrations::MigrationConfig, nil]
127
+ end
128
+
129
+ #
130
+ # The read execution order influences the parallelism and execution order for read operations involving multiple
131
+ # origins.
132
+ #
133
+ # @param order [Symbol]
134
+ #
135
+ def read_execution_order(order)
136
+ return unless VALID_EXECUTION_ORDERS.include? order
137
+
138
+ @read_execution_order = order
139
+ end
140
+
141
+ #
142
+ # Enable or disable latency tracking for migration operations. This latency information can be sent upstream to
143
+ # LaunchDarkly to enhance migration visibility.
144
+ #
145
+ # @param enabled [Boolean]
146
+ #
147
+ def track_latency(enabled)
148
+ @measure_latency = !!enabled
149
+ end
150
+
151
+ #
152
+ # Enable or disable error tracking for migration operations. This error information can be sent upstream to
153
+ # LaunchDarkly to enhance migration visibility.
154
+ #
155
+ # @param enabled [Boolean]
156
+ #
157
+ def track_errors(enabled)
158
+ @measure_errors = !!enabled
159
+ end
160
+
161
+ #
162
+ # Read can be used to configure the migration-read behavior of the resulting
163
+ # {LaunchDarkly::Interfaces::Migrations::Migrator} instance.
164
+ #
165
+ # Users are required to provide two different read methods -- one to read from the old migration origin, and one
166
+ # to read from the new origin. Additionally, customers can opt-in to consistency tracking by providing a
167
+ # comparison function.
168
+ #
169
+ # Depending on the migration stage, one or both of these read methods may be called.
170
+ #
171
+ # The read methods should accept a single nullable parameter. This parameter is a payload passed through the
172
+ # {LaunchDarkly::Interfaces::Migrations::Migrator#read} method. This method should return a {LaunchDarkly::Result}
173
+ # instance.
174
+ #
175
+ # The consistency method should accept 2 parameters of any type. These parameters are the results of executing the
176
+ # read operation against the old and new origins. If both operations were successful, the consistency method will
177
+ # be invoked. This method should return true if the two parameters are equal, or false otherwise.
178
+ #
179
+ # @param old_read [#call]
180
+ # @param new_read [#call]
181
+ # @param comparison [#call, nil]
182
+ #
183
+ def read(old_read, new_read, comparison = nil)
184
+ return unless old_read.respond_to?(:call) && old_read.arity == 1
185
+ return unless new_read.respond_to?(:call) && new_read.arity == 1
186
+ return unless comparison.nil? || (comparison.respond_to?(:call) && comparison.arity == 2)
187
+
188
+ @read_config = LaunchDarkly::Impl::Migrations::MigrationConfig.new(old_read, new_read, comparison)
189
+ end
190
+
191
+ #
192
+ # Write can be used to configure the migration-write behavior of the resulting
193
+ # {LaunchDarkly::Interfaces::Migrations::Migrator} instance.
194
+ #
195
+ # Users are required to provide two different write methods -- one to write to the old migration origin, and one
196
+ # to write to the new origin.
197
+ #
198
+ # Depending on the migration stage, one or both of these write methods may be called.
199
+ #
200
+ # The write methods should accept a single nullable parameter. This parameter is a payload passed through the
201
+ # {LaunchDarkly::Interfaces::Migrations::Migrator#write} method. This method should return a {LaunchDarkly::Result}
202
+ # instance.
203
+ #
204
+ # @param old_write [#call]
205
+ # @param new_write [#call]
206
+ #
207
+ def write(old_write, new_write)
208
+ return unless old_write.respond_to?(:call) && old_write.arity == 1
209
+ return unless new_write.respond_to?(:call) && new_write.arity == 1
210
+
211
+ @write_config = LaunchDarkly::Impl::Migrations::MigrationConfig.new(old_write, new_write, nil)
212
+ end
213
+
214
+ #
215
+ # Build constructs a {LaunchDarkly::Interfaces::Migrations::Migrator} instance to support migration-based reads
216
+ # and writes. A string describing any failure conditions will be returned if the build fails.
217
+ #
218
+ # @return [LaunchDarkly::Interfaces::Migrations::Migrator, string]
219
+ #
220
+ def build
221
+ return "client not provided" if @client.nil?
222
+ return "read configuration not provided" if @read_config.nil?
223
+ return "write configuration not provided" if @write_config.nil?
224
+
225
+ LaunchDarkly::Impl::Migrations::Migrator.new(@client, @read_execution_order, @read_config, @write_config, @measure_latency, @measure_errors)
226
+ end
227
+ end
228
+
229
+ end
230
+ end
@@ -1,6 +1,7 @@
1
1
  require "ldclient-rb/impl/repeating_task"
2
2
 
3
3
  require "concurrent/atomics"
4
+ require "json"
4
5
  require "thread"
5
6
 
6
7
  module LaunchDarkly
@@ -27,30 +28,75 @@ module LaunchDarkly
27
28
  end
28
29
 
29
30
  def stop
30
- @task.stop
31
- @config.logger.info { "[LDClient] Polling connection stopped" }
31
+ stop_with_error_info
32
32
  end
33
33
 
34
34
  def poll
35
35
  begin
36
36
  all_data = @requestor.request_all_data
37
37
  if all_data
38
- @config.feature_store.init(all_data)
38
+ update_sink_or_data_store.init(all_data)
39
39
  if @initialized.make_true
40
40
  @config.logger.info { "[LDClient] Polling connection initialized" }
41
41
  @ready.set
42
42
  end
43
43
  end
44
+ @config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
45
+ rescue JSON::ParserError => e
46
+ @config.logger.error { "[LDClient] JSON parsing failed for polling response." }
47
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
48
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
49
+ 0,
50
+ e.to_s,
51
+ Time.now
52
+ )
53
+ @config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
44
54
  rescue UnexpectedResponseError => e
55
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
56
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, e.status, nil, Time.now)
45
57
  message = Util.http_error_message(e.status, "polling request", "will retry")
46
58
  @config.logger.error { "[LDClient] #{message}" }
47
- unless Util.http_error_recoverable?(e.status)
59
+
60
+ if Util.http_error_recoverable?(e.status)
61
+ @config.data_source_update_sink&.update_status(
62
+ LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
63
+ error_info
64
+ )
65
+ else
48
66
  @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
49
- stop
67
+ stop_with_error_info error_info
50
68
  end
51
69
  rescue StandardError => e
52
70
  Util.log_exception(@config.logger, "Exception while polling", e)
71
+ @config.data_source_update_sink&.update_status(
72
+ LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
73
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN, 0, e.to_s, Time.now)
74
+ )
53
75
  end
54
76
  end
77
+
78
+ #
79
+ # The original implementation of this class relied on the feature store
80
+ # directly, which we are trying to move away from. Customers who might have
81
+ # instantiated this directly for some reason wouldn't know they have to set
82
+ # the config's sink manually, so we have to fall back to the store if the
83
+ # sink isn't present.
84
+ #
85
+ # The next major release should be able to simplify this structure and
86
+ # remove the need for fall back to the data store because the update sink
87
+ # should always be present.
88
+ #
89
+ private def update_sink_or_data_store
90
+ @config.data_source_update_sink || @config.feature_store
91
+ end
92
+
93
+ #
94
+ # @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
95
+ #
96
+ private def stop_with_error_info(error_info = nil)
97
+ @task.stop
98
+ @config.logger.info { "[LDClient] Polling connection stopped" }
99
+ @config.data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error_info)
100
+ end
55
101
  end
56
102
  end
@@ -109,6 +109,8 @@ module LaunchDarkly
109
109
  end
110
110
  private_class_method :new
111
111
 
112
+ protected attr_reader :components
113
+
112
114
  #
113
115
  # Creates a Reference from a string. For the supported syntax and examples,
114
116
  # see comments on the Reference type.
@@ -227,6 +229,15 @@ module LaunchDarkly
227
229
  @components[index]
228
230
  end
229
231
 
232
+ def ==(other)
233
+ self.error == other.error && self.components == other.components
234
+ end
235
+ alias eql? ==
236
+
237
+ def hash
238
+ ([error] + components).hash
239
+ end
240
+
230
241
  #
231
242
  # Performs unescaping of attribute reference path components:
232
243
  #
@@ -26,6 +26,8 @@ module LaunchDarkly
26
26
  @sdk_key = sdk_key
27
27
  @config = config
28
28
  @http_client = LaunchDarkly::Util.new_http_client(config.base_uri, config)
29
+ .use(:auto_inflate)
30
+ .headers("Accept-Encoding" => "gzip")
29
31
  @cache = @config.cache_store
30
32
  end
31
33
 
@@ -43,12 +45,10 @@ module LaunchDarkly
43
45
 
44
46
  private
45
47
 
46
- def request_single_item(kind, path)
47
- Impl::Model.deserialize(kind, make_request(path), @config.logger)
48
- end
49
-
50
48
  def make_request(path)
51
- uri = URI(@config.base_uri + path)
49
+ uri = URI(
50
+ Util.add_payload_filter_key(@config.base_uri + path, @config)
51
+ )
52
52
  headers = {}
53
53
  Impl::Util.default_http_headers(@sdk_key, @config).each { |k, v| headers[k] = v }
54
54
  headers["Connection"] = "keep-alive"
@@ -25,6 +25,7 @@ module LaunchDarkly
25
25
  def initialize(sdk_key, config, diagnostic_accumulator = nil)
26
26
  @sdk_key = sdk_key
27
27
  @config = config
28
+ @data_source_update_sink = config.data_source_update_sink
28
29
  @feature_store = config.feature_store
29
30
  @initialized = Concurrent::AtomicBoolean.new(false)
30
31
  @started = Concurrent::AtomicBoolean.new(false)
@@ -51,19 +52,40 @@ module LaunchDarkly
51
52
  reconnect_time: @config.initial_reconnect_delay,
52
53
  }
53
54
  log_connection_started
54
- @es = SSE::Client.new(@config.stream_uri + "/all", **opts) do |conn|
55
+
56
+ uri = Util.add_payload_filter_key(@config.stream_uri + "/all", @config)
57
+ @es = SSE::Client.new(uri, **opts) do |conn|
55
58
  conn.on_event { |event| process_message(event) }
56
59
  conn.on_error { |err|
57
60
  log_connection_result(false)
58
61
  case err
59
62
  when SSE::Errors::HTTPStatusError
60
63
  status = err.status
64
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
65
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::ERROR_RESPONSE, status, nil, Time.now)
61
66
  message = Util.http_error_message(status, "streaming connection", "will retry")
62
67
  @config.logger.error { "[LDClient] #{message}" }
63
- unless Util.http_error_recoverable?(status)
68
+
69
+ if Util.http_error_recoverable?(status)
70
+ @data_source_update_sink&.update_status(
71
+ LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
72
+ error_info
73
+ )
74
+ else
64
75
  @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
65
- stop
76
+ stop_with_error_info error_info
66
77
  end
78
+ when SSE::Errors::HTTPContentTypeError, SSE::Errors::HTTPProxyError, SSE::Errors::ReadTimeoutError
79
+ @data_source_update_sink&.update_status(
80
+ LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
81
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::NETWORK_ERROR, 0, err.to_s, Time.now)
82
+ )
83
+
84
+ else
85
+ @data_source_update_sink&.update_status(
86
+ LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
87
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN, 0, err.to_s, Time.now)
88
+ )
67
89
  end
68
90
  }
69
91
  end
@@ -72,46 +94,86 @@ module LaunchDarkly
72
94
  end
73
95
 
74
96
  def stop
97
+ stop_with_error_info
98
+ end
99
+
100
+ private
101
+
102
+ #
103
+ # @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
104
+ #
105
+ def stop_with_error_info(error_info = nil)
75
106
  if @stopped.make_true
76
107
  @es.close
108
+ @data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::OFF, error_info)
77
109
  @config.logger.info { "[LDClient] Stream connection stopped" }
78
110
  end
79
111
  end
80
112
 
81
- private
113
+ #
114
+ # The original implementation of this class relied on the feature store
115
+ # directly, which we are trying to move away from. Customers who might have
116
+ # instantiated this directly for some reason wouldn't know they have to set
117
+ # the config's sink manually, so we have to fall back to the store if the
118
+ # sink isn't present.
119
+ #
120
+ # The next major release should be able to simplify this structure and
121
+ # remove the need for fall back to the data store because the update sink
122
+ # should always be present.
123
+ #
124
+ def update_sink_or_data_store
125
+ @data_source_update_sink || @feature_store
126
+ end
82
127
 
83
128
  def process_message(message)
84
129
  log_connection_result(true)
85
130
  method = message.type
86
131
  @config.logger.debug { "[LDClient] Stream received #{method} message: #{message.data}" }
87
- if method == PUT
88
- message = JSON.parse(message.data, symbolize_names: true)
89
- all_data = Impl::Model.make_all_store_data(message[:data], @config.logger)
90
- @feature_store.init(all_data)
91
- @initialized.make_true
92
- @config.logger.info { "[LDClient] Stream initialized" }
93
- @ready.set
94
- elsif method == PATCH
95
- data = JSON.parse(message.data, symbolize_names: true)
96
- for kind in [FEATURES, SEGMENTS]
97
- key = key_for_path(kind, data[:path])
98
- if key
99
- item = Impl::Model.deserialize(kind, data[:data], @config.logger)
100
- @feature_store.upsert(kind, item)
101
- break
132
+
133
+ begin
134
+ if method == PUT
135
+ message = JSON.parse(message.data, symbolize_names: true)
136
+ all_data = Impl::Model.make_all_store_data(message[:data], @config.logger)
137
+ update_sink_or_data_store.init(all_data)
138
+ @initialized.make_true
139
+ @config.logger.info { "[LDClient] Stream initialized" }
140
+ @ready.set
141
+ elsif method == PATCH
142
+ data = JSON.parse(message.data, symbolize_names: true)
143
+ for kind in [FEATURES, SEGMENTS]
144
+ key = key_for_path(kind, data[:path])
145
+ if key
146
+ item = Impl::Model.deserialize(kind, data[:data], @config.logger)
147
+ update_sink_or_data_store.upsert(kind, item)
148
+ break
149
+ end
102
150
  end
103
- end
104
- elsif method == DELETE
105
- data = JSON.parse(message.data, symbolize_names: true)
106
- for kind in [FEATURES, SEGMENTS]
107
- key = key_for_path(kind, data[:path])
108
- if key
109
- @feature_store.delete(kind, key, data[:version])
110
- break
151
+ elsif method == DELETE
152
+ data = JSON.parse(message.data, symbolize_names: true)
153
+ for kind in [FEATURES, SEGMENTS]
154
+ key = key_for_path(kind, data[:path])
155
+ if key
156
+ update_sink_or_data_store.delete(kind, key, data[:version])
157
+ break
158
+ end
111
159
  end
160
+ else
161
+ @config.logger.warn { "[LDClient] Unknown message received: #{method}" }
112
162
  end
113
- else
114
- @config.logger.warn { "[LDClient] Unknown message received: #{method}" }
163
+
164
+ @data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
165
+ rescue JSON::ParserError => e
166
+ @config.logger.error { "[LDClient] JSON parsing failed for method #{method}. Ignoring event." }
167
+ error_info = LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
168
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA,
169
+ 0,
170
+ e.to_s,
171
+ Time.now
172
+ )
173
+ @data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED, error_info)
174
+
175
+ # Re-raise the exception so the SSE implementation can catch it and restart the stream.
176
+ raise
115
177
  end
116
178
  end
117
179
 
@@ -2,8 +2,97 @@ require "uri"
2
2
  require "http"
3
3
 
4
4
  module LaunchDarkly
5
+ #
6
+ # A Result is used to reflect the outcome of any operation.
7
+ #
8
+ # Results can either be considered a success or a failure.
9
+ #
10
+ # In the event of success, the Result will contain an option, nullable value to hold any success value back to the
11
+ # calling function.
12
+ #
13
+ # If the operation fails, the Result will contain an error describing the value.
14
+ #
15
+ class Result
16
+ #
17
+ # Create a successful result with the provided value.
18
+ #
19
+ # @param value [Object, nil]
20
+ # @return [Result]
21
+ #
22
+ def self.success(value)
23
+ Result.new(value)
24
+ end
25
+
26
+ #
27
+ # Create a failed result with the provided error description.
28
+ #
29
+ # @param error [String]
30
+ # @param exception [Exception, nil]
31
+ # @return [Result]
32
+ #
33
+ def self.fail(error, exception = nil)
34
+ Result.new(nil, error, exception)
35
+ end
36
+
37
+ #
38
+ # Was this result successful or did it encounter an error?
39
+ #
40
+ # @return [Boolean]
41
+ #
42
+ def success?
43
+ @error.nil?
44
+ end
45
+
46
+ #
47
+ # @return [Object, nil] The value returned from the operation if it was successful; nil otherwise.
48
+ #
49
+ attr_reader :value
50
+
51
+ #
52
+ # @return [String, nil] An error description of the failure; nil otherwise
53
+ #
54
+ attr_reader :error
55
+
56
+ #
57
+ # @return [Exception, nil] An optional exception which caused the failure
58
+ #
59
+ attr_reader :exception
60
+
61
+ private def initialize(value, error = nil, exception = nil)
62
+ @value = value
63
+ @error = error
64
+ @exception = exception
65
+ end
66
+ end
67
+
5
68
  # @private
6
69
  module Util
70
+ #
71
+ # Append the payload filter key query parameter to the provided URI.
72
+ #
73
+ # @param uri [String]
74
+ # @param config [Config]
75
+ # @return [String]
76
+ #
77
+ def self.add_payload_filter_key(uri, config)
78
+ return uri if config.payload_filter_key.nil?
79
+
80
+ unless config.payload_filter_key.is_a?(String) && !config.payload_filter_key.empty?
81
+ config.logger.warn { "[LDClient] Filter key must be a non-empty string. No filtering will be applied." }
82
+ return uri
83
+ end
84
+
85
+ begin
86
+ parsed = URI.parse(uri)
87
+ new_query_params = URI.decode_www_form(String(parsed.query)) << ["filter", config.payload_filter_key]
88
+ parsed.query = URI.encode_www_form(new_query_params)
89
+ parsed.to_s
90
+ rescue URI::InvalidURIError
91
+ config.logger.warn { "[LDClient] URI could not be parsed. No filtering will be applied." }
92
+ uri
93
+ end
94
+ end
95
+
7
96
  def self.new_http_client(uri_s, config)
8
97
  http_client_options = {}
9
98
  if config.socket_factory
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "7.0.2"
2
+ VERSION = "8.4.2" # x-release-please-version
3
3
  end
data/lib/ldclient-rb.rb CHANGED
@@ -9,6 +9,7 @@ require "ldclient-rb/version"
9
9
  require "ldclient-rb/interfaces"
10
10
  require "ldclient-rb/util"
11
11
  require "ldclient-rb/flags_state"
12
+ require "ldclient-rb/migrations"
12
13
  require "ldclient-rb/ldclient"
13
14
  require "ldclient-rb/cache_store"
14
15
  require "ldclient-rb/expiring_cache"