posthog-ruby 3.18.0 → 3.20.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: 75e17034bb60a9af9539cfe28137a91a71d83305e4d3dc7d1db3844870efac38
4
- data.tar.gz: b511f71723032dff61fc998ae145d1e650eb6d90bdb4df84024e9335613aed71
3
+ metadata.gz: 36dffe9438cc269541f1642261b27b020bf05993eda1ad9999887428caa1667b
4
+ data.tar.gz: 9575ff454af5b89c0be1eb6abf4c7a1158fdbca88d4c708860339a1710b1393d
5
5
  SHA512:
6
- metadata.gz: d27932884831bba0abb8b90315c1c8c684ae821aa85a5f6df8b486339d53097022ee2d58303b913a46cb05cf576230bf1f45b2ffdbdf465c79fce4875239cfb9
7
- data.tar.gz: 043c0f02fb14f744898e7910623757931021d03cb0fbcacc46a305ec2f4d753df81a1ed8482b72f6e9c7e8bc1c826c78b86e889491ceb4c112dfa1844b1cf82a
6
+ metadata.gz: 9badf7f03c2009d4c643b6e41f4ede382c0bf488eb0ae4d5cf33ec454110102e3fc342b5f3357b58b881f2a513628ba6056f0ccfa82d54ac8cafb4d814ae3811
7
+ data.tar.gz: e7918e45f9645da0052d65d205359e8384709f645f0f1b5201500a5cc97bb092aed771cd7fac7846f5bfc142fcb9e8168f55d9cbc750c6bbb8aab26e60e1138d
@@ -76,6 +76,13 @@ module PostHog
76
76
  # @option opts [Integer] :feature_flag_request_max_retries How many times to retry a flag request after a
77
77
  # transient network error. Each retry sleeps on the calling thread before retrying, so this adds to
78
78
  # worst-case latency. Defaults to 1. Set to 0 to disable retrying.
79
+ # @option opts [Boolean] :feature_flags_async_load +true+ to fetch feature flag definitions for local
80
+ # evaluation only on the poller's background thread instead of the calling thread. The constructor
81
+ # returns without waiting for definitions: the poller fetches immediately on boot and, if that fails,
82
+ # keeps retrying on its regular polling interval until a load succeeds. Until then local evaluation
83
+ # treats definitions as absent (check {#feature_flags_loaded?}), so evaluations return nil with
84
+ # +only_evaluate_locally: true+, or fall back to the remote flags endpoint (which still runs on the
85
+ # calling thread) without it. Defaults to +false+.
79
86
  # @option opts [Proc] :before_send A callback that receives the event hash and should return either a modified
80
87
  # hash to be sent to PostHog or nil to prevent the event from being sent. e.g. `before_send: ->(event) { event }`.
81
88
  # @option opts [Boolean] :disable_singleton_warning +true+ to suppress the warning when multiple clients share
@@ -162,7 +169,8 @@ module PostHog
162
169
  opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS,
163
170
  opts[:on_error],
164
171
  flag_definition_cache_provider: opts[:flag_definition_cache_provider],
165
- feature_flag_request_max_retries: opts[:feature_flag_request_max_retries]
172
+ feature_flag_request_max_retries: opts[:feature_flag_request_max_retries],
173
+ async_load: opts[:feature_flags_async_load] == true
166
174
  )
167
175
  end
168
176
 
@@ -609,7 +617,8 @@ module PostHog
609
617
  id: definition[:id],
610
618
  version: nil,
611
619
  reason: FeatureFlagEvaluations::EVALUATED_LOCALLY_REASON,
612
- locally_evaluated: true
620
+ locally_evaluated: true,
621
+ has_experiment: definition[:has_experiment]
613
622
  )
614
623
  locally_evaluated_keys << key.to_s
615
624
  end
@@ -649,7 +658,8 @@ module PostHog
649
658
  id: metadata ? metadata.id : nil,
650
659
  version: metadata ? metadata.version : nil,
651
660
  reason: reason ? (reason.description || reason.code) : nil,
