epic-2/story-2: Support Revenue Data and Tracking Payload Construction - #30
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements Story 2.2 of the Convert Python SDK, extending the conversion tracking functionality to support optional revenue, custom conversion data, and visitor attribution context (active segments and variation assignments). It introduces the tracking/payloads.py module to map internal snake_case conversion events to the verbose JS-SDK wire contract, along with dedicated tracking error classes and comprehensive test coverage. The review feedback suggests several improvements: passing location_attributes through the conversion creation and bucketing evaluation pipeline to support location-targeted experiences, validating that conversion_data is a mapping to prevent potential AttributeErrors, and validating that revenue is a valid number (and not a boolean) to fail fast on programmer misuse.
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.
| return create_conversion( | ||
| self._snapshot, | ||
| visitor_id=self._state.visitor_id, | ||
| goal_key=goal_key, | ||
| revenue=revenue, | ||
| conversion_data=conversion_data, | ||
| visitor_attributes=self._state.visitor_attributes, | ||
| ) |
There was a problem hiding this comment.
To ensure that experiences with location-targeting rules are correctly evaluated and attributed at conversion time, the context's location_attributes should be passed to create_conversion.
| return create_conversion( | |
| self._snapshot, | |
| visitor_id=self._state.visitor_id, | |
| goal_key=goal_key, | |
| revenue=revenue, | |
| conversion_data=conversion_data, | |
| visitor_attributes=self._state.visitor_attributes, | |
| ) | |
| return create_conversion( | |
| self._snapshot, | |
| visitor_id=self._state.visitor_id, | |
| goal_key=goal_key, | |
| revenue=revenue, | |
| conversion_data=conversion_data, | |
| visitor_attributes=self._state.visitor_attributes, | |
| location_attributes=self._location_attributes, | |
| ) |
| def _compute_bucketing_assignments( | ||
| snapshot: "ConfigSnapshot", | ||
| *, | ||
| visitor_id: str, | ||
| visitor_attributes: Optional[Mapping[str, Any]], | ||
| ) -> Dict[str, str]: |
There was a problem hiding this comment.
Add location_attributes to _compute_bucketing_assignments so that location-targeted experiences can be correctly evaluated.
def _compute_bucketing_assignments(
snapshot: "ConfigSnapshot",
*,
visitor_id: str,
visitor_attributes: Optional[Mapping[str, Any]],
location_attributes: Optional[Mapping[str, Any]] = None,
) -> Dict[str, str]:| result = select_experience( | ||
| str(key), | ||
| snapshot, | ||
| visitor_id=visitor_id, | ||
| visitor_attributes=visitor_attributes, | ||
| ) |
There was a problem hiding this comment.
| def create_conversion( | ||
| snapshot: "ConfigSnapshot", | ||
| *, | ||
| visitor_id: str, | ||
| goal_key: str, | ||
| revenue: Optional[float] = None, | ||
| conversion_data: Optional[Mapping[str, Any]] = None, | ||
| visitor_attributes: Optional[Mapping[str, Any]] = None, | ||
| ) -> ConversionResult: |
There was a problem hiding this comment.
Add location_attributes to create_conversion signature to support passing location context from the visitor session.
def create_conversion(
snapshot: "ConfigSnapshot",
*,
visitor_id: str,
goal_key: str,
revenue: Optional[float] = None,
conversion_data: Optional[Mapping[str, Any]] = None,
visitor_attributes: Optional[Mapping[str, Any]] = None,
location_attributes: Optional[Mapping[str, Any]] = None,
) -> ConversionResult:| bucketing_assignments = _compute_bucketing_assignments( | ||
| snapshot, visitor_id=visitor_id, visitor_attributes=visitor_attributes | ||
| ) |
| if not conversion_data: | ||
| return | ||
| for key, value in conversion_data.items(): |
There was a problem hiding this comment.
Ensure conversion_data is a mapping before checking if it is empty or iterating over its items, to prevent unhandled AttributeError if a non-mapping (like a list or string) is passed.
| if not conversion_data: | |
| return | |
| for key, value in conversion_data.items(): | |
| if conversion_data is None: | |
| return | |
| if not isinstance(conversion_data, Mapping): | |
| raise ConversionDataError( | |
| "conversion_data", | |
| reason="value must be a dictionary/mapping", | |
| ) | |
| if not conversion_data: | |
| return | |
| for key, value in conversion_data.items(): |
| # Fail fast on programmer misuse before any goal resolution (AC#3). | ||
| _validate_conversion_data(conversion_data) |
There was a problem hiding this comment.
Validate that revenue is a valid number (and not a boolean) to fail fast on programmer misuse, matching the documented behavior of ConversionDataError.
# Fail fast on programmer misuse before any goal resolution (AC#3).
if revenue is not None and (not isinstance(revenue, (int, float)) or isinstance(revenue, bool)):
raise ConversionDataError(
"revenue",
reason="value must be a number (int or float)",
)
_validate_conversion_data(conversion_data)Beads: ai-driven-product-dev-f05o Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… implementation (GREEN) Beads: ai-driven-product-dev-f05o Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tests (RED) Beads: ai-driven-product-dev-se3u Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…implementation (GREEN) Beads: ai-driven-product-dev-se3u Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-tg8a Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-tg8a Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-d2tg Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e5af437 to
4c2b98b
Compare
021044d to
7cf6b2f
Compare
|
Superseded — all commits already in main (bc76b64). Closing without merge as part of post-sprint cleanup. |
Summary
Extends Story 2.1's tracking slice with revenue + conversion attributes and owns raw outbound payload assembly (
tracking/payloads.py) matching the verbose JS-SDK wire contract. Stops before batching/dedup/queue/flush/network (Story 2.3/2.4 territory).ConversionEventdomain model (revenue, conversion_data, segments, bucketing attribution) + typedConversionDataErrorfor non-JSON-serializableconversion_data(AC feat(python-sdk): add api manager and event queue layer #3)Context.track_conversion(goal_key, revenue=None, conversion_data=None); attribution (segments + active variation assignments) computed at conversion time (AC feat(python-sdk): add deterministic bucketing and rule evaluation core #1, FR33/FR34)tracking/payloads.py: single owner of snake_case → verbose JS-SDK wire mapping. EnvelopeaccountId/projectId/source/enrichData/visitors[].events[]; eventdatacarries onlygoalId/goalData/bucketingData(AC feat(python-sdk): add in-memory config evaluation managers #2, FR35, NFR17/NFR21)segmentsfiltered to the JSVisitorSegmentsfixed-key allowlist so non-segment visitor traits don't leak into the payload (NFR21 parity)Audit findings honored: F-001 (no
goalKey/timestamp/conversionDatain the wire event —ConversionEventtype pertypes.gen.ts:2502-2530), F-002 (enrichDatacomputed from DataStore presence, not hardcoded;source: "js-sdk"fallback untilpython-sdkis confirmed in the backend allowlist).Tests: 230 → 268 (+38), full suite green (
uv run pytest).Traceability
sprint/2026-04-06-convert-python-sdk— stacked on epic-2/story-1: Track Conversions from a Visitor Context #29 (story 2-1) → epic-1/story-3: Create and Reuse Visitor Contexts (gap-fill) #28 → epic-1/story-6: Deliver Quickstart and First-Run Examples #27 → epic-1/story-4: Run Local Experience Evaluations #26 → epic-1/story-2: Support sdkKey and Direct-Config Initialization #25 → Epic 1 Story 1: Scaffold the publishable SDK foundation #24ai-driven-product-dev-q9p4; tasks-f05o(SDK-1),-se3u(SDK-2),-tg8a(SDK-3),-d2tg(R1 fix) — all closedai-driven-product-dev/work/2026-06-07-support-revenue-data-and-tracking-payload-construction/Notes for reviewer
source="js-sdk"fallback (F-002); computedenrichData(F-002); wire event field set per F-001. See conductor's readiness-assessment.md for details.VisitorSegmentsallowlist parity issue, fixed in021044d).goalKey/timestamp; Dev Notessource: python-sdk/enrichData: true) that contradicts the audit-patched AC feat(python-sdk): add in-memory config evaluation managers #2 / Task 3.2 — the patched text was followed.🤖 Generated with Claude Code