posthog-ruby 2.9.0 → 2.10.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.
@@ -7,7 +7,6 @@ require 'posthog/feature_flag'
7
7
  require 'digest'
8
8
 
9
9
  class PostHog
10
-
11
10
  class InconclusiveMatchError < StandardError
12
11
  end
13
12
 
@@ -15,7 +14,14 @@ class PostHog
15
14
  include PostHog::Logging
16
15
  include PostHog::Utils
17
16
 
18
- def initialize(polling_interval, personal_api_key, project_api_key, host, feature_flag_request_timeout_seconds, on_error = nil)
17
+ def initialize(
18
+ polling_interval,
19
+ personal_api_key,
20
+ project_api_key,
21
+ host,
22
+ feature_flag_request_timeout_seconds,
23
+ on_error = nil
24
+ )
19
25
  @polling_interval = polling_interval || 30
20
26
  @personal_api_key = personal_api_key
21
27
  @project_api_key = project_api_key
@@ -29,12 +35,12 @@ class PostHog
29
35
  @quota_limited = Concurrent::AtomicBoolean.new(false)
30
36
  @task =
31
37
  Concurrent::TimerTask.new(
32
- execution_interval: polling_interval,
38
+ execution_interval: polling_interval
33
39
  ) { _load_feature_flags }
34
40
 
35
41
  # If no personal API key, disable local evaluation & thus polling for definitions
36
42
  if @personal_api_key.nil?
37
- logger.info "No personal API key provided, disabling local evaluation"
43
+ logger.info 'No personal API key provided, disabling local evaluation'
38
44
  @loaded_flags_successfully_once.make_true
39
45
  else
40
46
  # load once before timer
@@ -44,45 +50,71 @@ class PostHog
44
50
  end
45
51
 
46
52
  def load_feature_flags(force_reload = false)
47
- if @loaded_flags_successfully_once.false? || force_reload
48
- _load_feature_flags
49
- end
53
+ return unless @loaded_flags_successfully_once.false? || force_reload
54
+
55
+ _load_feature_flags
50
56
  end
51
57
 
52
- def get_feature_variants(distinct_id, groups={}, person_properties={}, group_properties={}, raise_on_error=false)
58
+ def get_feature_variants(
59
+ distinct_id,
60
+ groups = {},
61
+ person_properties = {},
62
+ group_properties = {},
63
+ raise_on_error = false
64
+ )
53
65
  # TODO: Convert to options hash for easier argument passing
54
- flags_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties, false, raise_on_error)
55
- if !flags_data.key?(:featureFlags)
66
+ flags_data = get_all_flags_and_payloads(
67
+ distinct_id,
68
+ groups,
69
+ person_properties,
70
+ group_properties,
71
+ false,
72
+ raise_on_error
73
+ )
74
+
75
+ if flags_data.key?(:featureFlags)
76
+ stringify_keys(flags_data[:featureFlags] || {})
77
+ else
56
78
  logger.debug "Missing feature flags key: #{flags_data.to_json}"
57
79
  {}
58
- else
59
- stringify_keys(flags_data[:featureFlags] || {})
60
80
  end
61
81
  end
62
82
 
63
- def get_feature_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false)
64
- flags_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties)
65
- if !flags_data.key?(:featureFlagPayloads)
66
- logger.debug "Missing feature flag payloads key: #{flags_data.to_json}"
67
- return {}
68
- else
83
+ def get_feature_payloads(
84
+ distinct_id,
85
+ groups = {},
86
+ person_properties = {},
87
+ group_properties = {},
88
+ _only_evaluate_locally = false
89
+ )
90
+ flags_data = get_all_flags_and_payloads(
91
+ distinct_id,
92
+ groups,
93
+ person_properties,
94
+ group_properties
95
+ )
96
+
97
+ if flags_data.key?(:featureFlagPayloads)
69
98
  stringify_keys(flags_data[:featureFlagPayloads] || {})
99
+ else
100
+ logger.debug "Missing feature flag payloads key: #{flags_data.to_json}"
101
+ {}
70
102
  end