652
- locally_evaluated: false
661
+ locally_evaluated: false,
662
+ has_experiment: metadata&.has_experiment
653
663
  )
654
664
  end
655
665
  rescue StandardError => e
@@ -766,10 +776,12 @@ module PostHog
766
776
  # Remove internal information
767
777
  response.delete(:requestId)
768
778
  response.delete(:evaluatedAt)
779
+ response.delete(:flagDetails)
769
780
  response
770
781
  end
771
782
 
772
- # Reload locally cached feature flag definitions.
783
+ # Reload locally cached feature flag definitions synchronously on the
784
+ # calling thread.
773
785
  #
774
786
  # @return [void]
775
787
  def reload_feature_flags
@@ -784,6 +796,17 @@ module PostHog
784
796
  @feature_flags_poller.load_feature_flags(true)
785
797
  end
786
798
 
799
+ # Whether feature flag definitions for local evaluation are currently loaded.
800
+ # False until the first successful load, and false again if a quota-limited
801
+ # (402) response discards the definitions.
802
+ #
803
+ # @return [Boolean]
804
+ def feature_flags_loaded?
805
+ return false if @disabled
806
+
807
+ @feature_flags_poller.definitions_loaded?
808
+ end
809
+
787
810
  # Whether the client will actually send events. It is disabled when the
788
811
  # api_key is missing or blank, in which case every capture call no-ops.
789
812
  #
@@ -921,7 +944,8 @@ module PostHog
921
944
  person_properties, group_properties = add_local_person_and_group_properties(
922
945
  groups, person_properties, group_properties
923
946
  )
924
- feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload =
947
+ feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload,
948
+ has_experiment =
925
949
  @feature_flags_poller.get_feature_flag(
926
950
  key, distinct_id, groups, person_properties, group_properties, only_evaluate_locally
927
951
  )
@@ -931,6 +955,7 @@ module PostHog
931
955
  '$feature_flag_response' => feature_flag_response,
932
956
  'locally_evaluated' => flag_was_locally_evaluated
933
957
  }
958
+ properties['$feature_flag_has_experiment'] = has_experiment unless has_experiment.nil?
934
959
  properties['$feature_flag_request_id'] = request_id if request_id
935
960
  properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
936
961
  properties['$feature_flag_error'] = feature_flag_error if feature_flag_error
@@ -19,6 +19,7 @@ module PostHog
19
19
 
20
20
  module FeatureFlags
21
21
  FLAG_REQUEST_TIMEOUT_SECONDS = 3
22
+ POLLING_INTERVAL_SECONDS = 30
22
23
  # Number of retries for a flag request after a transient network error.
23
24
  # Flag requests are stateless and cause no server-side mutation, so
24
25
  # retrying is safe.
@@ -68,7 +68,7 @@ module PostHog
68
68
  #
69
69
  # @api private
70
70
  class FeatureFlagMetadata
71
- attr_reader :id, :version, :payload, :description
71
+ attr_reader :id, :version, :payload, :description, :has_experiment
72
72
 
73
73
  # @param json [Hash] Raw metadata returned by /flags.
74
74
  def initialize(json)
@@ -77,6 +77,9 @@ module PostHog
77
77
  @version = json['version']
78
78
  @payload = json['payload']
79
79
  @description = json['description']
80
+ # Whether the flag is linked to an experiment. nil when the server
81
+ # (an older deployment) does not report the field.
82
+ @has_experiment = json['has_experiment']
80
83
  end
81
84
  end
82
85
  end
@@ -13,7 +13,7 @@ module PostHog
13
13
  EVALUATED_LOCALLY_REASON = 'Evaluated locally'
14
14
 
15
15
  EvaluatedFlagRecord = Struct.new(
16
- :key, :enabled, :variant, :payload, :id, :version, :reason, :locally_evaluated,
16
+ :key, :enabled, :variant, :payload, :id, :version, :reason, :locally_evaluated, :has_experiment,
17
17
  keyword_init: true
18
18
  )
19
19
 
@@ -169,6 +169,7 @@ module PostHog
169
169
  }
170
170
 
171
171
  if flag
