Skip to content

feat: public feature_flags surface on track/patch (DEV-1214) - #21

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/dev-1214-rust-feature-flags
Open

feat: public feature_flags surface on track/patch (DEV-1214)#21
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/dev-1214-rust-feature-flags

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an additive, optional feature_flags map to the Rust SDK's public event surfaces (DEV-1214), mirroring the JS event-shipper's public featureFlags.

// New optional field (empty by default) on every public event/patch struct:
pub struct AiEvent    { /* … */ pub feature_flags: BTreeMap<String, String> }
pub struct Event      { /* … */ pub feature_flags: BTreeMap<String, String> }
pub struct BeginOptions  { /* … */ pub feature_flags: BTreeMap<String, String> }
pub struct PatchOptions  { /* … */ pub feature_flags: BTreeMap<String, String> }
pub struct FinishOptions { /* … */ pub feature_flags: BTreeMap<String, String> }

// Plus convenience methods on an in-flight interaction:
interaction.set_feature_flags(map).await?;
interaction.set_feature_flag("prompt-version", "v2").await?;

Flags flow through the existing EventPatch buffer and serialize verbatim as a top-level feature_flags string→string object on the wire — a sibling of ai_data / properties, matching the JS event-shipper.ts shape and dawn ingest's TrackEventSchema.feature_flags: z.record(z.string()):

POST /events/track_partial
{
  "event_id": "", "user_id": "flags-u1", "event": "chat",
  "ai_data": { "input": "", "output": "", "model": "mock-gpt" },
  "properties": {  },
  "feature_flags": { "prompt-version": "v2", "locale-label": "café-日本語" },  // ← new, top-level
  "is_pending": false
}

Across a beginpatchfinish lifecycle, flags merge like properties (last write wins per key).

Conformance driver: declares the events.feature_flags capability and maps the harness feature_flags step arg through to the public API (parsing only string values, as the wire contract is Record<string,string>).

Wire-shape proof (unit tests)

New tests in tests/wire_format.rs (all green):

  • feature_flags_serialize_as_top_level_string_map — asserts a track_ai call with flags emits a top-level feature_flags object with the exact keys/values (UTF-8 multibyte round-trips verbatim), snake_case (not featureFlags), and that flags do not leak into ai_data or properties.
  • omitted_feature_flags_leave_body_unchanged — the additive-only guarantee: a caller that passes no flags produces a body with no feature_flags key at all (asserted both on the parsed JSON and on the raw serialized bytes).
  • feature_flags_merge_across_partial_lifecycle — flags supplied via begin + set_feature_flag + finish merge (last-write-wins) onto the final payload.

Safety (bounded)

Additive-only; no behavior change for existing callers. The field defaults to an empty BTreeMap, and the wire payload uses #[serde(skip_serializing_if = "BTreeMap::is_empty")], so an omitted-flags request is byte-identical to before this change (no "feature_flags": {} and no null). This is proven by omitted_feature_flags_leave_body_unchanged, which asserts the raw request bytes contain no feature_flags token. Existing structs keep compiling via ..Default::default().

Local verification (all before opening this PR)

Full unit suite + linters on the SDK crate (e2e suite excluded — it hits the live backend, exactly as CI does):

cargo fmt --check                              # clean (lib + conformance)
cargo clippy --all-targets -- -D warnings      # clean
cargo test (lib + all non-e2e integration)     # 32 lib + 46 wire_format + … all pass
cargo doc --no-deps  (RUSTDOCFLAGS=-D warnings) # clean
cargo +1.88.0 build --locked                    # MSRV builds

Fault-lane harness proof

Ran the pinned harness fault lane locally against this repo's driver:

node runner/dist/src/index.js \
  --driver ".../conformance/target/debug/raindrop-conformance-driver" \
  --failures conformance/failures.txt --lane fault \
  --scenarios .../scenarios --server http://127.0.0.1:8787 --report /tmp/report.json

Scenario line and final summary (harness 1a646e1, sdk raindrop-rust@0.0.9):

feature-flags-request-shape@fault               fail              33  expect.requests[endpoint=events/track]: expected exactly 1 attempt(s), recorded 0
57 result(s): 33 expected_fail, 1 fail, 4 not_applicable, 11 pass, 8 unsupported

Runner exit code: 0 (overall fault lane green). ratchet-add.txt and ratchet-remove.txt were both empty — no ratchet change requested, so conformance/failures.txt is untouched.

What was verified vs. not — read this

feature-flags-request-shape@fault does not PASS for this SDK, and it cannot as currently architected — but this is a pre-existing route gap (DEV-1149), not a feature_flags problem. The scenario asserts the request lands on the batched endpoint: events/track (body_root: array) route. This Rust SDK deliberately ships every track/track_ai as a single-object POST to events/track_partial, so it records 0 attempts on events/track — identical to track-single, track-attachments, track-nested-properties, and every other events/track scenario, all already ratcheted missing_feature (DEV-1149). The driver did execute the scenario (attempts: 1) and correctly mapped the feature_flags arg through; the failure is purely the route assertion. Because the scenario is status: experimental at the pinned harness SHA, it runs report-only and the lane exits 0 with no ratchet entry required. The feature_flags wire shape itself is proven directly by the unit tests above on the route this SDK actually uses.

Harness pin bump (needs a reviewer's eye)

HARNESS_REF is bumped cf744e9 (#47) → 1a646e1 (#53). This was necessary and independently correct: the old pin was already broken for this repo — it predated 18 scenarios that conformance/failures.txt ratchets (batching-, size-limit-, unicode-, negative-path-, concurrency-*, hot-path-sustained-track-ai, shutdown-drain-hang-deadline), so a baseline run at the old pin exits 1 with "stale entry" run-failures, and the old pin also lacks the events.feature_flags capability this PR's driver advertises (a handshake refusal → exit 2). 1a646e1 (#53) is the harness SHA failures.txt is actually authored against: at that SHA all 33 ratcheted scenarios exist, the baseline (pre-this-PR) driver runs green (exit 0), and events.feature_flags + the feature_flags scenarios are present (the latter still experimental). Bumping the pin follows the workflow's own documented one-line-PR procedure.

Link to Devin session: https://app.devin.ai/sessions/2128759db2b0422e99df47a9bca5a82b
Requested by: @pavel-y-ivanov


Note

Medium Risk
Changes public event types and ingest payload shape, but empty flags are omitted and behavior for callers that do not set flags is unchanged; primary risk is incorrect flag merge or wire placement affecting downstream ingest.

Overview
Release 0.0.9 adds an optional feature_flags: BTreeMap<String, String> on Event, AiEvent, BeginOptions, PatchOptions, and FinishOptions, plus Interaction::set_feature_flags / set_feature_flag. Flags flow through EventPatch and serialize as a top-level feature_flags object on events/track_partial (omitted when empty so existing callers stay byte-identical).

Conformance: the driver advertises events.feature_flags, maps harness step args (string values only), and CI bumps HARNESS_REF to the SHA that includes feature-flag scenarios and matches failures.txt.

Tests: new wire_format cases cover top-level shape, omission when unset, and merge across begin/patch/finish (last write wins).

Reviewed by Cursor Bugbot for commit 09187c3. Bugbot is set up for automated code reviews on this repo. Configure here.

Add an optional feature_flags map (string->string) to the public event
surfaces (AiEvent, Event, BeginOptions, PatchOptions, FinishOptions) plus
Interaction::set_feature_flags/set_feature_flag. Flags serialize verbatim as a
top-level feature_flags object on the wire (sibling of ai_data/properties),
matching the JS event-shipper. Additive-only: omitted flags leave the request
body byte-identical (skip_serializing_if on the buffer payload).

Wire flags through EventPatch buffer + merge (last write wins per key). Declare
the events.feature_flags conformance capability and map the harness feature_flags
step arg through to the public API.

Bump harness pin cf744e9 (#47) -> 1a646e1 (#53): the old pin predated 18
ratcheted scenarios (baseline exit 1) and the feature_flags capability/scenarios;

Co-Authored-By: pavel <pavel.y.ivanov@gmail.com>
#53 is the SHA this repo's failures.txt is authored against (baseline green).
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

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.

0 participants