fix(analytics): allow capability to offload reportExposure to async thread (SDK-80)#157
Conversation
…hread Exposure tracking called the tracker_callback (and therefore an HTTP POST) inline, blocking every get_variant / get_variant_value / is_enabled call by the full /track round trip. Add an optional :exposure_executor config key on both local and remote flags configs. The executor is duck-typed — anything that responds to #post(&block) works (Concurrent::ExecutorService, or a Thread.new wrapper). When set, the tracker call is dispatched on the executor so flag evaluation returns as soon as the local logic finishes. Defaults to nil — preserves existing inline behavior. Mirrors mixpanel-java#85. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #157 +/- ##
==========================================
- Coverage 97.25% 97.16% -0.10%
==========================================
Files 15 15
Lines 764 775 +11
==========================================
+ Hits 743 753 +10
- Misses 21 22 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…rage - Add an "Async Exposure Tracking" section to the openfeature-provider README showing both the concurrent-ruby and Thread.new wrapper patterns. - Add specs covering the local provider with executor (previously only the remote provider had coverage), the manual track_exposure_event path, and explicit "default is inline" sanity checks on both providers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…#pop invoke_tracker only rescued MixpanelError. On the async path any other exception (RuntimeError, NoMethodError, unwrapped network errors) terminated the executor thread silently — the error_handler never saw it. Added a StandardError branch that wraps into MixpanelError so consumers see a consistent type. Also bound the two async-executor specs with Timeout.timeout(2) around tracker_ran.pop. If a future change makes the tracker block raise before pushing :done, the spec now fails with a clear timeout instead of hanging CI forever. New spec 'reports non-MixpanelError exceptions from the async tracker' locks in the widened rescue.
|
Pushed P1 — blocking P2 — All 88 flag specs pass locally. |
There was a problem hiding this comment.
Pull request overview
Adds optional async dispatch for feature-flag exposure tracking so get_variant* / is_enabled? don’t block on the /track HTTP round trip unless desired, by introducing a duck-typed :exposure_executor configuration.
Changes:
- Introduces
:exposure_executorconfig plumbing for bothLocalFlagsProviderandRemoteFlagsProvider, and dispatches exposure tracking via#post(&block)when configured. - Extends exposure tracking implementation to route through a dedicated dispatch helper and adds handling for async execution failures.
- Adds specs covering default inline behavior vs executor-backed async behavior, plus documentation for OpenFeature users.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/mixpanel-ruby/flags/flags_provider.rb | Implements inline-vs-executor exposure dispatch and centralizes tracker invocation/error handling. |
| lib/mixpanel-ruby/flags/local_flags_provider.rb | Adds :exposure_executor to default config and passes it into the base provider config. |
| lib/mixpanel-ruby/flags/remote_flags_provider.rb | Adds :exposure_executor to default config and passes it into the base provider config. |
| spec/mixpanel-ruby/flags/local_flags_spec.rb | Adds tests for inline vs async exposure dispatch and async error reporting behavior. |
| spec/mixpanel-ruby/flags/remote_flags_spec.rb | Adds tests for inline vs async exposure dispatch in the remote provider. |
| openfeature-provider/README.md | Documents how to configure async exposure tracking for OpenFeature provider usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…-executor # Conflicts: # lib/mixpanel-ruby/flags/flags_provider.rb # lib/mixpanel-ruby/flags/local_flags_provider.rb # lib/mixpanel-ruby/flags/remote_flags_provider.rb # spec/mixpanel-ruby/flags/local_flags_spec.rb
…ycle Copilot flagged two things on the merge into #157: 1. invoke_tracker had grown a rescue StandardError branch to keep the async executor thread from dying silently, but that same rescue was catching non-MixpanelError exceptions on the inline path too. Previously the inline path let those propagate to the flag evaluator's caller — the widened rescue changed that silently. Moved the StandardError trap into the .post block of dispatch_exposure so it only applies on the async path. invoke_tracker is back to rescuing MixpanelError only, matching the pre-SDK-80 inline behavior. New spec 'lets non-MixpanelError exceptions propagate on the inline path' pins this. 2. The Async Exposure Tracking README section created a Concurrent::FixedThreadPool but never told callers to shut it down — provider.shutdown only tears down polling, not a user-supplied executor. Added a lifecycle note pointing at at_exit + shutdown/wait_for_termination. All 108 flag specs pass.
|
Pushed `9bfef62` addressing the two Copilot threads. Thread 2 — Thread 3 — executor lifecycle undocumented: added a All 108 flag specs pass locally. |
Summary
Exposure tracking called the
tracker_callback— and therefore an HTTP POST — inline, blocking everyget_variant/get_variant_value/is_enabledcall by the full/trackround trip.Add an optional
:exposure_executorconfig key on bothLocalFlagsProviderandRemoteFlagsProvider. The executor is duck-typed — anything that responds to#post(&block)works:Concurrent::ExecutorServicefromconcurrent-rubyThread.newwrapper for users who don't want the dependencyWhen set, the tracker call is dispatched on the executor so flag evaluation returns as soon as the local logic finishes. Defaults to
nil— preserves the existing inline behavior, no breaking change for current users.Usage
Context
Linear: SDK-80. Mirrors mixpanel-java#85. Audit-driven; same fix being applied to mixpanel-python and mixpanel-go in parallel PRs.
Test plan
:exposure_executordefaults to nil so the old behavior is unchanged):exposure_executoris configured