posthog-ruby 3.4.0 → 3.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e99287bd9fe6eb8969c2481c0380c2a6eba12709de5f5189a053cb9ba5ca255
4
- data.tar.gz: 78b496c5feb9c8cb222393ababa36d0f6fd81c18e603469b0633ac3fcdbfd639
3
+ metadata.gz: 3d0dcf1963eb64bad7469885f038127408c5815a5cad8b5d338f220b2fac5263
4
+ data.tar.gz: 970b55467f0594423d57c36c9c546bb65ddcb68041ab11b4bea7ded9e25928bb
5
5
  SHA512:
6
- metadata.gz: dc1c52ff0243af441d895c05199067347931f0bf50a21ba897a3fb75640cb954ec66c79dddb29fb92338479014f3a7f42543a1ef651ebdcbd7fe6845ff9e9002
7
- data.tar.gz: e46932963b504e54fe4590db73e1956611190b2ab16a1f04ffff5877c01b1e71a2dafc5f9c373361b69bb5f63532d1400c244580ac3c229ced006b350880eef5
6
+ metadata.gz: 5cfebda482c0b67e1c12fce9cae0081e182eb1d92577a7f07405ebff817635c31f70a780f4416b5de0d4bdecff7393852c9a8f8d44e34f5d8edf80f0db4434b3
7
+ data.tar.gz: 1ec51e9f018c98cdc8d00b86a0c11613f48544d6ebbeaebe5d98c5084861dcea0ba63be12f506d01a3493db87a9c9fe0f9c1c339d6ed286c7845f5da8bc4db26
@@ -278,6 +278,40 @@ module PostHog
278
278
  group_properties: {},
279
279
  only_evaluate_locally: false,
280
280
  send_feature_flag_events: true
281
+ )
282
+ result = get_feature_flag_result(
283
+ key,
284
+ distinct_id,
285
+ groups: groups,
286
+ person_properties: person_properties,
287
+ group_properties: group_properties,
288
+ only_evaluate_locally: only_evaluate_locally,
289
+ send_feature_flag_events: send_feature_flag_events
290
+ )
291
+ result&.value
292
+ end
293
+
294
+ # Returns both the feature flag value and payload in a single call.
295
+ # This method raises the $feature_flag_called event with the payload included.
296
+ #
297
+ # @param [String] key The key of the feature flag
298
+ # @param [String] distinct_id The distinct id of the user
299
+ # @param [Hash] groups
300
+ # @param [Hash] person_properties key-value pairs of properties to associate with the user.
301
+ # @param [Hash] group_properties
302
+ # @param [Boolean] only_evaluate_locally
303
+ # @param [Boolean] send_feature_flag_events
304
+ #
305
+ # @return [FeatureFlagResult, nil] A FeatureFlagResult object containing the flag value and payload,
306
+ # or nil if the flag evaluation returned nil
307
+ def get_feature_flag_result(
308
+ key,
309
+ distinct_id,
310
+ groups: {},
311
+ person_properties: {},
312
+ group_properties: {},
313
+ only_evaluate_locally: false,
314
+ send_feature_flag_events: true
281
315
  )
282
316
  person_properties, group_properties = add_local_person_and_group_properties(
283
317
  distinct_id,
@@ -285,7 +319,7 @@ module PostHog
285
319
  person_properties,
286
320
  group_properties
287
321
  )
288
- feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at =
322
+ feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload =
289
323
  @feature_flags_poller.get_feature_flag(
290
324
  key,
291
325
  distinct_id,
@@ -294,8 +328,8 @@ module PostHog
294
328
  group_properties,
295
329
  only_evaluate_locally
296
330
  )
297
-
298
331
  feature_flag_reported_key = "#{key}_#{feature_flag_response}"
332
+
299
333
  if !@distinct_id_has_sent_flag_calls[distinct_id].include?(feature_flag_reported_key) && send_feature_flag_events