172
+ properties['$feature_flag_has_experiment'] = flag.has_experiment unless flag.has_experiment.nil?
172
173
  properties['$feature_flag_payload'] = flag.payload unless flag.payload.nil?
173
174
  properties['$feature_flag_id'] = flag.id if flag.id
174
175
  properties['$feature_flag_version'] = flag.version if flag.version
@@ -30,6 +30,7 @@ module PostHog
30
30
  include PostHog::Utils
31
31
 
32
32
  # @param polling_interval [Integer, nil] Seconds between local feature flag definition polls.
33
+ # Defaults to {Defaults::FeatureFlags::POLLING_INTERVAL_SECONDS}.
33
34
  # @param secret_key [String, nil] Credential used to fetch local evaluation definitions. Accepts either a
34
35
  # Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`).
35
36
  # @param project_api_key [String] Project API key.
@@ -40,6 +41,9 @@ module PostHog
40
41
  # @param feature_flag_request_max_retries [Integer, nil] Retries after a transient network error or retryable
41
42
  # HTTP response status on a flag request. Defaults to {Defaults::FeatureFlags::FLAG_REQUEST_MAX_RETRIES}.
42
43
  # Set to 0 to disable retrying.
44
+ # @param async_load [Boolean] When true, flag definitions are fetched only on the poller thread: an
45
+ # immediate first tick at construction, then the regular polling cadence, which keeps retrying until a
46
+ # load succeeds.
43
47
  def initialize(
44
48
  polling_interval,
45
49
  secret_key,
@@ -48,9 +52,10 @@ module PostHog
48
52
  feature_flag_request_timeout_seconds,
49
53
  on_error = nil,
50
54
  flag_definition_cache_provider: nil,
51
- feature_flag_request_max_retries: nil
55
+ feature_flag_request_max_retries: nil,
56
+ async_load: false
52
57
  )
53
- @polling_interval = polling_interval || 30
58
+ @polling_interval = polling_interval || Defaults::FeatureFlags::POLLING_INTERVAL_SECONDS
54
59
  @secret_key = secret_key
55
60
  @project_api_key = project_api_key
56
61
  @host = host
@@ -65,14 +70,16 @@ module PostHog
65
70
  @on_error = on_error || proc { |status, error| }
66
71
  @quota_limited = Concurrent::AtomicBoolean.new(false)
67
72
  @flags_etag = Concurrent::AtomicReference.new(nil)
68
- @flag_definitions_loaded_at = nil
73
+ @flag_definitions_loaded_at = Concurrent::AtomicReference.new(nil)
74
+ @async_load = async_load
69
75
 
70
76
  @flag_definition_cache_provider = flag_definition_cache_provider
71
77
  FlagDefinitionCacheProvider.validate!(@flag_definition_cache_provider) if @flag_definition_cache_provider
72
78
 
73
79
  @task =
74
80
  Concurrent::TimerTask.new(
75
- execution_interval: polling_interval
81
+ execution_interval: @polling_interval,
82
+ run_now: @async_load
76
83
  ) { _load_feature_flags }
77
84
 
78
85
  # If no secret_key, disable local evaluation & thus polling for definitions
@@ -80,19 +87,31 @@ module PostHog
80
87
  logger.info 'No secret_key provided, disabling local evaluation'
81
88
  @loaded_flags_successfully_once.make_true
82
89
  else
83
- # load once before timer
84
- load_feature_flags
90
+ # load once synchronously before timer, unless @async_load
91
+ load_feature_flags unless @async_load
85
92
  @task.execute
86
93
  end
87
94
  end
88
95
 
89
96
  def load_feature_flags(force_reload = false)
90
- return unless @loaded_flags_successfully_once.false? || force_reload
97
+ return if (@loaded_flags_successfully_once.true? || @async_load) && !force_reload
91
98
 
92
99
  _load_feature_flags
93
100
  end
94
101
 
