posthog-ruby 3.6.4 → 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 +312 -61
- data/lib/posthog/defaults.rb +1 -1
- 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
|
|
|
@@ -59,7 +60,7 @@ module PostHog
|
|
|
59
60
|
# on the calling thread. Useful in forking environments like Sidekiq
|
|
60
61
|
# and Resque. Defaults to +false+.
|
|
61
62
|
# @option opts [Proc] :on_error Handles error calls from the API.
|
|
62
|
-
# @option opts [String] :host Fully qualified hostname of the PostHog server. Defaults to `https://
|
|
63
|
+
# @option opts [String] :host Fully qualified hostname of the PostHog server. Defaults to `https://us.i.posthog.com`
|
|
63
64
|
# @option opts [Integer] :feature_flags_polling_interval How often to poll for feature flag definition changes.
|
|
64
65
|
# Measured in seconds, defaults to 30.
|
|
65
66
|
# @option opts [Integer] :feature_flag_request_timeout_seconds How long to wait for feature flag evaluation.
|
|
@@ -74,7 +75,9 @@ module PostHog
|
|
|
74
75
|
def initialize(opts = {})
|
|
75
76
|
symbolize_keys!(opts)
|
|
76
77
|
|
|
77
|
-
opts[:
|
|
78
|
+
opts[:api_key] = normalize_string_option(opts[:api_key])
|
|
79
|
+
opts[:personal_api_key] = normalize_string_option(opts[:personal_api_key], blank_as_nil: true)
|
|
80
|
+
opts[:host] = normalize_host_option(opts[:host])
|
|
78
81
|
|
|
79
82
|
@queue = Queue.new
|
|
80
83
|
@api_key = opts[:api_key]
|
|
@@ -102,6 +105,7 @@ module PostHog
|
|
|
102
105
|
@personal_api_key = opts[:personal_api_key]
|
|
103
106
|
|
|
104
107
|
check_api_key!
|
|
108
|
+
logger.error('api_key is empty after trimming whitespace; check your project API key') if @api_key == ''
|
|
105
109
|
|
|
106
110
|
# Warn when multiple clients are created with the same API key (can cause dropped events)
|
|
107
111
|
unless opts[:test_mode] || opts[:disable_singleton_warning]
|
|
@@ -132,6 +136,7 @@ module PostHog
|
|
|
132
136
|
end
|
|
133
137
|
|
|
134
138
|
@before_send = opts[:before_send]
|
|
139
|
+
@deprecation_emitted_for = Concurrent::Set.new
|
|
135
140
|
end
|
|
136
141
|
|
|
137
142
|
# Synchronously waits until the worker has cleared the queue.
|
|
@@ -172,6 +177,10 @@ module PostHog
|
|
|
172
177
|
# @option attrs [Hash] :properties Event properties (optional)
|
|
173
178
|
# @option attrs [Bool, Hash, SendFeatureFlagsOptions] :send_feature_flags
|
|
174
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`.
|
|
175
184
|
# @option attrs [String] :uuid ID that uniquely identifies an event;
|
|
176
185
|
# events in PostHog are deduplicated by the
|
|
177
186
|
# combination of teamId, timestamp date,
|
|
@@ -180,8 +189,39 @@ module PostHog
|
|
|
180
189
|
def capture(attrs)
|
|
181
190
|
symbolize_keys! attrs
|
|
182
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
|
+
|
|
183
216
|
send_feature_flags_param = attrs[:send_feature_flags]
|
|
184
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
|
+
)
|
|
185
225
|
# Handle different types of send_feature_flags parameter
|
|
186
226
|
case send_feature_flags_param
|
|
187
227
|
when true
|
|
@@ -222,7 +262,10 @@ module PostHog
|
|
|
222
262
|
# @param [Exception, String, Object] exception The exception to capture, a string message, or exception-like object
|
|
223
263
|
# @param [String] distinct_id The ID for the user (optional, defaults to a generated UUID)
|
|
224
264
|
# @param [Hash] additional_properties Additional properties to include with the exception event (optional)
|
|
225
|
-
|
|
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)
|
|
226
269
|
exception_info = ExceptionCapture.build_parsed_exception(exception)
|
|
227
270
|
|
|
228
271
|
return if exception_info.nil?
|
|
@@ -240,6 +283,7 @@ module PostHog
|
|
|
240
283
|
properties: properties,
|
|
241
284
|
timestamp: Time.now
|
|
242
285
|
}
|
|
286
|
+
event_data[:flags] = flags if flags
|
|
243
287
|
|
|
244
288
|
capture(event_data)
|
|
245
289
|
end
|
|
@@ -290,6 +334,7 @@ module PostHog
|
|
|
290
334
|
@queue.length
|
|
291
335
|
end
|
|
292
336
|
|
|
337
|
+
# @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#is_enabled} instead.
|
|
293
338
|
# TODO: In future version, rename to `feature_flag_enabled?`
|
|
294
339
|
def is_feature_enabled( # rubocop:disable Naming/PredicateName
|
|
295
340
|
flag_key,
|
|
@@ -300,15 +345,20 @@ module PostHog
|
|
|
300
345
|
only_evaluate_locally: false,
|
|
301
346
|
send_feature_flag_events: true
|
|
302
347
|
)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
311
360
|
)
|
|
361
|
+
response = result&.value
|
|
312
362
|
return nil if response.nil?
|
|
313
363
|
|
|
314
364
|
!!response
|
|
@@ -341,6 +391,7 @@ module PostHog
|
|
|
341
391
|
# ```ruby
|
|
342
392
|
# group_properties: {"organization": {"name": "PostHog", "employees": 11}}
|
|
343
393
|
# ```
|
|
394
|
+
# @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#get_flag} instead.
|
|
344
395
|
def get_feature_flag(
|
|
345
396
|
key,
|
|
346
397
|
distinct_id,
|
|
@@ -350,77 +401,175 @@ module PostHog
|
|
|
350
401
|
only_evaluate_locally: false,
|
|
351
402
|
send_feature_flag_events: true
|
|
352
403
|
)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
361
415
|
)
|
|
362
416
|
result&.value
|
|
363
417
|
end
|
|
364
418
|
|
|
365
|
-
#
|
|
366
|
-
#
|
|
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.
|
|
367
451
|
#
|
|
368
|
-
# @param [String] key The key of the feature flag
|
|
369
452
|
# @param [String] distinct_id The distinct id of the user
|
|
370
453
|
# @param [Hash] groups
|
|
371
|
-
# @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
|
|
372
455
|
# @param [Hash] group_properties
|
|
373
|
-
# @param [Boolean] only_evaluate_locally
|
|
374
|
-
# @param [Boolean]
|
|
375
|
-
#
|
|
376
|
-
#
|
|
377
|
-
#
|
|
378
|
-
|
|
379
|
-
|
|
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(
|
|
380
464
|
distinct_id,
|
|
381
465
|
groups: {},
|
|
382
466
|
person_properties: {},
|
|
383
467
|
group_properties: {},
|
|
384
468
|
only_evaluate_locally: false,
|
|
385
|
-
|
|
469
|
+
disable_geoip: nil,
|
|
470
|
+
flag_keys: nil
|
|
386
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
|
+
|
|
387
478
|
person_properties, group_properties = add_local_person_and_group_properties(
|
|
388
|
-
distinct_id,
|
|
389
|
-
groups,
|
|
390
|
-
person_properties,
|
|
391
|
-
group_properties
|
|
479
|
+
distinct_id, groups, person_properties, group_properties
|
|
392
480
|
)
|
|
393
|
-
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload =
|
|
394
|
-
@feature_flags_poller.get_feature_flag(
|
|
395
|
-
key,
|
|
396
|
-
distinct_id,
|
|
397
|
-
groups,
|
|
398
|
-
person_properties,
|
|
399
|
-
group_properties,
|
|
400
|
-
only_evaluate_locally
|
|
401
|
-
)
|
|
402
|
-
feature_flag_reported_key = "#{key}_#{feature_flag_response}"
|
|
403
481
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
'$feature_flag_response' => feature_flag_response,
|
|
408
|
-
'locally_evaluated' => flag_was_locally_evaluated
|
|
409
|
-
}
|
|
410
|
-
properties['$feature_flag_request_id'] = request_id if request_id
|
|
411
|
-
properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
|
|
412
|
-
properties['$feature_flag_error'] = feature_flag_error if feature_flag_error
|
|
482
|
+
records = {}
|
|
483
|
+
locally_evaluated_keys = Set.new
|
|
484
|
+
flag_keys_set = flag_keys&.to_set(&:to_s)
|
|
413
485
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
500
|
+
|
|
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
|
|
419
514
|
)
|
|
420
|
-
|
|
515
|
+
locally_evaluated_keys << key.to_s
|
|
421
516
|
end
|
|
422
517
|
|
|
423
|
-
|
|
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
|
+
)
|
|
424
573
|
end
|
|
425
574
|
|
|
426
575
|
# Returns all flags for a given user
|
|
@@ -457,6 +606,7 @@ module PostHog
|
|
|
457
606
|
# @option [Hash] group_properties
|
|
458
607
|
# @option [Boolean] only_evaluate_locally
|
|
459
608
|
#
|
|
609
|
+
# @deprecated Use {#evaluate_flags} and {FeatureFlagEvaluations#get_flag_payload} instead.
|
|
460
610
|
def get_feature_flag_payload(
|
|
461
611
|
key,
|
|
462
612
|
distinct_id,
|
|
@@ -466,6 +616,13 @@ module PostHog
|
|
|
466
616
|
group_properties: {},
|
|
467
617
|
only_evaluate_locally: false
|
|
468
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
|
+
)
|
|
469
626
|
person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups,
|
|
470
627
|
person_properties, group_properties)
|
|
471
628
|
@feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties,
|
|
@@ -527,6 +684,84 @@ module PostHog
|
|
|
527
684
|
|
|
528
685
|
private
|
|
529
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
|
+
|
|
530
765
|
# before_send should run immediately before the event is sent to the queue.
|
|
531
766
|
# @param [Object] action The event to be sent to PostHog
|
|
532
767
|
# @return [null, Object, nil] The processed event or nil if the event should not be sent
|
|
@@ -585,6 +820,22 @@ module PostHog
|
|
|
585
820
|
raise ArgumentError, 'API key must be initialized' if @api_key.nil?
|
|
586
821
|
end
|
|
587
822
|
|
|
823
|
+
def normalize_string_option(value, blank_as_nil: false)
|
|
824
|
+
return value unless value.is_a?(String)
|
|
825
|
+
|
|
826
|
+
normalized = value.strip
|
|
827
|
+
return nil if blank_as_nil && normalized.empty?
|
|
828
|
+
|
|
829
|
+
normalized
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def normalize_host_option(host)
|
|
833
|
+
normalized = normalize_string_option(host)
|
|
834
|
+
return 'https://us.i.posthog.com' if normalized.nil? || normalized.empty?
|
|
835
|
+
|
|
836
|
+
normalized
|
|
837
|
+
end
|
|
838
|
+
|
|
588
839
|
def ensure_worker_running
|
|
589
840
|
return if worker_running?
|
|
590
841
|
|
data/lib/posthog/defaults.rb
CHANGED
|
@@ -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
|