Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/posthog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
require 'posthog/response'
require 'posthog/logging'
require 'posthog/exception_capture'
require 'posthog/feature_flag_error'
3 changes: 2 additions & 1 deletion lib/posthog/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def get_feature_flag(
person_properties,
group_properties
)
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at =
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error =
@feature_flags_poller.get_feature_flag(
key,
distinct_id,
Expand All @@ -304,6 +304,7 @@ def get_feature_flag(
}
properties['$feature_flag_request_id'] = request_id if request_id
properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
properties['$feature_flag_error'] = feature_flag_error if feature_flag_error

capture(
{
Expand Down
36 changes: 36 additions & 0 deletions lib/posthog/feature_flag_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module PostHog
# Error type constants for the $feature_flag_error property.
#
# These values are sent in analytics events to track flag evaluation failures.
# They should not be changed without considering impact on existing dashboards
# and queries that filter on these values.
#
# Error values:
# ERRORS_WHILE_COMPUTING: Server returned errorsWhileComputingFlags=true
# FLAG_MISSING: Requested flag not in API response
# QUOTA_LIMITED: Rate/quota limit exceeded
# TIMEOUT: Request timed out
# CONNECTION_ERROR: Network connectivity issue
# UNKNOWN_ERROR: Unexpected exceptions
#
# For API errors with status codes, use the api_error() method which returns
# a string like "api_error_500".
class FeatureFlagError
ERRORS_WHILE_COMPUTING = 'errors_while_computing_flags'
FLAG_MISSING = 'flag_missing'
QUOTA_LIMITED = 'quota_limited'
TIMEOUT = 'timeout'
CONNECTION_ERROR = 'connection_error'
UNKNOWN_ERROR = 'unknown_error'

# Generate API error string with status code.
#
# @param status [Integer, String] The HTTP status code
# @return [String] Error string in format "api_error_STATUS"
def self.api_error(status)
"api_error_#{status.to_s.downcase}"
end
end
end
38 changes: 33 additions & 5 deletions lib/posthog/feature_flags.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,11 @@ def get_feature_flag(

request_id = nil
evaluated_at = nil
feature_flag_error = nil

if !flag_was_locally_evaluated && !only_evaluate_locally
begin
errors = []
flags_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties,
only_evaluate_locally, true)
if flags_data.key?(:featureFlags)
Expand All @@ -207,15 +209,30 @@ def get_feature_flag(
flags = {}
end

status = flags_data[:status]
errors << FeatureFlagError.api_error(status) if status && status >= 400
errors << FeatureFlagError::ERRORS_WHILE_COMPUTING if flags_data[:errorsWhileComputingFlags]
errors << FeatureFlagError::QUOTA_LIMITED if flags_data[:quotaLimited]&.include?('feature_flags')
errors << FeatureFlagError::FLAG_MISSING unless flags.key?(key.to_s)

response = flags[key]
response = false if response.nil?
feature_flag_error = errors.join(',') unless errors.empty?

logger.debug "Successfully computed flag remotely: #{key} -> #{response}"
rescue Timeout::Error => e
@on_error.call(-1, "Timeout while fetching flags remotely: #{e}")
feature_flag_error = FeatureFlagError::TIMEOUT
rescue Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, SocketError => e
@on_error.call(-1, "Connection error while fetching flags remotely: #{e}")
feature_flag_error = FeatureFlagError::CONNECTION_ERROR
rescue StandardError => e
@on_error.call(-1, "Error computing flag remotely: #{e}. #{e.backtrace.join("\n")}")
feature_flag_error = FeatureFlagError::UNKNOWN_ERROR
end
end

[response, flag_was_locally_evaluated, request_id, evaluated_at]
[response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error]
end

def get_all_flags(
Expand Down Expand Up @@ -270,24 +287,32 @@ def get_all_flags_and_payloads(
fallback_to_server = true
end

errors_while_computing = false
quota_limited = nil
status_code = nil

if fallback_to_server && !only_evaluate_locally
begin
flags_and_payloads = get_flags(distinct_id, groups, person_properties, group_properties)
errors_while_computing = flags_and_payloads[:errorsWhileComputingFlags] || false
quota_limited = flags_and_payloads[:quotaLimited]
status_code = flags_and_payloads[:status]

unless flags_and_payloads.key?(:featureFlags)
raise StandardError, "Error flags response: #{flags_and_payloads}"
end

request_id = flags_and_payloads[:requestId]
evaluated_at = flags_and_payloads[:evaluatedAt]

# Check if feature_flags are quota limited
if flags_and_payloads[:quotaLimited]&.include?('feature_flags')
if quota_limited&.include?('feature_flags')
logger.warn '[FEATURE FLAGS] Quota limited for feature flags'
flags = {}
payloads = {}
else
flags = stringify_keys(flags_and_payloads[:featureFlags] || {})
payloads = stringify_keys(flags_and_payloads[:featureFlagPayloads] || {})
request_id = flags_and_payloads[:requestId]
evaluated_at = flags_and_payloads[:evaluatedAt]
end
rescue StandardError => e
@on_error.call(-1, "Error computing flag remotely: #{e}")
Expand All @@ -299,7 +324,10 @@ def get_all_flags_and_payloads(
featureFlags: flags,
featureFlagPayloads: payloads,
requestId: request_id,
evaluatedAt: evaluated_at
evaluatedAt: evaluated_at,
errorsWhileComputingFlags: errors_while_computing,
quotaLimited: quota_limited,
status: status_code
}
end

Expand Down
Loading