txlemetry-ruby 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +48 -0
  3. data/NOTICE +5 -0
  4. data/bin/txlemetry +110 -0
  5. data/lib/txlemetry/backoff_policy.rb +49 -0
  6. data/lib/txlemetry/client.rb +1266 -0
  7. data/lib/txlemetry/defaults.rb +50 -0
  8. data/lib/txlemetry/exception_capture.rb +246 -0
  9. data/lib/txlemetry/feature_flag.rb +85 -0
  10. data/lib/txlemetry/feature_flag_error.rb +37 -0
  11. data/lib/txlemetry/feature_flag_evaluations.rb +237 -0
  12. data/lib/txlemetry/feature_flag_result.rb +81 -0
  13. data/lib/txlemetry/feature_flags.rb +1516 -0
  14. data/lib/txlemetry/field_parser.rb +219 -0
  15. data/lib/txlemetry/flag_definition_cache.rb +88 -0
  16. data/lib/txlemetry/internal/context.rb +119 -0
  17. data/lib/txlemetry/logging.rb +77 -0
  18. data/lib/txlemetry/mcp/README.md +53 -0
  19. data/lib/txlemetry/mcp/analytics.rb +116 -0
  20. data/lib/txlemetry/mcp/client.rb +257 -0
  21. data/lib/txlemetry/mcp/constants.rb +111 -0
  22. data/lib/txlemetry/mcp/conversation_id.rb +89 -0
  23. data/lib/txlemetry/mcp/event_builder.rb +168 -0
  24. data/lib/txlemetry/mcp/exceptions.rb +112 -0
  25. data/lib/txlemetry/mcp/identity.rb +194 -0
  26. data/lib/txlemetry/mcp/ids.rb +56 -0
  27. data/lib/txlemetry/mcp/instrumentation.rb +736 -0
  28. data/lib/txlemetry/mcp/intent.rb +101 -0
  29. data/lib/txlemetry/mcp/log.rb +37 -0
  30. data/lib/txlemetry/mcp/options.rb +128 -0
  31. data/lib/txlemetry/mcp/rack_middleware.rb +112 -0
  32. data/lib/txlemetry/mcp/request_scope.rb +72 -0
  33. data/lib/txlemetry/mcp/sanitization.rb +504 -0
  34. data/lib/txlemetry/mcp/schema_mutation.rb +179 -0
  35. data/lib/txlemetry/mcp/server_extension.rb +54 -0
  36. data/lib/txlemetry/mcp/session.rb +71 -0
  37. data/lib/txlemetry/mcp/session_token.rb +106 -0
  38. data/lib/txlemetry/mcp/sink.rb +98 -0
  39. data/lib/txlemetry/mcp/tools.rb +91 -0
  40. data/lib/txlemetry/mcp/tracking_data.rb +89 -0
  41. data/lib/txlemetry/mcp/truncation.rb +326 -0
  42. data/lib/txlemetry/mcp.rb +224 -0
  43. data/lib/txlemetry/message_batch.rb +80 -0
  44. data/lib/txlemetry/noop_worker.rb +39 -0
  45. data/lib/txlemetry/response.rb +17 -0
  46. data/lib/txlemetry/send_feature_flags_options.rb +49 -0
  47. data/lib/txlemetry/send_worker.rb +218 -0
  48. data/lib/txlemetry/transport.rb +216 -0
  49. data/lib/txlemetry/utils.rb +183 -0
  50. data/lib/txlemetry/version.rb +5 -0
  51. data/lib/txlemetry.rb +16 -0
  52. metadata +105 -0
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'txlemetry/utils'
4
+
5
+ module Txlemetry
6
+ # Options for configuring deprecated feature flag behavior in capture calls.
7
+ #
8
+ # @deprecated Prefer passing a {Txlemetry::FeatureFlagEvaluations} snapshot to `capture(flags:)`.
9
+ class SendFeatureFlagsOptions
10
+ # @return [Boolean, nil] Whether remote feature flag evaluation should be skipped.
11
+ attr_reader :only_evaluate_locally
12
+
13
+ # @return [Hash] Person properties to use for feature flag evaluation.
14
+ attr_reader :person_properties
15
+
16
+ # @return [Hash] Group properties to use for feature flag evaluation.
17
+ attr_reader :group_properties
18
+
19
+ # @param only_evaluate_locally [Boolean, nil] Skip remote feature flag evaluation.
20
+ # @param person_properties [Hash, nil] Person properties to use for feature flag evaluation.
21
+ # @param group_properties [Hash, nil] Group properties to use for feature flag evaluation.
22
+ def initialize(only_evaluate_locally: nil, person_properties: nil, group_properties: nil)
23
+ @only_evaluate_locally = only_evaluate_locally
24
+ @person_properties = person_properties || {}
25
+ @group_properties = group_properties || {}
26
+ end
27
+
28
+ # @return [Hash] A hash representation suitable for `capture(send_feature_flags:)`.
29
+ def to_h
30
+ {
31
+ only_evaluate_locally: @only_evaluate_locally,
32
+ person_properties: @person_properties,
33
+ group_properties: @group_properties
34
+ }
35
+ end
36
+
37
+ # @param hash [Hash]
38
+ # @return [Txlemetry::SendFeatureFlagsOptions, nil]
39
+ def self.from_hash(hash)
40
+ return nil unless hash.is_a?(Hash)
41
+
42
+ new(
43
+ only_evaluate_locally: Txlemetry::Utils.get_by_symbol_or_string_key(hash, :only_evaluate_locally),
44
+ person_properties: Txlemetry::Utils.get_by_symbol_or_string_key(hash, :person_properties),
45
+ group_properties: Txlemetry::Utils.get_by_symbol_or_string_key(hash, :group_properties)
46
+ )
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'txlemetry/defaults'
4
+ require 'txlemetry/message_batch'
5
+ require 'txlemetry/transport'
6
+ require 'txlemetry/utils'
7
+
8
+ module Txlemetry
9
+ # Background worker that batches and sends queued events.
10
+ #
11
+ # @api private
12
+ class SendWorker
13
+ include Txlemetry::Utils
14
+ include Txlemetry::Defaults
15
+ include Txlemetry::Logging
16
+
17
+ # public: Creates a new worker
18
+ #
19
+ # The worker continuously takes messages off the queue
20
+ # and makes requests to the txlemetry.com api
21
+ #
22
+ # @param queue [Queue] Queue synchronized between client and worker.
23
+ # @param api_key [String] Project API key.
24
+ # @param options [Hash] Worker options.
25
+ # @option options [Integer] :batch_size How many items to send in a batch.
26
+ # @option options [Numeric] :flush_interval_seconds Maximum seconds to wait for a batch to fill before sending.
27
+ # @option options [Proc] :on_error Callback invoked as `on_error.call(status, error)`.
28
+ # @option options [String] :host Txlemetry API host URL.
29
+ # @option options [Boolean] :skip_ssl_verification Disable SSL certificate verification.
30
+ # @option options [Boolean] :compress_request Set to +false+ to disable gzip batch request bodies.
31
+ def initialize(queue, api_key, options = {})
32
+ symbolize_keys! options
33
+ @queue = queue
34
+ @api_key = api_key
35
+ @on_error = options[:on_error] || proc { |status, error| }
36
+ batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE
37
+ flush_interval_seconds = options[:flush_interval_seconds] || Defaults::MessageBatch::FLUSH_INTERVAL_SECONDS
38
+ @flush_interval_seconds = flush_interval_seconds.to_f
39
+ @batch = MessageBatch.new(batch_size)
40
+ @lock = Mutex.new
41
+ @state_lock = Mutex.new
42
+ @condition = ConditionVariable.new
43
+ @flush_requested = false
44
+ @shutdown = false
45
+ @pid = Process.pid
46
+ @transport_options = {
47
+ api_host: options[:host],
48
+ headers: options[:headers],
49
+ skip_ssl_verification: options[:skip_ssl_verification],
50
+ compress_request: options[:compress_request]
51
+ }
52
+ @transport_options[:retries] = options[:max_retries].to_i + 1 if options.key?(:max_retries)
53
+ @transport = Transport.new(@transport_options)
54
+ end
55
+
56
+ # Continuously runs the loop to check for new events.
57
+ #
58
+ # @return [void]
59
+ def run
60
+ ensure_current_process!
61
+
62
+ until shutdown?
63
+ wait_for_work
64
+ break if shutdown?
65
+ next if @queue.empty?
66
+
67
+ build_batch
68
+
69
+ begin
70
+ send_batch unless @batch.empty?
71
+ ensure
72
+ @lock.synchronize { @batch.clear }
73
+ clear_flush_request_if_idle
74
+ end
75
+ end
76
+ ensure
77
+ shutdown_transport
78
+ end
79
+
80
+ # Request the worker to send any pending events without waiting for the
81
+ # configured flush interval. Used by Client#flush and shutdown paths.
82
+ #
83
+ # @return [void]
84
+ def request_flush
85
+ ensure_current_process!
86
+
87
+ @state_lock.synchronize do
88
+ @flush_requested = true
89
+ @condition.broadcast
90
+ end
91
+ end
92
+
93
+ # Wake the worker when producers enqueue new messages.
94
+ #
95
+ # @return [void]
96
+ def notify
97
+ ensure_current_process!
98
+
99
+ @state_lock.synchronize { @condition.signal }
100
+ end
101
+
102
+ # @return [void]
103
+ def shutdown
104
+ ensure_current_process!
105
+
106
+ @state_lock.synchronize do
107
+ @shutdown = true
108
+ @flush_requested = true
109
+ @condition.broadcast
110
+ end
111
+ end
112
+
113
+ # public: Check whether we have outstanding requests.
114
+ #
115
+ # @return [Boolean] Whether the worker has outstanding requests.
116
+ # TODO: Rename to `requesting?` in future version
117
+ def is_requesting? # rubocop:disable Naming/PredicatePrefix
118
+ ensure_current_process!
119
+
120
+ @lock.synchronize { !@batch.empty? }
121
+ end
122
+
123
+ private
124
+
125
+ def ensure_current_process!
126
+ return if @pid == Process.pid
127
+
128
+ @lock.synchronize { @batch.clear }
129
+ @state_lock.synchronize do
130
+ @pid = Process.pid
131
+ @shutdown = false
132
+ @flush_requested = false
133
+ @condition.broadcast
134
+ end
135
+ shutdown_transport
136
+ @transport = Transport.new(@transport_options)
137
+ end
138
+
139
+ def build_batch
140
+ deadline = monotonic_time + @flush_interval_seconds
141
+
142
+ loop do
143
+ @lock.synchronize do
144
+ consume_message_from_queue! until @batch.full? || @queue.empty?
145
+ end
146
+
147
+ break if @batch.full? || @batch.empty? || flush_requested?
148
+
149
+ remaining = deadline - monotonic_time
150
+ break unless remaining.positive?
151
+
152
+ wait_for_more_messages(remaining)
153
+ end
154
+ end
155
+
156
+ def send_batch
157
+ res = @transport.send @api_key, @batch
158
+ handle_error(res.status, res.error) unless res.status.between?(200, 299)
159
+ end
160
+
161
+ def consume_message_from_queue!
162
+ @batch << @queue.pop(true)
163
+ rescue ThreadError
164
+ # Queue was emptied by another thread between #empty? and #pop.
165
+ rescue MessageBatch::JSONGenerationError => e
166
+ handle_error(-1, e.to_s)
167
+ end
168
+
169
+ def wait_for_work
170
+ @state_lock.synchronize do
171
+ while @queue.empty? && !@shutdown
172
+ clear_flush_request_without_lock
173
+ @condition.wait(@state_lock)
174
+ end
175
+ end
176
+ end
177
+
178
+ def wait_for_more_messages(timeout)
179
+ @state_lock.synchronize do
180
+ return if @flush_requested || @shutdown || !@queue.empty?
181
+
182
+ @condition.wait(@state_lock, timeout)
183
+ end
184
+ end
185
+
186
+ def flush_requested?
187
+ @state_lock.synchronize { @flush_requested }
188
+ end
189
+
190
+ def shutdown?
191
+ @state_lock.synchronize { @shutdown }
192
+ end
193
+
194
+ def clear_flush_request
195
+ @state_lock.synchronize { clear_flush_request_without_lock }
196
+ end
197
+
198
+ def clear_flush_request_if_idle
199
+ @state_lock.synchronize { clear_flush_request_without_lock if @queue.empty? }
200
+ end
201
+
202
+ def clear_flush_request_without_lock
203
+ @flush_requested = false
204
+ end
205
+
206
+ def handle_error(status, error)
207
+ @on_error.call(status, error)
208
+ rescue StandardError => e
209
+ logger.error("Error in on_error callback: #{e.message}")
210
+ end
211
+
212
+ def shutdown_transport
213
+ @transport.shutdown
214
+ rescue StandardError => e
215
+ logger.error("Error shutting down transport: #{e.message}")
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'txlemetry/defaults'
4
+ require 'txlemetry/utils'
5
+ require 'txlemetry/response'
6
+ require 'txlemetry/logging'
7
+ require 'txlemetry/backoff_policy'
8
+ require 'net/http'
9
+ require 'net/https'
10
+ require 'json'
11
+ require 'time'
12
+ require 'zlib'
13
+
14
+ module Txlemetry
15
+ # HTTP transport used by the SDK workers.
16
+ #
17
+ # @api private
18
+ class Transport
19
+ include Txlemetry::Defaults::Request
20
+ include Txlemetry::Utils
21
+ include Txlemetry::Logging
22
+
23
+ # @param options [Hash] Transport configuration.
24
+ # @option options [String] :api_host Full Txlemetry API host URL.
25
+ # @option options [String] :host Hostname to connect to.
26
+ # @option options [Integer] :port Port to connect to.
27
+ # @option options [Boolean] :ssl Whether to use HTTPS.
28
+ # @option options [Hash] :headers HTTP headers for batch requests.
29
+ # @option options [String] :path HTTP path for batch requests.
30
+ # @option options [Integer] :retries Number of retry attempts for retryable failures.
31
+ # @option options [Txlemetry::BackoffPolicy] :backoff_policy Backoff policy used between retries.
32
+ # @option options [Boolean] :skip_ssl_verification Disable SSL certificate verification.
33
+ # @option options [Boolean] :compress_request Whether to gzip batch request bodies. Defaults to +true+.
34
+ def initialize(options = {})
35
+ if options[:api_host]
36
+ uri = URI.parse(options[:api_host])
37
+ options[:host] = uri.host
38
+ options[:ssl] = uri.scheme == 'https'
39
+ options[:port] = uri.port
40
+ end
41
+
42
+ options[:host] = options[:host].nil? ? HOST : options[:host]
43
+ options[:port] = options[:port].nil? ? PORT : options[:port]
44
+ options[:ssl] = options[:ssl].nil? ? SSL : options[:ssl]
45
+
46
+ @headers = (options[:headers] || HEADERS).dup
47
+ @path = options[:path] || PATH
48
+ @retries = options[:retries] || RETRIES
49
+ @backoff_policy = options[:backoff_policy] || Txlemetry::BackoffPolicy.new
50
+ @compress_request = options[:compress_request] != false
51
+ @last_retry_after = nil
52
+
53
+ http = Net::HTTP.new(options[:host], options[:port])
54
+ http.use_ssl = options[:ssl]
55
+ http.read_timeout = 8
56
+ http.open_timeout = 4
57
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if options[:skip_ssl_verification]
58
+
59
+ @http = http
60
+ @http_mutex = Mutex.new
61
+ end
62
+
63
+ # Sends a batch of messages to the API
64
+ #
65
+ # @param api_key [String] Project API key.
66
+ # @param batch [Txlemetry::MessageBatch, Array<Hash>] Batch of messages to send.
67
+ # @return [Response] API response
68
+ def send(api_key, batch)
69
+ logger.debug("Sending request for #{batch.length} items")
70
+
71
+ last_response, exception =
72
+ retry_with_backoff(@retries) do
73
+ status_code, body = send_request(api_key, batch)
74
+ error =
75
+ begin
76
+ body && JSON.parse(body)['error']
77
+ rescue JSON::ParserError
78
+ body
79
+ end
80
+ should_retry = should_retry_request?(status_code, body)
81
+ logger.debug("Response status code: #{status_code}")
82
+ logger.debug("Response error: #{error}") if error
83
+
84
+ [Response.new(status_code, error), should_retry]
85
+ end
86
+
87
+ if exception
88
+ logger.error(exception.message)
89
+ exception.backtrace.each { |line| logger.error(line) }
90
+ Response.new(-1, exception.to_s)
91
+ else
92
+ last_response
93
+ end
94
+ end
95
+
96
+ # Closes a persistent connection if it exists.
97
+ #
98
+ # @return [void]
99
+ def shutdown
100
+ @http_mutex.synchronize do
101
+ @http.finish if @http.started?
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def should_retry_request?(status_code, body)
108
+ if status_code >= 500 || [408, 429].include?(status_code)
109
+ true # Server error, request timeout, or rate limited
110
+ elsif status_code >= 400
111
+ logger.error(body)
112
+ false # Client error. Do not retry, but log
113
+ else
114
+ false
115
+ end
116
+ end
117
+
118
+ # Takes a block that returns [result, should_retry].
119
+ #
120
+ # Retries upto `retries_remaining` times, if `should_retry` is false or
121
+ # an exception is raised. `@backoff_policy` is used to determine the
122
+ # duration to sleep between attempts
123
+ #
124
+ # Returns [last_result, raised_exception]
125
+ def retry_with_backoff(retries_remaining, &block)
126
+ result, caught_exception = nil
127
+ should_retry = false
128
+
129
+ begin
130
+ result, should_retry = yield
131
+ return result, nil unless should_retry
132
+ rescue StandardError => e
133
+ should_retry = true
134
+ caught_exception = e
135
+ end
136
+
137
+ if should_retry && (retries_remaining > 1)
138
+ logger.debug("Retrying request, #{retries_remaining} retries left")
139
+ sleep(retry_delay_seconds)
140
+ retry_with_backoff(retries_remaining - 1, &block)
141
+ else
142
+ [result, caught_exception]
143
+ end
144
+ end
145
+
146
+ def retry_delay_seconds
147
+ retry_after = parse_retry_after(@last_retry_after)
148
+ @last_retry_after = nil
149
+ return retry_after if retry_after
150
+
151
+ @backoff_policy.next_interval.to_f / 1000
152
+ end
153
+
154
+ def parse_retry_after(value)
155
+ return nil if value.nil? || value.empty?
156
+
157
+ seconds = Float(value, exception: false)
158
+ return seconds if seconds && seconds >= 0
159
+
160
+ parsed_time = Time.httpdate(value)
161
+ delay = parsed_time - Time.now
162
+ delay.positive? ? delay : nil
163
+ rescue ArgumentError
164
+ nil
165
+ end
166
+
167
+ # Sends a request for the batch, returns [status_code, body]
168
+ def send_request(api_key, batch)
169
+ @last_retry_after = nil
170
+ payload = JSON.generate(api_key: api_key, batch: batch)
171
+
172
+ request_path, request_headers, request_payload = build_request(@path, @headers, payload)
173
+ request = Net::HTTP::Post.new(request_path, request_headers)
174
+
175
+ if self.class.stub
176
+ logger.debug "stubbed request to #{request_path}: " \
177
+ "api key = #{api_key}, batch = #{JSON.generate(batch)}"
178
+
179
+ [200, '{}']
180
+ else
181
+ @http_mutex.synchronize do
182
+ @http.start unless @http.started? # Maintain a persistent connection
183
+ response = @http.request(request, request_payload)
184
+ @last_retry_after = response['Retry-After']
185
+ [response.code.to_i, response.body]
186
+ end
187
+ end
188
+ end
189
+
190
+ def build_request(path, headers, payload)
191
+ return [path, headers, payload] unless @compress_request
192
+
193
+ compressed_payload = gzip_payload(payload)
194
+ return [path, headers, payload] unless compressed_payload
195
+
196
+ compressed_headers = headers.merge('Content-Encoding' => 'gzip')
197
+
198
+ [path, compressed_headers, compressed_payload]
199
+ end
200
+
201
+ def gzip_payload(payload)
202
+ Zlib.gzip(payload)
203
+ rescue Zlib::Error => e
204
+ logger.warn("gzip compression failed; sending uncompressed - #{e.message}")
205
+ nil
206
+ end
207
+
208
+ class << self
209
+ attr_writer :stub
210
+
211
+ def stub
212
+ @stub || ENV.fetch('STUB', nil)
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Txlemetry
4
+ class InconclusiveMatchError < StandardError
5
+ end
6
+
7
+ # Utility helpers used internally by the SDK.
8
+ #
9
+ # @api private
10
+ module Utils
11
+ module_function
12
+
13
+ # public: Return a new hash with keys converted from strings to symbols
14
+ #
15
+ def symbolize_keys(hash)
16
+ hash.transform_keys(&:to_sym)
17
+ end
18
+
19
+ # public: Convert hash keys from strings to symbols in place
20
+ #
21
+ def symbolize_keys!(hash)
22
+ hash.replace symbolize_keys hash
23
+ end
24
+
25
+ # public: Return a new hash with keys as strings
26
+ #
27
+ def stringify_keys(hash)
28
+ hash.transform_keys(&:to_s)
29
+ end
30
+
31
+ # public: Recursively convert all keys to symbols in a Hash/Array tree
32
+ #
33
+ def deep_symbolize_keys(obj)
34
+ case obj
35
+ when Hash
36
+ obj.each_with_object({}) do |(key, value), result|
37
+ result[key.to_sym] = deep_symbolize_keys(value)
38
+ end
39
+ when Array
40
+ obj.map { |item| deep_symbolize_keys(item) }
41
+ else
42
+ obj
43
+ end
44
+ end
45
+
46
+ # public: Returns a new hash with all the date values in the into iso8601
47
+ # strings
48
+ #
49
+ def isoify_dates(hash)
50
+ hash.transform_values do |v|
51
+ datetime_in_iso8601(v)
52
+ end
53
+ end
54
+
55
+ # public: Converts all the date values in the into iso8601 strings in place
56
+ #
57
+ def isoify_dates!(hash)
58
+ hash.replace isoify_dates hash
59
+ end
60
+
61
+ def datetime_in_iso8601(datetime)
62
+ case datetime
63
+ when Time
64
+ time_in_iso8601 datetime
65
+ when DateTime
66
+ time_in_iso8601 datetime.to_time
67
+ when Date
68
+ date_in_iso8601 datetime
69
+ else
70
+ datetime
71
+ end
72
+ end
73
+
74
+ def time_in_iso8601(time, fraction_digits = 3)
75
+ fraction =
76
+ (('.%06i' % time.usec)[0, fraction_digits + 1] if fraction_digits.positive?) # rubocop:disable Style/FormatString
77
+
78
+ "#{time.strftime('%Y-%m-%dT%H:%M:%S')}#{fraction}#{formatted_offset(time, true, 'Z')}"
79
+ end
80
+
81
+ def date_in_iso8601(date)
82
+ date.strftime('%F')
83
+ end
84
+
85
+ def formatted_offset(time, colon = true, alternate_utc_string = nil)
86
+ (time.utc? && alternate_utc_string) ||
87
+ seconds_to_utc_offset(time.utc_offset, colon)
88
+ end
89
+
90
+ def seconds_to_utc_offset(seconds, colon = true)
91
+ format((colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON), (seconds.negative? ? '-' : '+'),
92
+ seconds.abs / 3600, (seconds.abs % 3600) / 60)
93
+ end
94
+
95
+ def convert_to_datetime(value)
96
+ if value.respond_to?(:strftime)
97
+ value
98
+
99
+ elsif value.respond_to?(:to_str)
100
+ begin
101
+ DateTime.parse(value)
102
+ rescue ArgumentError
103
+ raise InconclusiveMatchError, "#{value} is not in a valid date format"
104
+ end
105
+ else
106
+ raise InconclusiveMatchError, 'The date provided must be a string or date object'
107
+ end
108
+ end
109
+
110
+ UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
111
+ UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '')
112
+
113
+ # TODO: Rename to `valid_regex?` in future version
114
+ def is_valid_regex(regex) # rubocop:disable Naming/PredicatePrefix
115
+ Regexp.new(regex)
116
+ true
117
+ rescue RegexpError
118
+ false
119
+ end
120
+
121
+ # public: Get value from hash by symbol key first, then string key
122
+ # Handles falsy values correctly (unlike ||)
123
+ #
124
+ # hash - Hash to lookup value in
125
+ # key - Symbol or String key to lookup
126
+ #
127
+ # Returns the value if found, nil otherwise
128
+ def get_by_symbol_or_string_key(hash, key)
129
+ symbol_key = key.to_sym
130
+ string_key = key.to_s
131
+
132
+ if hash.key?(symbol_key)
133
+ hash[symbol_key]
134
+ else
135
+ hash[string_key]
136
+ end
137
+ end
138
+
139
+ # public: Current monotonic clock time in seconds, for measuring durations
140
+ #
141
+ def monotonic_time
142
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
143
+ end
144
+
145
+ # Hash that clears itself when it reaches a maximum length.
146
+ #
147
+ # @api private
148
+ class SizeLimitedHash < Hash
149
+ # @param max_length [Integer]
150
+ def initialize(max_length, ...)
151
+ super(...)
152
+ @max_length = max_length
153
+ end
154
+
155
+ # @param key [Object]
156
+ # @param value [Object]
157
+ # @return [Object]
158
+ def []=(key, value)
159
+ clear if length >= @max_length
160
+ super
161
+ end
162
+ end
163
+
164
+ # Array that drops the oldest item when it reaches a maximum length.
165
+ #
166
+ # @api private
167
+ class SizeLimitedArray < Array
168
+ # @param max_length [Integer]
169
+ def initialize(max_length)
170
+ super()
171
+ @max_length = max_length
172
+ end
173
+
174
+ # @param value [Object]
175
+ # @return [Array]
176
+ def <<(value)
177
+ shift if length >= @max_length
178
+ super
179
+ end
180
+ end
181
+ private_constant :SizeLimitedArray
182
+ end
183
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Txlemetry
4
+ VERSION = '4.0.0'
5
+ end
data/lib/txlemetry.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'txlemetry/version'
4
+ require 'txlemetry/defaults'
5
+ require 'txlemetry/utils'
6
+ require 'txlemetry/field_parser'
7
+ require 'txlemetry/client'
8
+ require 'txlemetry/send_worker'
9
+ require 'txlemetry/transport'
10
+ require 'txlemetry/response'
11
+ require 'txlemetry/logging'
12
+ require 'txlemetry/exception_capture'
13
+ require 'txlemetry/feature_flag_error'
14
+ require 'txlemetry/feature_flag_result'
15
+ require 'txlemetry/feature_flag_evaluations'
16
+ require 'txlemetry/flag_definition_cache'