posthog-ruby 3.20.0 → 3.21.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: 36dffe9438cc269541f1642261b27b020bf05993eda1ad9999887428caa1667b
4
- data.tar.gz: 9575ff454af5b89c0be1eb6abf4c7a1158fdbca88d4c708860339a1710b1393d
3
+ metadata.gz: 4ae3a78450df91e2e4cb88cd69a58e167cdb291dcc02f8ab446e3b0975c270eb
4
+ data.tar.gz: b912b023ed02a37004cfbe22511437ee4df2a5ec63799d6d47e6f6aa9fc4f164
5
5
  SHA512:
6
- metadata.gz: 9badf7f03c2009d4c643b6e41f4ede382c0bf488eb0ae4d5cf33ec454110102e3fc342b5f3357b58b881f2a513628ba6056f0ccfa82d54ac8cafb4d814ae3811
7
- data.tar.gz: e7918e45f9645da0052d65d205359e8384709f645f0f1b5201500a5cc97bb092aed771cd7fac7846f5bfc142fcb9e8168f55d9cbc750c6bbb8aab26e60e1138d
6
+ metadata.gz: 42b8b2587ee825e9a5446def45d11940b52a3a2203828b4506b2a4e1c28e2deefd61406df6ab33800bf1055c1475130554d6edef55a1498b478c43a30591f7b0
7
+ data.tar.gz: 4eec6bdd11abbeee85720d2a68872ad366f39b5c9c28ce72fd96aaf54f3098218578ccac954e35fe01099975d6f5a2afcfe5016b4460520a6341e465f026ed99
@@ -22,6 +22,31 @@ module PostHog
22
22
  include PostHog::Utils
23
23
  include PostHog::Logging
24
24
 
25
+ # Strict allowlist of properties kept on minimal `$feature_flag_called`
26
+ # events (server-gated, non-experiment flags only). Everything else —
27
+ # context properties, `$feature/<key>`, payloads, system metadata — is
28
+ # stripped. Includes symbol forms so allowlisted properties supplied
29
+ # through context with symbol keys survive.
30
+ MINIMAL_FLAG_CALLED_EVENT_PROPERTIES = %w[
31
+ $feature_flag
32
+ $feature_flag_response
33
+ $feature_flag_has_experiment
34
+ $feature_flag_id
35
+ $feature_flag_version
36
+ $feature_flag_reason
37
+ $feature_flag_request_id
38
+ $feature_flag_evaluated_at
39
+ $feature_flag_error
40
+ locally_evaluated
41
+ $groups
42
+ $process_person_profile
43
+ $session_id
44
+ $is_server
45
+ $lib
46
+ $lib_version
47
+ ].flat_map { |key| [key, key.to_sym] }.freeze
48
+ private_constant :MINIMAL_FLAG_CALLED_EVENT_PROPERTIES
49
+
25
50
  # Thread-safe tracking of client instances per API key for singleton warnings
26
51
  @instances_by_api_key = {}
27
52
  @instances_mutex = Mutex.new
@@ -253,6 +278,7 @@ module PostHog
253
278
  return false if @disabled
254
279
 
255
280
  symbolize_keys! attrs
281
+ minimal_flag_called_event = attrs.delete(:_minimal_flag_called_event) == true
256
282
  enrich_capture_attrs_with_context(attrs)
257
283
 
258
284
  # Precedence: an explicit `flags` snapshot always wins, regardless of
@@ -321,7 +347,13 @@ module PostHog
321
347
  end
322
348
 
323
349
  attrs[:is_server] = @is_server
324
- enqueue(FieldParser.parse_for_capture(attrs))
350
+ message = FieldParser.parse_for_capture(attrs)
351
+ # Minimal events are built from the allowlist after full assembly so
352
+ # context properties and parser-added metadata can never leak in.
353
+ if minimal_flag_called_event
354
+ message[:properties] = message[:properties].slice(*MINIMAL_FLAG_CALLED_EVENT_PROPERTIES)
355
+ end
356
+ enqueue(message)
325
357
  end
326
358
 
327
359
  # Captures an exception as an event
@@ -627,6 +659,11 @@ module PostHog
627
659
  evaluated_at = nil
628
660
  errors_while_computing = false
629
661
  quota_limited = false
662
+ # Server-controlled gate for minimal `$feature_flag_called` events. When
663
+ # the snapshot uses a remote /flags response, the response's top-level
664
+ # `minimalFlagCalledEvents` field governs; a local-only snapshot reads
665
+ # the gate polled with the flag definitions.
666
+ minimal_flag_called_events = @feature_flags_poller.minimal_flag_called_events
630
667
 
631
668
  # Skip the remote `/flags` round-trip when the caller scoped the request