300
334
  properties = {
301
335
  '$feature_flag' => key,
@@ -304,18 +338,18 @@ module PostHog
304
338
  }
305
339
  properties['$feature_flag_request_id'] = request_id if request_id
306
340
  properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
341
+ properties['$feature_flag_error'] = feature_flag_error if feature_flag_error
307
342
 
308
343
  capture(
309
- {
310
- distinct_id: distinct_id,
311
- event: '$feature_flag_called',
312
- properties: properties,
313
- groups: groups
314
- }
344
+ distinct_id: distinct_id,
345
+ event: '$feature_flag_called',
346
+ properties: properties,
347
+ groups: groups
315
348
  )
316
349
  @distinct_id_has_sent_flag_calls[distinct_id] << feature_flag_reported_key
317
350
  end
318
- feature_flag_response
351
+
352
+ FeatureFlagResult.from_value_and_payload(key, feature_flag_response, payload)
319
353
  end
320
354
 
321
355
  # Returns all flags for a given user
@@ -341,6 +375,9 @@ module PostHog
341
375
 
342
376
  # Returns payload for a given feature flag
343
377
  #
378
+ # @deprecated Use {#get_feature_flag_result} instead, which returns both the flag value and payload
379
+ # and properly raises the $feature_flag_called event.
380
+ #
344
381
  # @param [String] key The key of the feature flag
345
382
  # @param [String] distinct_id The distinct id of the user
346
383
  # @option [String or boolean] match_value The value of the feature flag to be matched
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PostHog
4
+ # Error type constants for the $feature_flag_error property.
5
+ #
6
+ # These values are sent in analytics events to track flag evaluation failures.
7
+ # They should not be changed without considering impact on existing dashboards
8
+ # and queries that filter on these values.
9
+ #
10
+ # Error values:
11
+ # ERRORS_WHILE_COMPUTING: Server returned errorsWhileComputingFlags=true
12
+ # FLAG_MISSING: Requested flag not in API response
13
+ # QUOTA_LIMITED: Rate/quota limit exceeded
14
+ # TIMEOUT: Request timed out
15
+ # CONNECTION_ERROR: Network connectivity issue
16
+ # UNKNOWN_ERROR: Unexpected exceptions
17
+ #
18
+ # For API errors with status codes, use the api_error() method which returns
19
+ # a string like "api_error_500".
20
+ class FeatureFlagError
21
+ ERRORS_WHILE_COMPUTING = 'errors_while_computing_flags'
22
+ FLAG_MISSING = 'flag_missing'
23
+ QUOTA_LIMITED = 'quota_limited'
24
+ TIMEOUT = 'timeout'
25
+ CONNECTION_ERROR = 'connection_error'
26
+ UNKNOWN_ERROR = 'unknown_error'
27
+
28
+ # Generate API error string with status code.
29
+ #
30
+ # @param status [Integer, String] The HTTP status code
31
+ # @return [String] Error string in format "api_error_STATUS"
32
+ def self.api_error(status)
33
+ "api_error_#{status.to_s.downcase}"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module PostHog
6
+ # Represents the result of a feature flag evaluation
7
+ # containing both the flag value and payload
8
+ class FeatureFlagResult
9
+ attr_reader :key, :variant, :payload
10
+
11
+ def initialize(key:, enabled:, variant: nil, payload: nil)
12
+ @key = key
13
+ @enabled = enabled
14
+ @variant = variant
15
+ @payload = payload
16
+ end
17
+
18
+ # Returns the effective value of the feature flag
19
+ # variant if present, otherwise enabled status
20
+ def value
21
+ @variant || @enabled
22
+ end
23
+
24
+ # Returns whether or not the feature flag evaluated as enabled
25
+ def enabled?
26
+ @enabled
27
+ end
28
+
29
+ # Factory method to create from flag value and payload
30
+ def self.from_value_and_payload(key, value, payload)
31
+ return nil if value.nil?
32
+
33
+ parsed_payload = parse_payload(payload)
34
+
35
+ if value.is_a?(String)
36
+ new(key: key, enabled: true, variant: value, payload: parsed_payload)
37
+ else
38
+ new(key: key, enabled: value, payload: parsed_payload)
39
+ end
40
+ end
41
+
42
+ def self.parse_payload(payload)
43
+ return nil if payload.nil?
44
+ return payload unless payload.is_a?(String)
45
+ return nil if payload.empty?
46
+
47
+ begin
48
+ JSON.parse(payload)
49
+ rescue JSON::ParserError
50
+ payload
51
+ end
52
+ end
53
+
54
+ private_class_method :parse_payload
55
+ end
56
+ end
@@ -169,18 +169,13 @@ module PostHog
169
169
  end
