feat(flags): add semver targeting to local evaluation#446
Conversation
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
posthog-android Compliance ReportDate: 2026-03-04 22:59:13 UTC
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 2331ms |
| Format Validation.Event Has Uuid | ✅ | 2027ms |
| Format Validation.Event Has Lib Properties | ✅ | 2024ms |
| Format Validation.Distinct Id Is String | ✅ | 2025ms |
| Format Validation.Token Is Present | ✅ | 2023ms |
| Format Validation.Custom Properties Preserved | ✅ | 2023ms |
| Format Validation.Event Has Timestamp | ✅ | 2019ms |
| Retry Behavior.Retries On 503 | ❌ | 7027ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 4024ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 4021ms |
| Retry Behavior.Respects Retry After Header | ❌ | 7020ms |
| Retry Behavior.Implements Backoff | ❌ | 17022ms |
| Retry Behavior.Retries On 500 | ❌ | 7018ms |
| Retry Behavior.Retries On 502 | ❌ | 7021ms |
| Retry Behavior.Retries On 504 | ❌ | 7020ms |
| Retry Behavior.Max Retries Respected | ❌ | 17037ms |
| Deduplication.Generates Unique Uuids | ✅ | 2039ms |
| Deduplication.Preserves Uuid On Retry | ❌ | 7025ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ❌ | 12027ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ❌ | 7029ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 2031ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 2022ms |
| Compression.Sends Gzip When Enabled | ❌ | 2018ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 2017ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 2014ms |
| Batch Format.Multiple Events Batched Together | ✅ | 2028ms |
| Error Handling.Does Not Retry On 403 | ✅ | 4020ms |
| Error Handling.Does Not Retry On 413 | ❌ | 4019ms |
| Error Handling.Retries On 408 | ✅ | 7025ms |
Failures
retry_behavior.retries_on_503
Expected at least 3 requests, got 1
retry_behavior.respects_retry_after_header
Expected at least 2 requests, got 1
retry_behavior.implements_backoff
Expected at least 3 requests, got 1
retry_behavior.retries_on_500
Expected at least 2 requests, got 1
retry_behavior.retries_on_502
Expected at least 2 requests, got 1
retry_behavior.retries_on_504
Expected at least 2 requests, got 1
retry_behavior.max_retries_respected
Expected 4 requests, got 1
deduplication.preserves_uuid_on_retry
Need at least 2 requests to check retry
deduplication.preserves_uuid_and_timestamp_on_retry
Expected at least 3 requests, got 1
deduplication.preserves_uuid_and_timestamp_on_batch_retry
Expected at least 2 requests, got 1
compression.sends_gzip_when_enabled
Header 'Content-Encoding' with value 'gzip' not found in requests
error_handling.does_not_retry_on_413
Expected 1 requests, got 2
| /** | ||
| * A parsed semver version as (major, minor, patch) tuple | ||
| */ | ||
| private data class SemverVersion( |
There was a problem hiding this comment.
data class generates a bunch of stuff that you prob dont need here (eg hash, toString, etc)
| /** | ||
| * Compare two semver versions using the specified operator | ||
| */ | ||
| private fun compareSemver( |
There was a problem hiding this comment.
all methods that throw should be annotated with @throws(type...) so the caller knows what to do
| } | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
i think you could do this in a cleaner way with regex
eg
private val SEMVER_RE =
Regex("""^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.\d+)*?(?:[-+].*)?$""")
private fun parseSemverRegex(version: String): SemverVersion {
var cleaned = version.trim()
if (cleaned.startsWith("v", ignoreCase = true)) cleaned = cleaned.substring(1)
val m = SEMVER_PREFIX_RE.matchEntire(cleaned)
?: throw InconclusiveMatchException("Invalid semver version: '$version'")
val major = m.groupValues[1].toInt()
val minor = m.groupValues[2].takeIf { it.isNotEmpty() }?.toInt() ?: 0
val patch = m.groupValues[3].takeIf { it.isNotEmpty() }?.toInt() ?: 0
return SemverVersion(major, minor, patch)
}
ps: have not tested this, gpt generated, but i remember doing this before so it should work or just small changes needed
- Change SemverVersion from data class to regular class (avoids generating unnecessary methods) - Refactor parseSemver to use regex for cleaner parsing - Add @throws annotations to methods that throw InconclusiveMatchException 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
| @@ -0,0 +1,12 @@ | |||
| --- | |||
| "posthog-server": patch | |||
There was a problem hiding this comment.
you also need to release core because theres changes for PostHogLocalEvaluationModels.kt
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
💡 Motivation and Context
This PR adds semantic versioning (semver) comparison operators to local feature flag evaluation in the PostHog Android SDK. This enables targeting users based on app version using standard semver semantics.
Why this is needed:
user_blast_radiusposthog#44596)9 semver operators added:
semver_eq1.2.3semver_neq1.2.3semver_gt1.2.3semver_gte1.2.3semver_lt1.2.3semver_lte1.2.3semver_tilde~1.2.3semver_caret^1.2.3semver_wildcard1.2.*💚 How did you test it?
Unit tests added (25+ test cases in
FlagEvaluatorTest.kt):testSemverEq,testSemverNeq,testSemverGt,testSemverGte,testSemverLt,testSemverLtetestSemverTilde/testSemverTildeWithZeroMinor— patch-level rangestestSemverCaretMajorGreaterThanZero/testSemverCaretMajorZeroMinorGreaterThanZero/testSemverCaretMajorAndMinorZero— caret range edge casestestSemverWildcardMajorOnly/testSemverWildcardMajorAndMinor— wildcard patternstestSemverPartialVersions— "1.2" parsed as "1.2.0"testSemverFourPartVersions— "1.2.3.4" parsed as "1.2.3"testSemverLeadingZeros— "01.02.03" parsed as "1.2.3"testSemverInvalidEmptyString,testSemverInvalidLeadingDot,testSemverInvalidNonNumerictestSemverInvalidConditionValue,testSemverMissingPropertyKey,testSemverNullPropertyValuetestSemverInFeatureFlag— full flag evaluation with semver operatorAll tests pass locally.
📝 Checklist