632
669
  # to a fixed set of `flag_keys` and we've already resolved every one of
@@ -635,10 +672,16 @@ module PostHog
635
672
  all_requested_flags_resolved_locally = flag_keys_set && (flag_keys_set - locally_evaluated_keys).empty?
636
673
 
637
674
  if !only_evaluate_locally && !all_requested_flags_resolved_locally
675
+ # The gate is team-level, so the /flags response gate supersedes the
676
+ # poller gate for mixed snapshots — both signals come from the same
677
+ # server and agree in steady state. When the response omits the gate,
678
+ # the whole snapshot fails safe to full events.
679
+ minimal_flag_called_events = false
638
680
  begin
639
681
  flags_response = @feature_flags_poller.get_flags(
640
682
  distinct_id, groups, person_properties, group_properties, flag_keys, disable_geoip
641
683
  )
684
+ minimal_flag_called_events = flags_response[:minimalFlagCalledEvents] == true
642
685
  request_id = flags_response[:requestId]
643
686
  evaluated_at = flags_response[:evaluatedAt]
644
687
  errors_while_computing = flags_response[:errorsWhileComputingFlags] == true
@@ -677,7 +720,8 @@ module PostHog
677
720
  evaluated_at: evaluated_at,
678
721
  flag_definitions_loaded_at: @feature_flags_poller.flag_definitions_loaded_at,
679
722
  errors_while_computing: errors_while_computing,
680
- quota_limited: quota_limited
723
+ quota_limited: quota_limited,
724
+ minimal_flag_called_events: minimal_flag_called_events
681
725
  )
682
726
  end
683
727
 
@@ -777,6 +821,7 @@ module PostHog
777
821
  response.delete(:requestId)
778
822
  response.delete(:evaluatedAt)
779
823
  response.delete(:flagDetails)
824
+ response.delete(:minimalFlagCalledEvents)
780
825
  response
781
826
  end
782
827
 
@@ -885,7 +930,7 @@ module PostHog
885
930
  # separate event for each group a user is evaluated under.
886
931
  def _capture_feature_flag_called_if_needed(
887
932
  distinct_id: nil, key: nil, response: nil, properties: nil,
888
- groups: nil, disable_geoip: nil
933
+ groups: nil, disable_geoip: nil, minimal: false
889
934
  )
890
935
  response_repr = response.nil? ? '::null::' : response
891
936
  groups_repr =
@@ -915,6 +960,7 @@ module PostHog
915
960
  }
916
961
  msg[:groups] = groups if groups
917
962
  msg[:disable_geoip] = disable_geoip unless disable_geoip.nil?
963
+ msg[:_minimal_flag_called_event] = true if minimal
918
964
 
919
965
  capture(msg)
920
966
  end
@@ -945,7 +991,7 @@ module PostHog
945
991
  groups, person_properties, group_properties
946
992
  )
947
993
  feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload,
948
- has_experiment =
994
+ has_experiment, minimal_flag_called_events =
949
995
  @feature_flags_poller.get_feature_flag(
950
996
  key, distinct_id, groups, person_properties, group_properties, only_evaluate_locally
951
997
  )
@@ -960,9 +1006,13 @@ module PostHog
960
1006
  properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
961
1007
  properties['$feature_flag_error'] = feature_flag_error if feature_flag_error
962
1008
 
1009
+ # Emit a minimal event only when the server gate is on and the flag is
1010
+ # known to have no linked experiment. Any missing signal fails safe to
1011
+ # the full event.
1012
+ minimal = minimal_flag_called_events == true && has_experiment == false
963
1013
  _capture_feature_flag_called_if_needed(
964
1014
  distinct_id: distinct_id, key: key, response: feature_flag_response,
965
- properties: properties, groups: groups
1015
+ properties: properties, groups: groups, minimal: minimal
966
1016
  )
967
1017
  end
968
1018
 
@@ -44,6 +44,8 @@ module PostHog
44
44
  # @param flag_definitions_loaded_at [Time, nil] When local flag definitions were loaded.
45
45
  # @param errors_while_computing [Boolean] Whether the server reported errors while computing flags.
46
46
  # @param quota_limited [Boolean] Whether feature flag evaluation was quota limited.
47
+ # @param minimal_flag_called_events [Boolean] Server-controlled gate for minimal
48
+ # `$feature_flag_called` events.
47
49
  # @param accessed [Array<String>, Set<String>, nil] Flag keys already accessed by this snapshot.
48
50
  def initialize(
49
51
  host: nil,
@@ -56,6 +58,7 @@ module PostHog
56
58
  flag_definitions_loaded_at: nil,
57
59
  errors_while_computing: false,
58
60
  quota_limited: false,
61
+ minimal_flag_called_events: false,
59
62
  accessed: nil
60
63
  )