71
103
  end
72
104
 
73
- def get_flags(distinct_id, groups={}, person_properties={}, group_properties={})
105
+ def get_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {})
74
106
  request_data = {
75
- "distinct_id": distinct_id,
76
- "groups": groups,
77
- "person_properties": person_properties,
78
- "group_properties": group_properties,
107
+ distinct_id: distinct_id,
108
+ groups: groups,
109
+ person_properties: person_properties,
110
+ group_properties: group_properties
79
111
  }
80
112
 
81
113
  flags_response = _request_feature_flag_evaluation(request_data)
82
114
 
83
115
  # Only normalize if we have flags in the response
84
116
  if flags_response[:flags]
85
- #v4 format
117
+ # v4 format
86
118
  flags_hash = flags_response[:flags].transform_values do |flag|
87
119
  FeatureFlag.new(flag)
88
120
  end
@@ -90,7 +122,7 @@ class PostHog
90
122
  flags_response[:featureFlags] = flags_hash.transform_values(&:get_value).transform_keys(&:to_sym)
91
123
  flags_response[:featureFlagPayloads] = flags_hash.transform_values(&:payload).transform_keys(&:to_sym)
92
124
  elsif flags_response[:featureFlags]
93
- #v3 format
125
+ # v3 format
94
126
  flags_response[:featureFlags] = flags_response[:featureFlags] || {}
95
127
  flags_response[:featureFlagPayloads] = flags_response[:featureFlagPayloads] || {}
96
128
  flags_response[:flags] = flags_response[:featureFlags].map do |key, value|
@@ -101,10 +133,17 @@ class PostHog
101
133
  end
102
134
 
103
135
  def get_remote_config_payload(flag_key)
104
- return _request_remote_config_payload(flag_key)
136
+ _request_remote_config_payload(flag_key)
105
137
  end
106
138
 
107
- def get_feature_flag(key, distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false)
139
+ def get_feature_flag(
140
+ key,
141
+ distinct_id,
142
+ groups = {},
143
+ person_properties = {},
144
+ group_properties = {},
145
+ only_evaluate_locally = false
146
+ )
108
147
  # make sure they're loaded on first run
109
148
  load_feature_flags
110
149
 
@@ -112,8 +151,8 @@ class PostHog
112
151
  symbolize_keys! person_properties
113
152
  symbolize_keys! group_properties
114
153
 
115
- group_properties.each do |key, value|
116
- symbolize_keys! value
154
+ group_properties.each_value do |value|
155
+ symbolize_keys!(value)
117
156
  end
118
157
 
119
158
  response = nil
@@ -126,7 +165,7 @@ class PostHog
126
165
  end
127
166
  end
128
167
 
129
- if !feature_flag.nil?
168
+ unless feature_flag.nil?
130
169
  begin
131
170
  response = _compute_flag_locally(feature_flag, distinct_id, groups, person_properties, group_properties)
132
171
  logger.debug "Successfully computed flag locally: #{key} -> #{response}"
@@ -144,18 +183,16 @@ class PostHog
144
183
  if !flag_was_locally_evaluated && !only_evaluate_locally
145
184
  begin
146
185
  flags_data = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties, false, true)
147
- if !flags_data.key?(:featureFlags)
148
- logger.debug "Missing feature flags key: #{flags_data.to_json}"
149
- flags = {}
150
- else
186
+ if flags_data.key?(:featureFlags)
151
187
  flags = stringify_keys(flags_data[:featureFlags] || {})
152
188
  request_id = flags_data[:requestId]
189
+ else
190
+ logger.debug "Missing feature flags key: #{flags_data.to_json}"
191
+ flags = {}
153
192
  end
154
193
 
155
194
  response = flags[key]
156
- if response.nil?
157
- response = false
158
- end
195
+ response = false if response.nil?
159
196
  logger.debug "Successfully computed flag remotely: #{key} -> #{response}"
160
197
  rescue StandardError => e
