posthog-ruby 3.6.0 → 3.6.1
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 +5 -1
- data/lib/posthog/feature_flags.rb +83 -11
- data/lib/posthog/flag_definition_cache.rb +78 -0
- data/lib/posthog/utils.rb +15 -0
- 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: dc278624783a997ea124cc47e7d6724c2160b10509dde00293725e66ca4edceb
|
|
4
|
+
data.tar.gz: db194b891449029e91c8f20d82b873fe8aebe69182db77053ce966366af94ea9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 294eacce50eef253e5d6890c2c972c300e8caab9462889648f89cb2449a06357343515d54935b69b83ae0707e2c77adc2010bad8efc14f7e393c8427744422b3
|
|
7
|
+
data.tar.gz: ffa9da1ee5e32b462e83bc3a4f362bf855dd62931d3351dd41850909300c85921cd2e6b56851fd752f165c9a429cb02b28336a69155187451b4326ca65d6798d
|
data/lib/posthog/client.rb
CHANGED
|
@@ -68,6 +68,9 @@ module PostHog
|
|
|
68
68
|
# to be sent to PostHog or nil to prevent the event from being sent. e.g. `before_send: ->(event) { event }`
|
|
69
69
|
# @option opts [Bool] :disable_singleton_warning +true+ to suppress the warning when multiple clients
|
|
70
70
|
# share the same API key. Use only when you intentionally need multiple clients. Defaults to +false+.
|
|
71
|
+
# @option opts [Object] :flag_definition_cache_provider An object implementing the
|
|
72
|
+
# {FlagDefinitionCacheProvider} interface for distributed flag definition caching.
|
|
73
|
+
# EXPERIMENTAL: This API may change in future minor version bumps.
|
|
71
74
|
def initialize(opts = {})
|
|
72
75
|
symbolize_keys!(opts)
|
|
73
76
|
|
|
@@ -120,7 +123,8 @@ module PostHog
|
|
|
120
123
|
@api_key,
|
|
121
124
|
opts[:host],
|
|
122
125
|
opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS,
|
|
123
|
-
opts[:on_error]
|
|
126
|
+
opts[:on_error],
|
|
127
|
+
flag_definition_cache_provider: opts[:flag_definition_cache_provider]
|
|
124
128
|
)
|
|
125
129
|
|
|
126
130
|
@distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE) do |hash, key|
|
|
@@ -6,6 +6,7 @@ require 'json'
|
|
|
6
6
|
require 'posthog/version'
|
|
7
7
|
require 'posthog/logging'
|
|
8
8
|
require 'posthog/feature_flag'
|
|
9
|
+
require 'posthog/flag_definition_cache'
|
|
9
10
|
require 'digest'
|
|
10
11
|
|
|
11
12
|
module PostHog
|
|
@@ -29,7 +30,8 @@ module PostHog
|
|
|
29
30
|
project_api_key,
|
|
30
31
|
host,
|
|
31
32
|
feature_flag_request_timeout_seconds,
|
|
32
|
-
on_error = nil
|
|
33
|
+
on_error = nil,
|
|
34
|
+
flag_definition_cache_provider: nil
|
|
33
35
|
)
|
|
34
36
|
@polling_interval = polling_interval || 30
|
|
35
37
|
@personal_api_key = personal_api_key
|
|
@@ -44,6 +46,10 @@ module PostHog
|
|
|
44
46
|
@on_error = on_error || proc { |status, error| }
|
|
45
47
|
@quota_limited = Concurrent::AtomicBoolean.new(false)
|
|
46
48
|
@flags_etag = Concurrent::AtomicReference.new(nil)
|
|
49
|
+
|
|
50
|
+
@flag_definition_cache_provider = flag_definition_cache_provider
|
|
51
|
+
FlagDefinitionCacheProvider.validate!(@flag_definition_cache_provider) if @flag_definition_cache_provider
|
|
52
|
+
|
|
47
53
|
@task =
|
|
48
54
|
Concurrent::TimerTask.new(
|
|
49
55
|
execution_interval: polling_interval
|
|
@@ -372,6 +378,13 @@ module PostHog
|
|
|
372
378
|
|
|
373
379
|
def shutdown_poller
|
|
374
380
|
@task.shutdown
|
|
381
|
+
return unless @flag_definition_cache_provider
|
|
382
|
+
|
|
383
|
+
begin
|
|
384
|
+
@flag_definition_cache_provider.shutdown
|
|
385
|
+
rescue StandardError => e
|
|
386
|
+
logger.error("[FEATURE FLAGS] Cache provider shutdown error: #{e}")
|
|
387
|
+
end
|
|
375
388
|
end
|
|
376
389
|
|
|
377
390
|
# Class methods
|
|
@@ -1006,6 +1019,38 @@ module PostHog
|
|
|
1006
1019
|
end
|
|
1007
1020
|
|
|
1008
1021
|
def _load_feature_flags
|
|
1022
|
+
should_fetch = true
|
|
1023
|
+
|
|
1024
|
+
if @flag_definition_cache_provider
|
|
1025
|
+
begin
|
|
1026
|
+
should_fetch = @flag_definition_cache_provider.should_fetch_flag_definitions?
|
|
1027
|
+
rescue StandardError => e
|
|
1028
|
+
logger.error("[FEATURE FLAGS] Cache provider should_fetch error: #{e}")
|
|
1029
|
+
should_fetch = true
|
|
1030
|
+
end
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
unless should_fetch
|
|
1034
|
+
begin
|
|
1035
|
+
cached_data = @flag_definition_cache_provider.flag_definitions
|
|
1036
|
+
if cached_data
|
|
1037
|
+
logger.debug '[FEATURE FLAGS] Using cached flag definitions from external cache'
|
|
1038
|
+
_apply_flag_definitions(cached_data)
|
|
1039
|
+
return
|
|
1040
|
+
elsif @feature_flags.empty?
|
|
1041
|
+
# Emergency fallback: cache empty and no flags loaded -> fetch from API
|
|
1042
|
+
should_fetch = true
|
|
1043
|
+
end
|
|
1044
|
+
rescue StandardError => e
|
|
1045
|
+
logger.error("[FEATURE FLAGS] Cache provider get error: #{e}")
|
|
1046
|
+
should_fetch = true
|
|
1047
|
+
end
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
_fetch_and_apply_flag_definitions if should_fetch
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
def _fetch_and_apply_flag_definitions
|
|
1009
1054
|
begin
|
|
1010
1055
|
res = _request_feature_flag_definitions(etag: @flags_etag.value)
|
|
1011
1056
|
rescue StandardError => e
|
|
@@ -1040,21 +1085,48 @@ module PostHog
|
|
|
1040
1085
|
# Only update ETag on successful responses with flag data
|
|
1041
1086
|
@flags_etag.value = res[:etag]
|
|
1042
1087
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
@feature_flags.each do |flag|
|
|
1046
|
-
@feature_flags_by_key[flag[:key]] = flag unless flag[:key].nil?
|
|
1047
|
-
end
|
|
1048
|
-
@group_type_mapping = res[:group_type_mapping] || {}
|
|
1049
|
-
@cohorts = res[:cohorts] || {}
|
|
1050
|
-
|
|
1051
|
-
logger.debug "Loaded #{@feature_flags.length} feature flags and #{@cohorts.length} cohorts"
|
|
1052
|
-
@loaded_flags_successfully_once.make_true if @loaded_flags_successfully_once.false?
|
|
1088
|
+
_apply_flag_definitions(res)
|
|
1089
|
+
_store_in_cache_provider
|
|
1053
1090
|
else
|
|
1054
1091
|
logger.debug "Failed to load feature flags: #{res}"
|
|
1055
1092
|
end
|
|
1056
1093
|
end
|
|
1057
1094
|
|
|
1095
|
+
def _store_in_cache_provider
|
|
1096
|
+
return unless @flag_definition_cache_provider
|
|
1097
|
+
|
|
1098
|
+
begin
|
|
1099
|
+
data = {
|
|
1100
|
+
flags: @feature_flags.to_a,
|
|
1101
|
+
group_type_mapping: @group_type_mapping.to_h,
|
|
1102
|
+
cohorts: @cohorts.to_h
|
|
1103
|
+
}
|
|
1104
|
+
@flag_definition_cache_provider.on_flag_definitions_received(data)
|
|
1105
|
+
rescue StandardError => e
|
|
1106
|
+
logger.error("[FEATURE FLAGS] Cache provider store error: #{e}")
|
|
1107
|
+
end
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
def _apply_flag_definitions(data)
|
|
1111
|
+
flags = get_by_symbol_or_string_key(data, 'flags') || []
|
|
1112
|
+
group_type_mapping = get_by_symbol_or_string_key(data, 'group_type_mapping') || {}
|
|
1113
|
+
cohorts = get_by_symbol_or_string_key(data, 'cohorts') || {}
|
|
1114
|
+
|
|
1115
|
+
@feature_flags = Concurrent::Array.new(flags.map { |f| deep_symbolize_keys(f) })
|
|
1116
|
+
|
|
1117
|
+
new_by_key = {}
|
|
1118
|
+
@feature_flags.each do |flag|
|
|
1119
|
+
new_by_key[flag[:key]] = flag unless flag[:key].nil?
|
|
1120
|
+
end
|
|
1121
|
+
@feature_flags_by_key = new_by_key
|
|
1122
|
+
|
|
1123
|
+
@group_type_mapping = Concurrent::Hash[deep_symbolize_keys(group_type_mapping)]
|
|
1124
|
+
@cohorts = Concurrent::Hash[deep_symbolize_keys(cohorts)]
|
|
1125
|
+
|
|
1126
|
+
logger.debug "Loaded #{@feature_flags.length} feature flags and #{@cohorts.length} cohorts"
|
|
1127
|
+
@loaded_flags_successfully_once.make_true if @loaded_flags_successfully_once.false?
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1058
1130
|
def _request_feature_flag_definitions(etag: nil)
|
|
1059
1131
|
uri = URI("#{@host}/api/feature_flag/local_evaluation")
|
|
1060
1132
|
uri.query = URI.encode_www_form([['token', @project_api_key], %w[send_cohorts true]])
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostHog
|
|
4
|
+
# Interface for external caching of feature flag definitions.
|
|
5
|
+
#
|
|
6
|
+
# EXPERIMENTAL: This API may change in future minor version bumps.
|
|
7
|
+
#
|
|
8
|
+
# Enables multi-worker environments (Kubernetes, load-balanced servers,
|
|
9
|
+
# serverless functions) to share flag definitions via an external cache,
|
|
10
|
+
# reducing redundant API calls.
|
|
11
|
+
#
|
|
12
|
+
# Implement the four required methods on any object and pass it as the
|
|
13
|
+
# +:flag_definition_cache_provider+ option when creating a {Client}.
|
|
14
|
+
#
|
|
15
|
+
# == Required Methods
|
|
16
|
+
#
|
|
17
|
+
# [+flag_definitions+]
|
|
18
|
+
# Retrieve cached flag definitions. Return a Hash with +:flags+,
|
|
19
|
+
# +:group_type_mapping+, and +:cohorts+ keys, or +nil+ if the cache
|
|
20
|
+
# is empty. Returning +nil+ triggers an API fetch when no flags are
|
|
21
|
+
# loaded yet (emergency fallback).
|
|
22
|
+
#
|
|
23
|
+
# [+should_fetch_flag_definitions?+]
|
|
24
|
+
# Return +true+ if this instance should fetch new definitions from the
|
|
25
|
+
# API, +false+ to read from cache instead. Use for distributed lock
|
|
26
|
+
# coordination so only one worker fetches at a time.
|
|
27
|
+
#
|
|
28
|
+
# [+on_flag_definitions_received(data)+]
|
|
29
|
+
# 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.
|
|
33
|
+
#
|
|
34
|
+
# [+shutdown+]
|
|
35
|
+
# Called when the PostHog client shuts down. Release any distributed
|
|
36
|
+
# locks and clean up resources.
|
|
37
|
+
#
|
|
38
|
+
# == Error Handling
|
|
39
|
+
#
|
|
40
|
+
# All methods are wrapped in +begin/rescue+. Errors are logged but never
|
|
41
|
+
# break flag evaluation:
|
|
42
|
+
# - +should_fetch_flag_definitions?+ errors default to fetching (fail-safe)
|
|
43
|
+
# - +flag_definitions+ errors fall back to API fetch
|
|
44
|
+
# - +on_flag_definitions_received+ errors are logged; flags remain in memory
|
|
45
|
+
# - +shutdown+ errors are logged; shutdown continues
|
|
46
|
+
#
|
|
47
|
+
# == Example
|
|
48
|
+
#
|
|
49
|
+
# cache = RedisFlagCache.new(redis, service_key: 'my-service')
|
|
50
|
+
# client = PostHog::Client.new(
|
|
51
|
+
# api_key: '<project_api_key>',
|
|
52
|
+
# personal_api_key: '<personal_api_key>',
|
|
53
|
+
# flag_definition_cache_provider: cache
|
|
54
|
+
# )
|
|
55
|
+
#
|
|
56
|
+
module FlagDefinitionCacheProvider
|
|
57
|
+
REQUIRED_METHODS = %i[
|
|
58
|
+
flag_definitions
|
|
59
|
+
should_fetch_flag_definitions?
|
|
60
|
+
on_flag_definitions_received
|
|
61
|
+
shutdown
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
# Validates that +provider+ implements all required methods.
|
|
65
|
+
# Raises +ArgumentError+ listing any missing methods.
|
|
66
|
+
#
|
|
67
|
+
# @param provider [Object] the cache provider to validate
|
|
68
|
+
# @raise [ArgumentError] if any required methods are missing
|
|
69
|
+
def self.validate!(provider)
|
|
70
|
+
missing = REQUIRED_METHODS.reject { |m| provider.respond_to?(m) }
|
|
71
|
+
return if missing.empty?
|
|
72
|
+
|
|
73
|
+
raise ArgumentError,
|
|
74
|
+
"Flag definition cache provider is missing required methods: #{missing.join(', ')}. " \
|
|
75
|
+
'See PostHog::FlagDefinitionCacheProvider for the required interface.'
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/posthog/utils.rb
CHANGED
|
@@ -27,6 +27,21 @@ module PostHog
|
|
|
27
27
|
hash.transform_keys(&:to_s)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# public: Recursively convert all keys to symbols in a Hash/Array tree
|
|
31
|
+
#
|
|
32
|
+
def deep_symbolize_keys(obj)
|
|
33
|
+
case obj
|
|
34
|
+
when Hash
|
|
35
|
+
obj.each_with_object({}) do |(key, value), result|
|
|
36
|
+
result[key.to_sym] = deep_symbolize_keys(value)
|
|
37
|
+
end
|
|
38
|
+
when Array
|
|
39
|
+
obj.map { |item| deep_symbolize_keys(item) }
|
|
40
|
+
else
|
|
41
|
+
obj
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
30
45
|
# public: Returns a new hash with all the date values in the into iso8601
|
|
31
46
|
# strings
|
|
32
47
|
#
|
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.6.
|
|
4
|
+
version: 3.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
@@ -44,6 +44,7 @@ files:
|
|
|
44
44
|
- lib/posthog/feature_flag_result.rb
|
|
45
45
|
- lib/posthog/feature_flags.rb
|
|
46
46
|
- lib/posthog/field_parser.rb
|
|
47
|
+
- lib/posthog/flag_definition_cache.rb
|
|
47
48
|
- lib/posthog/logging.rb
|
|
48
49
|
- lib/posthog/message_batch.rb
|
|
49
50
|
- lib/posthog/noop_worker.rb
|