Skip to content

Codex/feat tracking delivery mvp - #12

Merged
usmanabbas7 merged 2 commits into
dev-branchfrom
codex/feat-tracking-delivery-mvp
Apr 10, 2026
Merged

Codex/feat tracking delivery mvp#12
usmanabbas7 merged 2 commits into
dev-branchfrom
codex/feat-tracking-delivery-mvp

Conversation

@usmanabbas7

Copy link
Copy Markdown
Collaborator

Summary

Adds the tracking delivery foundation for the Convert Python SDK.

This PR implements the next tracking slice after context-scoped conversion creation:

  • conversion data / revenue payload support
  • tracking payload serialization
  • in-process tracking queue
  • default visitor/goal deduplication
  • explicit repeated transaction reporting
  • explicit queue flush through the transport boundary

BMAD scope covered:

  • Story 2.2: Support Revenue Data and Tracking Payload Construction
  • Story 2.3: Batch, Deduplicate, and Flush Tracking Events

What Changed

  • Extended Context.track_conversion(...) with optional conversion_data
  • Added typed conversion-data validation
  • Preserved bucketing attribution on conversion events
  • Added Convert tracking payload serialization
  • Added shared SDK-owned tracking queue
  • Added TrackingConfig for queue behavior
  • Added Context.release_queues(...) for explicit flush/release
  • Added default deduplication for repeated visitor/goal conversions
  • Added force_multiple_transactions=True for explicit repeat transaction reporting
  • Extended the transport boundary with tracking delivery support
  • Added HTTPX-backed tracking POST support for:
    • /track/{sdk_key}
    • /track/{account_id}/{project_id}

Validation

Ran in ../python-sdk:

  • uv sync --group dev
  • uv run pytest -p no:cacheprovider
  • uv build

Results:

  • all 37/37 tests passed
  • wheel and sdist built successfully

Notes

This PR intentionally stops before Story 2.4.

Not included yet:

  • tracking lifecycle events
  • richer delivery diagnostics
  • delivery outcome callbacks/hooks
  • queue release event bus behavior

@usmanabbas7
usmanabbas7 merged commit 17bd8fe into dev-branch Apr 10, 2026

@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 a conversion tracking and event queuing system for the Convert Python SDK. It introduces a TrackingQueue for buffering events in-process, adds a TrackingConfig for batching and data enrichment settings, and extends the HttpxTransport to handle tracking delivery. The Context class now supports queuing conversion events with deduplication logic and provides a release_queues method for explicit flushing. A critical issue was identified in the TrackingQueue.release method where holding a thread lock during network I/O could cause performance bottlenecks and potential infinite loops upon transport failures.

Comment on lines +106 to +151
def release(self, reason: str | None = None) -> TrackingFlushResult:
"""Send queued events through the configured transport in batches."""

if reason is not None and not isinstance(reason, str):
raise TypeError("reason must be a string or None")

delivered_event_count = 0
delivered_batch_count = 0
with self._lock:
if not self._pending:
return TrackingFlushResult(
attempted=False,
delivered_event_count=0,
delivered_batch_count=0,
remaining_event_count=0,
reason=reason,
)

while self._pending:
batch = tuple(self._pending[: self._tracking_config.batch_size])
payload = serialize_tracking_payload(
batch,
source=self._tracking_config.source,
enrich_data=self._tracking_config.enrich_data,
)
self._transport.send_tracking(
TrackingRequest(
sdk_key=self._sdk_key,
sdk_key_secret=self._sdk_key_secret,
account_id=self._account_id,
project_id=self._project_id,
payload=payload,
transport=self._transport_config,
)
)
del self._pending[: len(batch)]
delivered_batch_count += 1
delivered_event_count += len(batch)