61
64
  @host = host
@@ -68,6 +71,7 @@ module PostHog
68
71
  @flag_definitions_loaded_at = flag_definitions_loaded_at
69
72
  @errors_while_computing = errors_while_computing
70
73
  @quota_limited = quota_limited
74
+ @minimal_flag_called_events = minimal_flag_called_events == true
71
75
  @accessed = Set.new(accessed || [])
72
76
  end
73
77
 
@@ -188,13 +192,19 @@ module PostHog
188
192
  errors << 'flag_missing' if flag.nil?
189
193
  properties['$feature_flag_error'] = errors.join(',') unless errors.empty?
190
194
 
195
+ # Emit a minimal event only when the server gate is on and the flag is
196
+ # known to have no linked experiment. Any missing signal (unknown flag,
197
+ # has_experiment absent) fails safe to the full event.
198
+ minimal = @minimal_flag_called_events && !flag.nil? && flag.has_experiment == false
199
+
191
200
  @host.capture_flag_called_event_if_needed.call(
192
201
  distinct_id: @distinct_id,
193
202
  key: key,
194
203
  response: response,
195
204
  properties: properties,
196
205
  groups: @groups,
197
- disable_geoip: @disable_geoip
206
+ disable_geoip: @disable_geoip,
207
+ minimal: minimal
198
208
  )
199
209
  end
200
210
 
@@ -210,6 +220,7 @@ module PostHog
210
220
  flag_definitions_loaded_at: @flag_definitions_loaded_at,
211
221
  errors_while_computing: @errors_while_computing,
212
222
  quota_limited: @quota_limited,
223
+ minimal_flag_called_events: @minimal_flag_called_events,
213
224
  accessed: @accessed.dup
214
225
  )
215
226
  end
@@ -72,7 +72,10 @@ module PostHog
72
72
  @flags_etag = Concurrent::AtomicReference.new(nil)
73
73
  @flag_definitions_loaded_at = Concurrent::AtomicReference.new(nil)
74
74
  @async_load = async_load
75
-
75
+ # Server-controlled gate for minimal `$feature_flag_called` events, read
76
+ # from the top-level `minimal_flag_called_events` key of the local
77
+ # evaluation definitions payload. false when the server does not send it.
78
+ @minimal_flag_called_events = false
76
79
  @flag_definition_cache_provider = flag_definition_cache_provider
77
80
  FlagDefinitionCacheProvider.validate!(@flag_definition_cache_provider) if @flag_definition_cache_provider
78
81
 
@@ -111,7 +114,7 @@ module PostHog
111
114
  @flag_definitions_loaded_at.value
112
115
  end
113
116
 
114
- attr_reader :feature_flags_by_key
117
+ attr_reader :feature_flags_by_key, :minimal_flag_called_events
115
118
 