170
170
 
171
171
  response = nil
172
- feature_flag = nil
173
-
174
- @feature_flags.each do |flag|
175
- if key == flag[:key]
176
- feature_flag = flag
177
- break
178
- end
179
- end
172
+ payload = nil
173
+ feature_flag = @feature_flags_by_key&.[](key)
180
174
 
181
175
  unless feature_flag.nil?
182
176
  begin
183
177
  response = _compute_flag_locally(feature_flag, distinct_id, groups, person_properties, group_properties)
178
+ payload = _compute_flag_payload_locally(key, response) unless response.nil?
184
179
  logger.debug "Successfully computed flag locally: #{key} -> #{response}"
185
180
  rescue RequiresServerEvaluation, InconclusiveMatchError => e
186
181
  logger.debug "Failed to compute flag #{key} locally: #{e}"
@@ -193,29 +188,49 @@ module PostHog
193
188
 
194
189
  request_id = nil
195
190
  evaluated_at = nil
191
+ feature_flag_error = nil
196
192
 
197
193
  if !flag_was_locally_evaluated && !only_evaluate_locally
198
194
  begin
195
+ errors = []
199
196
  flags_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties,
200
197
  only_evaluate_locally, true)
201
198
  if flags_data.key?(:featureFlags)
202
199
  flags = stringify_keys(flags_data[:featureFlags] || {})
200
+ payloads = stringify_keys(flags_data[:featureFlagPayloads] || {})
203
201
  request_id = flags_data[:requestId]
204
202
  evaluated_at = flags_data[:evaluatedAt]
205
203
  else
206
204
  logger.debug "Missing feature flags key: #{flags_data.to_json}"
207
205
  flags = {}
206
+ payloads = {}
208
207
  end
209
208
 
209
+ status = flags_data[:status]
210
+ errors << FeatureFlagError.api_error(status) if status && status >= 400
211
+ errors << FeatureFlagError::ERRORS_WHILE_COMPUTING if flags_data[:errorsWhileComputingFlags]
212
+ errors << FeatureFlagError::QUOTA_LIMITED if flags_data[:quotaLimited]&.include?('feature_flags')
213
+ errors << FeatureFlagError::FLAG_MISSING unless flags.key?(key.to_s)
214
+
210
215
  response = flags[key]
211
216
  response = false if response.nil?
217
+ payload = payloads[key]
218
+ feature_flag_error = errors.join(',') unless errors.empty?
219
+
212
220
  logger.debug "Successfully computed flag remotely: #{key} -> #{response}"
221
+ rescue Timeout::Error => e
222
+ @on_error.call(-1, "Timeout while fetching flags remotely: #{e}")
223
+ feature_flag_error = FeatureFlagError::TIMEOUT
224
+ rescue Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, SocketError => e
225
+ @on_error.call(-1, "Connection error while fetching flags remotely: #{e}")
226
+ feature_flag_error = FeatureFlagError::CONNECTION_ERROR
213
227
  rescue StandardError => e
214
228
  @on_error.call(-1, "Error computing flag remotely: #{e}. #{e.backtrace.join("\n")}")
229
+ feature_flag_error = FeatureFlagError::UNKNOWN_ERROR
215
230
  end
216
231
  end
217
232
 