161
198
  @on_error.call(-1, "Error computing flag remotely: #{e}. #{e.backtrace.join("\n")}")
@@ -165,17 +202,38 @@ class PostHog
165
202
  [response, flag_was_locally_evaluated, request_id]
166
203
  end
167
204
 
168
- def get_all_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false)
205
+ def get_all_flags(
206
+ distinct_id,
207
+ groups = {},
208
+ person_properties = {},
209
+ group_properties = {},
210
+ only_evaluate_locally = false
211
+ )
169
212
  if @quota_limited.true?
170
- logger.debug "Not fetching flags from server - quota limited"
213
+ logger.debug 'Not fetching flags from server - quota limited'
171
214
  return {}
172
215
  end
216
+
173
217
  # returns a string hash of all flags
174
- response = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties, only_evaluate_locally)
218
+ response = get_all_flags_and_payloads(
219
+ distinct_id,
220
+ groups,
221
+ person_properties,
222
+ group_properties,
223
+ only_evaluate_locally
224
+ )
225
+
175
226
  response[:featureFlags]
176
227
  end
177
228
 
178
- def get_all_flags_and_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false, raise_on_error = false)
229
+ def get_all_flags_and_payloads(
230
+ distinct_id,
231
+ groups = {},
232
+ person_properties = {},
233
+ group_properties = {},
234
+ only_evaluate_locally = false,
235
+ raise_on_error = false
236
+ )
179
237
  load_feature_flags
180
238
 
181
239
  flags = {}
@@ -188,27 +246,26 @@ class PostHog
188
246
  match_value = _compute_flag_locally(flag, distinct_id, groups, person_properties, group_properties)
189
247
  flags[flag[:key]] = match_value
190
248
  match_payload = _compute_flag_payload_locally(flag[:key], match_value)
191
- if match_payload
192
- payloads[flag[:key]] = match_payload
193
- end
194
- rescue InconclusiveMatchError => e
249
+ payloads[flag[:key]] = match_payload if match_payload
250
+ rescue InconclusiveMatchError
195
251
  fallback_to_server = true
196
252
  rescue StandardError => e
197
253
  @on_error.call(-1, "Error computing flag locally: #{e}. #{e.backtrace.join("\n")} ")
198
254
  fallback_to_server = true
199
255
  end
200
256
  end
257
+
201
258
  if fallback_to_server && !only_evaluate_locally
202
259
  begin
203
260
  flags_and_payloads = get_flags(distinct_id, groups, person_properties, group_properties)
204
261
 
205
262
  unless flags_and_payloads.key?(:featureFlags)
206
- raise StandardError.new("Error flags response: #{flags_and_payloads}")
263
+ raise StandardError, "Error flags response: #{flags_and_payloads}"
207
264
  end
208
265
 
209
266
  # Check if feature_flags are quota limited
210
- if flags_and_payloads[:quotaLimited]&.include?("feature_flags")
211
- logger.warn "[FEATURE FLAGS] Quota limited for feature flags"
267
+ if flags_and_payloads[:quotaLimited] && flags_and_payloads[:quotaLimited].include?('feature_flags')
268
+ logger.warn '[FEATURE FLAGS] Quota limited for feature flags'
212
269
  flags = {}
213
270
  payloads = {}
214
271
  else
@@ -221,46 +278,58 @@ class PostHog
221
278
  raise if raise_on_error
222
279
  end
223
280
  end
224
- {"featureFlags": flags, "featureFlagPayloads": payloads, "requestId": request_id}
281
+
282
+ {
283
+ featureFlags: flags,
284
+ featureFlagPayloads: payloads,
285
+ requestId: request_id
286
+ }
225
287
  end
226
288
 
227
- def get_feature_flag_payload(key, distinct_id, match_value = nil, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false)
228
- if match_value == nil
289
+ def get_feature_flag_payload(
290
+ key,
291
+ distinct_id,
292
+ match_value = nil,
293
+ groups = {},
294
+ person_properties = {},
295
+ group_properties = {},
296
+ only_evaluate_locally = false
297
+ )
298
+ if match_value.nil?
229
299
  match_value = get_feature_flag(
230
300
  key,
231
301
  distinct_id,
232
302
  groups,
233
303
  person_properties,
234
304
  group_properties,
235
- true,
305
+ true
236
306
  )[0]
