You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Client lib parity: audit, Phase A fixes, Phase B feature parity, Phase C alignment - #3408
The JS, Java, and Python client libs have drifted apart. This PR adds clientlibs/CLIENT_LIB_PARITY_AUDIT.md, an audit of all three against each other and the backend /v6 API with a phased reconciliation plan, and implements Phase A (correctness fixes) and Phase B (feature parity). No backend changes; everything uses the existing /v6 API.
Phase A: correctness fixes
Java
markDecisionPoint now sends experimentId and condition id from the cached assignment, across all overloads and Assignment.markDecisionPoint(). Previously Java sent only conditionCode, so the server re-ran experiment selection and could mark a different experiment when several share a decision point.
Within-subjects marks now rotate assignedFactor alongside assignedCondition.
The assignment and feature-flag caches are now filled beforeonSuccess runs. Previously a call chained from inside the callback (e.g. a mark) could hit a cold cache.
First unit test suite (src/test/java, TestNG). It runs ExperimentClient against an in-process HttpServer.
JS
DefaultHttpClient never retried: axios rejects non-2xx responses before the retry branch runs, and that branch would have retried 4xx rather than 5xx anyway. It now retries network errors and 5xx with backoff for idempotent requests only (see Retry policy), fails fast on 4xx, and sets a 5s default timeout.
ISendRewardResponse gains mode / success to match the backend.
markDecisionPoint no longer throws when the assignment cache is null (e.g. after a failed /assign).
Fixed 3 ApiService mark tests that already fail on dev: their expected bodies were missing context.
Python
The _sync methods use ApiService's real sync implementations instead of asyncio.run(), so they work inside a running event loop (Jupyter, async frameworks).
HTTP connections are pooled (see below). Adds close() / aclose() and with / async with support.
Phase B: feature parity
Rewards
Java (previously had none): sendRewardForExperiment(value, experimentId, [awaitResult], cb) and sendRewardForDecisionPoint(value, context, site, target, [awaitResult], cb). Named methods mean a caller can't pass neither identifier. context is explicit, matching JS and Python. A 409 (with awaitResult) is reported to onError and not retried.
JS:sendReward accepts awaitResult, sent as ?awaitResult=true. Python already had it.
Feature-flag session groups (groupsForSession + includeStoredUserGroups), previously JS-only
Java:setFeatureFlagUserGroupsForSession(groups, includeStored) / clearFeatureFlagUserGroupsForSession(). Setter-only, so the constructor overloads don't double.
Python: a feature_flag_user_groups_for_session= constructor kwarg plus set_feature_flag_user_groups_for_session(). Both accept a FeatureFlagUserGroupsForSession model or a dict, and both fields are required.
In all three libs, changing the groups clears the cached flags. A flag response that was requested under the previous groups is never cached over that change; the caller still receives it. JS had this race already.
Dead endpoint methods removed
These were breaking removals, but every call already failed with a 404.
Java addGroupMetrics() / addSingleMetrics(): POST /v6/metric was never a client route. MATHia doesn't use them. The Metric bean stays because the /log response uses it.
Retry policy
Retries are limited to requests that are safe to repeat, in both JS and Java:
Never retried:/mark, /log and /reward. None of them are deduplicated server-side, so resending after a lost response would record the write twice, and a duplicated reward biases adaptive assignment.
JS marks safe requests with an optional retryable flag on the request config, which custom lite-build HTTP clients can also honor. This changes Java's existing behavior: it previously retried /mark and /log once. Python never retries.
Python connection pooling
The sync client is pooled per UpgradeClient. The async client is pooled only inside async with client:, because an AsyncClient must be closed on the loop that opened it. Outside that block each async request uses a short-lived client, same as dev, so successive asyncio.run() calls don't strand connection pools.
Phase C: aligning behavior
Exact decision point matching everywhere. The server's /mark and /reward lookups are case-sensitive. Java matched case-insensitively, so it returned conditions whose marks the server couldn't attribute to the experiment. Python now treats a None target as "".
Caches cleared when the user may have changed. In all three libs, init(), setGroupMembership, setWorkingGroup and setAltUserIds clear the assignment and feature-flag caches once the server confirms the call. Previously only Python's init() cleared anything. The assignment cache gets the same generation guard as flags, so an in-flight /assign can't refill a cleared cache. One side effect: within-subjects rotation restarts after one of these calls.
Python Assignment.mark_decision_point() now rotates within-subjects assignments, like the client-level mark and like JS.
Java's "no experiment here" placeholder now has an empty Condition instead of null, so getAssignedCondition().getConditionCode() no longer throws an NPE. Assignment gains getCondition()/getPayload(). Its experiment type stays null, as Java's explicit "no experiment" signal (JS reports SIMPLE; Python returns None and is unchanged).
Java auth token is optional, like JS and Python. The Authorization header is only sent when there's a token; servers with client auth enabled (toCheckAuth) answer a request without one with a 401.
Documented rather than changed:markDecisionPoint argument order (changing it would silently swap string arguments for existing callers), and the timeout/retry differences, now described in each README (the comparison is in clientlibs/TIMEOUT_RETRY_POLICY.md). This also fixes the JS README quick start, which marked with the payload value instead of getCondition().
Testing
JS:jest: 111/111 pass, ESLint/Prettier clean.
Python:pytest: 242/242 pass; ruff check and mypy clean.
Java:mvn test passes at the branch head (ExperimentClientTest, 26 tests). Also 26/26 from a mvn clean build run directly via TestNG, 5–10 runs in a row.
⚠️ The first commit (2bf0337) has a Java compile error (a stray brace); it's fixed in b19d284. Pre-commit hooks don't build the client libs, so review the Java at the branch head.
Not in this PR (see audit §5)
Phase D: guardrails / contract tests (and CI builds/tests for the Java/Python libs, which would have caught the compile error above).
clientlibs/js/docs (TypeDoc output) is stale and needs regenerating, and the Java README still documents several methods that no longer exist.
Adds clientlibs/CLIENT_LIB_PARITY_AUDIT.md, an audit of the JS, Java and
Python client libs against each other and the backend /v6 API, with a
phased reconciliation plan. Implements Phase A: fixes that need no API
or product decisions.
Java:
- markDecisionPoint sends the cached assignment's experimentId and
condition id (all overloads, plus Assignment.markDecisionPoint), so the
server marks the experiment the user was actually assigned instead of
re-selecting one
- rotate assignedFactor alongside assignedCondition on within-subjects marks
- populate assignment/feature-flag caches before invoking onSuccess, so
calls chained from inside the callback see a warm cache
- null-safe callbacks and properties map; null-safe target lookup
- first TestNG suite (src/test/java) against an in-process HttpServer
JS:
- DefaultHttpClient: retries never ran (axios rejects non-2xx before the
retry branch); now retries network errors and 5xx with backoff, fails
fast on 4xx, and sets a 5s default timeout
- ISendRewardResponse gains mode/success
- markDecisionPoint no longer throws when the assignment cache is null
- fix 3 pre-existing ApiService mark tests missing the context field
Python:
- _sync methods call ApiService's sync implementations instead of
asyncio.run(), so they work inside a running event loop
- reuse pooled httpx clients per UpgradeClient; add close()/aclose()
and sync/async context manager support
Docs: root CLAUDE.md lists the python lib (not a nonexistent csharp
lib); java/CLAUDE.md documents tests and configurable timeouts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Java rewards: sendRewardForExperiment / sendRewardForDecisionPoint,
explicit context (matching JS and Python), boolean awaitResult overload
- Feature-flag session groups: setter in Java and Python, plus a
constructor kwarg in Python only
- Dead endpoint methods to be deleted with no version bump
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix: the Phase A Java cache-timing change left a stray closing brace in
ExperimentClient.java, so the Java lib did not compile at 2bf0337 (the
tests that "passed" had run against stale class files). All Java results
here come from a clean build.
Java:
- sendRewardForExperiment / sendRewardForDecisionPoint, each with a
boolean awaitResult overload; context is explicit (matching JS and
Python); new RewardRequest, DecisionPoint, SendRewardResponse beans and
BinaryRewardValue enum
- setFeatureFlagUserGroupsForSession / clearFeatureFlagUserGroupsForSession;
feature flag requests use a FeatureFlagRequest bean that omits the
session-group fields when unset
- APIService.prepareRequest gains a query-param overload
- remove addGroupMetrics / addSingleMetrics (POST /v6/metric never existed
as a client route) and the GroupMetric, SingleMetric, MetricUnitBody
beans; Metric and MetricMetaData stay because LogEventResponse uses them
- tests for rewards (body shape, awaitResult query, 409 handling, argument
validation) and session groups (body shape, cache invalidation)
JS:
- sendReward accepts awaitResult, sent as ?awaitResult=true; params typed
as ISendRewardParams
- remove logCaliper (POST /v6/log/caliper was removed in #1985)
- sendReward tests
Python:
- feature_flag_user_groups_for_session constructor kwarg and
set_feature_flag_user_groups_for_session setter, taking a
FeatureFlagUserGroupsForSession model or dict; both fields required;
changing it clears the flag cache
Docs: Java/Python READMEs, Java/JS CLAUDE.md, audit marked Phase B done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bcb37
changed the title
Client lib parity audit
Client lib parity: audit, Phase A fixes, Phase B feature parity
Oct 28, 2026
…sync pool
Retries (JS and Java): only reads and idempotent upserts are retried
(/init, /groupmembership, /workinggroup, /useraliases, /assign,
/featureflag). /mark, /log and /reward are never retried: a timeout or 5xx
doesn't mean the write wasn't applied, and none are deduplicated
server-side. A resent /reward double-counts the observation and biases
adaptive assignment.
- JS: ApiService marks safe requests with an optional `retryable` flag on
IHttpClientWrapperRequestConfig; DefaultHttpClient retries only those
- Java: /mark, /log and /reward use NO_RETRIES (Java previously retried
/mark and /log once, and Phase B's reward did too)
Python async pooling: an AsyncClient must be closed on the loop that
opened it, so successive asyncio.run() calls stranded one pool per loop.
The async pool now exists only inside `async with client:`; outside it,
each async request uses a short-lived client (dev's behavior). The sync
pool is unchanged. Tests verify every client created across separate
asyncio.run() loops is closed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Both Python and Java clients allow in-flight feature-flag responses to repopulate invalidated caches after session groups change, causing subsequent lookups to keep returning flags evaluated for the old groups.
Address Copilot review: if the session groups were changed or cleared
while a feature-flag request was in flight, its response was cached when
it landed, and later lookups kept returning flags evaluated for the old
groups. Same bug in all three libs (pre-existing in JS; from Phase B in
Java and Python).
Changing the groups (and, in Python, init()) now bumps a generation
counter; a response is only cached if the generation it was requested
under is still current. The caller still receives the response.
- Java: generation, cache and groups guarded by one lock, since responses
arrive on Jersey threads
- Python: generation check and cache write are atomic under a lock, for
_sync callers on multiple threads
- JS/Python hasFeatureFlag check the flags that call fetched rather than
the possibly-invalidated cache (Java already did)
Each lib gets a delayed-response regression test; each fails on the
previous code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Unresolved thread-safety issues can leak HTTP client pools during lazy initialization and cause feature-flag lookups to raise AttributeError when session groups are cleared concurrently.
No open issues 🎉
2 previously missed issues
In code that hasn't changed since last review
Protect lazy HTTP client initialization from concurrent access
Two threads making their first _sync call can both pass this check and construct separate httpx.Client instances. One assignment can overwrite the other pool, leaving it unavailable to close() or aclose() for cleanup. Protect lazy initialization with a per-instance lock and coordinate close() with the same lock.
Use a stable feature-flag options snapshot during request construction
A synchronous feature-flag lookup can fail if another thread clears the session groups between this check and model_dump(): the second attribute read sees None and raises AttributeError before sending the request. The cache-generation lock does not protect request-body construction. Capture the options in a local variable and use that same snapshot for both the check and serialization.
The server's client-path decision point lookups are case-sensitive
(/mark matches `partitions.site = :site AND partitions.target = :target`,
then re-checks with ===), so the client libs now match the same way.
- Java: findExperimentResponse uses equals instead of equalsIgnoreCase.
Its case-insensitive lookup returned conditions for a differently-cased
site/target, and the mark sent with the caller's casing couldn't be
attributed to the experiment server-side.
- Python: an explicit None target now matches "" (as JS and Java do), and
/mark sends "" rather than null.
- JS: unchanged (already exact); tests added.
Audit doc: records this decision, and defers the "no experiment at
decision point" item with notes on current behavior.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assignments and feature flags both depend on the user's groups, but no
lib invalidated its caches when they changed (only Python's init()
cleared them, and before the request). Now, in all three libs, init()
(always), setGroupMembership and setWorkingGroup clear both caches once
the server confirms the call; a failed call leaves them alone.
setAltUserIds doesn't clear them, since aliases don't change groups.
- The assignment cache gets the same generation guard as feature flags,
so an /assign that was in flight during a group change isn't cached
over the clear (the caller still receives it). Java guards it with a
lock; Python's check-and-write is atomic under a lock.
- JS/Python getDecisionPointAssignment look up in the list they just
fetched rather than the possibly-cleared cache.
- Side effect: within-subjects condition rotation restarts after a group
change, since the refetched list comes back in the server's order.
Tests in each lib cover each clearing call, the failed-call and alias
cases, and the in-flight race. Java's test server now handles requests
concurrently so the race test exercises the guard (it fails with the
guard disabled). The JS cache spec is renamed to UpgradeClient.cache.spec.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documents how JS, Java and Python differ in default timeout, what the
timeout limits, configurability and retry count/backoff, the resulting
worst-case durations, and options for making them consistent (Phase C).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decided to document rather than change the libs' differing argument
order: changing either would silently swap values for existing callers,
since the swapped arguments are all strings.
- Java: Javadoc on ExperimentClient/Assignment markDecisionPoint notes
clientError comes before uniquifier, unlike JS and Python. The README's
stale markExperimentPoint section is replaced with markDecisionPoint,
including a cross-library signature table.
- Python README: notes condition/status come first and recommends keyword
arguments.
- JS README: points to the options-object form. Also fixes the quick
start, which marked with the payload value instead of getCondition().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The constructor threw on a null or empty token, so Java users had to pass
a placeholder even against servers that don't check client auth. Now,
like JS and Python, the token is optional and the Authorization header is
only sent when there is one. Whether a token is required is a server
setting (toCheckAuth, enforced in ClientLibMiddleware), so a missing
token surfaces as a 401 from servers that check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bcb37
changed the title
Client lib parity: audit, Phase A fixes, Phase B feature parity
Client lib parity: audit, Phase A fixes, Phase B feature parity, Phase C alignment
Oct 29, 2026
@copilot can you turn this PR summary into a code-review requirement / QA checklist, organized by client library, that I can check off one by one online here in the github UI? refer to the phased planning docs within the files being added. This should be concatenated to the PR description (don't overwrite the current summary).
@copilot can you edit this checklist based on the changes in the most recent commit?
@copilot can you turn this PR summary into a code-review requirement / QA checklist, organized by client library, that I can check off one by one online here in the github UI? refer to the phased plan...
I can’t directly edit the PR description with the GitHub operations available in this session. Here is an append-ready checklist for the description; it follows the phased plan in the parity audit, §5 and the timeout/retry policy.
Code-review / QA checklist
Java client
Verify all markDecisionPoint overloads, including Assignment.markDecisionPoint(), send the cached experiment ID and condition ID.
Verify within-subjects marks rotate both the assigned condition and factor.
Verify assignment and feature-flag caches are populated before success callbacks; null callbacks, properties, and target lookups are safe.
Verify reward methods cover both experiment-ID and decision-point lookup forms, require explicit identifiers/context, default awaitResult to false, and report awaited 409 responses through onError.
Verify session feature-flag groups can be set/cleared, invalidate the flag cache, and prevent an in-flight response for old groups from repopulating it.
Verify decision-point matching is exact; successful init, group-membership, and working-group changes invalidate the relevant caches, while alias changes do not.
Verify a missing auth token omits the Authorization header, and retries are limited to safe requests (never /mark, /log, or /reward).
Verify removed metric request methods are absent while response types still used by /log remain.
Run mvn test at the PR head; confirm request serialization coverage for the client endpoints.
JavaScript client
Verify the default HTTP client retries only network failures and 5xx for requests marked retryable, fails fast on 4xx, and applies the documented default timeout.
Verify /mark, /log, and /reward are never retried; custom/lite HTTP clients can honor the optional retryable flag.
Verify reward responses include mode/success and sendReward forwards awaitResult.
Verify markDecisionPoint handles a missing assignment cache and sends the expected request context.
Verify changing/clearing feature-flag session groups invalidates the cache and an old in-flight response is not cached.
Verify successful init, group-membership, and working-group changes invalidate assignment and flag caches, with a generation guard against stale assignment responses; alias changes do not invalidate them.
Verify the removed logCaliper method is absent.
Run Jest, ESLint, and Prettier; confirm the documented test suite passes.
Python client
Verify sync methods use the real synchronous API implementations and work when called from a running event loop.
Verify sync pooling and async context-managed pooling close their clients; requests outside async with do not reuse a pool across event loops.
Verify session feature-flag groups accept the model/dict forms, require both fields, invalidate the cache, and use generation/locking so stale in-flight responses are not cached.
Verify successful init, group-membership, and working-group changes invalidate assignment and flag caches; alias changes do not. Confirm stale /assign responses cannot refill the cache.
Verify decision-point matching is exact and None target is treated as an empty string.
Run pytest, Ruff, and mypy; confirm the documented test suite passes.
Cross-client / scope checks
Verify the retry allowlist is consistent with the audit: /init, /groupmembership, /workinggroup, /useraliases, /assign, and /featureflag may retry; /mark, /log, and /reward may not.
Confirm the timeout/retry alignment decision and the no-experiment-at-decision-point behavior remain explicitly deferred as described in the planning docs.
Confirm Phase D guardrails (shared endpoint contract checklist/tests and Java/Python CI coverage) are identified as follow-up work, not represented as completed in this PR.
Confirm the PR remains limited to client libraries and existing /v6 APIs; no backend behavior or endpoint contract changed.
… docs
- Java: when no experiment is assigned at a decision point, the
placeholder Assignment's getAssignedCondition() is an empty Condition
instead of null (also in getAllExperimentConditions(site, target)), so
chaining getConditionCode() no longer throws. Assignment gains
getCondition()/getPayload(). The experiment type stays null, Java's
explicit "no experiment" signal (JS keeps reporting SIMPLE); the
Javadoc warns that switching on it throws. Also guards an assignment
with an empty condition list.
- All libs: setAltUserIds now clears the assignment and feature-flag
caches once the server confirms, like init/setGroupMembership/
setWorkingGroup. The shared helper is renamed to "user-dependent".
- Python: Assignment.mark_decision_point (async and sync) now rotates the
cached within-subjects assignments, via an on_marked callback wired to
the client's own rotation, matching JS.
- Timeouts and retries: left as they are, by decision, and documented in
each README and in js/java CLAUDE.md; TIMEOUT_RETRY_POLICY.md records
the decision. The Java README's stale getExperimentCondition section is
replaced.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@copilot can you edit the checklist you created (or append an updated one) based on the changes from the most recent commit?
I can’t directly edit the PR description with the available GitHub operations, but here is a corrected, append-ready checklist. Use this version in place of the earlier checklist; it reflects the latest commit and the decisions in the phased audit and timeout/retry policy.
Code-review / QA checklist
Java client
Verify markDecisionPoint overloads, including Assignment.markDecisionPoint(), send the cached experiment and condition IDs; within-subjects marks rotate the assigned condition and factor.
Verify assignment and feature-flag caches are populated before success callbacks, and null callbacks, properties, and target lookups are safe.
Verify reward methods support experiment-ID and decision-point forms, require the relevant identifiers/context, default awaitResult to false, and report awaited 409 responses through onError.
Verify session feature-flag groups can be set/cleared, invalidate the flag cache, and prevent an in-flight response for old groups from repopulating it.
Verify decision-point matching is exact. Successful init, group-membership, working-group, and alt-user-ID calls clear assignment and flag caches; failed calls leave caches intact.
Verify no-experiment results use an empty Condition (including the site/target lookup), getCondition()/getPayload() are safe, and the experiment type remains null as documented.
Verify a missing auth token omits the Authorization header; safe requests alone are retried, never /mark, /log, or /reward.
Verify removed metric request methods are absent while response types used by /log remain.
Run mvn test at the PR head and verify request serialization coverage for the client endpoints.
JavaScript client
Verify the default HTTP client retries only network failures and 5xx for requests marked retryable, fails fast on 4xx, and uses the documented default timeout; /mark, /log, and /reward are never retried.
Verify reward responses include mode/success and sendReward forwards awaitResult; markDecisionPoint safely handles a missing assignment cache.
Verify changing/clearing feature-flag session groups invalidates the cache and an old in-flight response is not cached.
Verify successful init, group-membership, working-group, and alt-user-ID calls clear assignment and flag caches; failures do not. Stale in-flight assignment responses must not refill a cleared cache.
Verify the removed logCaliper method is absent.
Verify the README documents the chosen timeout/retry policy: keep the current per-library differences rather than standardize them.
Run Jest, ESLint, and Prettier.
Python client
Verify sync methods use synchronous API implementations and work when called from a running event loop; sync pooling and async context-managed pooling close clients correctly without reusing pools across event loops.
Verify session feature-flag groups accept model/dict forms, require both fields, invalidate the cache, and prevent stale in-flight responses from being cached.
Verify successful init, group-membership, working-group, and alt-user-ID calls clear assignment and flag caches; failures do not, and stale assignment responses cannot refill a cleared cache.
Verify decision-point matching is exact, None target is treated as an empty string, and Assignment.mark_decision_point() rotates within-subjects assignments in both async and sync paths.
Verify the README documents the chosen timeout/retry policy and run pytest, Ruff, and mypy.
Cross-client / follow-up scope
Verify the retry allowlist is consistent: /init, /groupmembership, /workinggroup, /useraliases, /assign, and /featureflag may retry; /mark, /log, and /reward may not.
Verify Phase D guardrails (shared endpoint contract checklist/tests and Java/Python CI coverage) remain identified as follow-up work, not completed functionality.
Confirm the PR remains limited to client libraries and existing /v6 APIs; no backend behavior or endpoint contract changed.
The latest commit adds the Java placeholder behavior, Python assignment-level within-subjects rotation, and cache invalidation after setAltUserIds in all three libraries; those items are included above. The timeout/retry decision is now documented as keeping the current differences. The latest workflow is still in progress, so I can’t confirm a full test run at this head yet; the previously listed PR checks passed before this commit.
The reason will be displayed to describe this comment to others. Learn more.
can we leave this new 'generation' stuff out of these updates, for this and the other libs? it's really a new feature, the scope of this update should be making the libs reasonably consistent.
IMO it's not worth it anyway, it's for an edge case that shouldn't come up unless the consumer app is shooting themselves in the foot by not waiting; it may hide this more than help, so i don't think it's worth the effort.
The reason will be displayed to describe this comment to others. Learn more.
Right, I wasn't sure where we landed on this. It doesn't change the method contract, and it was done to address a (theoretical) vulnerability introduced by the new behavior of emptying the cache when user/group information is changed. But, yeah, nobody should be changing group information after getting assignments without waiting for the assignments data to return.
The "new feature" is arguably really clearing the assignment cache on successful calls to any of the four endpoints that change user data (init, workingGroup, groupMembership, alias). It's perhaps a stretch to claim that those changes were made for consistency, since only one of them previously did that, and only for init. @danoswaltCL should we hold off on the cache-clearing change for this version?
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't make this update, we have always recommended using payload by default, as using payloads let's the implementing code be more experiment agnostic... i.e. a decision-point set up to only look for the condition itself will need updated later the moment someone wants to use payloads in an experiment, whereas if they just used the payload in the first place, they always get the correct thing, regardless of whether they use custom payloads or not.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The JS, Java, and Python client libs have drifted apart. This PR adds
clientlibs/CLIENT_LIB_PARITY_AUDIT.md, an audit of all three against each other and the backend/v6API with a phased reconciliation plan, and implements Phase A (correctness fixes) and Phase B (feature parity). No backend changes; everything uses the existing/v6API.Phase A: correctness fixes
Java
markDecisionPointnow sendsexperimentIdand conditionidfrom the cached assignment, across all overloads andAssignment.markDecisionPoint(). Previously Java sent onlyconditionCode, so the server re-ran experiment selection and could mark a different experiment when several share a decision point.assignedFactoralongsideassignedCondition.onSuccessruns. Previously a call chained from inside the callback (e.g. a mark) could hit a cold cache.propertiesmap, null-safe target lookup.src/test/java, TestNG). It runsExperimentClientagainst an in-processHttpServer.JS
DefaultHttpClientnever retried: axios rejects non-2xx responses before the retry branch runs, and that branch would have retried 4xx rather than 5xx anyway. It now retries network errors and 5xx with backoff for idempotent requests only (see Retry policy), fails fast on 4xx, and sets a 5s default timeout.ISendRewardResponsegainsmode/successto match the backend.markDecisionPointno longer throws when the assignment cache is null (e.g. after a failed/assign).ApiServicemark tests that already fail ondev: their expected bodies were missingcontext.Python
_syncmethods useApiService's real sync implementations instead ofasyncio.run(), so they work inside a running event loop (Jupyter, async frameworks).close()/aclose()andwith/async withsupport.Phase B: feature parity
Rewards
sendRewardForExperiment(value, experimentId, [awaitResult], cb)andsendRewardForDecisionPoint(value, context, site, target, [awaitResult], cb). Named methods mean a caller can't pass neither identifier.contextis explicit, matching JS and Python. A 409 (withawaitResult) is reported toonErrorand not retried.sendRewardacceptsawaitResult, sent as?awaitResult=true. Python already had it.Feature-flag session groups (
groupsForSession+includeStoredUserGroups), previously JS-onlysetFeatureFlagUserGroupsForSession(groups, includeStored)/clearFeatureFlagUserGroupsForSession(). Setter-only, so the constructor overloads don't double.feature_flag_user_groups_for_session=constructor kwarg plusset_feature_flag_user_groups_for_session(). Both accept aFeatureFlagUserGroupsForSessionmodel or a dict, and both fields are required.Dead endpoint methods removed
These were breaking removals, but every call already failed with a 404.
logCaliper():POST /v6/log/caliperwas removed in remove all client API code for versions < 5 #1985.addGroupMetrics()/addSingleMetrics():POST /v6/metricwas never a client route. MATHia doesn't use them. TheMetricbean stays because the/logresponse uses it.Retry policy
Retries are limited to requests that are safe to repeat, in both JS and Java:
/init,/groupmembership,/workinggroup,/useraliases(upserts),/assign(read-only), and/featureflag(records exposures if-not-exists)./mark,/logand/reward. None of them are deduplicated server-side, so resending after a lost response would record the write twice, and a duplicated reward biases adaptive assignment.JS marks safe requests with an optional
retryableflag on the request config, which custom lite-build HTTP clients can also honor. This changes Java's existing behavior: it previously retried/markand/logonce. Python never retries.Python connection pooling
The sync client is pooled per
UpgradeClient. The async client is pooled only insideasync with client:, because anAsyncClientmust be closed on the loop that opened it. Outside that block each async request uses a short-lived client, same asdev, so successiveasyncio.run()calls don't strand connection pools.Phase C: aligning behavior
/markand/rewardlookups are case-sensitive. Java matched case-insensitively, so it returned conditions whose marks the server couldn't attribute to the experiment. Python now treats aNonetarget as"".init(),setGroupMembership,setWorkingGroupandsetAltUserIdsclear the assignment and feature-flag caches once the server confirms the call. Previously only Python'sinit()cleared anything. The assignment cache gets the same generation guard as flags, so an in-flight/assigncan't refill a cleared cache. One side effect: within-subjects rotation restarts after one of these calls.Assignment.mark_decision_point()now rotates within-subjects assignments, like the client-level mark and like JS.Conditioninstead ofnull, sogetAssignedCondition().getConditionCode()no longer throws an NPE.AssignmentgainsgetCondition()/getPayload(). Its experiment type staysnull, as Java's explicit "no experiment" signal (JS reportsSIMPLE; Python returnsNoneand is unchanged).Authorizationheader is only sent when there's a token; servers with client auth enabled (toCheckAuth) answer a request without one with a 401.markDecisionPointargument order (changing it would silently swap string arguments for existing callers), and the timeout/retry differences, now described in each README (the comparison is inclientlibs/TIMEOUT_RETRY_POLICY.md). This also fixes the JS README quick start, which marked with the payload value instead ofgetCondition().Testing
jest: 111/111 pass, ESLint/Prettier clean.pytest: 242/242 pass;ruff checkandmypyclean.mvn testpasses at the branch head (ExperimentClientTest, 26 tests). Also 26/26 from amvn cleanbuild run directly via TestNG, 5–10 runs in a row.2bf0337) has a Java compile error (a stray brace); it's fixed inb19d284. Pre-commit hooks don't build the client libs, so review the Java at the branch head.Not in this PR (see audit §5)
clientlibs/js/docs(TypeDoc output) is stale and needs regenerating, and the Java README still documents several methods that no longer exist.🤖 Generated with Claude Code