Skip to content

fix(transport): use /api/v1/config/{sdk_key} route for remote config fetch - #46

Closed
usmanabbas7 wants to merge 133 commits into
mainfrom
fix/config-route-api-v1-prefix
Closed

fix(transport): use /api/v1/config/{sdk_key} route for remote config fetch#46
usmanabbas7 wants to merge 133 commits into
mainfrom
fix/config-route-api-v1-prefix

Conversation

@usmanabbas7

Copy link
Copy Markdown
Collaborator

Problem

HttpxTransport._build_route() built /config/{sdk_key}, but the real Convert config-serving CDN only serves that resource under /api/v1. Every sdk_key-mode initialization 404'd against the production and staging endpoints. The direct-config (data=) path was unaffected.

Evidence

  • php-sdk generated client (ProjectConfigApi.php): server base https://cdn-4.convertexperiments.com/api/v1 + resource path /config/{sdk_key} → full path /api/v1/config/{sdk_key}.
  • Live staging fetch: base_url=https://cdn-4-staging.convertexperiments.com returned 404; the identical fetch with the /api/v1 prefix returned 200 and a valid ConfigSnapshot (experience test-experience-ab-fullstack-4).

Fix

  • _build_route now emits /api/v1/config/{sdk_key}. The prefix lives in the route (not DEFAULT_CONFIG_BASE_URL) so the default and any user-supplied base_url stay pure hosts.
  • Conditional query params (environment, _conv_low_cache=1) preserved.
  • Docstring/comments corrected; removed an unverifiable JS-parity claim about the URL path.

Tests

  • 5 new regression tests asserting the /api/v1 prefix across query-param variants.
  • Integration mock endpoint updated to /api/v1/config/{key}.
  • Full suite: 804 passed; ruff clean; mypy --strict clean.
  • Verified live against the staging account with a pure-host base_url (404 → 200).

