feat: deferred tracking buffer and prerequisite exposure reporting - #133
feat: deferred tracking buffer and prerequisite exposure reporting#133bryce-fitzsimons wants to merge 4 commits into
Conversation
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.
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.
Confidence Score: 3/5The 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 |
| prereq_res = eval_prereqs( | ||
| parentConditions=experiment.parentConditions, | ||
| evalContext=evalContext, | ||
| tracking_cb=tracking_cb, | ||
| feature_usage_cb=feature_usage_cb, | ||
| ) |
There was a problem hiding this 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.
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!
| user = call.get("user") or {} | ||
| attributes = user.get("attributes") |
There was a problem hiding this 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.
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.
What this fixes and adds
Prerequisite telemetry. Experiments that decide a prerequisite feature now fire
on_experiment_viewed— previouslyeval_prereqsdropped the tracking callback, so those exposures were silently lost.on_feature_usagefires 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=Truebuffers each exposure as a{experiment, result, user}dict in the JS SDK's shape, for servers that forward tracking to client SDKs. OnGrowthBookread them withget_deferred_tracking_calls(); onGrowthBookClientthe buffer lives on eachUserContext(defer_tracking=True)so requests never mix. The receiving side loads them withset_deferred_tracking_calls()and sends them through its callback withfire_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'ssetDeferredTrackingCalls.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,UserContextasdict/replace round-trips, and per-UserContextisolation on the async client. Full suite passes; mypy clean; pyright unchanged vsmain.