Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fresh-flag-reload-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"posthog-ruby": patch
"posthog-rails": patch
---

Reset feature flag event deduplication when local flag definitions are refreshed or discarded, allowing the next flag access to emit a fresh event.
15 changes: 9 additions & 6 deletions lib/posthog/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ def initialize(opts = {})
end
end

# Initialize tracking before the poller can load definitions (including async loads).
@distinct_id_has_sent_flag_calls_mutex = Mutex.new
@distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE) do |hash, key|
hash[key] = SizeLimitedArray.new(Defaults::MAX_HASH_SIZE)
end

unless @disabled
@feature_flags_poller =
FeatureFlagsPoller.new(
Expand All @@ -203,15 +209,12 @@ def initialize(opts = {})
flag_definition_cache_provider: opts[:flag_definition_cache_provider],
feature_flag_request_max_retries: opts[:feature_flag_request_max_retries],
async_load: opts[:feature_flags_async_load] == true,
user_agent: @headers['User-Agent']
user_agent: @headers['User-Agent'],
flag_definitions_update_mutex: @distinct_id_has_sent_flag_calls_mutex,
on_flag_definitions_updated: -> { @distinct_id_has_sent_flag_calls.clear }
)
end

@distinct_id_has_sent_flag_calls_mutex = Mutex.new
@distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE) do |hash, key|
hash[key] = SizeLimitedArray.new(Defaults::MAX_HASH_SIZE)
end

@before_send = opts[:before_send]
@is_server = opts.fetch(:is_server, true) != false
@deprecation_emitted_for = Concurrent::Set.new
Expand Down
58 changes: 37 additions & 21 deletions lib/posthog/feature_flags.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ class FeatureFlagsPoller
# immediate first tick at construction, then the regular polling cadence, which keeps retrying until a
# load succeeds.
# @param user_agent [String] User-Agent header sent with feature flag requests.
# @param flag_definitions_update_mutex [Mutex] Internal lock shared with flag-called deduplication.
# @param on_flag_definitions_updated [Proc, nil] Internal callback after definitions are applied or discarded,
# called while holding flag_definitions_update_mutex.
def initialize(
polling_interval,
secret_key,
Expand All @@ -55,7 +58,9 @@ def initialize(
flag_definition_cache_provider: nil,
feature_flag_request_max_retries: nil,
async_load: false,
user_agent: "posthog-ruby/#{PostHog::VERSION}"
user_agent: "posthog-ruby/#{PostHog::VERSION}",
on_flag_definitions_updated: nil,
flag_definitions_update_mutex: Mutex.new
)
@polling_interval = polling_interval || Defaults::FeatureFlags::POLLING_INTERVAL_SECONDS
@secret_key = secret_key
Expand All @@ -75,6 +80,8 @@ def initialize(
@flag_definitions_loaded_at = Concurrent::AtomicReference.new(nil)
@async_load = async_load
@user_agent = user_agent
@on_flag_definitions_updated = on_flag_definitions_updated
@flag_definitions_update_mutex = flag_definitions_update_mutex
# Server-controlled gate for minimal `$feature_flag_called` events, read
# from the top-level `minimal_flag_called_events` key of the local
# evaluation definitions payload. false when the server does not send it.
Expand Down Expand Up @@ -1221,14 +1228,18 @@ def _fetch_and_apply_flag_definitions
'[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all local flags. ' \
'Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts'
)
@feature_flags = Concurrent::Array.new
@feature_flags_by_key = {}
@group_type_mapping = Concurrent::Hash.new
@cohorts = Concurrent::Hash.new
@flag_definitions_loaded_at.value = nil
@minimal_flag_called_events = false
@loaded_flags_successfully_once.make_false
@quota_limited.make_true
@flag_definitions_update_mutex.synchronize do
definitions_were_loaded = definitions_loaded?
@feature_flags = Concurrent::Array.new
@feature_flags_by_key = {}
@group_type_mapping = Concurrent::Hash.new
@cohorts = Concurrent::Hash.new
@flag_definitions_loaded_at.value = nil
@minimal_flag_called_events = false
@loaded_flags_successfully_once.make_false
@quota_limited.make_true
@on_flag_definitions_updated&.call if definitions_were_loaded
end
return
end

Expand Down Expand Up @@ -1265,21 +1276,26 @@ def _apply_flag_definitions(data)
cohorts = get_by_symbol_or_string_key(data, 'cohorts') || {}
minimal_flag_called_events = get_by_symbol_or_string_key(data, 'minimal_flag_called_events')

@feature_flags = Concurrent::Array.new(flags.map { |f| deep_symbolize_keys(f) })

new_flags = Concurrent::Array.new(flags.map { |f| deep_symbolize_keys(f) })
new_by_key = {}
@feature_flags.each do |flag|
new_flags.each do |flag|
new_by_key[flag[:key]] = flag unless flag[:key].nil?
end
@feature_flags_by_key = new_by_key

@group_type_mapping = Concurrent::Hash[deep_symbolize_keys(group_type_mapping)]
@cohorts = Concurrent::Hash[deep_symbolize_keys(cohorts)]
@minimal_flag_called_events = minimal_flag_called_events == true

logger.debug "Loaded #{@feature_flags.length} feature flags and #{@cohorts.length} cohorts"
@flag_definitions_loaded_at.value = (Time.now.to_f * 1000).to_i
@loaded_flags_successfully_once.make_true if @loaded_flags_successfully_once.false?
new_group_type_mapping = Concurrent::Hash[deep_symbolize_keys(group_type_mapping)]
new_cohorts = Concurrent::Hash[deep_symbolize_keys(cohorts)]

# A read of the new definitions must not record its event before the tracker reset.
@flag_definitions_update_mutex.synchronize do
@feature_flags = new_flags
@feature_flags_by_key = new_by_key
@group_type_mapping = new_group_type_mapping
@cohorts = new_cohorts
@minimal_flag_called_events = minimal_flag_called_events == true
@flag_definitions_loaded_at.value = (Time.now.to_f * 1000).to_i
@loaded_flags_successfully_once.make_true if @loaded_flags_successfully_once.false?
@on_flag_definitions_updated&.call
end
logger.debug "Loaded #{new_flags.length} feature flags and #{new_cohorts.length} cohorts"
end

def _request_feature_flag_definitions(etag: nil)
Expand Down
Loading