Skip to content

fix(analytics): surface dropped exposure when distinct_id missing from context (SDK-84)#154

Merged
tylerjroach merged 2 commits into
masterfrom
fix/sdk-84-warn-on-silent-exposure-drop
Jul 21, 2026
Merged

fix(analytics): surface dropped exposure when distinct_id missing from context (SDK-84)#154
tylerjroach merged 2 commits into
masterfrom
fix/sdk-84-warn-on-silent-exposure-drop

Conversation

@tylerjroach

Copy link
Copy Markdown
Contributor

Summary

When a flag uses a non-default Variant Assignment Key (e.g. device_id) and the evaluation context doesn't include distinct_id, local eval succeeds — flag definitions are cached, bucketing only needs the configured key — but track_exposure_event then silently returns because it can only attribute the exposure event to a distinct_id. Caller gets the right flag value with no signal that analytics were dropped.

Pass a Mixpanel::MixpanelError to @error_handler (consistent with how the rest of the SDK surfaces problems) so the drop becomes visible to anyone who's configured a real handler.

Context

Linear: SDK-84. Only Ruby and Java are actually affected of the five SDKs the audit listed — Python / Go / Node already log when this happens.

This bug only affects local evaluation. Remote eval requires distinct_id at the eval step itself, so it errors out before reaching exposure tracking.

Behavior when distinct_id IS present (alongside the bucketing key) is unchanged — exposure still fires correctly attributed to the user.

Test plan

  • Existing track_exposure_event test still passes
  • New test asserts MixpanelError is reported through error_handler when the context has device_id but no distinct_id
  • Full flags spec suite (84 examples) passes

🤖 Generated with Claude Code

…m context

When a flag uses a non-default Variant Assignment Key (e.g. device_id),
local evaluation succeeds via that key independently of whether
distinct_id is present. Exposure tracking, however, requires
distinct_id to attribute the event to the user. Previously the SDK
silently returned from track_exposure_event with no signal to the
caller — the flag value came back correctly but analytics were
silently dropped.

Surface the drop through the configured error_handler so callers can
see they need to include distinct_id in the context alongside the
bucketing key.

Behavior when distinct_id IS present is unchanged.

Linear: SDK-84

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tylerjroach
tylerjroach requested review from a team and tdumitrescu June 29, 2026 17:19
@linear-code

linear-code Bot commented Jun 29, 2026

Copy link
Copy Markdown

SDK-84

@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.11%. Comparing base (6a9e75f) to head (ff3878a).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #154      +/-   ##
==========================================
+ Coverage   96.97%   97.11%   +0.13%     
==========================================
  Files          15       15              
  Lines         761      762       +1     
==========================================
+ Hits          738      740       +2     
+ Misses         23       22       -1     
Flag Coverage Δ
openfeature 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tylerjroach

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge — the change adds one error_handler notification before an existing early return and leaves all other code paths untouched.

The fix is a single-line addition that only executes when distinct_id is already absent. The happy path (distinct_id present) is completely unaffected, the safe-navigation operator handles a nil error_handler gracefully, and the new test covers the failure path directly.

No files require special attention.

Important Files Changed

Filename Overview
lib/mixpanel-ruby/flags/flags_provider.rb Adds error_handler notification before the early return when distinct_id is missing; minimal, focused change with no impact on the happy path.
spec/mixpanel-ruby/flags/local_flags_spec.rb New test asserts error handler is called with a MixpanelError and tracker is not invoked when context omits distinct_id; uses existing mock_error_handler double correctly.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant LocalFlagsProvider
    participant ErrorHandler
    participant Tracker

    Caller->>LocalFlagsProvider: "get_flag(flag_key, context={device_id:...})"
    LocalFlagsProvider-->>Caller: SelectedVariant (local eval succeeds)

    Caller->>LocalFlagsProvider: track_exposure_event(flag_key, variant, context)
    LocalFlagsProvider->>LocalFlagsProvider: "distinct_id = context['distinct_id'] → nil"

    alt Before this PR (silent drop)
        LocalFlagsProvider-->>Caller: return (no signal)
    else After this PR
        LocalFlagsProvider->>ErrorHandler: handle(MixpanelError("...distinct_id missing..."))
        LocalFlagsProvider-->>Caller: return
    end

    note over Tracker: Tracker is never called when distinct_id is absent
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant LocalFlagsProvider
    participant ErrorHandler
    participant Tracker

    Caller->>LocalFlagsProvider: "get_flag(flag_key, context={device_id:...})"
    LocalFlagsProvider-->>Caller: SelectedVariant (local eval succeeds)

    Caller->>LocalFlagsProvider: track_exposure_event(flag_key, variant, context)
    LocalFlagsProvider->>LocalFlagsProvider: "distinct_id = context['distinct_id'] → nil"

    alt Before this PR (silent drop)
        LocalFlagsProvider-->>Caller: return (no signal)
    else After this PR
        LocalFlagsProvider->>ErrorHandler: handle(MixpanelError("...distinct_id missing..."))
        LocalFlagsProvider-->>Caller: return
    end

    note over Tracker: Tracker is never called when distinct_id is absent
Loading

Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/sdk-84-wa..." | Re-trigger Greptile

@tylerjroach
tylerjroach enabled auto-merge (squash) July 21, 2026 03:42
@tylerjroach
tylerjroach merged commit 961ee20 into master Jul 21, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants