Class: PostHog::FeatureFlagsPoller
- Inherits:
-
Object
- Object
- PostHog::FeatureFlagsPoller
- Defined in:
- lib/posthog/feature_flags.rb
Constant Summary
Constants included from Utils
Utils::UTC_OFFSET_WITHOUT_COLON, Utils::UTC_OFFSET_WITH_COLON
Class Method Summary collapse
-
.compare(lhs, rhs, operator) ⇒ Object
Class methods.
- .match_property(property, property_values) ⇒ Object
- .relative_date_parse_for_feature_flag_matching(value) ⇒ Object
Instance Method Summary collapse
- #get_all_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) ⇒ Object
- #get_all_flags_and_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false, raise_on_error = false) ⇒ Object
- #get_decide(distinct_id, groups = {}, person_properties = {}, group_properties = {}) ⇒ Object
- #get_feature_flag(key, distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) ⇒ Object
- #get_feature_flag_payload(key, distinct_id, match_value = nil, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) ⇒ Object
- #get_feature_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) ⇒ Object
- #get_feature_variants(distinct_id, groups = {}, person_properties = {}, group_properties = {}, raise_on_error = false) ⇒ Object
- #get_remote_config_payload(flag_key) ⇒ Object
-
#initialize(polling_interval, personal_api_key, project_api_key, host, feature_flag_request_timeout_seconds, on_error = nil) ⇒ FeatureFlagsPoller
constructor
A new instance of FeatureFlagsPoller.
- #load_feature_flags(force_reload = false) ⇒ Object
- #shutdown_poller ⇒ Object
Methods included from Utils
#convert_to_datetime, #date_in_iso8601, #datetime_in_iso8601, #formatted_offset, #is_valid_regex, #isoify_dates, #isoify_dates!, #seconds_to_utc_offset, #stringify_keys, #symbolize_keys, #symbolize_keys!, #time_in_iso8601, #uid
Methods included from Logging
Constructor Details
#initialize(polling_interval, personal_api_key, project_api_key, host, feature_flag_request_timeout_seconds, on_error = nil) ⇒ FeatureFlagsPoller
Returns a new instance of FeatureFlagsPoller.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/posthog/feature_flags.rb', line 18 def initialize(polling_interval, personal_api_key, project_api_key, host, feature_flag_request_timeout_seconds, on_error = nil) @polling_interval = polling_interval || 30 @personal_api_key = personal_api_key @project_api_key = project_api_key @host = host @feature_flags = Concurrent::Array.new @group_type_mapping = Concurrent::Hash.new @loaded_flags_successfully_once = Concurrent::AtomicBoolean.new @feature_flags_by_key = nil @feature_flag_request_timeout_seconds = feature_flag_request_timeout_seconds @on_error = on_error || proc { |status, error| } @quota_limited = Concurrent::AtomicBoolean.new(false) @task = Concurrent::TimerTask.new( execution_interval: polling_interval, ) { _load_feature_flags } # If no personal API key, disable local evaluation & thus polling for definitions if @personal_api_key.nil? logger.info "No personal API key provided, disabling local evaluation" @loaded_flags_successfully_once.make_true else # load once before timer load_feature_flags @task.execute end end |
Class Method Details
.compare(lhs, rhs, operator) ⇒ Object
Class methods
255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/posthog/feature_flags.rb', line 255 def self.compare(lhs, rhs, operator) if operator == "gt" return lhs > rhs elsif operator == "gte" return lhs >= rhs elsif operator == "lt" return lhs < rhs elsif operator == "lte" return lhs <= rhs else raise "Invalid operator: #{operator}" end end |
.match_property(property, property_values) ⇒ Object
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 |
# File 'lib/posthog/feature_flags.rb', line 301 def self.match_property(property, property_values) # only looks for matches where key exists in property_values # doesn't support operator is_not_set PostHog::Utils.symbolize_keys! property PostHog::Utils.symbolize_keys! property_values key = property[:key].to_sym value = property[:value] operator = property[:operator] || 'exact' if !property_values.key?(key) raise InconclusiveMatchError.new("Property #{key} not found in property_values") elsif operator == 'is_not_set' raise InconclusiveMatchError.new("Operator is_not_set not supported") end override_value = property_values[key] case operator when 'exact', 'is_not' if value.is_a?(Array) values_stringified = value.map { |val| val.to_s.downcase } if operator == 'exact' return values_stringified.any?(override_value.to_s.downcase) else return !values_stringified.any?(override_value.to_s.downcase) end end if operator == 'exact' value.to_s.downcase == override_value.to_s.downcase else value.to_s.downcase != override_value.to_s.downcase end when 'is_set' property_values.key?(key) when 'icontains' override_value.to_s.downcase.include?(value.to_s.downcase) when 'not_icontains' !override_value.to_s.downcase.include?(value.to_s.downcase) when 'regex' PostHog::Utils.is_valid_regex(value.to_s) && !Regexp.new(value.to_s).match(override_value.to_s).nil? when 'not_regex' PostHog::Utils.is_valid_regex(value.to_s) && Regexp.new(value.to_s).match(override_value.to_s).nil? when 'gt', 'gte', 'lt', 'lte' parsed_value = nil begin parsed_value = Float(value) rescue StandardError => e end if !parsed_value.nil? && !override_value.nil? if override_value.is_a?(String) self.compare(override_value, value.to_s, operator) else self.compare(override_value, parsed_value, operator) end else self.compare(override_value.to_s, value.to_s, operator) end when 'is_date_before', 'is_date_after' override_date = PostHog::Utils.convert_to_datetime(override_value.to_s) parsed_date = self.relative_date_parse_for_feature_flag_matching(value.to_s) if parsed_date.nil? parsed_date = PostHog::Utils.convert_to_datetime(value.to_s) end if !parsed_date raise InconclusiveMatchError.new("Invalid date format") end if operator == 'is_date_before' return override_date < parsed_date elsif operator == 'is_date_after' return override_date > parsed_date end else raise InconclusiveMatchError.new("Unknown operator: #{operator}") end end |
.relative_date_parse_for_feature_flag_matching(value) ⇒ Object
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
# File 'lib/posthog/feature_flags.rb', line 269 def self.relative_date_parse_for_feature_flag_matching(value) match = /^-?([0-9]+)([a-z])$/.match(value) parsed_dt = DateTime.now.new_offset(0) if match number = match[1].to_i if number >= 10000 # Guard against overflow, disallow numbers greater than 10_000 return nil end interval = match[2] if interval == "h" parsed_dt = parsed_dt - (number/24r) elsif interval == "d" parsed_dt = parsed_dt.prev_day(number) elsif interval == "w" parsed_dt = parsed_dt.prev_day(number*7) elsif interval == "m" parsed_dt = parsed_dt.prev_month(number) elsif interval == "y" parsed_dt = parsed_dt.prev_year(number) else return nil end parsed_dt else nil end end |
Instance Method Details
#get_all_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) ⇒ Object
168 169 170 171 172 173 174 175 176 |
# File 'lib/posthog/feature_flags.rb', line 168 def get_all_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) if @quota_limited.true? logger.debug "Not fetching flags from decide - quota limited" return {} end # returns a string hash of all flags response = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties, only_evaluate_locally) response[:featureFlags] end |
#get_all_flags_and_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false, raise_on_error = false) ⇒ Object
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/posthog/feature_flags.rb', line 178 def get_all_flags_and_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false, raise_on_error = false) load_feature_flags flags = {} payloads = {} fallback_to_decide = @feature_flags.empty? request_id = nil # Only for /decide requests @feature_flags.each do |flag| begin match_value = _compute_flag_locally(flag, distinct_id, groups, person_properties, group_properties) flags[flag[:key]] = match_value match_payload = _compute_flag_payload_locally(flag[:key], match_value) if match_payload payloads[flag[:key]] = match_payload end rescue InconclusiveMatchError => e fallback_to_decide = true rescue StandardError => e @on_error.call(-1, "Error computing flag locally: #{e}. #{e.backtrace.join("\n")} ") fallback_to_decide = true end end if fallback_to_decide && !only_evaluate_locally begin flags_and_payloads = get_decide(distinct_id, groups, person_properties, group_properties) unless flags_and_payloads.key?(:featureFlags) raise StandardError.new("Error flags response: #{flags_and_payloads}") end # Check if feature_flags are quota limited if flags_and_payloads[:quotaLimited]&.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] end rescue StandardError => e @on_error.call(-1, "Error computing flag remotely: #{e}") raise if raise_on_error end end {"featureFlags": flags, "featureFlagPayloads": payloads, "requestId": request_id} end |
#get_decide(distinct_id, groups = {}, person_properties = {}, group_properties = {}) ⇒ Object
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/posthog/feature_flags.rb', line 73 def get_decide(distinct_id, groups={}, person_properties={}, group_properties={}) request_data = { "distinct_id": distinct_id, "groups": groups, "person_properties": person_properties, "group_properties": group_properties, } decide_response = _request_feature_flag_evaluation(request_data) # Only normalize if we have flags in the response if decide_response[:flags] #v4 format flags_hash = decide_response[:flags].transform_values do |flag| FeatureFlag.new(flag) end decide_response[:flags] = flags_hash decide_response[:featureFlags] = flags_hash.transform_values(&:get_value).transform_keys(&:to_sym) decide_response[:featureFlagPayloads] = flags_hash.transform_values(&:payload).transform_keys(&:to_sym) elsif decide_response[:featureFlags] #v3 format decide_response[:featureFlags] = decide_response[:featureFlags] || {} decide_response[:featureFlagPayloads] = decide_response[:featureFlagPayloads] || {} decide_response[:flags] = decide_response[:featureFlags].map do |key, value| [key, FeatureFlag.from_value_and_payload(key, value, decide_response[:featureFlagPayloads][key])] end.to_h end decide_response end |
#get_feature_flag(key, distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) ⇒ Object
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/posthog/feature_flags.rb', line 107 def get_feature_flag(key, distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) # make sure they're loaded on first run load_feature_flags symbolize_keys! groups symbolize_keys! person_properties symbolize_keys! group_properties group_properties.each do |key, value| symbolize_keys! value end response = nil feature_flag = nil @feature_flags.each do |flag| if key == flag[:key] feature_flag = flag break end end if !feature_flag.nil? begin response = _compute_flag_locally(feature_flag, distinct_id, groups, person_properties, group_properties) logger.debug "Successfully computed flag locally: #{key} -> #{response}" rescue InconclusiveMatchError => e logger.debug "Failed to compute flag #{key} locally: #{e}" rescue StandardError => e @on_error.call(-1, "Error computing flag locally: #{e}. #{e.backtrace.join("\n")}") end end flag_was_locally_evaluated = !response.nil? request_id = nil if !flag_was_locally_evaluated && !only_evaluate_locally begin decide_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties, false, true) if !decide_data.key?(:featureFlags) logger.debug "Missing feature flags key: #{decide_data.to_json}" flags = {} else flags = stringify_keys(decide_data[:featureFlags] || {}) request_id = decide_data[:requestId] end response = flags[key] if response.nil? response = false end logger.debug "Successfully computed flag remotely: #{key} -> #{response}" rescue StandardError => e @on_error.call(-1, "Error computing flag remotely: #{e}. #{e.backtrace.join("\n")}") end end [response, flag_was_locally_evaluated, request_id] end |
#get_feature_flag_payload(key, distinct_id, match_value = nil, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) ⇒ Object
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/posthog/feature_flags.rb', line 227 def get_feature_flag_payload(key, distinct_id, match_value = nil, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) if match_value == nil match_value = get_feature_flag( key, distinct_id, groups, person_properties, group_properties, true, )[0] end response = nil if match_value != nil response = _compute_flag_payload_locally(key, match_value) end if response == nil and !only_evaluate_locally decide_payloads = get_feature_payloads(distinct_id, groups, person_properties, group_properties) response = decide_payloads[key.downcase] || nil end response end |
#get_feature_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) ⇒ Object
63 64 65 66 67 68 69 70 71 |
# File 'lib/posthog/feature_flags.rb', line 63 def get_feature_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false) decide_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties) if !decide_data.key?(:featureFlagPayloads) logger.debug "Missing feature flag payloads key: #{decide_data.to_json}" return {} else stringify_keys(decide_data[:featureFlagPayloads] || {}) end end |
#get_feature_variants(distinct_id, groups = {}, person_properties = {}, group_properties = {}, raise_on_error = false) ⇒ Object
52 53 54 55 56 57 58 59 60 61 |
# File 'lib/posthog/feature_flags.rb', line 52 def get_feature_variants(distinct_id, groups={}, person_properties={}, group_properties={}, raise_on_error=false) # TODO: Convert to options hash for easier argument passing decide_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties, false, raise_on_error) if !decide_data.key?(:featureFlags) logger.debug "Missing feature flags key: #{decide_data.to_json}" {} else stringify_keys(decide_data[:featureFlags] || {}) end end |
#get_remote_config_payload(flag_key) ⇒ Object
103 104 105 |
# File 'lib/posthog/feature_flags.rb', line 103 def get_remote_config_payload(flag_key) return _request_remote_config_payload(flag_key) end |
#load_feature_flags(force_reload = false) ⇒ Object
46 47 48 49 50 |
# File 'lib/posthog/feature_flags.rb', line 46 def load_feature_flags(force_reload = false) if @loaded_flags_successfully_once.false? || force_reload _load_feature_flags end end |
#shutdown_poller ⇒ Object
249 250 251 |
# File 'lib/posthog/feature_flags.rb', line 249 def shutdown_poller() @task.shutdown end |