posthog-ruby 3.6.5 → 3.7.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 +4 -4
- data/lib/posthog/client.rb +291 -59
- data/lib/posthog/feature_flag_evaluations.rb +177 -0
- data/lib/posthog/feature_flag_result.rb +4 -2
- data/lib/posthog/feature_flags.rb +9 -2
- data/lib/posthog/version.rb +1 -1
- data/lib/posthog.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afcf66d4afc793e15c83b53e455ef605218def4581460279d45721b82c1f73f2
|
|
4
|
+
data.tar.gz: 96f13bf72ddc840302d71c21072db26bdd098b73fd862455dc65b745357dc7d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b3e3871c5be034db9928b012cc0a3417121dc6f1ab4e7c5ece24e1c637dc04959ef50e4f551de598e11c4922d4f54048a9438bb03c86eb1e0eac67eabefdf7d
|
|
7
|
+
data.tar.gz: 3bf3128f6333b9678aa9c3a616494fcbff369ece30a0b0c422468cdab8856902a4184a560da84d6dbadb3dc935555b7402fe63d9158ea1da62589115247e7357
|
data/lib/posthog/client.rb
CHANGED
|
@@ -11,6 +11,7 @@ require 'posthog/noop_worker'
|
|
|
11
11
|
require 'posthog/message_batch'
|
|
12
12
|
require 'posthog/transport'
|
|
13
13
|
require 'posthog/feature_flags'
|
|
14
|
+
require 'posthog/feature_flag_evaluations'
|
|
14
15
|
require 'posthog/send_feature_flags_options'
|
|
15
16
|
require 'posthog/exception_capture'
|
|
16
17
|
|
|
@@ -135,6 +136,7 @@ module PostHog
|
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
@before_send = opts[:before_send]
|
|
139
|
+
@deprecation_emitted_for = Concurrent::Set.new
|
|
138
140
|
end
|
|
139
141
|
|
|
140
142
|
# Synchronously waits until the worker has cleared the queue.
|
|
@@ -175,6 +177,10 @@ module PostHog
|
|
|
175
177
|
# @option attrs [Hash] :properties Event properties (optional)
|
|
176
178
|
# @option attrs [Bool, Hash, SendFeatureFlagsOptions] :send_feature_flags
|
|
177
179
|
# Whether to send feature flags with this event, or configuration for feature flag evaluation (optional)
|
|
180
|
+
# @option attrs [PostHog::FeatureFlagEvaluations] :flags A snapshot returned by
|
|
181
|
+
# {#evaluate_flags}. When present, `$feature/<key>` and `$active_feature_flags` are
|
|
182
|
+
# attached from the snapshot without making an additional /flags request, and this
|
|
183
|
+
# takes precedence over `:send_feature_flags`.
|
|
178
184
|
# @option attrs [String] :uuid ID that uniquely identifies an event;
|
|
179
185
|
# events in PostHog are deduplicated by the
|
|
180
186
|
# combination of teamId, timestamp date,
|
|
@@ -183,8 +189,39 @@ module PostHog
|
|
|
183
189
|
def capture(attrs)
|
|
184
190
|
symbolize_keys! attrs
|
|
185
191
|
|
|
192
|
+
# Precedence: an explicit `flags` snapshot always wins, regardless of
|
|
193
|
+
# `send_feature_flags`. The snapshot guarantees the event carries the same
|
|
194
|
+
# values the developer branched on with no additional network call.
|
|
195
|
+
if attrs[:flags]
|
|
196
|
+
if attrs[:flags].is_a?(FeatureFlagEvaluations)
|
|
197
|
+
if attrs[:send_feature_flags]
|
|
198
|
+
logger.warn(
|
|
199
|
+
'[FEATURE FLAGS] Both `flags` and `send_feature_flags` were passed to ' \
|
|
200
|
+
'capture(); using `flags` and ignoring `send_feature_flags`.'
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
snapshot_props = attrs[:flags]._get_event_properties
|
|
204
|
+
attrs[:properties] = snapshot_props.merge(attrs[:properties] || {})
|
|
205
|
+
attrs.delete(:flags)
|
|
206
|
+
attrs.delete(:send_feature_flags)
|
|
207
|
+
else
|
|
208
|
+
logger.warn(
|
|
209
|
+
'[FEATURE FLAGS] capture(flags:) expects a PostHog::FeatureFlagEvaluations snapshot ' \
|
|
210
|
+
"from `client.evaluate_flags(...)`; got #{attrs[:flags].class}. Ignoring."
|
|
211
|
+
)
|
|
212
|
+
attrs.delete(:flags)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
186
216
|
send_feature_flags_param = attrs[:send_feature_flags]
|
|
187
217
|
if send_feature_flags_param
|
|
218
|
+
_emit_deprecation(
|
|
219
|
+
:capture_send_feature_flags,
|
|
220
|
+
'`send_feature_flags` on `capture` is deprecated and will be removed in a future major ' \
|
|
221
|
+
'version. Pass a `flags` snapshot from `client.evaluate_flags(...)` instead — it ' \
|
|
222
|
+
'avoids a second `/flags` request per capture and guarantees the event carries the ' \
|
|
223
|
+
'exact flag values your code branched on.'
|
|
224
|
+
)
|
|
188
225
|
# Handle different types of send_feature_flags parameter
|
|
189
226
|
case send_feature_flags_param
|
|
190
227
|
when true
|
|
@@ -225,7 +262,10 @@ module PostHog
|
|
|
225
262
|
# @param [Exception, String, Object] exception The exception to capture, a string message, or exception-like object
|
|
226
263
|
# @param [String] distinct_id The ID for the user (optional, defaults to a generated UUID)
|
|
227
264
|
# @param [Hash] additional_properties Additional properties to include with the exception event (optional)
|
|
228
|
-
|
|
265
|
+
# @param [PostHog::FeatureFlagEvaluations] flags A snapshot returned by {#evaluate_flags}.
|
|
266
|
+
# Forwarded to the inner {#capture} call so the captured `$exception` event carries the
|
|
267
|
+
# same `$feature/<key>` and `$active_feature_flags` properties as the snapshot.
|
|
268
|
+
def capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil)
|
|
229
269
|
exception_info = ExceptionCapture.build_parsed_exception(exception)
|
|
230
270
|
|
|
231
271
|
return if exception_info.nil?
|
|
@@ -243,6 +283,7 @@ module PostHog
|
|
|
243
283
|
properties: properties,
|
|
244
284
|
timestamp: Time.now
|
|
245
285
|
}
|
|
286
|
+
event_data[:flags] = flags if flags
|
|
246
287
|
|
|
247
288
|
capture(event_data)
|
|
248
289
|
end
|
|
@@ -293,6 +334,7 @@ module PostHog
|
|
|
293
334
|
@queue.length
|
|
294
335
|
end
|
|
295
336
|
|
|
337
|
+
# @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#is_enabled} instead.
|
|
296
338
|
# TODO: In future version, rename to `feature_flag_enabled?`
|
|
297
339
|
def is_feature_enabled( # rubocop:disable Naming/PredicateName
|
|
298
340
|
flag_key,
|
|
@@ -303,15 +345,20 @@ module PostHog
|
|
|
303
345
|
only_evaluate_locally: false,
|
|
304
346
|
send_feature_flag_events: true
|
|
305
347
|
)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
348
|
+
_emit_deprecation(
|
|
349
|
+
:is_feature_enabled,
|
|
350
|
+
'`is_feature_enabled` is deprecated and will be removed in a future major version. ' \
|
|
351
|
+
'Use `client.evaluate_flags(distinct_id, ...)` and call `flags.enabled?(key)` instead — ' \
|
|
352
|
+
'this consolidates flag evaluation into a single `/flags` request per incoming request.'
|
|
353
|
+
)
|
|
354
|
+
# Bypass the public `get_feature_flag` so the user only sees a single deprecation
|
|
355
|
+
# warning per call, not a cascade.
|
|
356
|
+
result = _get_feature_flag_result(
|
|
357
|
+
flag_key, distinct_id,
|
|
358
|
+
groups: groups, person_properties: person_properties, group_properties: group_properties,
|
|
359
|
+
only_evaluate_locally: only_evaluate_locally, send_feature_flag_events: send_feature_flag_events
|
|
314
360
|
)
|
|
361
|
+
response = result&.value
|
|
315
362
|
return nil if response.nil?
|
|
316
363
|
|
|
317
364
|
!!response
|
|
@@ -344,6 +391,7 @@ module PostHog
|
|
|
344
391
|
# ```ruby
|
|
345
392
|
# group_properties: {"organization": {"name": "PostHog", "employees": 11}}
|
|
346
393
|
# ```
|
|
394
|
+
# @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#get_flag} instead.
|
|
347
395
|
def get_feature_flag(
|
|
348
396
|
key,
|
|
349
397
|
distinct_id,
|
|
@@ -353,77 +401,175 @@ module PostHog
|
|
|
353
401
|
only_evaluate_locally: false,
|
|
354
402
|
send_feature_flag_events: true
|
|
355
403
|
)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
404
|
+
_emit_deprecation(
|
|
405
|
+
:get_feature_flag,
|
|
406
|
+
'`get_feature_flag` is deprecated and will be removed in a future major version. ' \
|
|
407
|
+
'Use `client.evaluate_flags(distinct_id, ...)` and call `flags.get_flag(key)` instead — ' \
|
|
408
|
+
'this consolidates flag evaluation into a single `/flags` request per incoming request.'
|
|
409
|
+
)
|
|
410
|
+
# Bypass the public `get_feature_flag_result` so the user only sees one deprecation warning.
|
|
411
|
+
result = _get_feature_flag_result(
|
|
412
|
+
key, distinct_id,
|
|
413
|
+
groups: groups, person_properties: person_properties, group_properties: group_properties,
|
|
414
|
+
only_evaluate_locally: only_evaluate_locally, send_feature_flag_events: send_feature_flag_events
|
|
364
415
|
)
|
|
365
416
|
result&.value
|
|
366
417
|
end
|
|
367
418
|
|
|
368
|
-
#
|
|
369
|
-
#
|
|
419
|
+
# @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#get_flag} /
|
|
420
|
+
# {FeatureFlagEvaluations#get_flag_payload} instead.
|
|
421
|
+
def get_feature_flag_result(
|
|
422
|
+
key,
|
|
423
|
+
distinct_id,
|
|
424
|
+
groups: {},
|
|
425
|
+
person_properties: {},
|
|
426
|
+
group_properties: {},
|
|
427
|
+
only_evaluate_locally: false,
|
|
428
|
+
send_feature_flag_events: true
|
|
429
|
+
)
|
|
430
|
+
_emit_deprecation(
|
|
431
|
+
:get_feature_flag_result,
|
|
432
|
+
'`get_feature_flag_result` is deprecated and will be removed in a future major version. ' \
|
|
433
|
+
'Use `client.evaluate_flags(distinct_id, ...)` and call `flags.get_flag(key)` / ' \
|
|
434
|
+
'`flags.get_flag_payload(key)` instead — this consolidates flag evaluation into a single ' \
|
|
435
|
+
'`/flags` request per incoming request.'
|
|
436
|
+
)
|
|
437
|
+
_get_feature_flag_result(
|
|
438
|
+
key, distinct_id,
|
|
439
|
+
groups: groups, person_properties: person_properties, group_properties: group_properties,
|
|
440
|
+
only_evaluate_locally: only_evaluate_locally, send_feature_flag_events: send_feature_flag_events
|
|
441
|
+
)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Evaluate feature flags for a distinct id and return a snapshot.
|
|
445
|
+
#
|
|
446
|
+
# The returned {PostHog::FeatureFlagEvaluations} can be queried with
|
|
447
|
+
# `is_enabled` / `get_flag` / `get_flag_payload`, narrowed with
|
|
448
|
+
# `only_accessed` / `only`, and passed to {#capture} via the `flags:` option
|
|
449
|
+
# to attach `$feature/<key>` and `$active_feature_flags` without an extra
|
|
450
|
+
# /flags request.
|
|
370
451
|
#
|
|
371
|
-
# @param [String] key The key of the feature flag
|
|
372
452
|
# @param [String] distinct_id The distinct id of the user
|
|
373
453
|
# @param [Hash] groups
|
|
374
|
-
# @param [Hash] person_properties key-value pairs of properties to associate with the user
|
|
454
|
+
# @param [Hash] person_properties key-value pairs of properties to associate with the user
|
|
375
455
|
# @param [Hash] group_properties
|
|
376
|
-
# @param [Boolean] only_evaluate_locally
|
|
377
|
-
# @param [Boolean]
|
|
378
|
-
#
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
|
|
382
|
-
|
|
456
|
+
# @param [Boolean] only_evaluate_locally Skip the remote /flags call entirely
|
|
457
|
+
# @param [Boolean] disable_geoip Stamped on captured access events
|
|
458
|
+
# @param [Array<String>] flag_keys When set, scopes the underlying /flags
|
|
459
|
+
# request to only these flag keys (sent as `flag_keys_to_evaluate`).
|
|
460
|
+
# Distinct from {FeatureFlagEvaluations#only}, which filters the
|
|
461
|
+
# already-fetched snapshot in memory.
|
|
462
|
+
# @return [PostHog::FeatureFlagEvaluations]
|
|
463
|
+
def evaluate_flags(
|
|
383
464
|
distinct_id,
|
|
384
465
|
groups: {},
|
|
385
466
|
person_properties: {},
|
|
386
467
|
group_properties: {},
|
|
387
468
|
only_evaluate_locally: false,
|
|
388
|
-
|
|
469
|
+
disable_geoip: nil,
|
|
470
|
+
flag_keys: nil
|
|
389
471
|
)
|
|
472
|
+
host = _feature_flag_evaluations_host
|
|
473
|
+
|
|
474
|
+
if distinct_id.nil? || distinct_id.to_s.empty?
|
|
475
|
+
return FeatureFlagEvaluations.new(host: host, distinct_id: '', flags: {})
|
|
476
|
+
end
|
|
477
|
+
|
|
390
478
|
person_properties, group_properties = add_local_person_and_group_properties(
|
|
391
|
-
distinct_id,
|
|
392
|
-
groups,
|
|
393
|
-
person_properties,
|
|
394
|
-
group_properties
|
|
479
|
+
distinct_id, groups, person_properties, group_properties
|
|
395
480
|
)
|
|
396
|
-
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload =
|
|
397
|
-
@feature_flags_poller.get_feature_flag(
|
|
398
|
-
key,
|
|
399
|
-
distinct_id,
|
|
400
|
-
groups,
|
|
401
|
-
person_properties,
|
|
402
|
-
group_properties,
|
|
403
|
-
only_evaluate_locally
|
|
404
|
-
)
|
|
405
|
-
feature_flag_reported_key = "#{key}_#{feature_flag_response}"
|
|
406
481
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
482
|
+
records = {}
|
|
483
|
+
locally_evaluated_keys = Set.new
|
|
484
|
+
flag_keys_set = flag_keys&.to_set(&:to_s)
|
|
485
|
+
|
|
486
|
+
@feature_flags_poller.load_feature_flags
|
|
487
|
+
poller_flags_by_key = @feature_flags_poller.feature_flags_by_key || {}
|
|
488
|
+
|
|
489
|
+
poller_flags_by_key.each do |key, definition|
|
|
490
|
+
next if flag_keys_set && !flag_keys_set.include?(key.to_s)
|
|
491
|
+
|
|
492
|
+
begin
|
|
493
|
+
match = @feature_flags_poller.send(
|
|
494
|
+
:_compute_flag_locally,
|
|
495
|
+
definition, distinct_id, groups, person_properties, group_properties
|
|
496
|
+
)
|
|
497
|
+
rescue PostHog::RequiresServerEvaluation, PostHog::InconclusiveMatchError, StandardError
|
|
498
|
+
next
|
|
499
|
+
end
|
|
416
500
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
501
|
+
next if match.nil?
|
|
502
|
+
|
|
503
|
+
records[key.to_s] = FeatureFlagEvaluations::EvaluatedFlagRecord.new(
|
|
504
|
+
key: key.to_s,
|
|
505
|
+
enabled: match.is_a?(String) || (match ? true : false),
|
|
506
|
+
variant: match.is_a?(String) ? match : nil,
|
|
507
|
+
payload: FeatureFlagResult.parse_payload(
|
|
508
|
+
@feature_flags_poller.send(:_compute_flag_payload_locally, key, match)
|
|
509
|
+
),
|
|
510
|
+
id: definition[:id],
|
|
511
|
+
version: nil,
|
|
512
|
+
reason: FeatureFlagEvaluations::EVALUATED_LOCALLY_REASON,
|
|
513
|
+
locally_evaluated: true
|
|
422
514
|
)
|
|
423
|
-
|
|
515
|
+
locally_evaluated_keys << key.to_s
|
|
424
516
|
end
|
|
425
517
|
|
|
426
|
-
|
|
518
|
+
request_id = nil
|
|
519
|
+
evaluated_at = nil
|
|
520
|
+
errors_while_computing = false
|
|
521
|
+
quota_limited = false
|
|
522
|
+
|
|
523
|
+
# Skip the remote `/flags` round-trip when the caller scoped the request
|
|
524
|
+
# to a fixed set of `flag_keys` and we've already resolved every one of
|
|
525
|
+
# them locally. Without `flag_keys` set, we can't know whether the server
|
|
526
|
+
# has flags we don't have definitions for, so we still hit `/flags`.
|
|
527
|
+
all_requested_flags_resolved_locally = flag_keys_set && (flag_keys_set - locally_evaluated_keys).empty?
|
|
528
|
+
|
|
529
|
+
if !only_evaluate_locally && !all_requested_flags_resolved_locally
|
|
530
|
+
begin
|
|
531
|
+
flags_response = @feature_flags_poller.get_flags(
|
|
532
|
+
distinct_id, groups, person_properties, group_properties, flag_keys, disable_geoip
|
|
533
|
+
)
|
|
534
|
+
request_id = flags_response[:requestId]
|
|
535
|
+
evaluated_at = flags_response[:evaluatedAt]
|
|
536
|
+
errors_while_computing = flags_response[:errorsWhileComputingFlags] == true
|
|
537
|
+
quota_limited = (flags_response[:quotaLimited] || []).include?('feature_flags')
|
|
538
|
+
remote_flags = flags_response[:flags] || {}
|
|
539
|
+
remote_flags.each do |key, ff|
|
|
540
|
+
key_str = key.to_s
|
|
541
|
+
next if locally_evaluated_keys.include?(key_str)
|
|
542
|
+
|
|
543
|
+
metadata = ff.metadata
|
|
544
|
+
reason = ff.reason
|
|
545
|
+
records[key_str] = FeatureFlagEvaluations::EvaluatedFlagRecord.new(
|
|
546
|
+
key: key_str,
|
|
547
|
+
enabled: ff.enabled ? true : false,
|
|
548
|
+
variant: ff.variant,
|
|
549
|
+
payload: FeatureFlagResult.parse_payload(ff.payload),
|
|
550
|
+
id: metadata ? metadata.id : nil,
|
|
551
|
+
version: metadata ? metadata.version : nil,
|
|
552
|
+
reason: reason ? (reason.description || reason.code) : nil,
|
|
553
|
+
locally_evaluated: false
|
|
554
|
+
)
|
|
555
|
+
end
|
|
556
|
+
rescue StandardError => e
|
|
557
|
+
@on_error&.call(-1, "Error evaluating flags remotely: #{e}")
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
FeatureFlagEvaluations.new(
|
|
562
|
+
host: host,
|
|
563
|
+
distinct_id: distinct_id,
|
|
564
|
+
flags: records,
|
|
565
|
+
groups: groups,
|
|
566
|
+
disable_geoip: disable_geoip,
|
|
567
|
+
request_id: request_id,
|
|
568
|
+
evaluated_at: evaluated_at,
|
|
569
|
+
flag_definitions_loaded_at: @feature_flags_poller.flag_definitions_loaded_at,
|
|
570
|
+
errors_while_computing: errors_while_computing,
|
|
571
|
+
quota_limited: quota_limited
|
|
572
|
+
)
|
|
427
573
|
end
|
|
428
574
|
|
|
429
575
|
# Returns all flags for a given user
|
|
@@ -460,6 +606,7 @@ module PostHog
|
|
|
460
606
|
# @option [Hash] group_properties
|
|
461
607
|
# @option [Boolean] only_evaluate_locally
|
|
462
608
|
#
|
|
609
|
+
# @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#get_flag_payload} instead.
|
|
463
610
|
def get_feature_flag_payload(
|
|
464
611
|
key,
|
|
465
612
|
distinct_id,
|
|
@@ -469,6 +616,13 @@ module PostHog
|
|
|
469
616
|
group_properties: {},
|
|
470
617
|
only_evaluate_locally: false
|
|
471
618
|
)
|
|
619
|
+
_emit_deprecation(
|
|
620
|
+
:get_feature_flag_payload,
|
|
621
|
+
'`get_feature_flag_payload` is deprecated and will be removed in a future major version. ' \
|
|
622
|
+
'Use `client.evaluate_flags(distinct_id, ...)` and call `flags.get_flag_payload(key)` ' \
|
|
623
|
+
'instead — this consolidates flag evaluation into a single `/flags` request per ' \
|
|
624
|
+
'incoming request.'
|
|
625
|
+
)
|
|
472
626
|
person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups,
|
|
473
627
|
person_properties, group_properties)
|
|
474
628
|
@feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties,
|
|
@@ -530,6 +684,84 @@ module PostHog
|
|
|
530
684
|
|
|
531
685
|
private
|
|
532
686
|
|
|
687
|
+
# Shared by the legacy single-flag path ({#get_feature_flag_result}) and the
|
|
688
|
+
# snapshot's access-recording. Owns dedup-key construction, the
|
|
689
|
+
# per-distinct_id sent-flags cache, and the `$feature_flag_called` capture call.
|
|
690
|
+
def _capture_feature_flag_called_if_needed(
|
|
691
|
+
distinct_id: nil, key: nil, response: nil, properties: nil,
|
|
692
|
+
groups: nil, disable_geoip: nil
|
|
693
|
+
)
|
|
694
|
+
reported_key = "#{key}_#{response.nil? ? '::null::' : response}"
|
|
695
|
+
return if @distinct_id_has_sent_flag_calls[distinct_id].include?(reported_key)
|
|
696
|
+
|
|
697
|
+
msg = {
|
|
698
|
+
distinct_id: distinct_id,
|
|
699
|
+
event: '$feature_flag_called',
|
|
700
|
+
properties: properties
|
|
701
|
+
}
|
|
702
|
+
msg[:groups] = groups if groups
|
|
703
|
+
msg[:disable_geoip] = disable_geoip unless disable_geoip.nil?
|
|
704
|
+
|
|
705
|
+
capture(msg)
|
|
706
|
+
@distinct_id_has_sent_flag_calls[distinct_id] << reported_key
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def _feature_flag_evaluations_host
|
|
710
|
+
@feature_flag_evaluations_host ||= FeatureFlagEvaluations::Host.new(
|
|
711
|
+
capture_flag_called_event_if_needed: method(:_capture_feature_flag_called_if_needed),
|
|
712
|
+
log_warning: ->(message) { logger.warn(message) }
|
|
713
|
+
)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Implementation of {#get_feature_flag_result}, called by both the public
|
|
717
|
+
# method and the legacy `is_feature_enabled` / `get_feature_flag` paths so
|
|
718
|
+
# a single user-level call emits exactly one deprecation warning.
|
|
719
|
+
def _get_feature_flag_result(
|
|
720
|
+
key,
|
|
721
|
+
distinct_id,
|
|
722
|
+
groups: {},
|
|
723
|
+
person_properties: {},
|
|
724
|
+
group_properties: {},
|
|
725
|
+
only_evaluate_locally: false,
|
|
726
|
+
send_feature_flag_events: true
|
|
727
|
+
)
|
|
728
|
+
person_properties, group_properties = add_local_person_and_group_properties(
|
|
729
|
+
distinct_id, groups, person_properties, group_properties
|
|
730
|
+
)
|
|
731
|
+
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload =
|
|
732
|
+
@feature_flags_poller.get_feature_flag(
|
|
733
|
+
key, distinct_id, groups, person_properties, group_properties, only_evaluate_locally
|
|
734
|
+
)
|
|
735
|
+
if send_feature_flag_events
|
|
736
|
+
properties = {
|
|
737
|
+
'$feature_flag' => key,
|
|
738
|
+
'$feature_flag_response' => feature_flag_response,
|
|
739
|
+
'locally_evaluated' => flag_was_locally_evaluated
|
|
740
|
+
}
|
|
741
|
+
properties['$feature_flag_request_id'] = request_id if request_id
|
|
742
|
+
properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
|
|
743
|
+
properties['$feature_flag_error'] = feature_flag_error if feature_flag_error
|
|
744
|
+
|
|
745
|
+
_capture_feature_flag_called_if_needed(
|
|
746
|
+
distinct_id: distinct_id, key: key, response: feature_flag_response,
|
|
747
|
+
properties: properties, groups: groups
|
|
748
|
+
)
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
FeatureFlagResult.from_value_and_payload(key, feature_flag_response, payload)
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Emits a deprecation warning at most once per `(method_name, process)` pair.
|
|
755
|
+
# Ruby's `Kernel.warn(..., category: :deprecated)` is suppressed by default
|
|
756
|
+
# since 2.7.2; we emit without the category so messages reach users on a
|
|
757
|
+
# default Ruby setup. Standard logger configuration / `$VERBOSE = nil` / IO
|
|
758
|
+
# redirection still silences as expected.
|
|
759
|
+
def _emit_deprecation(method_name, message)
|
|
760
|
+
return unless @deprecation_emitted_for.add?(method_name)
|
|
761
|
+
|
|
762
|
+
Kernel.warn("[posthog-ruby] DEPRECATION: #{message}", uplevel: 2)
|
|
763
|
+
end
|
|
764
|
+
|
|
533
765
|
# before_send should run immediately before the event is sent to the queue.
|
|
534
766
|
# @param [Object] action The event to be sent to PostHog
|
|
535
767
|
# @return [null, Object, nil] The processed event or nil if the event should not be sent
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module PostHog
|
|
6
|
+
# A snapshot of feature flag evaluations for one distinct_id, returned by
|
|
7
|
+
# PostHog::Client#evaluate_flags. Calls to {#is_enabled} / {#get_flag} fire the
|
|
8
|
+
# `$feature_flag_called` event (deduped through the existing per-distinct_id
|
|
9
|
+
# cache); {#get_flag_payload} does not. Pass the snapshot to `capture(flags:)`
|
|
10
|
+
# to attach `$feature/<key>` and `$active_feature_flags` without a second
|
|
11
|
+
# /flags request.
|
|
12
|
+
class FeatureFlagEvaluations
|
|
13
|
+
EVALUATED_LOCALLY_REASON = 'Evaluated locally'
|
|
14
|
+
|
|
15
|
+
EvaluatedFlagRecord = Struct.new(
|
|
16
|
+
:key, :enabled, :variant, :payload, :id, :version, :reason, :locally_evaluated,
|
|
17
|
+
keyword_init: true
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
Host = Struct.new(:capture_flag_called_event_if_needed, :log_warning, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
attr_reader :distinct_id, :groups, :request_id, :evaluated_at, :flag_definitions_loaded_at
|
|
23
|
+
|
|
24
|
+
def initialize(
|
|
25
|
+
host: nil,
|
|
26
|
+
distinct_id: nil,
|
|
27
|
+
flags: {},
|
|
28
|
+
groups: nil,
|
|
29
|
+
disable_geoip: nil,
|
|
30
|
+
request_id: nil,
|
|
31
|
+
evaluated_at: nil,
|
|
32
|
+
flag_definitions_loaded_at: nil,
|
|
33
|
+
errors_while_computing: false,
|
|
34
|
+
quota_limited: false,
|
|
35
|
+
accessed: nil
|
|
36
|
+
)
|
|
37
|
+
@host = host
|
|
38
|
+
@distinct_id = distinct_id || ''
|
|
39
|
+
@flags = flags || {}
|
|
40
|
+
@groups = groups
|
|
41
|
+
@disable_geoip = disable_geoip
|
|
42
|
+
@request_id = request_id
|
|
43
|
+
@evaluated_at = evaluated_at
|
|
44
|
+
@flag_definitions_loaded_at = flag_definitions_loaded_at
|
|
45
|
+
@errors_while_computing = errors_while_computing
|
|
46
|
+
@quota_limited = quota_limited
|
|
47
|
+
@accessed = Set.new(accessed || [])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def keys
|
|
51
|
+
@flags.keys
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def enabled?(key)
|
|
55
|
+
key = key.to_s
|
|
56
|
+
flag = @flags[key]
|
|
57
|
+
_record_access(key, flag)
|
|
58
|
+
flag&.enabled ? true : false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get_flag(key)
|
|
62
|
+
key = key.to_s
|
|
63
|
+
flag = @flags[key]
|
|
64
|
+
_record_access(key, flag)
|
|
65
|
+
_flag_value(flag)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def get_flag_payload(key)
|
|
69
|
+
flag = @flags[key.to_s]
|
|
70
|
+
flag&.payload
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Order-dependent: if nothing has been accessed yet, the returned snapshot is
|
|
74
|
+
# empty. The method honors its name — pre-access flags before calling this if
|
|
75
|
+
# you want a populated result.
|
|
76
|
+
def only_accessed
|
|
77
|
+
_clone_with(@flags.slice(*@accessed))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def only(keys)
|
|
81
|
+
keys = Array(keys).map(&:to_s)
|
|
82
|
+
missing = keys.reject { |k| @flags.key?(k) }
|
|
83
|
+
unless missing.empty?
|
|
84
|
+
@host.log_warning.call(
|
|
85
|
+
'FeatureFlagEvaluations#only was called with flag keys that are not in the ' \
|
|
86
|
+
"evaluation set and will be dropped: #{missing.join(', ')}"
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
filtered = @flags.slice(*keys)
|
|
90
|
+
_clone_with(filtered)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Builds the `$feature/<key>` and `$active_feature_flags` properties for a
|
|
94
|
+
# captured event. Called from PostHog::Client#capture when `flags:` is set.
|
|
95
|
+
def _get_event_properties
|
|
96
|
+
properties = {}
|
|
97
|
+
active = []
|
|
98
|
+
@flags.each do |key, flag|
|
|
99
|
+
properties["$feature/#{key}"] = flag.enabled ? (flag.variant || true) : false
|
|
100
|
+
active << key if flag.enabled
|
|
101
|
+
end
|
|
102
|
+
properties['$active_feature_flags'] = active.sort unless active.empty?
|
|
103
|
+
properties
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Canonical "stored" value for a flag — used for both the
|
|
109
|
+
# `$feature_flag_response` event property and the dedup cache key, so
|
|
110
|
+
# `enabled?` and `get_flag` collapse to a single exposure per flag.
|
|
111
|
+
# Variant string when present, else boolean enabled status; `nil` for
|
|
112
|
+
# unknown flags.
|
|
113
|
+
def _flag_value(flag)
|
|
114
|
+
return nil if flag.nil?
|
|
115
|
+
return flag.variant if flag.variant
|
|
116
|
+
|
|
117
|
+
flag.enabled ? true : false
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def _record_access(key, flag)
|
|
121
|
+
@accessed.add(key)
|
|
122
|
+
return if @distinct_id.nil? || @distinct_id.to_s.empty?
|
|
123
|
+
|
|
124
|
+
response = _flag_value(flag)
|
|
125
|
+
properties = {
|
|
126
|
+
'$feature_flag' => key,
|
|
127
|
+
'$feature_flag_response' => response,
|
|
128
|
+
'locally_evaluated' => flag&.locally_evaluated ? true : false,
|
|
129
|
+
"$feature/#{key}" => response
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if flag
|
|
133
|
+
properties['$feature_flag_payload'] = flag.payload unless flag.payload.nil?
|
|
134
|
+
properties['$feature_flag_id'] = flag.id if flag.id
|
|
135
|
+
properties['$feature_flag_version'] = flag.version if flag.version
|
|
136
|
+
properties['$feature_flag_reason'] = flag.reason if flag.reason
|
|
137
|
+
if flag.locally_evaluated && @flag_definitions_loaded_at
|
|
138
|
+
properties['$feature_flag_definitions_loaded_at'] = @flag_definitions_loaded_at
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
properties['$feature_flag_request_id'] = @request_id if @request_id
|
|
143
|
+
properties['$feature_flag_evaluated_at'] = @evaluated_at if @evaluated_at && !(flag && flag.locally_evaluated)
|
|
144
|
+
|
|
145
|
+
errors = []
|
|
146
|
+
errors << 'errors_while_computing_flags' if @errors_while_computing
|
|
147
|
+
errors << 'quota_limited' if @quota_limited
|
|
148
|
+
errors << 'flag_missing' if flag.nil?
|
|
149
|
+
properties['$feature_flag_error'] = errors.join(',') unless errors.empty?
|
|
150
|
+
|
|
151
|
+
@host.capture_flag_called_event_if_needed.call(
|
|
152
|
+
distinct_id: @distinct_id,
|
|
153
|
+
key: key,
|
|
154
|
+
response: response,
|
|
155
|
+
properties: properties,
|
|
156
|
+
groups: @groups,
|
|
157
|
+
disable_geoip: @disable_geoip
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def _clone_with(flags)
|
|
162
|
+
self.class.new(
|
|
163
|
+
host: @host,
|
|
164
|
+
distinct_id: @distinct_id,
|
|
165
|
+
flags: flags,
|
|
166
|
+
groups: @groups,
|
|
167
|
+
disable_geoip: @disable_geoip,
|
|
168
|
+
request_id: @request_id,
|
|
169
|
+
evaluated_at: @evaluated_at,
|
|
170
|
+
flag_definitions_loaded_at: @flag_definitions_loaded_at,
|
|
171
|
+
errors_while_computing: @errors_while_computing,
|
|
172
|
+
quota_limited: @quota_limited,
|
|
173
|
+
accessed: @accessed.dup
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -39,6 +39,10 @@ module PostHog
|
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Deserialize a flag payload. Strings are JSON-parsed (with the raw string
|
|
43
|
+
# returned when the body is not valid JSON); already-deserialized values
|
|
44
|
+
# pass through. Public so {FeatureFlagEvaluations} can normalize payloads
|
|
45
|
+
# the same way {FeatureFlagResult} does.
|
|
42
46
|
def self.parse_payload(payload)
|
|
43
47
|
return nil if payload.nil?
|
|
44
48
|
return payload unless payload.is_a?(String)
|
|
@@ -50,7 +54,5 @@ module PostHog
|
|
|
50
54
|
payload
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
|
-
|
|
54
|
-
private_class_method :parse_payload
|
|
55
57
|
end
|
|
56
58
|
end
|
|
@@ -46,6 +46,7 @@ module PostHog
|
|
|
46
46
|
@on_error = on_error || proc { |status, error| }
|
|
47
47
|
@quota_limited = Concurrent::AtomicBoolean.new(false)
|
|
48
48
|
@flags_etag = Concurrent::AtomicReference.new(nil)
|
|
49
|
+
@flag_definitions_loaded_at = nil
|
|
49
50
|
|
|
50
51
|
@flag_definition_cache_provider = flag_definition_cache_provider
|
|
51
52
|
FlagDefinitionCacheProvider.validate!(@flag_definition_cache_provider) if @flag_definition_cache_provider
|
|
@@ -72,6 +73,8 @@ module PostHog
|
|
|
72
73
|
_load_feature_flags
|
|
73
74
|
end
|
|
74
75
|
|
|
76
|
+
attr_reader :flag_definitions_loaded_at, :feature_flags_by_key
|
|
77
|
+
|
|
75
78
|
def get_feature_variants(
|
|
76
79
|
distinct_id,
|
|
77
80
|
groups = {},
|
|
@@ -120,13 +123,16 @@ module PostHog
|
|
|
120
123
|
end
|
|
121
124
|
end
|
|
122
125
|
|
|
123
|
-
def get_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}
|
|
126
|
+
def get_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}, flag_keys = nil,
|
|
127
|
+
disable_geoip = nil)
|
|
124
128
|
request_data = {
|
|
125
129
|
distinct_id: distinct_id,
|
|
126
130
|
groups: groups,
|
|
127
131
|
person_properties: person_properties,
|
|
128
132
|
group_properties: group_properties
|
|
129
133
|
}
|
|
134
|
+
request_data[:flag_keys_to_evaluate] = flag_keys if flag_keys && !flag_keys.empty?
|
|
135
|
+
request_data[:geoip_disable] = true if disable_geoip
|
|
130
136
|
|
|
131
137
|
flags_response = _request_feature_flag_evaluation(request_data)
|
|
132
138
|
|
|
@@ -1124,11 +1130,12 @@ module PostHog
|
|
|
1124
1130
|
@cohorts = Concurrent::Hash[deep_symbolize_keys(cohorts)]
|
|
1125
1131
|
|
|
1126
1132
|
logger.debug "Loaded #{@feature_flags.length} feature flags and #{@cohorts.length} cohorts"
|
|
1133
|
+
@flag_definitions_loaded_at = (Time.now.to_f * 1000).to_i
|
|
1127
1134
|
@loaded_flags_successfully_once.make_true if @loaded_flags_successfully_once.false?
|
|
1128
1135
|
end
|
|
1129
1136
|
|
|
1130
1137
|
def _request_feature_flag_definitions(etag: nil)
|
|
1131
|
-
uri = URI("#{@host}/
|
|
1138
|
+
uri = URI("#{@host}/flags/definitions")
|
|
1132
1139
|
uri.query = URI.encode_www_form([['token', @project_api_key], %w[send_cohorts true]])
|
|
1133
1140
|
req = Net::HTTP::Get.new(uri)
|
|
1134
1141
|
req['Authorization'] = "Bearer #{@personal_api_key}"
|
data/lib/posthog/version.rb
CHANGED
data/lib/posthog.rb
CHANGED
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.
|
|
4
|
+
version: 3.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
@@ -41,6 +41,7 @@ files:
|
|
|
41
41
|
- lib/posthog/exception_capture.rb
|
|
42
42
|
- lib/posthog/feature_flag.rb
|
|
43
43
|
- lib/posthog/feature_flag_error.rb
|
|
44
|
+
- lib/posthog/feature_flag_evaluations.rb
|
|
44
45
|
- lib/posthog/feature_flag_result.rb
|
|
45
46
|
- lib/posthog/feature_flags.rb
|
|
46
47
|
- lib/posthog/field_parser.rb
|