Base is the cumulative story-tip branch (not main, an empty placeholder in this repo's stacked-branch model) so the diff is isolated to this fix.

🤖 Generated with Claude Code

usmanabbas7 and others added 30 commits June 5, 2026 19:49
…ementation (GREEN)

Scaffold-only Python SDK foundation (Story 1.1). Frozen decisions honored:
distribution convert-python-sdk, import package convert_sdk (src/ layout),
hatchling backend, requires-python >=3.9, version single-sourced at
src/convert_sdk/version.py = 0.1.0, public boundary Core/Context/__version__
(placeholders), empty runtime deps, py.typed shipped. Author Convert.com
<[email protected]>; uv.lock gitignored (library). Removed .DS_Store and
tests/__pycache__/*.pyc accidentally tracked in the RED commit.

9 tests pass; wheel convert_python_sdk-0.1.0-py3-none-any.whl + sdist build
valid and ship convert_sdk + py.typed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-vs7n

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
SDKConfig/TransportConfig (dataclass, no pydantic), typed error hierarchy
(ConvertSDKError/ConfigError/InvalidConfigError/ConfigLoadError/TransportError),
NFR8 TLS-only enforced at config time, qs-08 inline URL-redaction shim,
httpx>=0.28,<1.0 runtime dep (qs-09 F-060). Extended public surface preserves
frozen Story 1.1 boundary.

Beads: ai-driven-product-dev-vs7n

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-9i6g

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…n (GREEN)

config_loader (validators/normalizer/loader) validates raw payloads, deep-copies
boundary data, and builds a frozen ConfigSnapshot with precomputed
experience/feature key+id indexes. Snapshots are immutable and never alias the
caller's input dict.

Beads: ai-driven-product-dev-9i6g

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-2rxo

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…GREEN)

Transport Protocol port + httpx adapter (long-lived client, trust_env=False,
TLS-only enforced upstream, optional bearer auth). JS-parity config route
/config/{sdkKey} with conditional environment= and _conv_low_cache=1 params.
Core initializes from direct config (no network) or sdkKey (fetch via
transport), exposes authoritative is_ready and immutable current_config.
Failures surface as typed ConfigLoadError/InvalidConfigError with redacted
endpoints.

Beads: ai-driven-product-dev-2rxo

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…8 NFR23)

The inline redaction shim now masks the /config/{sdkKey} path segment
(first4***last4, short keys fully ***) in addition to stripping the query
string, so full SDK keys never appear in error messages. Added redaction unit
tests; corrected the transport 5xx test to assert masking.

Beads: ai-driven-product-dev-scbi

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-lm55

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Pure-Python murmurhash3_32 (seed 9999, unsigned 32-bit) byte-exact with npm
murmurhash v3 (charCodeAt&0xff UTF-16 semantics). 72 JS-derived parity vectors.

Beads: ai-driven-product-dev-lm55

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-scbd

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
is_rule_matched (OR/AND/OR_WHEN tree + JS comparison operators) and qualifies()
(matchRulesByField MVP policy). Added audience id/key indexes to ConfigSnapshot.

Beads: ai-driven-product-dev-scbd

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-ranl

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ation (GREEN)

ExperienceResult frozen dataclass (read-only variation payload) and
select_experience (qualify -> build buckets -> deterministic select),
mirroring JS _retrieveBucketing minus storage/tracking.

Beads: ai-driven-product-dev-ranl

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-84o7

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…EEN)

Minimal visitor-scoped Context (immutable stored attributes, ephemeral
request-time overlay), Core.create_context, run_experience/run_experiences
(local-only, no network/tracking). ExperienceResult exported additively.

Beads: ai-driven-product-dev-84o7

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-kvc5

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…tion (GREEN)

Beads: ai-driven-product-dev-kvc5

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…le config

Beads: ai-driven-product-dev-chtr

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-0qgb

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…GREEN)

Beads: ai-driven-product-dev-0qgb

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-vo1o

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… implementation (GREEN)

Beads: ai-driven-product-dev-vo1o

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…(RED)

Beads: ai-driven-product-dev-5zfa

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… implementation (GREEN)

Beads: ai-driven-product-dev-5zfa

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-nd5o

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ate attr access)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-1bqe

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…/Event — implementation (GREEN)

Beads: ai-driven-product-dev-1bqe

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
usmanabbas7 and others added 12 commits June 8, 2026 18:52
…— implementation (GREEN)

Beads: ai-driven-product-dev-577c

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…t — tests (RED)

Beads: ai-driven-product-dev-usaq

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…king re-point — implementation (GREEN)

Beads: ai-driven-product-dev-usaq

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-eugx

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…rier fragment

Beads: ai-driven-product-dev-5r84

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…shed coverage)

- stop() skips self-join when called from worker thread (deadlock guard)
- remove pragma; test refresh.worker_crashed guard + re-entrant stop

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-1pug
Agent: fullstack-sdk-dev
Status: completed

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…-compat audit

Beads: ai-driven-product-dev-gfy3
Agent: fullstack-sdk-dev
Status: completed
Forward-compat audit grounded in actual post-5.2 module tree; no Phase-3 code.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Beads: ai-driven-product-dev-ugkj
Agent: fullstack-sdk-dev
Status: completed
Looking-ahead in index; async/framework forward-refs in migration + extending
docs; changes/+story-5-3-async-framework-roadmap.internal.md fragment.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…py + flush.py rows)

Beads: ai-driven-product-dev-gfy3
Review finding: audit table claimed to cover every MVP module but omitted
tracking/tracker.py and tracking/flush.py. Both added with extend-with-sibling
verdicts. No production code; gates green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… parity

The rules engine implemented only 8 of the 10 comparison operators present
in the JS reference and PHP SDK. isIn and regexMatches were missing, so any
config using "value in list" or regex audience rules silently never matched
(unknown match_type resolves to None -> rule item returns False).

- Add _is_in and _regex_matches, registered under "isIn"/"regexMatches".
  Semantics mirror the JS source of truth (regex catches re.error -> False).
- 20 unit tests (10 per operator) matching PHP ComparisonsTest granularity.
- 17 cross-SDK parity vectors (9 isIn, 8 regexMatches).

Full suite: 799 passed; ruff + mypy --strict clean; rule parity 44 vectors pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…fetch

HttpxTransport._build_route built /config/{sdk_key}, but the real
config-serving CDN only serves that resource under /api/v1, so every
sdk_key-mode initialization 404'd against the production and staging
endpoints. Confirmed against the php-sdk generated ProjectConfigApi
(server base https://cdn-4.convertexperiments.com/api/v1 + resource
/config/{sdk_key}) and a live staging fetch (404 without the prefix,
200 with it).

The /api/v1 prefix lives in _build_route so DEFAULT_CONFIG_BASE_URL and
any user-supplied base_url stay pure hosts. Query-param variants
(environment, _conv_low_cache) are preserved. Direct-config (data=) is
unaffected.

Adds 5 regression tests asserting the prefix across query variants and
updates the integration mock endpoint and docstrings/comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request updates the configuration route in HttpxTransport._build_route() to use the correct /api/v1/config/{sdkKey} prefix instead of /config/{sdkKey}, aligning it with the real Convert config-serving CDN. The changes include updated documentation, mock endpoints, and comprehensive regression tests. The reviewer recommended adding a defensive guard in _build_route to raise a ValueError if config.sdk_key is missing or None (such as during offline initialization), which prevents invalid network requests.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 82 to 85
def _build_route(config: SDKConfig) -> str:
query = HttpxTransport._build_query(config)
route = f"/config/{config.sdk_key}"
route = f"/api/v1/config/{config.sdk_key}"
return f"{route}?{query}" if query else route

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since SDKConfig.sdk_key is defined as Optional[str] (and can be None when initializing the SDK offline with direct data), calling _build_route with an offline configuration will result in an invalid route path /api/v1/config/None. To enforce defensive programming and prevent invalid network requests, we should add a guard to ensure sdk_key is present before constructing the route.

Suggested change
def _build_route(config: SDKConfig) -> str:
query = HttpxTransport._build_query(config)
route = f"/config/{config.sdk_key}"
route = f"/api/v1/config/{config.sdk_key}"
return f"{route}?{query}" if query else route
def _build_route(config: SDKConfig) -> str:
if not config.sdk_key:
raise ValueError("SDK key is required to build the config route.")
query = HttpxTransport._build_query(config)
route = f"/api/v1/config/{config.sdk_key}"
return f"{route}?{query}" if query else route

@abbaseya abbaseya left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review — final implementation (this branch is the stack tip = the full SDK)

The /api/v1 route fix in this PR is correct and well-evidenced (pure-host base + /api/v1/config/{sdk_key} route, query params preserved, live 404→200 against staging, regression tests added). Two notes on the fix, then the implementation-side parity findings this branch carries (spec-side findings are on convertcom/ai-driven-product-dev#59). All claims are file:line-grounded; empirical ones were executed.

On the fix itself

  • ✅ Correct and verified. Good call sourcing the prefix from the php-sdk generated client + a live staging fetch.
  • 🟡 Minor structural divergence from JS parity: JS keeps /api/v1 in the base (DEFAULT_CONFIG_ENDPOINT = 'https://cdn-4.convertexperiments.com/api/v1', route /config/{sdkKey}); this fix puts /api/v1 in the route with a pure-host base. Same full URL — cosmetic, but it means the Python "base" no longer matches the JS "configEndpoint" concept. Fine to keep; just flagging for cross-SDK mental-model consistency.
  • 🟠 Context: this is the third parity defect caught post-sprint (after the missing rule operators in 2605a59), all with green tests during the audited sprint. The pattern matters more than any single fix — see below.

🔴 CRITICAL — Bucketing hashes UTF-16 code units, not UTF-8 (non-ASCII visitors bucket differently than JS/PHP)

src/convert_sdk/evaluation/bucketing.py feeds _utf16_code_units(value) (lines 40-58) into MurmurHash3. JS uses the npm murmurhash package's TextEncoder().encode()UTF-8 bytes; PHP uses UTF-8 (unpack('C*') / native murmur3a). Python is the sole outlier.

Verified empirically against the real npm package the JS SDK ships:

input Python (this branch) npm murmurhash (JS)
test_visitor (ASCII) 2397688595 2397688595 ✓
用户123 531569922 3859151469

A non-ASCII visitorId (or composite experienceId+visitorId) computes a different hash → can select a different variation in Python vs JS/PHP. This breaks same-experiment cross-SDK consistency.

Compounding it:

  • qs-04-murmurhash3-wrapper.md:66 mandated UTF-8; the implementation did UTF-16, and the bucketing.py docstring (lines 14-17, 43-46) defends UTF-16 with a false "byte-exact against the JS reference" claim.
  • The parity fixtures cannot catch it: scripts/js_reference/bucketing.js is a hand port using key.charCodeAt(i) & 0xff (UTF-16, co-buggy) — not the real npm module — so tests/parity/fixtures/bucketing_vectors.json pins 用户123 to the wrong value and the suite is green against a wrong oracle.

Fix: hash value.encode("utf-8"), use the UTF-8 byte length for the length-mix, and regenerate fixtures from the real npm murmurhash package (or a TextEncoder UTF-8 source). The hash core (seed 9999, constants, finalizer, composite order f"{experience_id}{visitor_id}", bucket math) is otherwise correct — the defect is isolated to the input encoding.

🟠 Rule operators (now patched here) shipped incomplete during the audited sprint

2605a59 added isIn + regexMatches. The on-disk _COMPARATORS map (rules.py:168-182) now has all 13 JS/PHP names — good. AND/OR/OR_WHEN tree, empty-group/short-circuit, and key-not-found behavior all match JS. Two minor notes: isIn lowercases only testAgainst (matches JS; PHP lowercases both — PHP is the outlier, Python is correctly JS-aligned); isIn scalar-testAgainst wraps→can-match (follows PHP) vs JS []→never-match (low blast radius).

🟠 Segment latch claimed but not implemented

src/convert_sdk/evaluation/segments.py:13,74 docstring claims a JS-style segments_matched latch, but select_custom_segments (lines 86-105) evaluates each segment's rule independently (if not this_matched: continue) — no latch variable. JS (segments-manager.ts:~100) latches after the first match and adds subsequent rule-bearing segments. For ordered multi-segment lists where an early segment matches and a later one's rule does not, JS records both, Python records only the first. Verify against JS and fix the code or the docstring.

🟡 Public return-type drift (undocumented vs PRD)

Ships ExperienceResult/FeatureResult (domain/results.py:22,69); the PRD declares BucketedVariation/BucketedFeature (PHP-parity) with no reconciling footnote. ExperienceResult lacks the variables field the PRD's BucketedVariation declares. Core.current_config (core.py:108) vs PRD's Core.config_version. Sentinels collapse all misses to None (deliberate/documented; JS returns RuleError/BucketingError sentinels and a DISABLED feature object, never null — reasons in Python are available only via the opt-in diagnose_* surface).

🟡 DoD #5 — no staging demo; examples use invented keys

examples/_sample_config.py hard-codes account_id "100123", experience checkout-experiment, feature checkout-banner (self-labeled placeholders). The canonical staging keys test-experience-ab-fullstack-1 / test-feature-rollout-1 appear nowhere in the repo; there is no demo/ directory and no path that sends a real conversion. The examples meet the (offline-scoped) Story 1-6 spec but not the runbook's staging-verification DoD — which is also what would have caught this PR's 404 before it shipped.

What's correct (stated plainly)

  • Config shape reads flat at the root end-to-end (config_loader/{loader,normalizer,validators}.py) — no Ruby qs-05 envelope bug; config_available/ready requires account_id AND project.id (no empty-but-ready masquerade).
  • Tracking payload matches the JS wire contract (tracking/payloads.py): {accountId, projectId, source, enrichData, visitors[].events[]}, conversion event goalId/goalData(array)/bucketingData(flat map), no goalKey/timestamp (F-001 implemented), revenue→{key:"amount"}, enrichData = data_store is None. Dedup keyed (visitor, goal), batch size 10, explicit/timeout/atexit flush — parity-correct. source="js-sdk" is a deliberate safe fallback; confirm the backend allowlist before GA.
  • Error model is the strongest area — clean typed hierarchy, never raises on normal misses.
  • Feature variable casting is parity-correct for canonical config; diverges only on off-spec values ("0"→bool, dirty int strings).

Blockers before merge to main: the UTF-8 bucketing fix (+ real-oracle fixtures) and the staging demo. Full write-up in the Asana review task; spec-side findings on ai-driven-product-dev#59.

@usmanabbas7
usmanabbas7 force-pushed the epic-5/story-3-add-planned-expansion-surfaces-for-async-and-framework-integrations branch 2 times, most recently from f09ee36 to c934e6b Compare June 15, 2026 12:13
…h in custom-segment evaluation — F-066

Propagates the F-066 latch to the fix/config-route-api-v1-prefix branch (PR #46),
which is on a separate accumulation lineage from the rebased story stack and was
not reached by the story-branch propagation. select_custom_segments now latches
`segmentsMatched` (JS segments-manager.ts:100-121, PHP SegmentsManager.php:104-128):
once an earlier segment's rule matches, subsequent segments are recorded without
re-evaluating their own rules. segments.py is byte-identical to the remediated
3-3 (f2d8916); added the two ordered-multi-segment parity tests. Full suite 806
passed; ruff + mypy --strict clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@usmanabbas7
usmanabbas7 changed the base branch from epic-5/story-3-add-planned-expansion-surfaces-for-async-and-framework-integrations to main June 15, 2026 13:37
@usmanabbas7 usmanabbas7 reopened this Jun 15, 2026
@usmanabbas7

Copy link
Copy Markdown
Collaborator Author

F-066 — segments_matched latch propagated here

This branch (fix/config-route-api-v1-prefix) is a separate accumulation lineage from the rebased story stack, so it was not reached by the story-by-story propagation. Applied the F-066 latch surgically in commit ca686d5: evaluation/segments.py is now byte-identical to the remediated story-3-3 latch (segments-manager.ts:100-121, SegmentsManager.php:104-128 parity), plus the two ordered-multi-segment parity tests. The only pre-fix difference on this branch was the matcher loop itself.

Also re-pointed this PR's base epic-5/story-3main: the story branch was rebased to a new lineage during the F-066 propagation, leaving this PR unmergeable against it. Against main (of which this branch is a clean linear superset) the PR is mergeable again and CI runs.

  • Local: uv run pytest → 806 passed; ruff check + mypy --strict clean (44 files)
  • CI (commit ca686d5): 21/21 checks green — ruff, mypy --strict, changelog, build, bounds-check, and the full py3.9–3.13 × {ubuntu,macos,windows} test matrix.

usmanabbas7 and others added 3 commits June 15, 2026 22:55
…t UA and report status

Tracking events were POSTed to the config CDN host (returning 403) instead of
the separate metrics endpoint. Adds TransportConfig.track_base_url (default
https://[project_id].metrics.convertexperiments.com/v1, JS/PHP parity),
substitutes [project_id] at request time, and POSTs to {track_base}/track/{key}.

Also sets User-Agent: ConvertAgent/1.0 so the metrics bot filter does not
silently drop events (200-but-discarded), and propagates the HTTP status code
on the success path so QueueReleasedPayload.status_code carries the real 2xx
(send_tracking now returns int).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds demo/run_demo.py — an OFFLINE (committed fixture, deterministic, no
network) and LIVE (sdk_key against staging) manual-verification harness that
runs the full FullStack arc: experience -> feature-rollout experience ->
segments -> conversion + delivery report.

Mirrors the JS and PHP demos: buckets the feature rollout via run_experience
(key test-experience-ab-fullstack-4, per php-sdk config; the JS demo's
test-feature-rollout-1 is stale), passes location=events and the mobile=true
audience attribute, and sets the country=US default segment. Self-diagnoses
any non-bucketing via diagnose_experience. Excludes demo/ from the sdist,
gitignores demo/.env, and lints demo/ in CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ollout-1 typed vars

The committed demo harness (49de4e9) had unilaterally overridden the frozen
qs-12 spec's feature-rollout decision: it used PHP's run_experience on
test-experience-ab-fullstack-4 and printed raw string variables_data, instead
of the spec's JS-authoritative run_feature('test-feature-rollout-1') resolving
typed variables (bool/string/integer). This left two gaps: the OFFLINE output
did not match the README contract, and the typed-variables acceptance criterion
was unmet.

Align the implementation to the spec:
- config_fixture.json: re-key the feature to test-feature-rollout-1; declare the
  4 typed vars the README shows; drop the redundant rollout-as-experience block;
  correct the _comment (JS-authoritative; LIVE override via env var).
- run_demo.py: step [2] now calls run_feature(test-feature-rollout-1) printing
  typed vars + status + via experience/variation; move the audience attrs +
  country default-segments LIVE-only so OFFLINE output is byte-identical to the
  README; correct the misleading comments.
- test_demo_harness.py: flip the feature-key assertions/comments back to
  test-feature-rollout-1; the /api/v1/config route guard is unchanged.

OFFLINE output now byte-matches demo/README.md verbatim. Full suite 828 passed,
ruff + mypy green, demo/ excluded from sdist + wheel, zero src/ changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
abbaseya

This comment was marked as outdated.

@abbaseya abbaseya left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Corrected round-3 review (supersedes the earlier version of this review)

My earlier review of this PR led with "bucketing UTF-8 fix never landed." That is true for this branch, but I wrongly presented it as the SDK's state overall. The fix did land on PR #45 (epic-5/story-3). The accurate framing: this PR is a divergent branch that never received the F-063 propagation.

What this branch (fix/config-route) uniquely gets right

  • Config /api/v1 route fix — correct (resolves the sdk_key-mode 404). ✅
  • Tracking delivery → metrics endpoint + ConvertAgent UA (7dd639f) — fixes events POSTing to the config CDN (403, silently dropped). ✅ Surfaced by building the demo — exactly what the "verify against staging" leg exists to catch.
  • Demo harness with real staging keys (test-experience-ab-fullstack-1, test-feature-rollout-1, project 10035569/10034190). ✅
  • Operators ✅, segment latch ✅.

What this branch is missing

  • 🔴 F-063 bucketing UTF-8. bucketing.py here still hashes UTF-16 code units (executed: 用户123531569922 vs correct 3859151469); fixture still pins the buggy value; js_reference/bucketing.js is still the co-buggy hand port. PR #45 fixed all three. This branch was cut off the old, pre-patch-sprint stack and never rebased onto the F-063 work.

This PR is confusing — please clarify or close it

  • Its base is main (the empty placeholder), so the diff is the entire ~25k-line tree instead of an isolated fix.
  • It diverges from the rebased canonical story stack (PR #45) — neither is an ancestor of the other.
  • It's missing the F-063 bucketing fix that PR #45 carries.

There's no clear reason to keep this as a parallel open PR. Recommendation: move this branch's three unique fixes (config /api/v1, tracking→metrics+UA, demo) onto the canonical PR #45 line, then close this PR — so there's a single authoritative branch to review and merge. The merged branch must carry bucketing UTF-8 and config /api/v1 and tracking→metrics+UA and the demo. Then re-run the demo LIVE against staging and confirm a conversion lands in reports.

@usmanabbas7

Copy link
Copy Markdown
Collaborator Author

Superseded by PR #45 (epic-5/story-3), which now carries this PR's transport + demo work via cherry-pick:

  • fix(transport): use /api/v1/config/{sdk_key} route (76e240b)
  • fix(transport): metrics endpoint + ConvertAgent UA delivery (7dd639f)
  • feat(demo): qs-12 dual-mode staging demo harness (49de4e9, 9ffe8c2)

The bucketing/parity changes on this branch were not carried over: this branch's MurmurHash3 used UTF-16 code units (charCodeAt), but the shipped JS SDK depends on npm [email protected], whose v3 hashes UTF-8 bytes via TextEncoder().encode(). PR #45 has the correct UTF-8 F-063 implementation (verified byte-exact against the real npm oracle). Closing to avoid shipping the UTF-16 regression.

Reconciled state also pushed as integration/python-sdk-round3-final for traceability.

@abbaseya
abbaseya deleted the fix/config-route-api-v1-prefix branch June 18, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants