Skip to content

feat(flags): add semver targeting to local evaluation#446

Merged
dmarticus merged 5 commits intomainfrom
feat/semver-targeting
Mar 4, 2026
Merged

feat(flags): add semver targeting to local evaluation#446
dmarticus merged 5 commits intomainfrom
feat/semver-targeting

Conversation

@dmarticus
Copy link
Contributor

@dmarticus dmarticus commented Mar 2, 2026

💡 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:

9 semver operators added:

Operator Example Meaning
semver_eq 1.2.3 Exact match
semver_neq 1.2.3 Not equal
semver_gt 1.2.3 Greater than
semver_gte 1.2.3 Greater than or equal
semver_lt 1.2.3 Less than
semver_lte 1.2.3 Less than or equal
semver_tilde ~1.2.3 Patch-level range (>=1.2.3 <1.3.0)
semver_caret ^1.2.3 Compatible-with range (>=1.2.3 <2.0.0)
semver_wildcard 1.2.* Wildcard range (>=1.2.0 <1.3.0)

💚 How did you test it?

Unit tests added (25+ test cases in FlagEvaluatorTest.kt):

  • Basic comparison operators: testSemverEq, testSemverNeq, testSemverGt, testSemverGte, testSemverLt, testSemverLte
  • Range operators:
    • testSemverTilde / testSemverTildeWithZeroMinor — patch-level ranges
    • testSemverCaretMajorGreaterThanZero / testSemverCaretMajorZeroMinorGreaterThanZero / testSemverCaretMajorAndMinorZero — caret range edge cases
    • testSemverWildcardMajorOnly / testSemverWildcardMajorAndMinor — wildcard patterns
  • Parsing edge cases:
    • testSemverPartialVersions — "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"
    • v-prefix handling, whitespace trimming, pre-release suffix stripping
  • Error handling:
    • testSemverInvalidEmptyString, testSemverInvalidLeadingDot, testSemverInvalidNonNumeric
    • testSemverInvalidConditionValue, testSemverMissingPropertyKey, testSemverNullPropertyValue
  • Integration: testSemverInFeatureFlag — full flag evaluation with semver operator

All tests pass locally.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

@dmarticus dmarticus requested a review from a team as a code owner March 2, 2026 22:09
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

posthog-android Compliance Report

Date: 2026-03-04 22:59:13 UTC
Duration: 147084ms

⚠️ Some Tests Failed

17/29 tests passed, 12 failed


Capture Tests

⚠️ 17/29 tests passed, 12 failed

View Details
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(
Copy link
Member

Choose a reason for hiding this comment

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

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(
Copy link
Member

@marandaneto marandaneto Mar 3, 2026

Choose a reason for hiding this comment

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

all methods that throw should be annotated with @throws(type...) so the caller knows what to do

@marandaneto marandaneto requested review from a team and dustinbyrne March 3, 2026 07:17
}
}

/**
Copy link
Member

Choose a reason for hiding this comment

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

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

dmarticus and others added 2 commits March 3, 2026 08:42
- 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>
@dmarticus dmarticus changed the title add semver targeting to local evaluation feat(flags): add semver targeting to local evaluation Mar 3, 2026
@@ -0,0 +1,12 @@
---
"posthog-server": patch
Copy link
Member

Choose a reason for hiding this comment

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

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>
@dmarticus dmarticus merged commit 17ba416 into main Mar 4, 2026
12 checks passed
@dmarticus dmarticus deleted the feat/semver-targeting branch March 4, 2026 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants