Skip to content

[FFL-2934] Add browser configuration fetchers - #351

Merged
sameerank merged 24 commits into
mainfrom
sameerank/FFL-2934/configuration-fetchers-bootstrap-init
Sep 14, 2026
Merged

sameerank merged 24 commits into
mainfrom
sameerank/FFL-2934/configuration-fetchers-bootstrap-init

Conversation

@sameerank

@sameerank sameerank commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add public browser fetchers for precomputed and rules-based flag configurations
  • keep browser-specific HTTP and authentication concerns in the browser package and protobuf decoding in flagging core
  • send dd-application-id only to the precomputed endpoint; rules-based requests use dd-client-token, the protobuf Accept header, and SDK identification headers
  • keep the default browser entry point free of protobuf parsing dependencies
  • preserve the existing DatadogProvider.initialFlagsConfiguration public API as a network-failure fallback
  • use DatadogOfflineProvider.setConfiguration() for explicit caller-managed configuration
  • defer conditional ETag/304 support until the live endpoints expose ETags and permit conditional request headers through CORS

User impact

Consumers can fetch portable configuration explicitly and load it into DatadogOfflineProvider:

const configuration = await fetchPrecomputedConfiguration(options)
const provider = new DatadogOfflineProvider()
provider.setConfiguration(configuration)

The same offline-provider flow works with fetchRulesConfiguration from the opt-in /rules-based entry point.

DatadogProvider remains network-first and continues to fetch precomputed configuration during initialization and context changes. For backward compatibility, it still accepts initialFlagsConfiguration; a matching precomputed configuration remains available as the in-memory fallback if the network request fails. Applications that want explicit offline configuration ownership can use DatadogOfflineProvider and transition to the online provider through provider composition or handoff once it is ready.

This matches the React Native separation between offline configuration delivery and online fetching while leaving transition policy outside either provider.

Stack and merge order

This PR is stacked on #346. The getPrecomputedContext work from #353 is now included in #344.

  1. Merge #344.
  2. Retarget and merge #336.
  3. Retarget and merge #346.
  4. Retarget this PR and sibling #349 to main; they can merge independently.

Jira: FFL-2934

Validation

  • full browser suite passed: 16 suites and 204 tests
  • browser TypeScript checks, Biome checks, production build, and Prettier checks passed
  • regression coverage verifies initialFlagsConfiguration remains an in-memory fallback after a network failure
  • successful HTTP responses are validated through the shared core parser; malformed configurations produce parse errors and malformed flags remain isolated
  • direct precomputed and rules-based fetching were verified in the browser
  • a live rules-based protobuf response decoded successfully and all returned flags were exercised through core evaluation and DatadogOfflineProvider without evaluation errors

@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch 2 times, most recently from b9aeedc to 03b8806 Compare August 6, 2026 20:56
@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch from 03b8806 to 8f61547 Compare August 6, 2026 21:30
@sameerank sameerank changed the title [FFL-2934] Add configuration fetchers and provider bootstrap initialization [FFL-2934] Add browser configuration fetchers Aug 6, 2026
@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch from 8f61547 to 1bd9371 Compare August 6, 2026 23:26
@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch from 1bd9371 to 6313399 Compare August 7, 2026 00:54
@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch from 6313399 to 88b113c Compare August 7, 2026 01:25
@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch from 88b113c to aee08b1 Compare August 9, 2026 14:08
@sameerank
sameerank marked this pull request as ready for review August 9, 2026 16:54
@sameerank
sameerank requested a review from a team as a code owner August 9, 2026 16:54
@sameerank
sameerank requested review from dd-oleksii and leoromanovsky and removed request for a team August 9, 2026 16:54
@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch from aee08b1 to bdc3616 Compare August 11, 2026 14:21
Comment on lines +88 to +101
export async function fetchPrecomputedConfiguration(
options: PrecomputedConfigurationFetchOptions
): Promise<FlagsConfiguration> {
const url = buildConfigurationUrl(options, 'precomputed')
const defaultHeaders = buildConfigurationHeaders(
options,
{
'Content-Type': 'application/vnd.api+json',
...(options.previousConfiguration?.precomputed?.etag && {
'If-None-Match': options.previousConfiguration.precomputed.etag,
}),
},
'precomputed'
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey I was looking at this function for correctness. I didn't trust the codex review without writing my own test to prove it, and at that point I decided I might as well add a fix to. PTAL and merge if accepted: #364

@sameerank sameerank Aug 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. Oleksii clarified that ETags should represent the generated response. Reusing an ETag across contexts is safe when both contexts produce the same response; if the assignments differ, the server must return a different ETag.

Since the live endpoints don’t currently expose ETags or support If-None-Match through CORS, I’m going to remove ETag/304 handling from #351 and defer this. When we add it back, a 304 should reuse the response while binding it to the newly requested context.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leoromanovsky leoromanovsky left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about etag some more and since the backend function doesn't return one anyways, nor has support for preflight we should punt on it now anyways. Do you want this support added? In terms of billing it'll still be a request to the CDN so we aren't saving customers any money but if the preflight succeeds their application will startup faster with the full network wait.

@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch from 357967f to 9b34154 Compare August 13, 2026 22:52
@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch 2 times, most recently from 5cb9a76 to 567a26c Compare August 14, 2026 20:22
@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch from fda2716 to efa1d35 Compare August 19, 2026 20:47
@sameerank
sameerank requested a review from btthomas August 20, 2026 00:01
this.latestContextUpdate = this.retrieveFlagsConfiguration(evaluationContext, { signal })
const configurationPromise = this.retrieveFlagsConfiguration(evaluationContext, { signal })

this.latestContextUpdate = configurationPromise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: seems to be an unrelated change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup unrelated and unnecessary. Removed in c4f5732

site?: string
customHeaders?: Record<string, string>
overwriteRequestHeaders?: boolean
flaggingProxy?: string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major (given this is now part of our public API): we've chatted with Leo in a PR adding polling and retries that it could be simpler and more flexible to allow users to pass their own fetch function instead of providing tons of configuration options. This makes the interface and our implementation simpler, as well as allow further customization (e.g. overriding URL, adding retries etc)

@sameerank sameerank Aug 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 4b46e33

Both standalone fetchers and DatadogProvider can now use a caller-supplied fetch implementation, allowing it to own retries, timeouts, proxying, or header customization.

The new standalone option types do not expose the proxy/header-specific fields. I retained them only on FlaggingInitConfiguration because they were already part of the existing provider API:

/**
* Custom headers to add to the request to the Datadog API.
*/
customHeaders?: Record<string, string>
/**
* Whether to overwrite the default request headers.
*/
overwriteRequestHeaders?: boolean
/**
* Proxy URL for flagging configuration requests. If set, this will be used instead of the site parameter.
*/
flaggingProxy?: string

Comment thread packages/browser/src/index.ts Resolved
Comment on lines +83 to +84
'DD-Client-Library-Language': sourcePayload.sdk_name,
'DD-Client-Library-Version': sourcePayload.sdk_version,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: shall we send these to precomputed endpoint as well? they are helpful

EDIT: ok, it looks like we'd need to modify CORS first, so feel free to ignore

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and I’ll leave these headers rules-only for now (until the precomputed endpoint’s CORS configuration allows them)


const configuration = configurationFromRulesBinary(new Uint8Array(await response.arrayBuffer()))
if (configuration.rules) {
configuration.rules.fetchedAt = timeStampNow()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I think the fetchedAt timestamp should be stored before we make the request. It's primarily going to be used to determine whether the client needs to refresh their configuration or the current one is fresh enough. Using an earlier timestamp is more conservative

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 973dd01

Both fetchers now capture fetchedAt before starting the request. I also added regression assertions verifying that the timestamp has already been recorded when the supplied fetch implementation is invoked.

Comment thread packages/browser/src/rules-based.ts Resolved
Comment on lines +13 to +14
/** @internal */
export { parsePrecomputedConfigurationResponse } from './wire-validation'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: hm... does this attach @internal to parsePrecomputedConfigurationResponse correctly? shall we have this comment on the function definition?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. @internal is now attached directly to the function definition:

/** @internal */
export function parsePrecomputedConfigurationResponse(

I also removed the redundant annotation from the re-export:

2c9e9fc

return decodeUniversalFlagConfigurationBinary(base64Decode(response))
}

export function decodeUniversalFlagConfigurationBinary(response: Uint8Array): FlagsConfiguration {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: very opinionated but I'm trying to gradually phase out "universal flag configuration" usage in favor of simple "flags configuration." We have so many different configuration formats that calling them all "universal" doesn't make sense

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! Renamed the protobuf helpers from decode/encodeUniversalFlagConfiguration to decode/encodeFlagsConfiguration and updated the related terminology.

d7797c5

Comment on lines 7 to 13
export function decodeUniversalFlagConfiguration(response: string): FlagsConfiguration {
return fromBinary(FlagsConfigurationSchema, base64Decode(response))
return decodeUniversalFlagConfigurationBinary(base64Decode(response))
}

export function decodeUniversalFlagConfigurationBinary(response: Uint8Array): FlagsConfiguration {
return fromBinary(FlagsConfigurationSchema, response)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: imo "protobuf" module should not care about base64 encoding/decoding the whole response. Protobuf is binary-only.

Base64 is part of configuration wire spec, so base64 encoding/decoding should happen there

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protobuf codec now accepts and returns binary data only. Base64 decoding/encoding happens at the configuration wire boundary, while binary endpoint responses pass directly to the protobuf decoder.

73c493c

…meerank/FFL-2934/configuration-fetchers-bootstrap-init

# Conflicts:
#	packages/browser/src/domain/configuration.ts
#	packages/browser/src/transport/fetchConfiguration.ts
@sameerank
sameerank requested a review from dd-oleksii September 9, 2026 23:54
@datadog-prod-us1-6

This comment has been minimized.

@sameerank
sameerank force-pushed the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch from 45e3074 to 04d7a48 Compare September 10, 2026 18:04
…meerank/FFL-2934/configuration-fetchers-bootstrap-init
Base automatically changed from sameerank/FFL-2871/static-context-offline-init to main September 14, 2026 18:47
@github-actions

Copy link
Copy Markdown

OpenFeature Browser Entrypoint Bundle Sizes

Measured from the Vite production output after installing packed @datadog/flagging-core and @datadog/openfeature-browser tarballs.

Entrypoint HTML JS Assets Raw JS Gzip JS Protobuf Markers
root provider smoke index.html 3 78.9 KiB 25.6 KiB no
precomputed configuration precomputed.html 3 5.9 KiB 2.5 KiB no
rules-based configuration protobuf.html 4 170.8 KiB 51.6 KiB yes

Default and precomputed entrypoints are expected to keep Protobuf-ES out of their bundles. Rules-based entrypoints are expected to include protobuf markers as a positive control. The marker check is a packed-artifact backstop; the source import boundary is enforced by packages/core/test/entrypoint-boundaries.spec.ts.

This report shows current PR artifact sizes only; it does not compare against the base branch.

@sameerank
sameerank merged commit 20cf750 into main Sep 14, 2026
5 checks passed
@sameerank
sameerank deleted the sameerank/FFL-2934/configuration-fetchers-bootstrap-init branch September 14, 2026 19:26
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.

4 participants