Skip to content

feat(flags): Add feature flags with local evaluation support#36

Merged
dmarticus merged 32 commits intomainfrom
feat/feature-flags
Jan 29, 2026
Merged

feat(flags): Add feature flags with local evaluation support#36
dmarticus merged 32 commits intomainfrom
feat/feature-flags

Conversation

@dmarticus
Copy link
Copy Markdown
Contributor

@dmarticus dmarticus commented Sep 18, 2025

Summary

This PR adds comprehensive feature flag support to the PostHog Rust SDK.

Key features:

  • 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
  • Cohort membership evaluation
  • Flag dependency evaluation (flags that depend on other flags)
  • Support for US, EU, and self-hosted endpoints with automatic endpoint detection

Major additions:

  • feature_flags.rs: Core feature flag evaluation logic including all operators
  • local_evaluation.rs: Local caching and evaluation system with async/sync pollers
  • endpoints.rs: PostHog API endpoint management with region detection
  • Comprehensive examples demonstrating all flag patterns
  • Full test coverage for async/blocking/local evaluation modes

Breaking changes:

  • ClientOptions now uses host instead of api_endpoint
  • Minimum supported Rust version remains 1.78.0

Other changes:

  • Added tracing for structured logging/observability
  • GitHub Actions workflow updated to use GitHub App tokens instead of bot tokens
  • Updated README with feature flag documentation

@dmarticus dmarticus changed the title initial commit feat(flags): Add feature flags with local evaluation support to posthog-rs Sep 18, 2025
@dmarticus dmarticus changed the title feat(flags): Add feature flags with local evaluation support to posthog-rs feat(flags): Add feature flags with local evaluation support Sep 18, 2025
dmarticus and others added 16 commits September 18, 2025 17:54
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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 extended ClientOptions.
  • 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.

Comment on lines +118 to +122
/// Get all feature flags for a user
pub fn get_feature_flags(
&self,
distinct_id: String,
groups: Option<HashMap<String, String>>,
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +245 to +251
"distinct_id": distinct_id.into(),
});

let response = self
.client
.post(&flags_endpoint)
.header(CONTENT_TYPE, "application/json")
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
"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,
))

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@haacked haacked left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

Just a few issues in-line and some overall issues here:

  1. There's a lot of duplicate code in async_client.rs and blocking.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 missing disable_geoip and timeout).

  2. 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. The Cohort struct exists in local_evaluation.rs but 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.

Copy link
Copy Markdown

@dustinbyrne dustinbyrne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
dmarticus and others added 2 commits January 29, 2026 14:17
- 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>
@dmarticus dmarticus self-assigned this Jan 29, 2026
@github-project-automation github-project-automation bot moved this to Approved in Feature Flags Jan 29, 2026
@dmarticus dmarticus moved this from Approved to In Progress in Feature Flags Jan 29, 2026
dmarticus and others added 2 commits January 29, 2026 15:01
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>
@dmarticus dmarticus merged commit 1dfa749 into main Jan 29, 2026
8 checks passed
@dmarticus dmarticus deleted the feat/feature-flags branch January 29, 2026 23:20
@github-project-automation github-project-automation bot moved this from In Progress to Done in Feature Flags Jan 29, 2026
@sagoez sagoez mentioned this pull request Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

8 participants