Repository navigation
fix: send tracking events to /track with the JS/Go wire contract - #262
vazarkevych wants to merge 1 commit into
Conversation
The tracking plugin POSTed {ingestorHost}/events with {client_key, events:[...]} and
flat event fields, but the GrowthBook ingestor only serves POST /track, so no events
were recorded (same bug the Go and Python SDKs fixed).
Post to {ingestorHost}/track?client_key={clientKey} with a top-level JSON array of
events shaped like the JS and Go SDKs: event_name ("Experiment Viewed" /
"Feature Evaluated"), a properties map, the user's attributes at evaluation time, and
sdk_language/sdk_version. variationId now carries the variation key, not its index.
To supply attributes, GrowthBookPlugin gains attribute-carrying onExperimentViewed /
onFeatureEvaluated overloads (default-delegating to the existing ones, so custom plugins
are unaffected); PluginRegistry and both evaluators thread the evaluated user's
attributes through to them.
|
|
|
||
| JsonObject properties = event.getAsJsonObject("properties"); | ||
| assertEquals("exp1", properties.get("experimentId").getAsString()); | ||
| assertEquals("3", properties.get("variationId").getAsString()); |
There was a problem hiding this comment.
Distinct variation keys go untested
Both new event tests use variation keys that equal their numeric indexes. They would still pass if variationId were changed back to the stringified index. Add a result with index 1 and key "treatment", then assert that both event types send "treatment".
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/src/test/java/growthbook/sdk/java/plugin/tracking/GrowthBookTrackingPluginTest.java
Line: 126
Comment:
**Distinct variation keys go untested**
Both new event tests use variation keys that equal their numeric indexes. They would still pass if `variationId` were changed back to the stringified index. Add a result with index `1` and key `"treatment"`, then assert that both event types send `"treatment"`.
---
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!
| GrowthBookTrackingPlugin plugin = GrowthBookTrackingPlugin.of(configBuilder().batchSize(1).build()); | ||
| plugin.init(); | ||
| plugin.onExperimentViewed(experiment("exp1"), experimentResult(3)); | ||
| plugin.onExperimentViewed(experiment("exp1"), experimentResult(3), attributes()); |
There was a problem hiding this comment.
The new attribute tests flush immediately and never change the original attributes. They do not protect the promised copy-at-evaluation behavior. Add a test that buffers an event, changes a nested attribute on the original object, then flushes and checks that the event kept the old value.
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/src/test/java/growthbook/sdk/java/plugin/tracking/GrowthBookTrackingPluginTest.java
Line: 114
Comment:
**Attribute copies go untested**
The new attribute tests flush immediately and never change the original attributes. They do not protect the promised copy-at-evaluation behavior. Add a test that buffers an event, changes a nested attribute on the original object, then flushes and checks that the event kept the old value.
---
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!
fix: tracking plugin posts to
/trackwith the JS/Go wire contractGrowthBookTrackingPluginnow actually records events — correct endpoint, correct payload, with user attributesJS / Go / Python SDK parity · fixes a silent no-op · backward compatible
Branch:
fix/tracking-plugin-track-endpoint→mainCommit:
efad482Summary
GrowthBookTrackingPluginsent events to the wrong endpoint with the wrong body, so no events were ever recorded by the GrowthBook ingestor. This change makes the plugin speak the same wire contract as the JS and Go SDKs:POST {ingestorHost}/track?client_key={clientKey}(wasPOST {ingestorHost}/events){"client_key": ..., "events": [...]})event_name+properties+attributes+sdk_language+sdk_versionvariationIdis the variation key, not its numeric indexattributescarries the user's attributes at evaluation time, so events can be tied to users and sliced by dimensionThe fix is additive and backward compatible for custom
GrowthBookPluginimplementations.Why
The GrowthBook ingestor only serves
POST /track. The Java plugin posted to/track's predecessor shape (/eventswith a{client_key, events}envelope), which the ingestor does not accept — so telemetry silently went nowhere.The other server SDKs hit the same problem and fixed it:
This brings the Java SDK in line with both, and with the JS SDK's
Experiment Viewed/Feature Evaluatedevent names and payload shape.A second, related gap: the plugin SPI never received the evaluated user's attributes, so even a correctly-delivered event could not be attributed to a user. This change threads attributes through to the plugin.
Before → after (on the wire)
Before —
POST {ingestorHost}/events{ "client_key": "sdk-abc123", "events": [ { "event_type": "experiment_viewed", "experiment_id": "my-experiment", "variation_id": 1, "timestamp": 1736500000000, "...": "flat Go-internal fields" } ] }After —
POST {ingestorHost}/track?client_key=sdk-abc123[ { "event_name": "Experiment Viewed", "properties": { "experimentId": "my-experiment", "variationId": "1", "hashAttribute": "id", "hashValue": "user-123" }, "attributes": { "id": "user-123", "country": "US" }, "sdk_language": "java", "sdk_version": "0.11.1" } ]Feature-usage events use
"event_name": "Feature Evaluated"withfeature,value,source,ruleId, andvariationIdinproperties:{ "event_name": "Feature Evaluated", "properties": { "feature": "flag", "value": true, "source": "experiment", "ruleId": "rule-7", "variationId": "2" }, "attributes": { "id": "user-123" }, "sdk_language": "java", "sdk_version": "0.11.1" }What was done
1. Carry user attributes through the plugin SPI (additive)
GrowthBookPlugingains attribute-carrying overloads that default-delegate to the existing ones, so custom plugins are untouched:PluginRegistrygains matching 3-argfire*methods (the old 2-arg ones delegate withnull), and both dispatch sites —ExperimentEvaluatorandFeatureEvaluator— passcontext.getUser().getAttributes()(null-guarded).2. New event shape (
TrackingEvent)Rebuilt around
event_name/properties/attributes/sdk_language/sdk_version. Null properties are omitted;attributesis omitted when none are supplied. Attributes are deep-copied at construction (on the evaluation thread, before buffering) so later mutation of the user context cannot alter a buffered event.variationIdis sourced from the variation key (ExperimentResult.getKey()).3. Correct endpoint and body (
GrowthBookTrackingPlugin)Builds the URL with OkHttp
HttpUrl—{ingestorHost}/track+client_keyquery parameter (properly encoded; malformed host is logged and skipped, never thrown) — and posts a top-levelJsonArrayof events. Batching, backpressure, and close/flush semantics are unchanged.4. Docs
GrowthBookTrackingPluginandTrackingPluginConfigJavadoc updated from/eventsto the/track?client_key=contract.Backward compatibility
PluginRegistrycalls the 3-arg overload; the interface default routes to a plugin's existing 2-arg override, so plugins that only implementonExperimentViewed(e, r)keep receiving every event.public/protectedsignatures removed or changed; only additive default methods and overloads.TrackingEventis package-private, so its field reshaping is internal.Tests
GrowthBookTrackingPluginTestasserts the contract at the wire level:POST /track,client_key=sdk-testquery, top-level JSON array bodyevent_name("Experiment Viewed"/"Feature Evaluated"), nestedproperties.*,variationIdas the key stringattributespresence and contentvariationIdomitted for a non-experiment feature evaluationforwardsUserAttributesFromEvaluationthroughGrowthBook(evaluator → registry → plugin → POST)close(), caller-supplied executor wait,closeTimeoutbound, shared-instance misuse, disabled/no-client-key no-ops, HTTP-failure toleranceRecordingHttpServernow captures the request query string. Full./gradlew :lib:testis green on JDK 17.Notes / out of scope
propertiescarry exactlyexperimentId,variationId,hashAttribute,hashValue— matching the agreed contract (novalue/name onExperiment Viewed).variationIdfalls back to the stringified index (or is omitted if unknown) — same as the JS SDK.RecordingHttpServer(JDKcom.sunHTTP server); converting the tracking tests to WireMock is pre-existing and left out of this focused fix.