237
307
  end
238
308
  response = nil
239
- if match_value != nil
240
- response = _compute_flag_payload_locally(key, match_value)
241
- end
242
- if response == nil and !only_evaluate_locally
309
+ response = _compute_flag_payload_locally(key, match_value) unless match_value.nil?
310
+ if response.nil? && !only_evaluate_locally
243
311
  flags_payloads = get_feature_payloads(distinct_id, groups, person_properties, group_properties)
244
312
  response = flags_payloads[key.downcase] || nil
245
313
  end
246
314
  response
247
315
  end
248
316
 
249
- def shutdown_poller()
317
+ def shutdown_poller
250
318
  @task.shutdown
251
319
  end
252
320
 
253
321
  # Class methods
254
322
 
255
323
  def self.compare(lhs, rhs, operator)
256
- if operator == "gt"
257
- return lhs > rhs
258
- elsif operator == "gte"
259
- return lhs >= rhs
260
- elsif operator == "lt"
261
- return lhs < rhs
262
- elsif operator == "lte"
263
- return lhs <= rhs
324
+ case operator
325
+ when 'gt'
326
+ lhs > rhs
327
+ when 'gte'
328
+ lhs >= rhs
329
+ when 'lt'
330
+ lhs < rhs
331
+ when 'lte'
332
+ lhs <= rhs
264
333
  else
265
334
  raise "Invalid operator: #{operator}"
266
335
  end
@@ -269,34 +338,33 @@ class PostHog
269
338
  def self.relative_date_parse_for_feature_flag_matching(value)
270
339
  match = /^-?([0-9]+)([a-z])$/.match(value)
271
340
  parsed_dt = DateTime.now.new_offset(0)
272
- if match
273
- number = match[1].to_i
341
+ return unless match
274
342
 
275
- if number >= 10000
276
- # Guard against overflow, disallow numbers greater than 10_000
277
- return nil
278
- end
343
+ number = match[1].to_i
279
344
 
280
- interval = match[2]
281
- if interval == "h"
282
- parsed_dt = parsed_dt - (number/24r)
283
- elsif interval == "d"
284
- parsed_dt = parsed_dt.prev_day(number)
285
- elsif interval == "w"
286
- parsed_dt = parsed_dt.prev_day(number*7)
287
- elsif interval == "m"
288
- parsed_dt = parsed_dt.prev_month(number)
289
- elsif interval == "y"
290
- parsed_dt = parsed_dt.prev_year(number)
291
- else
292
- return nil
293
- end
294
- parsed_dt
345
+ if number >= 10_000
346
+ # Guard against overflow, disallow numbers greater than 10_000
347
+ return nil
348
+ end
349
+
350
+ interval = match[2]
351
+ case interval
352
+ when 'h'
353
+ parsed_dt -= (number / 24.0)
354
+ when 'd'
355
+ parsed_dt = parsed_dt.prev_day(number)
356
+ when 'w'
357
+ parsed_dt = parsed_dt.prev_day(number * 7)
358
+ when 'm'
359
+ parsed_dt = parsed_dt.prev_month(number)
360
+ when 'y'
361
+ parsed_dt = parsed_dt.prev_year(number)
295
362
  else
296
- nil
363
+ return nil
297
364
  end
298
- end
299
365
 
366
+ parsed_dt
367
+ end
300
368
 
301
369
  def self.match_property(property, property_values)
302
370
  # only looks for matches where key exists in property_values
@@ -310,9 +378,9 @@ class PostHog
310
378
  operator = property[:operator] || 'exact'
311
379
 
312
380
  if !property_values.key?(key)
313
- raise InconclusiveMatchError.new("Property #{key} not found in property_values")
381
+ raise InconclusiveMatchError, "Property #{key} not found in property_values"
314
382
  elsif operator == 'is_not_set'
