Skip to content

epic-2/story-1: Track Conversions from a Visitor Context - #29

Closed
usmanabbas7 wants to merge 5 commits into
epic-1/story-3-create-and-reuse-visitor-contextsfrom
epic-2/story-1-track-conversions-from-a-visitor-context
Closed

epic-2/story-1: Track Conversions from a Visitor Context#29
usmanabbas7 wants to merge 5 commits into
epic-1/story-3-create-and-reuse-visitor-contextsfrom
epic-2/story-1-track-conversions-from-a-visitor-context

Conversation

@usmanabbas7

Copy link
Copy Markdown
Collaborator

Summary

First tracking-slice story (Epic 2), implemented fresh per the audit-corrected spec (findings F-038, F-051, F-052, F-055; original Dev Agent Record referred to the superseded PR 17 lineage):

  • Context.track_conversion(goal_key) — public MVP tracking call, forward-compatible with Story 2.2's conversion_data= kwargs. Exports ConversionResult + ConversionStatus from the package root.
  • F-051 (auto-delegation): ConfigSnapshot stored goals but had no precomputed index/accessor (Story 1.2 scope gap on this lineage) — added minimal _goals_by_key index + get_goal_by_key(), mirroring the existing get_feature_by_key pattern.
  • F-052 / FR50: unknown goal key returns a typed NON-EXCEPTION ConversionResult(status=GOAL_NOT_FOUND, event=None) — distinguishable from a successful enqueue via .status without try/except (1-4/1-6 typed-result precedent).
  • F-055: tracking/ package created as __init__.py + conversions.py (deliberate scope-narrowing — tracker.py/queue.py/deduplication.py/payloads.py/flush.py deferred to later Epic 2 stories). No network delivery, no payload JSON, no batching in this story.

Tests: 218 → 230 passing (+12, tests/test_conversion_tracking.py). No regressions.

Traceability

Notes for reviewer

  • Readiness gate scored 8/10 with 4 questions, auto-delegated in sprint mode (F-051 goal-index gap; outcome shape; event identity; forward-compat surface). See ai-driven-product-dev/work/2026-06-07-track-conversions-from-a-visitor-context/readiness-assessment.md.
  • Code review: clean (round 1 finding — str(None) coercion of missing goal id — fixed; round 2 clean).

🤖 Generated with Claude Code

@usmanabbas7 usmanabbas7 self-assigned this Jun 7, 2026
@usmanabbas7
usmanabbas7 requested a review from clllaur June 7, 2026 11:01

@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 implements the foundation for conversion tracking (Story 2.1), introducing the track_conversion method on the visitor context, indexing goals by key in the configuration snapshot for O(1) lookups, and defining the core conversion models (ConversionStatus, ConversionEvent, and ConversionResult). The review feedback recommends adding **kwargs to track_conversion and create_conversion to ensure true forward-compatibility with future stories, and suggests using None instead of an empty string to represent missing goal IDs in ConversionEvent to align with standard Pythonic practices.

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 +228 to +254
def track_conversion(self, goal_key: str) -> ConversionResult:
"""Track a goal conversion for this visitor (Story 2.1).

Resolves ``goal_key`` against the current immutable snapshot and creates
an in-process conversion event associated with this visitor and the
resolved goal identity. Returns a typed
:class:`~convert_sdk.domain.results.ConversionResult`:

* ``status == ConversionStatus.QUEUED`` — the goal resolved and an event
was created (``result.event`` carries the
:class:`~convert_sdk.domain.results.ConversionEvent`).
* ``status == ConversionStatus.GOAL_NOT_FOUND`` — the goal key is absent
from the loaded config. This is a diagnosable NON-EXCEPTION outcome
(FR50), distinguishable from success via ``status`` alone — callers
never need ``try``/``except`` to tell the two apart.

Performs no network I/O and no payload serialization; event delivery,
payload shaping, batching, deduplication, and flush land in later Epic 2
stories. The Story 2.1 surface is goal-key only; richer conversion
attributes (e.g. revenue) arrive in Story 2.2 as additional keyword
arguments, keeping this signature forward-compatible.
"""
return create_conversion(
self._snapshot,
visitor_id=self._state.visitor_id,
goal_key=goal_key,
)

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

