Repository navigation
[FFL-3370] Browser SDK: support flagSelection (tags + key patterns) - #426
Conversation
OpenFeature Browser Provider Bundle SizesMeasured from the Vite production output after installing packed Both scenarios initialize an OpenFeature provider, evaluate a boolean flag, change context, and evaluate again. Telemetry is disabled for DatadogProvider; no tracking hooks are registered for DatadogCoreProvider. DatadogProvider fetches precomputed assignments for each context; DatadogCoreProvider receives rules from fetchRulesConfiguration once and evaluates locally. Sizes include OpenFeature and the same small scenario harness. Configuration responses are supplied by Playwright and are not bundled. These are complete scenario JS sizes, not configuration payload sizes or isolated provider/Protobuf costs; the difference between rows is not a decoder-only delta.
Dependency checks passed: no Protobuf markers in default/precomputed scenarios; markers present in rules-based scenarios. OpenFeature Browser Tracking Hook Bundle SizesSynthetic entrypoints import and call tracking hook factories from the packed
OpenFeature Node Server Bundle SizesIndependent Webpack production builds of the installed core and Node SDK tarballs, targeting Node 18 with ESM (
Adding SSR helpers to the provider adds 79.7 KiB raw / 22.2 KiB gzip in this build. This includes fetching, parsing, and serialization, not just the decoder. SSR helpers fetch rules for the browser, not for configuring that provider (which uses its existing JSON API). These are bundler sizes, not npm install sizes, unbundled Node require/import costs, payload sizes, or latency. Gzip compares compressed artifacts. The browser scenarios include OpenFeature and a different harness, so their sizes are not directly comparable. Dependency checks passed: no Protobuf runtime or generated schema modules in the provider-only bundle; present in both SSR bundles (checked using Webpack module statistics). This report shows current PR artifact sizes only; it does not compare against the base branch. |
aarsilv
left a comment
There was a problem hiding this comment.
🤖 Comment from Claude working with Aaron:
Review of flag selection in the browser SDK, read together with ddoghq/dd-source#132875 (Compute and ffe-service). The inline comments carry a priority tag ([P1] should fix, [P2] worthwhile, [P3] optional), plus one heads-up. Folds marked "tested" ran on this branch merged with main.
leoromanovsky
left a comment
There was a problem hiding this comment.
🤖 Review from Codex working with Leo Romanovsky:
Approved at ef40519. No blocking implementation defects found in the browser flag-selection change; the inline P2 is a non-blocking README clarification.
Validation: 1,127 core tests and 562 browser tests passed, browser typecheck passed, and four additional rollout probes passed. Removing selection cache isolation made the relevant probe fail; restoring it passed. This approval covers the SDK change, not backend deployment or gate activation.
Add flagSelection to the browser init configuration and to RulesConfigurationFetchOptions. canonicalizeFlagSelection in @datadog/flagging-core trims Unicode White_Space, drops empty values, dedupes, sorts in UTF-8 byte order, and validates the 31-value and 200-byte limits in Compute's check order. The precompute POST sends data.attributes.flag_selection, including through flaggingProxy. The rules-based GET appends repeated selected_flag_tags and selected_flag_key_patterns params, RFC 3986 encoded, and drops the selection when the URL would exceed 8192 bytes. Dropped selections and DD-Flag-Selection-Ignored responses log one display.error and emit one telemetry debug event with only the reason. Move compareBytes to utf8.ts for reuse. FFL-3370 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
FFL-3370 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Unknown keys in flagSelection no longer make the selection malformed. Strings with unpaired UTF-16 surrogates are type-invalid and return malformed_selection. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pin ffe-system-test-data to PR #50's head (99cd41e) and check each vector's sdk expectation against canonicalizeFlagSelection. rawQuery cases carry no sdk expectation and are skipped. The pin moves to FFL-3370, FFL-3470 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… calls With withRetry, a retried request whose final response carries DD-Flag-Selection-Ignored logs and emits telemetry once, on both the precompute POST and the rules-based GET. The rules-based GET reports a dropped selection on every call. FFL-3370 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Key the IndexedDB snapshot by the canonical selection when one is set; unselected keys are unchanged and selected providers skip legacy entries. - Type FlagSelection lists as readonly string[]. - Treat sparse-array holes as malformed_selection instead of throwing. - Mark the shared selection helpers @internal. - Cite Fastly's 8 KB URL limit for MAX_RULES_URL_BYTES. - Read DD-Flag-Selection-Ignored only when the request carried a selection. - README: either-list matching and server-side too_many_values. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Older servers or disabled selection gates may return the full config, and initialFlagsConfiguration is not filtered locally. Environment: Datadog workspace Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
a949c93 to
2a27bd2
Compare
Motivation
FFL-3370: let
@datadog/openfeature-browserusers configureflagSelection: { tags, keyPatterns }so the SDK loads only matching flags, on both the precompute POST and the rules-based GET.Changes
@datadog/flagging-corecanonicalizeFlagSelection(input: unknown)(configuration/flag-selection.ts, exported from the package root and marked@internalwith the other shared selection helpers and limits) returns{ selection?: { tags, keyPatterns }; error?: 'malformed_selection' | 'too_many_values' | 'invalid_key_pattern' | 'invalid_tag' }.str::trim; notString.prototype.trim), drop empty values, dedupe, sort in UTF-8 byte order.malformed_selection: a non-object, a list that is not an array, a sparse-array hole, a non-string value, or a string with an unpaired UTF-16 surrogate. Unknown keys are ignored. As in Compute,nullfor the selection or for a list counts as absent.compareBytesmoves fromevaluateProtobufConfiguration.tstoutf8.ts; both places use it.@datadog/openfeature-browserflagSelection?: { tags?: readonly string[]; keyPatterns?: readonly string[] }on the init configuration and onRulesConfigurationFetchOptions, with JSDoc.createFlagsConfigurationFetchercanonicalizes once and sendsdata.attributes.flag_selection: { tags, key_patterns }, including whenflaggingProxyis set. The standalonefetchPrecomputedConfigurationdoes not takeflagSelection.fetchRulesConfiguration): appends repeatedselected_flag_tagsandselected_flag_key_patternsparams to the URL's existing query string, keepingdd_env. Values are percent-encoded per RFC 3986 (encodeURIComponentplus!'()*as uppercase%XX);URLSearchParamsis not used. If the full URL is over 8192 bytes (Fastly's 8 KB URL limit, which answers longer URLs with414 URI Too Long), the single request is sent without the selection, with reasontoo_many_values.DD-Flag-Selection-Ignoredwithresponse.headers?.get, once per successful response, and only when the request carried a selection.-sel-<md5 of the canonical selection>suffix when a selection is set, so providers with the same token and context but different selections never share an entry. Unselected keys are unchanged; a selected provider does not read legacy (pre-v2) entries.display.errornaming the limit or reason and emits oneaddTelemetryDebug('Flag selection dropped' | 'Flag selection ignored', { reason }). Telemetry carries only the reason, never selection values. The full configuration loads, and the provider stays READY.too_many_values), that older servers or disabled selection gates may return the full configuration without a warning, and that caller-suppliedinitialFlagsConfigurationis not filtered locally.With no selection, or one that is empty after canonicalization, both requests are unchanged.
Out of scope
flagSelectionpasses through)flagSelectionon the standalonefetchPrecomputedConfigurationTest instructions
yarn build && yarn test && yarn lintpackages/core/test/configuration/flag-selection.spec.ts: canonicalization, White_Space trimming, UTF-8 byte-order sort, each limit at the 200/201-byte and 31/32-value boundaries, type-invalid input (including lone high and low surrogates), well-formed surrogate pairs accepted, unknown keys ignored, sparse-array holes, readonly (as const) lists type-check, check order.packages/browser/test/transport/fetchConfiguration.spec.ts: POST body (includingflaggingProxy), GET params viaRulesConfigurationFetchOptions, encoding (ffe-*→ffe-%2A,a b→a%20b,(x)!→%28x%29%21), URL at 8192 bytes and over, the Ignored header on both paths and not read without a selection, telemetry emitted without values.packages/browser/test/cache/indexeddb-flags-cache.spec.tsandpackages/browser/test/openfeature/provider-persistence.spec.ts: different selections get different cache keys, the same canonical selection recovers its snapshot, the unselected key is unchanged.packages/core/test/configuration/flag-selection-vectors.spec.ts: eachsdkexpectation in the shared flag-selection vectors checked againstcanonicalizeFlagSelection;rawQuerycases have nosdkexpectation and are skipped.fetchConfiguration.spec.tsalso covers a request retried throughwithRetry: the final response's Ignored header is reported once on both paths. The precompute POST reports a dropped selection once per provider; the rules-based GET reports it on every call.Shared tests
Shared flag-selection vectors from ffe-system-test-data #50 (FFL-3470), through a separate submodule,
packages/core/test/ffe-flag-selection-data, pinned to ffe-system-test-data e8777633 (#50 merged). The mainpackages/core/test/ffe-system-test-datapin is unchanged because e8777633 breaks an existing flags-v1 opt-out case.Validation
End-to-end in ffe-dogfooding (headless Chromium
app-browser, locally built SDK) against staging:1a0ce73161375aon shared stagingmark.cai/FFL-3393at4c48c574GET /api/v2/feature-flags), intersected with the no-selection baseline. A flag is expected if it has any requested tag (exact match) or its whole key matches the glob.team:feature-flaggingabin-**-v2*flag*abin-*display.error, noflag_selectionsentdisplay.error, noflag_selectionsentEvery case matched the expected key set exactly. Requests carried canonical
flag_selectionbodies, and responses carriedAccess-Control-Expose-Headers: DD-Flag-Selection-Ignored.Not covered end-to-end: the rules-based GET (the harness uses only the precompute provider; jest covers it), flag values (key sets only), and a server-side Ignored response (the SDK dropped invalid selections before sending; jest covers it).
Checklist
🤖 Generated with Claude Code