315
- raise InconclusiveMatchError.new("Operator is_not_set not supported")
383
+ raise InconclusiveMatchError, 'Operator is_not_set not supported'
316
384
  end
317
385
 
318
386
  override_value = property_values[key]
@@ -321,11 +389,10 @@ class PostHog
321
389
  when 'exact', 'is_not'
322
390
  if value.is_a?(Array)
323
391
  values_stringified = value.map { |val| val.to_s.downcase }
324
- if operator == 'exact'
325
- return values_stringified.any?(override_value.to_s.downcase)
326
- else
327
- return !values_stringified.any?(override_value.to_s.downcase)
328
- end
392
+ return values_stringified.any?(override_value.to_s.downcase) if operator == 'exact'
393
+
394
+ return values_stringified.none?(override_value.to_s.downcase)
395
+
329
396
  end
330
397
  if operator == 'exact'
331
398
  value.to_s.downcase == override_value.to_s.downcase
@@ -346,74 +413,68 @@ class PostHog
346
413
  parsed_value = nil
347
414
  begin
348
415
  parsed_value = Float(value)
349
- rescue StandardError => e
416
+ rescue StandardError # rubocop:disable Lint/SuppressedException
350
417
  end
351
418
  if !parsed_value.nil? && !override_value.nil?
352
419
  if override_value.is_a?(String)
353
- self.compare(override_value, value.to_s, operator)
420
+ compare(override_value, value.to_s, operator)
354
421
  else
355
- self.compare(override_value, parsed_value, operator)
422
+ compare(override_value, parsed_value, operator)
356
423
  end
357
424
  else
358
- self.compare(override_value.to_s, value.to_s, operator)
425
+ compare(override_value.to_s, value.to_s, operator)
359
426
  end
360
427
  when 'is_date_before', 'is_date_after'
361
428
  override_date = PostHog::Utils.convert_to_datetime(override_value.to_s)
362
- parsed_date = self.relative_date_parse_for_feature_flag_matching(value.to_s)
429
+ parsed_date = relative_date_parse_for_feature_flag_matching(value.to_s)
363
430
 
364
- if parsed_date.nil?
365
- parsed_date = PostHog::Utils.convert_to_datetime(value.to_s)
366
- end
431
+ parsed_date = PostHog::Utils.convert_to_datetime(value.to_s) if parsed_date.nil?
432
+
433
+ raise InconclusiveMatchError, 'Invalid date format' unless parsed_date
367
434
 
368
- if !parsed_date
369
- raise InconclusiveMatchError.new("Invalid date format")
370
- end
371
435
  if operator == 'is_date_before'
372
- return override_date < parsed_date
436
+ override_date < parsed_date
373
437
  elsif operator == 'is_date_after'
374
- return override_date > parsed_date
438
+ override_date > parsed_date
375
439
  end
376
440
  else
377
- raise InconclusiveMatchError.new("Unknown operator: #{operator}")
441
+ raise InconclusiveMatchError, "Unknown operator: #{operator}"
378
442
  end
379
443
  end
380
444
 
381
445
  private
382
446
 
383
447
  def _compute_flag_locally(flag, distinct_id, groups = {}, person_properties = {}, group_properties = {})
384
- if flag[:ensure_experience_continuity]
385
- raise InconclusiveMatchError.new("Flag has experience continuity enabled")
386
- end
448
+ raise InconclusiveMatchError, 'Flag has experience continuity enabled' if flag[:ensure_experience_continuity]
387
449
 
388
- return false if !flag[:active]
450
+ return false unless flag[:active]
389
451
 
390
452
  flag_filters = flag[:filters] || {}
391
453
 
392
454
  aggregation_group_type_index = flag_filters[:aggregation_group_type_index]
393
- if !aggregation_group_type_index.nil?
394
- group_name = @group_type_mapping[aggregation_group_type_index.to_s.to_sym]
455
+ return match_feature_flag_properties(flag, distinct_id, person_properties) if aggregation_group_type_index.nil?
395
456
 