return TrackingFlushResult(
attempted=True,
delivered_event_count=delivered_event_count,
delivered_batch_count=delivered_batch_count,
remaining_event_count=0,
reason=reason,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The current implementation of release has two critical issues:

  1. Lock Contention: The lock is held during network I/O (send_tracking), which can block other threads from enqueuing events for the duration of all batched network requests. In a highly concurrent application, this can become a significant performance bottleneck.
  2. Infinite Loop on Error: If send_tracking raises a persistent exception (e.g., a 4xx error), the events are not removed from the queue. The while loop will then attempt to send the same failing batch repeatedly, causing an infinite loop and high CPU usage.

To address this, the method should be refactored to release the lock before performing network I/O and to handle transport errors gracefully to prevent infinite loops and event loss.

def release(self, reason: str | None = None) -> TrackingFlushResult:
        """Send queued events through the configured transport in batches."""

        if reason is not None and not isinstance(reason, str):
            raise TypeError("reason must be a string or None")

        with self._lock:
            if not self._pending:
                return TrackingFlushResult(
                    attempted=False,
                    delivered_event_count=0,
                    delivered_batch_count=0,
                    remaining_event_count=0,
                    reason=reason,
                )
            events_to_send = self._pending
            self._pending = []

        delivered_event_count = 0
        delivered_batch_count = 0
        try:
            while events_to_send:
                batch = tuple(events_to_send[: self._tracking_config.batch_size])
                payload = serialize_tracking_payload(
                    batch,
                    source=self._tracking_config.source,
                    enrich_data=self._tracking_config.enrich_data,
                )
                self._transport.send_tracking(
                    TrackingRequest(
                        sdk_key=self._sdk_key,
                        sdk_key_secret=self._sdk_key_secret,
                        account_id=self._account_id,
                        project_id=self._project_id,
                        payload=payload,
                        transport=self._transport_config,
                    )
                )
                del events_to_send[: len(batch)]
                delivered_batch_count += 1
                delivered_event_count += len(batch)
        except Exception:
            # Re-queue unsent events on failure to prevent event loss
            with self._lock:
                self._pending = events_to_send + self._pending
            raise

        with self._lock:
            remaining_count = len(self._pending)

        return TrackingFlushResult(
            attempted=True,
            delivered_event_count=delivered_event_count,
            delivered_batch_count=delivered_batch_count,
            remaining_event_count=remaining_count,
            reason=reason,
        )

@abbaseya
abbaseya deleted the codex/feat-tracking-delivery-mvp branch June 18, 2026 16:36
abbaseya added a commit that referenced this pull request Aug 8, 2026
All eight open Dependabot alerts on this repo sit in `yarn.lock` — the
dev-only semantic-release tooling. None of them reach the published
wheel/sdist (httpx is still the only runtime dependency).

Three of the four highs were already reachable inside the existing semver
ranges; the fourth (sigstore GHSA-52v5-jr5w-gjxr, `certificateOIDs`
verification constraints silently dropped) needed sigstore >= 4.1.1, which
only arrives through a major bump:

  semantic-release 24.2.9 -> 25.0.9
    @semantic-release/npm 12.0.2 -> 13.1.5
      npm 10.9.8 -> 11.19.0
        libnpmpublish 10 -> 11.2.0  }-> sigstore ^3 -> ^4  (4.1.1)
        pacote        19 -> 21.5.1  }   @sigstore/core 2.0.0 -> 3.2.1
        tar 7.5.16 -> 7.5.22

`@semantic-release/github` moves to ^12 to match what semantic-release 25
depends on — leaving it at ^11 would hoist the older copy to the project
root and shadow the one core resolves.

Resolved (4 high, 4 medium — no criticals were open):

  #14 high    ip-address       10.2.0 -> 10.4.0   (needs >= 10.3.1)
  #9  high    brace-expansion  2.1.1  -> 5.0.9    (needs >= 2.1.2)
  #7  high    js-yaml          4.2.0  -> 4.3.1    (needs >= 4.3.0)
  #2  high    sigstore         3.1.0  -> 4.1.1    (needs >= 4.1.1)
  #13 medium  ip-address       (same bump as #14)
  #12 medium  ip-address       (same bump as #14)
  #4  medium  tar              7.5.16 -> 7.5.22   (needs >= 7.5.18)
  #1  medium  @sigstore/core   2.0.0  -> 3.2.1    (needs >= 3.2.1)

Node: semantic-release 25 requires ^22.14.0 || >= 24.10.0. release.yml
installs `lts/*`, currently Node 24.19.0 — satisfied, and every future LTS
line stays above the floor.

Verified: `yarn install --immutable` (what release.yml runs) passes against
the regenerated lockfile with the lockfile format unchanged (__metadata
version 10), and `yarn npm audit --all --recursive` reports no suggestions.
A `semantic-release --dry-run` against this branch loads all four configured
plugins, passes verifyConditions for both exec and github (GitHub
authentication + push permission), and analyzes commits to "no release" —
correct, since `chore` is a non-releasing type in release.config.mjs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant