feat: add event engine WASM for OpenFeature track() support - #524
feat: add event engine WASM for OpenFeature track() support#524vahidlazio wants to merge 4 commits into
Conversation
| COPY confidence-cloudflare-resolver/Cargo.toml ./confidence-cloudflare-resolver/ | ||
| COPY wasm-msg/Cargo.toml ./wasm-msg/ | ||
| COPY wasm/rust-guest/Cargo.toml ./wasm/rust-guest/ | ||
| COPY wasm/event-guest/Cargo.toml ./wasm/event-guest/ |
There was a problem hiding this comment.
Can we also make sure the event engine is included in the Docker stages that actually run from the final all target? I guess copying the crate into the build context is the first step, but since this PR adds a committed confidence_event_engine.wasm, I just want to make sure CI also builds/lints/tests the standalone module rather than only making its sources available.
| Ok(VOID) | ||
| } | ||
|
|
||
| fn bounded_flush_events(_request: Void) -> WasmResult<PublishEventsRequest> { |
There was a problem hiding this comment.
I don't remember exactly how we handle the flushing of logs in the resolver but I think it makes sense to have the event flushing semantics intentionally match the resolver log flushing path? I think bounded_flush_events using the same bounded, drain-available approach as bounded_flush_logs makes sense, but I just want to make sure we’re not accidentally diverging on bounded vs unbounded flush behavior for the new WASM module.
400dd8e to
066d19a
Compare
| } | ||
| } | ||
|
|
||
| if req.value != 0.0 { |
There was a problem hiding this comment.
don't we need to be able to differentiate absence of a value with 0.0?
|
|
||
| static EVENT_LOGGER: LazyLock<EventLogger> = LazyLock::new(EventLogger::new); | ||
|
|
||
| fn build_payload(req: &TrackEventRequest) -> Option<Struct> { |
There was a problem hiding this comment.
Can we add a some tests and/or note + readme for the payload merge behavior here? I guess value and context intentionally win over same-named fields in data, but I just want to make sure that collision behavior is defined since OpenFeature custom data can contain arbitrary keys.
| # ============================================================================== | ||
| # Build confidence-event-engine | ||
| # ============================================================================== | ||
| FROM rust-test-base AS confidence-event-engine.build |
There was a problem hiding this comment.
Can we also reference these new event-engine stages from the final all target further down in this file? I thing Docker won’t run this stage family in the main build unless something copies from confidence-event-engine.test / .lint there.
| # ============================================================================== | ||
| # Build wasm/event-guest WASM | ||
| # ============================================================================== | ||
| FROM wasm-deps AS wasm-event-guest.build |
There was a problem hiding this comment.
Can we also reference this event WASM stage from the final all target further down in this file? Docker won’t force wasm-event-guest.build / .lint / .artifact during the main build unless all copies from them.
4f6269c to
ecd5258
Compare
|
|
||
| // EventResolver wraps a WASM event engine instance and exposes TrackEvent | ||
| // and FlushEvents operations using the wasm-msg protocol. | ||
| type EventResolver struct { |
There was a problem hiding this comment.
Why is this called Resolver? EventTracker?
| @@ -0,0 +1,207 @@ | |||
| // Package event_resolver provides a WASM-based event engine for tracking | |||
| // and flushing Confidence events via the wasm-msg protocol. | |||
| package event_resolver | |||
|
|
||
| // call implements the wasm-msg protocol: marshal request into a Request envelope, | ||
| // allocate WASM memory, write, call the export, read the Response envelope, free. | ||
| func (er *EventResolver) call(fnName string, request proto.Message, response proto.Message) error { |
There was a problem hiding this comment.
For these classes (this class and the resolver) where we do wasm integration we now have this duplication. Does it make sense to break it out in to an abstract class? (maybe that's not a Go thing, but anyway, code reuse)
There was a problem hiding this comment.
i didn't want to touch the resolver, since we might switch to state to wasm etc, but if we want to use same code, i'd do that in a separate PR.
There was a problem hiding this comment.
sounds completely fair. didn't think about the upcoming plans.
| } | ||
| req.Header.Set("Content-Type", "application/json") | ||
|
|
||
| resp, err := p.httpClient.Do(req) |
There was a problem hiding this comment.
why did we choose http json here instead of grpc (which we use for the apply logs)?
|
|
||
| // Initialize event resolver if WASM bytes are provided | ||
| if len(options.eventWasmBytes) > 0 { | ||
| eventResolver, err := er.NewEventResolver(options.eventWasmBytes, options.useWasmInterpreter) |
There was a problem hiding this comment.
In the Resolver we have these "RecoveringResolvers" that wrap the resolvers and support reconstructing/restarting the WASM if a problem occurs.
I don't expect us to salvage the events in a broken wasm instance but I think it makes sense to be able to keep the provider functioning.
| def track( | ||
| self, | ||
| event_name: str, | ||
| context: Optional[EvaluationContext] = None, | ||
| value: Optional[float] = None, | ||
| data: Optional[Dict[str, Any]] = None, | ||
| ) -> None: |
There was a problem hiding this comment.
this does not match the python provider interface declaration
There was a problem hiding this comment.
naming across this file -> event_tracker
| logger = logging.getLogger(__name__) | ||
|
|
||
| # Exception types that indicate a WASM crash requiring reload | ||
| WasmCrashError = (RuntimeError, WasmTrap) |
There was a problem hiding this comment.
Could we make this narrower than all RuntimeErrors? I guess _consume_response also raises RuntimeError for a clean guest error envelope, so this path would reload the event WASM and drop the buffered instance even though the WASM may still be healthy.
Python (review r3843953168): track() did not match FeatureProvider.track. It
took (event_name, context, value, data); the interface declares
(tracking_event_name, evaluation_context, tracking_event_details). Renamed the
parameters and now unpack value/attributes from TrackingEventDetails. Confirmed
openfeature.track.TrackingEventDetails exists in the pinned openfeature-sdk
>=0.10.0 (the local venv has a stale 0.8.4 that predates it).
JS: same class of problem. The third parameter was a loose inline type
`{ value?: number; [key: string]: any }` rather than the SDK's
TrackingEventDetails. It typechecked only because the loose type is permissive
enough to accept it. Now imports and uses the real exported type, and the first
parameter is named trackingEventName to match.
Go and Java were already conformant and are unchanged — Go has a compile-time
`_ openfeature.Tracker = (*LocalResolverProvider)(nil)` assertion and Java's
track() is an @OverRide. This is the second signature mismatch found in review
(Go's was fixed earlier), so the conformance checks matter: TypeScript's
structural typing and Python's duck typing both let a wrong signature compile.
Also corrects the README: TrackingEventDetails.value is Optional[float] in
Python, so Python preserves an explicit 0 like Java and JS. Go remains the only
provider that cannot distinguish 0 from unset.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8d1de9d to
8fedcca
Compare
Two review comments on event_resolver.py. Reload trigger was too broad — the same defect already fixed in Go and JS, and Python was missed. WasmCrashError was (RuntimeError, WasmTrap), but the module itself raises RuntimeError for a clean guest error envelope, so a politely reported guest error was treated as a crash: the instance got rebuilt and every event buffered inside it thrown away. Now EventEngineError (a RuntimeError subclass, so existing callers still work) carries guest-reported errors and is propagated, while WasmCrashError is narrowed to (WasmTrap, WasmtimeError) — actual faults. Mirrors errWasmFatal in the Go tracker. Also added the resp_ptr == 0 guard in flush_events, matching the guards added to Go and Java: falling through would read the length prefix at addr-4. Renamed event_resolver.py -> event_tracker.py and EventResolver -> EventTracker, matching the Go provider's event_tracking package. It tracks events; it does not resolve anything. Verified in Docker rather than the local venv, which has a stale openfeature-sdk 0.8.4 that predates openfeature.track: the Docker stage installs the pinned 0.10.0 and all 90 Python tests pass, confirming both the new import and the rename. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three of the four providers had a track() that did not match its SDK's declared interface (Go, then Python and JS). Go catches this at compile time via `_ openfeature.Tracker = (*LocalResolverProvider)(nil)` and Java via @OverRide, so those two broke loudly. Python's duck typing and TypeScript's structural typing both let a wrong signature through silently — Python had no guard at all. Adds tests/test_event_tracking.py: - asserts ConfidenceProvider.track's parameter names match AbstractProvider.track via inspect.signature, so drift fails CI - asserts the documented no-op call shapes don't raise - asserts EventEngineError is NOT in WasmCrashError, locking in the narrow reload trigger — a guest-reported error must not discard buffered events - covers the eventDefinitions/ prefix, buffer draining, and that an explicit value of 0 survives (Python's TrackingEventDetails.value is Optional[float], so unlike Go it can represent it) Also copies the event WASM into the Python Docker stage, mirroring the resolver, so those tests have a module to load. Verified in Docker with the pinned openfeature-sdk 0.10.0: 97 tests pass, up from 90. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
confidence_event_engine.wasm, 56KB) that batches OpenFeaturetrack()events for server-side providers (JS, Java, Go, Python)SegQueue+ bounded flush pattern as the resolver'sAssignLogger— proven, no new patterns to audittrack_event()to queue events andbounded_flush_events()to drain up to 4MB batches, then POST toevents.confidence.dev/v1/events:publish(matching Android SDK format)New components
confidence-event-engine/EventLoggerwith lock-free batching, 8 unit testswasm/event-guest/openfeature-provider/proto/confidence/events/v1/Event,PublishEventsRequesttests/event-engine-e2e/{go,js,python}/WASM exports
wasm_msg_guest_track_eventEvent → Voidwasm_msg_guest_bounded_flush_eventsVoid → PublishEventsRequestProvider integration pattern
Performance (Apple M1 Max)
Test plan
cargo test -p confidence-event-engine— 8 unit tests (size limits, cross-flush persistence, data integrity)cargo build -p event-guest --target wasm32-unknown-unknown --profile wasm— WASM compiles (56KB)cargo clippy+cargo fmt— clean on both crates🤖 Generated with Claude Code