396
- if group_name.nil?
397
- logger.warn "[FEATURE FLAGS] Unknown group type index #{aggregation_group_type_index} for feature flag #{flag[:key]}"
398
- # failover to `/flags/`
399
- raise InconclusiveMatchError.new("Flag has unknown group type index")
400
- end
457
+ group_name = @group_type_mapping[aggregation_group_type_index.to_s.to_sym]
401
458
 
402
- group_name_symbol = group_name.to_sym
459
+ if group_name.nil?
460
+ logger.warn(
461
+ "[FEATURE FLAGS] Unknown group type index #{aggregation_group_type_index} for feature flag #{flag[:key]}"
462
+ )
463
+ # failover to `/flags/`
464
+ raise InconclusiveMatchError, 'Flag has unknown group type index'
465
+ end
403
466
 
404
- if !groups.key?(group_name_symbol)
405
- # Group flags are never enabled if appropriate `groups` aren't passed in
406
- # don't failover to `/flags/`, since response will be the same
407
- logger.warn "[FEATURE FLAGS] Can't compute group feature flag: #{flag[:key]} without group names passed in"
408
- return false
409
- end
467
+ group_name_symbol = group_name.to_sym
410
468
 
411
- focused_group_properties = group_properties[group_name_symbol]
412
- return match_feature_flag_properties(flag, groups[group_name_symbol], focused_group_properties)
413
- else
414
- return match_feature_flag_properties(flag, distinct_id, person_properties)
469
+ unless groups.key?(group_name_symbol)
470
+ # Group flags are never enabled if appropriate `groups` aren't passed in
471
+ # don't failover to `/flags/`, since response will be the same
472
+ logger.warn "[FEATURE FLAGS] Can't compute group feature flag: #{flag[:key]} without group names passed in"
473
+ return false
415
474
  end
416
475
 
476
+ focused_group_properties = group_properties[group_name_symbol]
477
+ match_feature_flag_properties(flag, groups[group_name_symbol], focused_group_properties)
417
478
  end
418
479
 
419
480
  def _compute_flag_payload_locally(key, match_value)
@@ -437,23 +498,27 @@ class PostHog
437
498
 
438
499
  # Stable sort conditions with variant overrides to the top. This ensures that if overrides are present, they are
439
500
  # evaluated first, and the variant override is applied to the first matching condition.
440
- sorted_flag_conditions = flag_conditions.each_with_index.sort_by { |condition, idx| [condition[:variant].nil? ? 1 : -1, idx] }
501
+ sorted_flag_conditions = flag_conditions.each_with_index.sort_by do |condition, idx|
502
+ [condition[:variant].nil? ? 1 : -1, idx]
503
+ end
441
504
 
442
- sorted_flag_conditions.each do |condition, idx|
505
+ # NOTE: This NEEDS to be `each` because `each_key` breaks
506
+ # This is not a hash, it's just an array with 2 entries
507
+ sorted_flag_conditions.each do |condition, _idx| # rubocop:disable Style/HashEachMethods
443
508
  begin
444
509
  if is_condition_match(flag, distinct_id, condition, properties)
445
510
  variant_override = condition[:variant]
446
511
  flag_multivariate = flag_filters[:multivariate] || {}
447
512
  flag_variants = flag_multivariate[:variants] || []
448
- if flag_variants.map{|variant| variant[:key]}.include?(condition[:variant])
449
- variant = variant_override
450
- else
451
- variant = get_matching_variant(flag, distinct_id)
452
- end
513
+ variant = if flag_variants.map { |variant| variant[:key] }.include?(condition[:variant])
514
+ variant_override
515
+ else
516
+ get_matching_variant(flag, distinct_id)
517
+ end
453
518
  result = variant || true
454
519
  break
455
520
  end
456
- rescue InconclusiveMatchError => e
521
+ rescue InconclusiveMatchError
457
522
  is_inconclusive = true
458
523
  end
459
524
  end
