feat(flags): Add feature flags with local evaluation support#36
feat(flags): Add feature flags with local evaluation support#36
Conversation
chore(ci): replace PAT with GitHub App
chore(ci): switch to GitHub's official create-app-token action
chore: Add acknowledgement
This revives PR #36 with security and production cleanups: - Full feature flag support (boolean, multivariate, payloads) - Local evaluation with background polling for cached flag definitions - Advanced configuration options via ClientOptionsBuilder - Support for both async and blocking clients - Property-based targeting and batch evaluation Cleanups from original PR: - Remove eprintln! from library code (libraries shouldn't print to stderr) - Remove emojis from examples for production-friendliness - Fix examples/README.md to only reference examples that exist - Fix clippy warnings (bool asserts, double parens, clone on copy) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add tracing crate as dependency - Instrument local evaluation polling with info/warn/debug/trace logs - Instrument flag evaluation with debug/trace logs - Instrument client capture/get_feature_flag methods - Add tracing-subscriber as dev dependency for tests - Document tracing usage in README with examples Log levels: - error: Connection failures, HTTP errors - warn: Configuration issues, failed flag fetches - info: Client initialization, poller start/stop - debug: Flag evaluation results, API fallback decisions - trace: Detailed cache updates, individual flag lookups Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The CodeQL scanner flags false positives for an analytics SDK: 1. "Cleartext transmission of sensitive information" for project_api_key - The PostHog project API key (token) is a PUBLIC key, not a secret - It's designed to be included in client-side code and transmitted - This is different from the personal_api_key which IS kept secret 2. "Cleartext logging of sensitive information" for user_id in examples - Example code intentionally shows user IDs for demonstration - This is the expected behavior for an analytics SDK Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Conflicts with GitHub's default CodeQL setup. Will manually dismiss the false positive alerts instead. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove user_id from example println! statements to avoid logging PII - Move project_api_key from URL query param to X-PostHog-Project-Api-Key header to avoid cleartext transmission in URLs - Update test mock to expect the new header format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces full feature flag support to the PostHog Rust SDK, including local evaluation with background polling, richer client configuration, and comprehensive async/blocking tests and examples.
Changes:
- Add core feature flag data structures and evaluation logic (
feature_flags.rs), including multivariate flags, property-based targeting, and payload handling. - Implement local evaluation infrastructure (
local_evaluation.rs) with synchronous and async pollers plus a cache, and wire it into both async and blocking clients via extendedClientOptions. - Expand public API surface, examples, tests, and documentation to cover feature flags, local evaluation, endpoint management, and observability; update dependencies and CI to support the new functionality.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/feature_flags.rs | Defines FlagValue, feature flag/filter/condition types, evaluation and hashing logic, response normalization, and extensive unit tests for flag behavior. |
| src/local_evaluation.rs | Implements FlagCache, synchronous FlagPoller, async AsyncFlagPoller, and LocalEvaluator for local flag and batch evaluation. |
| src/endpoints.rs | Centralizes ingestion host constants and endpoint paths, adds EndpointManager for host normalization and URL construction, plus tests. |
| src/client/mod.rs | Refactors ClientOptions to support host selection, local evaluation, gzip, disable_geoip, and feature-flag-specific timeouts; adds EndpointManager wiring and convenience From impls. |
| src/client/async_client.rs | Async client now uses EndpointManager, supports gzip and disable_geoip, adds full feature flag and payload APIs, and integrates async local evaluation via AsyncFlagPoller/LocalEvaluator. |
| src/client/blocking.rs | Blocking client updated to use EndpointManager, adds feature flag and payload APIs, and integrates local evaluation via FlagPoller/LocalEvaluator. |
| src/lib.rs | Wires new modules into the crate and re-exports endpoints, feature flag types, and local evaluation APIs (including AsyncFlagPoller behind the feature flag). |
| src/event.rs | Minor test cleanup to avoid unnecessary timestamp cloning. |
| src/error.rs | Modernizes Display implementation for Error using inline formatting. |
| tests/test_async.rs | Adds comprehensive async integration tests for feature flag fetching, evaluation, payloads, groups, error handling, and malformed responses using httpmock. |
| tests/test_blocking.rs | Adds parallel blocking tests ensuring /flags/?v=2 integration, flag enablement, multivariate behavior, and API error handling. |
| tests/test_local_evaluation.rs | Tests FlagCache, LocalEvaluator, and async local evaluation via a mocked local evaluation endpoint and client configuration. |
| tests/test.rs | Splits E2E tests into async and blocking variants under appropriate feature flags. |
| examples/local_evaluation.rs | Demonstrates configuring local evaluation, compares remote vs local performance, and shows batch flag fetching in a realistic async example. |
| examples/feature_flags.rs | Provides end-to-end async examples for boolean flags, multivariate flags, property-based targeting, batch evaluation, and payload usage (with a demo-mode client). |
| examples/advanced_config.rs | Shows various configuration patterns (US/EU/self-hosted, production settings, local evaluation) using ClientOptionsBuilder. |
| examples/README.md | Documents how to run and what to expect from the new examples, including key feature-flag concepts and use cases. |
| README.md | Extends crate-level docs to cover feature flags, usage patterns (including groups and payloads), and observability via tracing, plus quick flag usage examples. |
| Cargo.toml | Adds dependencies (sha1, regex, tracing, tokio, httpmock, etc.), enables json and gzip features on reqwest, and defines the async-client feature with tokio support. |
| .github/workflows/label-version-bump.yml | Pins actions to specific SHAs, switches to a GitHub App token, and improves version/changelog bump scripting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Get all feature flags for a user | ||
| pub fn get_feature_flags( | ||
| &self, | ||
| distinct_id: String, | ||
| groups: Option<HashMap<String, String>>, |
There was a problem hiding this comment.
ClientOptions exposes disable_geoip and feature_flags_request_timeout_seconds, and the async client honours these for event and feature flag requests, but the blocking client’s feature flag requests here always use the global timeout and ignore disable_geoip. For consistency and to avoid surprising users, it would be better if the blocking client also applied the feature-flags timeout and disable_geoip settings (or the docs clearly stated these options are async-only).
| "distinct_id": distinct_id.into(), | ||
| }); | ||
|
|
||
| let response = self | ||
| .client | ||
| .post(&flags_endpoint) | ||
| .header(CONTENT_TYPE, "application/json") |
There was a problem hiding this comment.
For the blocking client, feature-flag payload requests here always use the global HTTP timeout and don’t add the disable_geoip parameter, whereas the async client uses feature_flags_request_timeout_seconds and honours disable_geoip for the same call. To keep configuration semantics consistent across async/blocking clients, consider wiring these options through here as well (or clearly documenting that they only apply to the async client).
| "distinct_id": distinct_id.into(), | |
| }); | |
| let response = self | |
| .client | |
| .post(&flags_endpoint) | |
| .header(CONTENT_TYPE, "application/json") | |
| "distinct_id": distinct_id.into(), | |
| "disable_geoip": self.options.disable_geoip, | |
| }); | |
| let response = self | |
| .client | |
| .post(&flags_endpoint) | |
| .header(CONTENT_TYPE, "application/json") | |
| .timeout(Duration::from_secs( | |
| self.options.feature_flags_request_timeout_seconds, | |
| )) |
haacked
left a comment
There was a problem hiding this comment.
Nice work!
Just a few issues in-line and some overall issues here:
-
There's a lot of duplicate code in
async_client.rsandblocking.rs. It seems like a lot of that functionality could be extracted into pure functions that both clients would use, reducing our future maintenance burden (for example, the blocking client appears to be missingdisable_geoipandtimeout). -
There appears to be some missing operators:
i. Python supports relative date parsing like "-7d" (7 days ago) and ISO dates.
ii. Python can evaluate cohort membership locally. TheCohortstruct exists inlocal_evaluation.rsbut is never used.
iii. Python supports flags that depend on other flags.
iv. Python raises inconclusive match error on unknown operators. Rust seems to return false.
dustinbyrne
left a comment
There was a problem hiding this comment.
Looking good! I've left some comments below, but approving now as to not hold this up any longer. The gzip and disable_geoip comments look like bugs, but I'll let you be the judge.
- Add InconclusiveMatch error variant instead of using Error::Connection for flag evaluation errors (distinguishes evaluation logic from network issues) - Fix README get_feature_flags example to use tuple destructuring - Add disable_geoip support to blocking client (was async-only) - Add feature_flags_request_timeout to blocking client (was async-only) - Fix disable_geoip to add $geoip_disable property to events instead of query parameter (proper PostHog API contract) - Make blocking client API consistent with async (use impl Into<String> for key/distinct_id parameters) - Remove unused gzip field (reqwest gzip feature only decompresses responses, doesn't compress requests) - Simplify endpoints.rs match arm (use trimmed_host directly) - Improve test assertion specificity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add std::error::Error impl for InconclusiveMatchError - Add global regex cache to avoid recompiling patterns on every evaluation - Replace RwLock<bool> with AtomicBool for stop signals in both sync and async pollers (cheaper atomic operations) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…fixes - Add cohort membership evaluation with 'in' and 'not_in' operators - Add flag dependency evaluation ($feature/ prefix) - Add EvaluationContext to pass cohorts and flags through evaluation - Add is_date_before/is_date_after operators with relative date parsing - Add unknown operator handling returning InconclusiveMatchError - Fix CodeQL warning: use TEST_SALT constant instead of empty string in tests - Optimize props handling in clients (avoid unnecessary cloning) - Update LocalEvaluator to use context-aware evaluation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…tring The empty string is intentionally used for rollout percentage hashing to match PostHog's consistent hashing algorithm across all SDKs. Using a named constant with documentation makes the intent explicit and satisfies CodeQL. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add VARIANT_HASH_SALT constant for multivariate variant selection hashing. All hash_key() calls now use named constants for clarity and to satisfy CodeQL security scanning: - ROLLOUT_HASH_SALT: empty string for rollout percentage hashing - VARIANT_HASH_SALT: "variant" for multivariate variant selection - TEST_SALT: "test-salt" for test isolation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix actions/checkout@v6 → v4 (v6 doesn't exist yet) - Add length check in parse_relative_date to prevent panic on short strings - Add #[must_use] attributes to key feature flag functions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
This PR adds comprehensive feature flag support to the PostHog Rust SDK.
Key features:
ClientOptionsBuilderMajor additions:
feature_flags.rs: Core feature flag evaluation logic including all operatorslocal_evaluation.rs: Local caching and evaluation system with async/sync pollersendpoints.rs: PostHog API endpoint management with region detectionBreaking changes:
ClientOptionsnow useshostinstead ofapi_endpointOther changes:
tracingfor structured logging/observability