116
119
  def get_feature_variants(
117
120
  distinct_id,
@@ -245,6 +248,11 @@ module PostHog
245
248
  # evaluated flags carry it in the response metadata. nil when the server
246
249
  # (an older deployment) does not report it.
247
250
  has_experiment = feature_flag[:has_experiment] if flag_was_locally_evaluated
251
+ # Server-controlled gate for minimal `$feature_flag_called` events.
252
+ # Locally-evaluated flags read it from the definitions payload; remotely
253
+ # evaluated flags read it from the /flags response. nil when the signal
254
+ # is unavailable, which fails safe to the full event.
255
+ minimal_flag_called_events = @minimal_flag_called_events if flag_was_locally_evaluated
248
256
 
249
257
  request_id = nil
250
258
  evaluated_at = nil
@@ -279,6 +287,7 @@ module PostHog
279
287
 
280
288
  flag_detail = flags_data[:flagDetails]&.[](key.to_sym)
281
289
  has_experiment = flag_detail&.metadata&.has_experiment
290
+ minimal_flag_called_events = flags_data[:minimalFlagCalledEvents]
282
291
 
283
292
  logger.debug "Successfully computed flag remotely: #{key} -> #{response}"
284
293
  rescue Timeout::Error => e
@@ -293,7 +302,8 @@ module PostHog
293
302
  end
294
303
  end
295
304
 
296
- [response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload, has_experiment]
305
+ [response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload, has_experiment,
306
+ minimal_flag_called_events]
297
307
  end
298
308
 
299
309
  def get_all_flags(
@@ -352,6 +362,7 @@ module PostHog
352
362
  quota_limited = nil
353
363
  status_code = nil
354
364
  flag_details = nil
365
+ minimal_flag_called_events = nil
355
366
 
356
367
  if fallback_to_server && !only_evaluate_locally
357
368
  begin
@@ -367,6 +378,7 @@ module PostHog
367
378
 
368
379
  request_id = flags_and_payloads[:requestId]
369
380
  evaluated_at = flags_and_payloads[:evaluatedAt]
381
+ minimal_flag_called_events = flags_and_payloads[:minimalFlagCalledEvents]
370
382
 
371
383
  # Check if feature_flags are quota limited
372
384
  if quota_limited&.include?('feature_flags')
@@ -402,7 +414,8 @@ module PostHog
402
414
  evaluatedAt: evaluated_at,
403
415
  errorsWhileComputingFlags: errors_while_computing,
404
416
  quotaLimited: quota_limited,
405
- status: status_code
417
+ status: status_code,
418
+ minimalFlagCalledEvents: minimal_flag_called_events
406
419
  }
407
420
  end
408
421
 
@@ -1203,6 +1216,7 @@ module PostHog
1203
1216
  @group_type_mapping = Concurrent::Hash.new
1204
1217
  @cohorts = Concurrent::Hash.new
1205
1218
  @flag_definitions_loaded_at.value = nil
1219
+ @minimal_flag_called_events = false
1206
1220
  @loaded_flags_successfully_once.make_false
1207
1221
  @quota_limited.make_true
1208
1222
  return
@@ -1226,7 +1240,8 @@ module PostHog
1226
1240
  data = {
1227
1241
  flags: @feature_flags.to_a,
1228
1242
  group_type_mapping: @group_type_mapping.to_h,
1229
- cohorts: @cohorts.to_h
1243
+ cohorts: @cohorts.to_h,
1244
+ minimal_flag_called_events: @minimal_flag_called_events
1230
1245
  }
1231
1246
  @flag_definition_cache_provider.on_flag_definitions_received(data)
1232
1247
  rescue StandardError => e
@@ -1238,6 +1253,7 @@ module PostHog
1238
1253
  flags = get_by_symbol_or_string_key(data, 'flags') || []
1239
1254
  group_type_mapping = get_by_symbol_or_string_key(data, 'group_type_mapping') || {}
1240
1255
  cohorts = get_by_symbol_or_string_key(data, 'cohorts') || {}
1256
+ minimal_flag_called_events = get_by_symbol_or_string_key(data, 'minimal_flag_called_events')
1241
1257
 
1242
1258
  @feature_flags = Concurrent::Array.new(flags.map { |f| deep_symbolize_keys(f) })
1243
1259
 
@@ -1249,6 +1265,7 @@ module PostHog
1249
1265
 
1250
1266
  @group_type_mapping = Concurrent::Hash[deep_symbolize_keys(group_type_mapping)]
1251
1267
  @cohorts = Concurrent::Hash[deep_symbolize_keys(cohorts)]
1268
+ @minimal_flag_called_events = minimal_flag_called_events == true
1252
1269
 
1253
1270
  logger.debug "Loaded #{@feature_flags.length} feature flags and #{@cohorts.length} cohorts"
1254
1271
  @flag_definitions_loaded_at.value = (Time.now.to_f * 1000).to_i
@@ -14,9 +14,11 @@ module PostHog
14
14
  #
15
15
  # @!method flag_definitions
16
16
  # Retrieve cached flag definitions. Return a Hash with +:flags+,
17
- # +:group_type_mapping+, and +:cohorts+ keys, or +nil+ if the cache
18
- # is empty. Returning +nil+ triggers an API fetch when no flags are
19
- # loaded yet (emergency fallback).
17
+ # +:group_type_mapping+, +:cohorts+, and +:minimal_flag_called_events+
18
+ # keys, or +nil+ if the cache is empty. Returning +nil+ triggers an API
19
+ # fetch when no flags are loaded yet (emergency fallback). Providers
20
+ # written before +:minimal_flag_called_events+ existed continue to work;
21
+ # a missing key is treated as +false+.
20
22
  # @return [Hash, nil]
21
23
  #
22
24
  # @!method should_fetch_flag_definitions?
@@ -27,9 +29,9 @@ module PostHog
27
29
  #
28
30
  # @!method on_flag_definitions_received(data)
29
31
  # Called after successfully fetching new definitions from the API.
30
- # +data+ is a Hash with +:flags+, +:group_type_mapping+, and +:cohorts+
31
- # keys (plain Ruby types, not Concurrent:: wrappers). Store it in your
32
- # external cache.
32
+ # +data+ is a Hash with +:flags+, +:group_type_mapping+, +:cohorts+, and
33
+ # +:minimal_flag_called_events+ keys (plain Ruby types, not Concurrent::
34
+ # wrappers). Store it in your external cache.
33
35
  # @param data [Hash]
34
36
  # @return [void]
35
37
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.20.0'
4
+ VERSION = '3.21.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.20.0
4
+ version: 3.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''