@@ -461,47 +526,46 @@ class PostHog
461
526
  if !result.nil?
462
527
  return result
463
528
  elsif is_inconclusive
464
- raise InconclusiveMatchError.new("Can't determine if feature flag is enabled or not with given properties")
529
+ raise InconclusiveMatchError, "Can't determine if feature flag is enabled or not with given properties"
465
530
  end
466
531
 
467
532
  # We can only return False when all conditions are False
468
- return false
533
+ false
469
534
  end
470
535
 
471
- def is_condition_match(flag, distinct_id, condition, properties)
536
+ # TODO: Rename to `condition_match?` in future version
537
+ def is_condition_match(flag, distinct_id, condition, properties) # rubocop:disable Naming/PredicateName
472
538
  rollout_percentage = condition[:rollout_percentage]
473
539
 
474
- if !(condition[:properties] || []).empty?
475
- if !condition[:properties].all? { |prop|
476
- FeatureFlagsPoller.match_property(prop, properties)
477
- }
540
+ unless (condition[:properties] || []).empty?
541
+ if !condition[:properties].all? do |prop|
542
+ FeatureFlagsPoller.match_property(prop, properties)
543
+ end
478
544
  return false
479
545
  elsif rollout_percentage.nil?
480
546
  return true
481
547
  end
482
548
  end
483
549
 
484
- if !rollout_percentage.nil? and _hash(flag[:key], distinct_id) > (rollout_percentage.to_f/100)
485
- return false
486
- end
550
+ return false if !rollout_percentage.nil? && (_hash(flag[:key], distinct_id) > (rollout_percentage.to_f / 100))
487
551
 
488
- return true
552
+ true
489
553
  end
490
554
 
491
555
  # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
492
556
  # Given the same distinct_id and key, it'll always return the same float. These floats are
493
557
  # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
494
558
  # we can do _hash(key, distinct_id) < 0.2
495
- def _hash(key, distinct_id, salt="")
559
+ def _hash(key, distinct_id, salt = '')
496
560
  hash_key = Digest::SHA1.hexdigest "#{key}.#{distinct_id}#{salt}"
497
- return (Integer(hash_key[0..14], 16).to_f / 0xfffffffffffffff)
561
+ (Integer(hash_key[0..14], 16).to_f / 0xfffffffffffffff)
498
562
  end
499
563
 
500
564
  def get_matching_variant(flag, distinct_id)
501
- hash_value = _hash(flag[:key], distinct_id, salt="variant")
502
- matching_variant = variant_lookup_table(flag).find { |variant|
503
- hash_value >= variant[:value_min] and hash_value < variant[:value_max]
504
- }
565
+ hash_value = _hash(flag[:key], distinct_id, 'variant')
566
+ matching_variant = variant_lookup_table(flag).find do |variant|
567
+ hash_value >= variant[:value_min] and hash_value < variant[:value_max]
568
+ end
505
569
  matching_variant.nil? ? nil : matching_variant[:key]
506
570
  end
507
571
 
@@ -512,14 +576,14 @@ class PostHog
512
576
  variants = flag_filters[:multivariate] || {}
513
577
  multivariates = variants[:variants] || []
514
578
  multivariates.each do |variant|
515
- value_max = value_min + variant[:rollout_percentage].to_f / 100
516
- lookup_table << {'value_min': value_min, 'value_max': value_max, 'key': variant[:key]}
579
+ value_max = value_min + (variant[:rollout_percentage].to_f / 100)
580
+ lookup_table << { value_min: value_min, value_max: value_max, key: variant[:key] }
517
581
  value_min = value_max
518
582
  end
519
- return lookup_table
583
+ lookup_table
520
584
  end
521
585
 
522
- def _load_feature_flags()
586
+ def _load_feature_flags
523
587
  begin
524
588
  res = _request_feature_flag_definitions
525
589
  rescue StandardError => e
@@ -529,7 +593,10 @@ class PostHog
529
593
 
530
594
  # Handle quota limits with 402 status
531
595
  if res.is_a?(Hash) && res[:status] == 402