218
- [response, flag_was_locally_evaluated, request_id, evaluated_at]
233
+ [response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload]
219
234
  end
220
235
 
221
236
  def get_all_flags(
@@ -270,24 +285,32 @@ module PostHog
270
285
  fallback_to_server = true
271
286
  end
272
287
 
288
+ errors_while_computing = false
289
+ quota_limited = nil
290
+ status_code = nil
291
+
273
292
  if fallback_to_server && !only_evaluate_locally
274
293
  begin
275
294
  flags_and_payloads = get_flags(distinct_id, groups, person_properties, group_properties)
295
+ errors_while_computing = flags_and_payloads[:errorsWhileComputingFlags] || false
296
+ quota_limited = flags_and_payloads[:quotaLimited]
297
+ status_code = flags_and_payloads[:status]
276
298
 
277
299
  unless flags_and_payloads.key?(:featureFlags)
278
300
  raise StandardError, "Error flags response: #{flags_and_payloads}"
279
301
  end
280
302
 
303
+ request_id = flags_and_payloads[:requestId]
304
+ evaluated_at = flags_and_payloads[:evaluatedAt]
305
+
281
306
  # Check if feature_flags are quota limited
282
- if flags_and_payloads[:quotaLimited]&.include?('feature_flags')
307
+ if quota_limited&.include?('feature_flags')
283
308
  logger.warn '[FEATURE FLAGS] Quota limited for feature flags'
284
309
  flags = {}
285
310
  payloads = {}
286
311
  else
287
312
  flags = stringify_keys(flags_and_payloads[:featureFlags] || {})
288
313
  payloads = stringify_keys(flags_and_payloads[:featureFlagPayloads] || {})
289
- request_id = flags_and_payloads[:requestId]
290
- evaluated_at = flags_and_payloads[:evaluatedAt]
291
314
  end
292
315
  rescue StandardError => e
293
316
  @on_error.call(-1, "Error computing flag remotely: #{e}")
@@ -299,7 +322,10 @@ module PostHog
299
322
  featureFlags: flags,
300
323
  featureFlagPayloads: payloads,
301
324
  requestId: request_id,
302
- evaluatedAt: evaluated_at
325
+ evaluatedAt: evaluated_at,
326
+ errorsWhileComputingFlags: errors_while_computing,
327
+ quotaLimited: quota_limited,
328
+ status: status_code
303
329
  }
304
330
  end
305
331
 
@@ -41,8 +41,8 @@ module PostHog
41
41
  return @logger if @logger
42
42
 
43
43
  base_logger =
44
- if defined?(Rails)
45
- Rails.logger
44
+ if defined?(::Rails)
45
+ ::Rails.logger
46
46
  else
47
47
  logger = Logger.new $stdout
48
48
  logger.progname = 'PostHog'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.4.0'
4
+ VERSION = '3.5.0'
5
5
  end
data/lib/posthog.rb CHANGED
@@ -10,3 +10,5 @@ require 'posthog/transport'
10
10
  require 'posthog/response'
11
11
  require 'posthog/logging'
12
12
  require 'posthog/exception_capture'
13
+ require 'posthog/feature_flag_error'
14
+ require 'posthog/feature_flag_result'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: posthog-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-12-04 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby
@@ -40,6 +40,8 @@ files:
40
40
  - lib/posthog/defaults.rb
41
41
  - lib/posthog/exception_capture.rb
42
42
  - lib/posthog/feature_flag.rb
43
+ - lib/posthog/feature_flag_error.rb
44
+ - lib/posthog/feature_flag_result.rb
43
45
  - lib/posthog/feature_flags.rb
44
46
  - lib/posthog/field_parser.rb
45
47
  - lib/posthog/logging.rb
@@ -70,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
72
  - !ruby/object:Gem::Version
71
73
  version: '0'
72
74
  requirements: []
73
- rubygems_version: 3.6.6
75
+ rubygems_version: 4.0.3
74
76
  specification_version: 4
75
77
  summary: PostHog library
76
78
  test_files: []