Skip to content

feat: add tracking lifecycle events - #13

Merged
usmanabbas7 merged 1 commit into
dev-branchfrom
codex/feat-tracking-lifecycle-outcomes
Apr 14, 2026
Merged

feat: add tracking lifecycle events#13
usmanabbas7 merged 1 commit into
dev-branchfrom
codex/feat-tracking-lifecycle-outcomes

Conversation

@usmanabbas7

Copy link
Copy Markdown
Collaborator

Summary

Adds tracking lifecycle observability for the Convert Python SDK.

This PR completes the tracking MVP by adding:

  • lifecycle hooks for conversion and queue activity
  • diagnosable delivery outcomes
  • privacy-safe event payloads
  • privacy-safe delivery failure logging

BMAD scope covered:

  • Story 2.4: Expose Tracking Lifecycle Events and Delivery Outcomes

What Changed

  • Added typed lifecycle event support:
    • LifecycleEvent
    • LifecycleEventPayload
  • Added a lightweight in-memory event bus
  • Added public lifecycle subscription methods on Core:
    • Core.on(...)
    • Core.off(...)
  • Emitted lifecycle events for:
    • conversion creation
    • duplicate prevention
    • event queueing
    • queue release start
    • queue release success
    • tracking delivery failure
  • Added privacy-safe visitor references for diagnostics instead of raw visitor ids
  • Added standard-library logging for tracking delivery failures
  • Preserved the existing explicit flush flow through Context.release_queues(...)
  • Kept Story 2.3 batching, deduplication, and forced repeat transaction behavior intact

Why

The tracking queue from Story 2.3 made delivery explicit and efficient, but it still lacked observability for integrators and maintainers.

This PR makes the tracking pipeline diagnosable without changing the control model:

  • queue release is still explicit
  • delivery failures still surface to the caller
  • event hooks now let applications observe conversion and delivery behavior
  • diagnostics stay safe by avoiding sdk keys, secrets, raw visitor ids, and raw conversion payloads

Validation

Ran in ../python-sdk:

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

Results:

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

Notes

This PR completes the tracking MVP boundary for Epic 2.

Not included here:

  • automatic retry/backoff
  • automatic timed flush
  • persistent queue storage
  • advanced logging customization
  • event streaming infrastructure beyond the lightweight in-process hook model

@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 introduces a lifecycle event system for SDK observability, including an in-memory event bus, event definitions, and integration across the context and tracking modules. The feedback identifies a potential infinite loop in the tracking queue if the batch size is not properly constrained and highlights a race condition where concurrent flushes could lead to duplicate event delivery, suggesting the implementation of a dedicated release lock.

with self._lock:
if not self._pending:
break
batch = tuple(self._pending[: self._tracking_config.batch_size])

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

If batch_size is configured to 0 or a negative value, this loop will become infinite because self._pending will never be reduced. Ensure a minimum batch size of 1 to prevent hanging the process.

Suggested change
batch = tuple(self._pending[: self._tracking_config.batch_size])
batch_size = max(1, self._tracking_config.batch_size)
batch = tuple(self._pending[:batch_size])

self._event_bus = event_bus
self._pending: list[ConversionEvent] = []
self._triggered_goals: set[tuple[str, str]] = set()
self._lock = Lock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Add a dedicated lock to prevent concurrent flushes. While _lock protects the queue's integrity, it is released during network transport, which allows multiple release calls to process and potentially duplicate the same events.

Suggested change
self._lock = Lock()
self._lock = Lock()
self._release_lock = Lock()

Comment on lines +152 to 156
while True:
with self._lock:
if not self._pending:
break
batch = tuple(self._pending[: self._tracking_config.batch_size])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

This loop is susceptible to a race condition if multiple threads call release() concurrently. Since _lock is released during the network call (send_tracking), another thread can enter the loop, take the same batch of events, and send them again. Use the _release_lock suggested in __init__ to wrap the release method body to ensure mutual exclusion during flushes.

@usmanabbas7
usmanabbas7 merged commit 812a88c into dev-branch Apr 14, 2026
@abbaseya
abbaseya deleted the codex/feat-tracking-lifecycle-outcomes 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