diff --git a/lib/mixpanel-ruby/flags/flags_provider.rb b/lib/mixpanel-ruby/flags/flags_provider.rb index 116f011..584b671 100644 --- a/lib/mixpanel-ruby/flags/flags_provider.rb +++ b/lib/mixpanel-ruby/flags/flags_provider.rb @@ -88,6 +88,14 @@ def track_exposure_event(flag_key, selected_variant, context, latency_ms = nil) distinct_id = context['distinct_id'] || context[:distinct_id] unless distinct_id + # Local eval succeeds when the flag's Variant Assignment Key is + # something other than distinct_id (e.g., device_id), but the + # exposure event still needs distinct_id to attribute the user. + # Surface the drop instead of silently returning so callers can + # see they need to include distinct_id in the context. + @error_handler&.handle(MixpanelError.new( + "Cannot track exposure event for flag '#{flag_key}' without a distinct_id in the context" + )) return end diff --git a/spec/mixpanel-ruby/flags/local_flags_spec.rb b/spec/mixpanel-ruby/flags/local_flags_spec.rb index 6969521..df3c60a 100644 --- a/spec/mixpanel-ruby/flags/local_flags_spec.rb +++ b/spec/mixpanel-ruby/flags/local_flags_spec.rb @@ -718,6 +718,29 @@ def user_context_with_properties(properties) provider.send(:track_exposure_event, 'test_flag', variant, test_context) end + + # SDK-84: when local eval succeeds via a non-distinct_id Variant Assignment + # Key but the context lacks distinct_id, the exposure can't fire. Surface + # via error_handler instead of silently returning. + it 'reports through error_handler when distinct_id is missing' do + variant = Mixpanel::Flags::SelectedVariant.new( + variant_key: 'treatment', variant_value: 'treatment' + ) + + expect(mock_tracker).not_to receive(:call) + expect(mock_error_handler).to receive(:handle) do |err| + expect(err).to be_a(Mixpanel::MixpanelError) + expect(err.message).to include('test_flag') + expect(err.message).to include('distinct_id') + end + + provider.send( + :track_exposure_event, + 'test_flag', + variant, + { 'device_id' => 'abc-123' } + ) + end end describe 'polling' do