The docstring and PR description mention that the signature of track_conversion is designed to be forward-compatible with Story 2.2's conversion_data= kwargs. However, the current signature def track_conversion(self, goal_key: str) does not accept arbitrary keyword arguments. Any caller attempting to pass keyword arguments (e.g., conversion_data) will encounter a TypeError. To make the signature truly forward-compatible, we should add **kwargs: Any to both track_conversion and create_conversion.

    def track_conversion(self, goal_key: str, **kwargs: Any) -> ConversionResult:
        """Track a goal conversion for this visitor (Story 2.1).

        Resolves ``goal_key`` against the current immutable snapshot and creates
        an in-process conversion event associated with this visitor and the
        resolved goal identity. Returns a typed
        :class:`~convert_sdk.domain.results.ConversionResult`:

        * ``status == ConversionStatus.QUEUED`` — the goal resolved and an event
          was created (``result.event`` carries the
          :class:`~convert_sdk.domain.results.ConversionEvent`).
        * ``status == ConversionStatus.GOAL_NOT_FOUND`` — the goal key is absent
          from the loaded config. This is a diagnosable NON-EXCEPTION outcome
          (FR50), distinguishable from success via ``status`` alone — callers
          never need ``try``/``except`` to tell the two apart.

        Performs no network I/O and no payload serialization; event delivery,
        payload shaping, batching, deduplication, and flush land in later Epic 2
        stories. The Story 2.1 surface is goal-key only; richer conversion
        attributes (e.g. revenue) arrive in Story 2.2 as additional keyword
        arguments, keeping this signature forward-compatible.
        """
        return create_conversion(
            self._snapshot,
            visitor_id=self._state.visitor_id,
            goal_key=goal_key,
            **kwargs,
        )

Comment on lines +38 to +43
def create_conversion(
snapshot: "ConfigSnapshot",
*,
visitor_id: str,
goal_key: str,
) -> ConversionResult:

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

To support the forward-compatible signature of Context.track_conversion, create_conversion should also accept arbitrary keyword arguments (**kwargs: Any). This ensures that when additional attributes (like revenue) are introduced in Story 2.2, the underlying creation service can receive them without breaking the current signature. Note: You will need to import Any from typing in this file.

def create_conversion(
    snapshot: "ConfigSnapshot",
    *,
    visitor_id: str,
    goal_key: str,
    **kwargs: Any,
) -> ConversionResult:

"""

visitor_id: str
goal_id: str

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

In Python, it is idiomatic to represent the absence of an optional value using None rather than an empty string "". Since ConversionResult.goal_id is already typed as Optional[str], we should also type ConversionEvent.goal_id as Optional[str] to maintain consistency and avoid using empty strings as a sentinel for missing IDs.

Suggested change
goal_id: str
goal_id: Optional[str]
References
  1. Representing missing optional values as None is standard Pythonic practice (PEP 8 / idiomatic Python), rather than using empty strings as a sentinel. (link)

Comment on lines +71 to +72
raw_goal_id = goal.get("id")
goal_id = str(raw_goal_id) if raw_goal_id is not None else ""

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

Aligning with the updated Optional[str] type for ConversionEvent.goal_id, we should use None instead of "" when raw_goal_id is missing. This avoids using empty strings as a sentinel for missing IDs and keeps the representation of missing IDs consistent across both ConversionEvent and ConversionResult.

Suggested change
raw_goal_id = goal.get("id")
goal_id = str(raw_goal_id) if raw_goal_id is not None else ""
raw_goal_id = goal.get("id")
goal_id = str(raw_goal_id) if raw_goal_id is not None else None
References
  1. Representing missing optional values as None is standard Pythonic practice (PEP 8 / idiomatic Python), rather than using empty strings as a sentinel. (link)

usmanabbas7 and others added 5 commits June 14, 2026 21:58
Beads: ai-driven-product-dev-1bqe

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/Event — implementation (GREEN)

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…plementation (GREEN)

Beads: ai-driven-product-dev-z46t

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lementation (GREEN)

Beads: ai-driven-product-dev-ck5j

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-ck5j

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@usmanabbas7
usmanabbas7 force-pushed the epic-1/story-3-create-and-reuse-visitor-contexts branch from 9ec000b to 3962b4e Compare June 14, 2026 16:58
@usmanabbas7
usmanabbas7 force-pushed the epic-2/story-1-track-conversions-from-a-visitor-context branch from e5af437 to 4c2b98b Compare June 14, 2026 16:58
@abbaseya

Copy link
Copy Markdown
Collaborator

Superseded — all commits already in main (bc76b64). Closing without merge as part of post-sprint cleanup.

@abbaseya abbaseya closed this Jun 18, 2026
@abbaseya
abbaseya deleted the epic-2/story-1-track-conversions-from-a-visitor-context branch June 18, 2026 16:31
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