Skip to content

Commit bccd3ac

Browse files
committed
feat: Set $feature_flag_error on $feature_flag_called
If an error occurs while fetching flags, the property will be set to indicate what happened.
1 parent e610f57 commit bccd3ac

5 files changed

Lines changed: 357 additions & 6 deletions

File tree

lib/posthog.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
require 'posthog/response'
1111
require 'posthog/logging'
1212
require 'posthog/exception_capture'
13+
require 'posthog/feature_flag_error'

lib/posthog/client.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def get_feature_flag(
285285
person_properties,
286286
group_properties
287287
)
288-
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at =
288+
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error =
289289
@feature_flags_poller.get_feature_flag(
290290
key,
291291
distinct_id,
@@ -304,6 +304,7 @@ def get_feature_flag(
304304
}
305305
properties['$feature_flag_request_id'] = request_id if request_id
306306
properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
307+
properties['$feature_flag_error'] = feature_flag_error if feature_flag_error
307308

308309
capture(
309310
{

lib/posthog/feature_flag_error.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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}"
34+
end
35+
end
36+
end

lib/posthog/feature_flags.rb

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,11 @@ def get_feature_flag(
193193

194194
request_id = nil
195195
evaluated_at = nil
196+
feature_flag_error = nil
196197

197198
if !flag_was_locally_evaluated && !only_evaluate_locally
198199
begin
200+
errors = []
199201
flags_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties,
200202
only_evaluate_locally, true)
201203
if flags_data.key?(:featureFlags)
@@ -205,17 +207,44 @@ def get_feature_flag(
205207
else
206208
logger.debug "Missing feature flags key: #{flags_data.to_json}"
207209
flags = {}
210+
errors << FeatureFlagError::UNKNOWN_ERROR
211+
end
212+
213+
status = flags_data[:status]
214+
if status && status >= 400
215+
errors << FeatureFlagError.api_error(status)
216+
end
217+
218+
if flags_data[:errorsWhileComputingFlags]
219+
errors << FeatureFlagError::ERRORS_WHILE_COMPUTING
220+
end
221+
222+
if flags_data[:quotaLimited]&.include?('feature_flags')
223+
errors << FeatureFlagError::QUOTA_LIMITED
224+
end
225+
226+
unless flags.key?(key.to_s)
227+
errors << FeatureFlagError::FLAG_MISSING
208228
end
209229

210230
response = flags[key]
211231
response = false if response.nil?
232+
feature_flag_error = errors.join(',') unless errors.empty?
233+
212234
logger.debug "Successfully computed flag remotely: #{key} -> #{response}"
235+
rescue Timeout::Error, Net::ReadTimeout, Net::WriteTimeout
236+
@on_error.call(-1, 'Timeout while fetching flags remotely')
237+
feature_flag_error = FeatureFlagError::TIMEOUT
238+
rescue Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, SocketError => e
239+
@on_error.call(-1, "Connection error while fetching flags remotely: #{e}")
240+
feature_flag_error = FeatureFlagError::CONNECTION_ERROR
213241
rescue StandardError => e
214242
@on_error.call(-1, "Error computing flag remotely: #{e}. #{e.backtrace.join("\n")}")
243+
feature_flag_error = FeatureFlagError::UNKNOWN_ERROR
215244
end
216245
end
217246

218-
[response, flag_was_locally_evaluated, request_id, evaluated_at]
247+
[response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error]
219248
end
220249

221250
def get_all_flags(
@@ -270,19 +299,26 @@ def get_all_flags_and_payloads(
270299
fallback_to_server = true
271300
end
272301

302+
errors_while_computing = false
303+
quota_limited = nil
304+
status_code = nil
305+
273306
if fallback_to_server && !only_evaluate_locally
274307
begin
275308
flags_and_payloads = get_flags(distinct_id, groups, person_properties, group_properties)
309+
errors_while_computing = flags_and_payloads[:errorsWhileComputingFlags] || false
310+
quota_limited = flags_and_payloads[:quotaLimited]
311+
status_code = flags_and_payloads[:status]
276312

277313
unless flags_and_payloads.key?(:featureFlags)
278314
raise StandardError, "Error flags response: #{flags_and_payloads}"
279-
end
280-
281315
# Check if feature_flags are quota limited
282-
if flags_and_payloads[:quotaLimited]&.include?('feature_flags')
316+
if quota_limited&.include?('feature_flags')
283317
logger.warn '[FEATURE FLAGS] Quota limited for feature flags'
284318
flags = {}
285319
payloads = {}
320+
request_id = flags_and_payloads[:requestId]
321+
evaluated_at = flags_and_payloads[:evaluatedAt]
286322
else
287323
flags = stringify_keys(flags_and_payloads[:featureFlags] || {})
288324
payloads = stringify_keys(flags_and_payloads[:featureFlagPayloads] || {})
@@ -299,7 +335,10 @@ def get_all_flags_and_payloads(
299335
featureFlags: flags,
300336
featureFlagPayloads: payloads,
301337
requestId: request_id,
302-
evaluatedAt: evaluated_at
338+
evaluatedAt: evaluated_at,
339+
errorsWhileComputingFlags: errors_while_computing,
340+
quotaLimited: quota_limited,
341+
status: status_code
303342
}
304343
end
305344

0 commit comments

Comments
 (0)