532
- logger.warn "[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all local flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts"
596
+ logger.warn(
597
+ '[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all local flags. ' \
598
+ 'Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts'
599
+ )
533
600
  @feature_flags = Concurrent::Array.new
534
601
  @feature_flags_by_key = {}
535
602
  @group_type_mapping = Concurrent::Hash.new
@@ -538,22 +605,18 @@ class PostHog
538
605
  return
539
606
  end
540
607
 
541
- if !res.key?(:flags)
542
- logger.debug "Failed to load feature flags: #{res}"
543
- else
608
+ if res.key?(:flags)
544
609
  @feature_flags = res[:flags] || []
545
610
  @feature_flags_by_key = {}
546
611
  @feature_flags.each do |flag|
547
- if flag[:key] != nil
548
- @feature_flags_by_key[flag[:key]] = flag
549
- end
612
+ @feature_flags_by_key[flag[:key]] = flag unless flag[:key].nil?
550
613
  end
551
614
  @group_type_mapping = res[:group_type_mapping] || {}
552
615
 
553
616
  logger.debug "Loaded #{@feature_flags.length} feature flags"
554
- if @loaded_flags_successfully_once.false?
555
- @loaded_flags_successfully_once.make_true
556
- end
617
+ @loaded_flags_successfully_once.make_true if @loaded_flags_successfully_once.false?
618
+ else
619
+ logger.debug "Failed to load feature flags: #{res}"
557
620
  end
558
621
  end
559
622
 
@@ -565,7 +628,7 @@ class PostHog
565
628
  _request(uri, req)
566
629
  end
567
630
 
568
- def _request_feature_flag_evaluation(data={})
631
+ def _request_feature_flag_evaluation(data = {})
569
632
  uri = URI("#{@host}/flags/?v=2")
570
633
  req = Net::HTTP::Post.new(uri)
571
634
  req['Content-Type'] = 'application/json'
@@ -584,23 +647,29 @@ class PostHog
584
647
  _request(uri, req, @feature_flag_request_timeout_seconds)
585
648
  end
586
649
 
650
+ # rubocop:disable Lint/ShadowedException
587
651
  def _request(uri, request_object, timeout = nil)
588
652
  request_object['User-Agent'] = "posthog-ruby#{PostHog::VERSION}"
589
653
  request_timeout = timeout || 10
590
654
 
591
655
  begin
592
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https', :read_timeout => request_timeout) do |http|
656
+ Net::HTTP.start(
657
+ uri.hostname,
658
+ uri.port,
659
+ use_ssl: uri.scheme == 'https',
660
+ read_timeout: request_timeout
661
+ ) do |http|
593
662
  res = http.request(request_object)
594
-
663
+
595
664
  # Parse response body to hash
596
665
  begin
597
- response = JSON.parse(res.body, {symbolize_names: true})
666
+ response = JSON.parse(res.body, { symbolize_names: true })
598
667
  # Only add status if response is a hash
599
- response = response.is_a?(Hash) ? response.merge({status: res.code.to_i}) : response
668
+ response = response.merge({ status: res.code.to_i }) if response.is_a?(Hash)
600
669
  return response
601
670
  rescue JSON::ParserError
602
671
  # Handle case when response isn't valid JSON
603
- return {error: "Invalid JSON response", body: res.body, status: res.code.to_i}
672
+ return { error: 'Invalid JSON response', body: res.body, status: res.code.to_i }
604
673
  end
605
674
  end
606
675
  rescue Timeout::Error,
@@ -611,10 +680,11 @@ class PostHog
611
680
  Net::HTTPHeaderSyntaxError,
612
681
  Net::ReadTimeout,
613
682
  Net::WriteTimeout,
614
- Net::ProtocolError => e
683
+ Net::ProtocolError
615
684
  logger.debug("Unable to complete request to #{uri}")
616
685
  raise
617
686
  end
618
687
  end
688
+ # rubocop:enable Lint/ShadowedException
619
689
  end
620
690
  end