Skip to content

feat: deferred tracking buffer and prerequisite exposure reporting - #133

Closed
bryce-fitzsimons wants to merge 4 commits into
mainfrom
prereq-telemetry-deferred-tracking
Closed

feat: deferred tracking buffer and prerequisite exposure reporting#133
bryce-fitzsimons wants to merge 4 commits into
mainfrom
prereq-telemetry-deferred-tracking

Conversation

@bryce-fitzsimons

@bryce-fitzsimons bryce-fitzsimons commented Sep 4, 2026

Copy link
Copy Markdown
Member

What this fixes and adds

Prerequisite telemetry. Experiments that decide a prerequisite feature now fire on_experiment_viewed — previously eval_prereqs dropped the tracking callback, so those exposures were silently lost. on_feature_usage fires for every feature evaluated, prerequisites included (once per evaluation), and subscriptions see prerequisite experiments too. Passthrough assignments were already tracked and stay that way.

Deferred tracking (opt-in). defer_tracking=True buffers each exposure as a {experiment, result, user} dict in the JS SDK's shape, for servers that forward tracking to client SDKs. On GrowthBook read them with get_deferred_tracking_calls(); on GrowthBookClient the buffer lives on each UserContext(defer_tracking=True) so requests never mix. The receiving side loads them with set_deferred_tracking_calls() and sends them through its callback with fire_deferred_tracking_calls(), each with the user it was recorded for. Buffering is independent of callbacks, matching the Go SDK. Verified end-to-end into a JS client's setDeferredTrackingCalls.

The tracking dedupe key is shared by both clients and separator-delimited, so distinct exposures can't collide on field boundaries.

Testing

New tests/test_tracking_telemetry.py: prerequisite tracking and usage at rule and experiment level, once-per-evaluation dedupe, passthrough, subscriptions, unserializable values, opt-in default, buffering alongside a callback, forward-and-replay with per-user fidelity, malformed-entry hydration, failed-callback retention, UserContext asdict/replace round-trips, and per-UserContext isolation on the async client. Full suite passes; mypy clean; pyright unchanged vs main.

Experiments deciding prerequisite features now fire on_experiment_viewed
(eval_prereqs previously dropped the tracking callback), and
on_feature_usage fires for every feature evaluated, prerequisites
included, once per evaluation unless the value changed.

With no on_experiment_viewed configured, exposures buffer in the JS SDK's
TrackingData shape: GrowthBook gains get/set/fire_deferred_tracking_calls
and set_tracking_callback; GrowthBookClient buffers on each UserContext.
The tracking dedupe key is shared by both clients and separator-delimited.
… fidelity

Deferred tracking is opt-in (defer_tracking on GrowthBook and UserContext)
so a reusable sync instance with no callback no longer grows an unbounded
buffer. Feature usage plumbing runs only when a callback is configured
and its value-dedupe cannot raise out of evaluation. Replay fires with
the buffered user snapshot and keeps entries the callback raised on;
hydration drops result-less or malformed entries. The buffer is not a
dataclass field, so asdict/replace round-trips work. Subscriptions now
see prerequisite experiments.
@bryce-fitzsimons bryce-fitzsimons changed the title Deferred tracking buffer and prerequisite exposure reporting feat: deferred tracking buffer and prerequisite exposure reporting Sep 4, 2026
Buffering depends on defer_tracking alone, independent of callbacks.
Drop set_tracking_callback (it replaced the tracking plugin's wrapper),
replay pops each entry as it fires, usage dedupes by key only, and
hydration is a single dict-shape check.
@bryce-fitzsimons
bryce-fitzsimons marked this pull request as ready for review September 4, 2026 02:20
@bryce-fitzsimons

Copy link
Copy Markdown
Member Author

@greptileai

@greptile-apps

greptile-apps Bot commented Sep 4, 2026

Copy link
Copy Markdown

Confidence Score: 3/5

The PR should not merge until run-level prerequisite subscriptions are propagated and malformed deferred users can no longer abort replay.

Experiment-level prerequisite assignments remain invisible to subscribers, and one malformed forwarded user value can stop subsequent valid deferred exposures from being delivered.

Files Needing Attention: growthbook/core.py, growthbook/growthbook.py, growthbook/common_types.py

Prompt To Fix All With AI
### Issue 1
growthbook/core.py:957-962
When `GrowthBook.run` or `GrowthBookClient.run` evaluates an experiment with `parentConditions`, `run_experiment` forwards tracking and feature-usage callbacks into prerequisite evaluation but not the subscription callback. Consequently, subscribers are not notified of prerequisite experiment assignments, and the synchronous client's assigned-results state omits them.

### Issue 2
growthbook/growthbook.py:1484-1485
When a forwarded entry has valid experiment and result dictionaries but a truthy non-dict `user`, hydration accepts it and replay calls `user.get` outside the callback exception handler. This raises `AttributeError`, aborting replay before later valid exposures are delivered.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "deslop and align deferral semantics with..." | Re-trigger Greptile

Comment thread growthbook/core.py
Comment on lines +957 to +962
prereq_res = eval_prereqs(
parentConditions=experiment.parentConditions,
evalContext=evalContext,
tracking_cb=tracking_cb,
feature_usage_cb=feature_usage_cb,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 When GrowthBook.run or GrowthBookClient.run evaluates an experiment with parentConditions, run_experiment forwards tracking and feature-usage callbacks into prerequisite evaluation but not the subscription callback. Consequently, subscribers are not notified of prerequisite experiment assignments, and the synchronous client's assigned-results state omits them.

Prompt To Fix With AI
This is a comment left during a code review.
Path: growthbook/core.py
Line: 957-962

Comment:
When `GrowthBook.run` or `GrowthBookClient.run` evaluates an experiment with `parentConditions`, `run_experiment` forwards tracking and feature-usage callbacks into prerequisite evaluation but not the subscription callback. Consequently, subscribers are not notified of prerequisite experiment assignments, and the synchronous client's assigned-results state omits them.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread growthbook/growthbook.py Outdated
Comment on lines +1484 to +1485
user = call.get("user") or {}
attributes = user.get("attributes")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 When a forwarded entry has valid experiment and result dictionaries but a truthy non-dict user, hydration accepts it and replay calls user.get outside the callback exception handler. This raises AttributeError, aborting replay before later valid exposures are delivered.

Prompt To Fix With AI
This is a comment left during a code review.
Path: growthbook/growthbook.py
Line: 1484-1485

Comment:
When a forwarded entry has valid experiment and result dictionaries but a truthy non-dict `user`, hydration accepts it and replay calls `user.get` outside the callback exception handler. This raises `AttributeError`, aborting replay before later valid exposures are delivered.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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