fix: emit experience-activation (bucketing) tracking events with per-call enable_tracking control - #51
Conversation
…king Story 2.5. Tests for the additive bucketing-event delivery path and keyword-only enable_tracking control. Implementation follows (GREEN). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…call enable_tracking control
Closes the cross-SDK exposure-tracking parity gap (Story 2.5). A visitor that
buckets into a variation now produces a standalone bucketing/activation event
delivered to the metrics tracking endpoint, batched alongside conversion events
in the same events array, so experiment exposure is recorded instead of only
conversions.
Strictly additive: run_experience / run_experiences gain a keyword-only
enable_tracking=True parameter; the ExperienceResult return value and all
local-evaluation behavior are unchanged. Bucketing events dedup by
(visitor_id, experience_id) via the existing DataStore boundary and flow through
the existing Tracker queue/transport and the LifecycleEvent.BUCKETING EventBus.
Wire shape: {"eventType":"bucketing","data":{"experienceId","variationId"}}
(JS VisitorTrackingEvents/BucketingEvent parity; no timestamp).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-review follow-up (Story 2.5). Extract the event-construction + payload extraction into _serialize_event and reuse a single parametrize decorator so the parity tests stay under the SonarCloud new-code-duplication gate. No behavior change; 41 bucketing tests + full suite (857) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements Story 2.5, introducing tracking and deduplication for bucketing activation events. It adds a new BucketingEvent domain object, a BucketingEventPayload for lifecycle events, and a dedicated serializer build_bucketing_payload matching the JS-SDK wire contract. The Context evaluation methods (run_experience and run_experiences) now accept an enable_tracking keyword-only parameter to control whether bucketing events are enqueued. Deduplication logic has been added via evaluate_bucketing_dedup to ensure each visitor-experience pair produces at most one bucketing event. Extensive unit, integration, and JS-parity tests have been added to verify these changes. No review comments were provided, so there is no feedback to address.
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.
Empty commit to fire pull_request:synchronize so CodeQL default setup produces an ingested analysis for this PR (its original analysis predated code scanning being enabled, leaving the code_scanning merge-protection rule with no PR results to evaluate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Closes the cross-SDK exposure-tracking parity gap (Story 2.5 / FR40a + FR8). The released Python SDK (v1.0.0) tracked conversions only — a visitor who was bucketed but never converted produced no exposure signal, so experiment reports could show conversions without the visitor denominator. Every sibling SDK (JS reference, Android FR18, iOS FR19/FR39) already delivers a standalone bucketing/activation event; this brings the Python SDK to parity.
When a visitor buckets into a variation, the SDK now enqueues a bucketing event delivered to the metrics tracking endpoint, batched alongside conversion events in the same
eventsarray, gated per-call by a new keyword-onlyenable_tracking=Trueparameter onContext.run_experience/run_experiences.Strictly additive — intended release bump = PATCH (1.0.1)
This is a parity defect fix, not a feature. It is strictly additive:
enable_trackingis keyword-only with defaultTrue— additive to the existingrun_experience/run_experiencessignatures (no positional breakage).ExperienceResultreturn value and all local-evaluation behavior are byte-for-byte unchanged — the bucketing event is a pure side effect.ConversionEvent, and evaluation determinism are untouched.LifecycleEventmember (BUCKETINGalready exists).All commits and this PR title are
fix:/test:-typed with no!and noBREAKING CHANGE:footer, so semantic-release computes 1.0.1 (PATCH) — not a minor or major bump.Wire shape (JS parity)
{"eventType": "bucketing", "data": {"experienceId": "<experience_id>", "variationId": "<variation_id>"}}VisitorTrackingEventswrapper +BucketingEventbody parity (types.gen.ts:2467-2473,:2486-2497); notimestamp, no extra keys; ids string-typed (.toString()parity). A golden-fixture parity test pins these bytes.Behavior
events[].enable_tracking=Falsesuppresses the enqueue; the returnedExperienceResultis identical.(visitor_id, experience_id)via the existingDataStoreboundary (bucketing:namespaced, collision-safe JSON key); at most one event per visitor-experience in scope.Context(noTracker) is a safe no-op.EventBus/LifecycleEvent.BUCKETING; a raising handler does not break tracking flow.User-Agent: ConvertAgent/1.0unchanged); no blocking network I/O on the enqueue path.Deferred / resolved design decision (dedup key & scope)
The JS reference (
data-manager.ts:679-685) enqueues the bucketing event unconditionally whenenableTrackingis set and leans on the server-sideenrichDataflag for dedup — it has no client-side(visitor, experience)gate. Story 2.5 AC#3 is the authoritative contract and explicitly mandates client-side dedup keyed by(visitor_id, experience_id), mirroring the Story 2.3 conversion dedup. This PR honors the story: it implements an explicit client-side dedup marker (evaluate_bucketing_dedup) rather than the JS unconditional-enqueue behavior.run_experiences()emits one event per newly-bucketed qualified experience (the story Dev Notes default).Tests / gates
tests/test_bucketing_events.py(unit, ACs 1-5) +tests/parity/test_js_bucketing_event_parity.py+tests/parity/fixtures/bucketing_event_vectors.json(golden wire-byte parity).mypy src/clean.ruff check src/ tests/clean. Parity suite green.enable_trackinggate, dedup, mixed-batch flush, EventBus isolation, layering, PATCH-qualifying); one duplication finding fixed.References Story 2.5.
🤖 Generated with Claude Code