95
- attr_reader :flag_definitions_loaded_at, :feature_flags_by_key
102
+ # Whether flag definitions are currently loaded. False until the first
103
+ # successful load, and false again if a quota-limited (402) response
104
+ # discards them.
105
+ def definitions_loaded?
106
+ !@flag_definitions_loaded_at.value.nil?
107
+ end
108
+
109
+ # Epoch milliseconds of the last successful definitions load, or nil.
110
+ def flag_definitions_loaded_at
111
+ @flag_definitions_loaded_at.value
112
+ end
113
+
114
+ attr_reader :feature_flags_by_key
96
115
 
97
116
  def get_feature_variants(
98
117
  distinct_id,
@@ -221,6 +240,11 @@ module PostHog
221
240
  end
222
241
 
223
242
  flag_was_locally_evaluated = !response.nil?
243
+ # Whether the flag is linked to an experiment, as reported by the server.
244
+ # Locally-evaluated flags carry it in the stored definition; remotely
245
+ # evaluated flags carry it in the response metadata. nil when the server
246
+ # (an older deployment) does not report it.
247
+ has_experiment = feature_flag[:has_experiment] if flag_was_locally_evaluated
224
248
 
225
249
  request_id = nil
226
250
  evaluated_at = nil
@@ -253,6 +277,9 @@ module PostHog
253
277
  payload = payloads[key]
254
278
  feature_flag_error = errors.join(',') unless errors.empty?
255
279
 
280
+ flag_detail = flags_data[:flagDetails]&.[](key.to_sym)
281
+ has_experiment = flag_detail&.metadata&.has_experiment
282
+
256
283
  logger.debug "Successfully computed flag remotely: #{key} -> #{response}"
257
284
  rescue Timeout::Error => e
258
285
  @on_error.call(-1, "Timeout while fetching flags remotely: #{e}")
@@ -266,7 +293,7 @@ module PostHog
266
293
  end
267
294
  end
268
295
 
269
- [response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload]
296
+ [response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload, has_experiment]
270
297
  end
271
298
 
272
299
  def get_all_flags(
@@ -324,10 +351,12 @@ module PostHog
324
351
  errors_while_computing = false
325
352
  quota_limited = nil
326
353
  status_code = nil
354
+ flag_details = nil
327
355
 
328
356
  if fallback_to_server && !only_evaluate_locally
329
357
  begin
330
358
  flags_and_payloads = get_flags(distinct_id, groups, person_properties, group_properties)
359
+ flag_details = flags_and_payloads[:flags]
331
360
  errors_while_computing = flags_and_payloads[:errorsWhileComputingFlags] || false
332
361
  quota_limited = flags_and_payloads[:quotaLimited]
333
362
  status_code = flags_and_payloads[:status]
@@ -368,6 +397,7 @@ module PostHog
368
397
  {
369
398
  featureFlags: flags,
370
399
  featureFlagPayloads: payloads,
400
+ flagDetails: flag_details,
371
401
  requestId: request_id,
372
402
  evaluatedAt: evaluated_at,
373
403
  errorsWhileComputingFlags: errors_while_computing,
@@ -1172,6 +1202,7 @@ module PostHog
1172
1202
  @feature_flags_by_key = {}
1173
1203
  @group_type_mapping = Concurrent::Hash.new
1174
1204
  @cohorts = Concurrent::Hash.new
1205
+ @flag_definitions_loaded_at.value = nil
1175
1206
  @loaded_flags_successfully_once.make_false
1176
1207
  @quota_limited.make_true
1177
1208
  return
@@ -1220,7 +1251,7 @@ module PostHog
1220
1251
  @cohorts = Concurrent::Hash[deep_symbolize_keys(cohorts)]
1221
1252
 
1222
1253
  logger.debug "Loaded #{@feature_flags.length} feature flags and #{@cohorts.length} cohorts"
1223
- @flag_definitions_loaded_at = (Time.now.to_f * 1000).to_i
1254
+ @flag_definitions_loaded_at.value = (Time.now.to_f * 1000).to_i
1224
1255
  @loaded_flags_successfully_once.make_true if @loaded_flags_successfully_once.false?
1225
1256
  end
1226
1257
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.18.0'
4
+ VERSION = '3.20.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: posthog-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.18.0
4
+ version: 3.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''