Repository navigation
[FFL-2926] Add opt-in tracking hooks for browser CoreProvider - #349
Conversation
4601849 to
b077ce1
Compare
c5671fe to
d22ce15
Compare
b077ce1 to
35e7307
Compare
d22ce15 to
e4d574a
Compare
35e7307 to
d45c3b0
Compare
e4d574a to
592713b
Compare
d45c3b0 to
dfaa31f
Compare
592713b to
d22d4e7
Compare
dfaa31f to
26553ff
Compare
d22d4e7 to
9d718e1
Compare
26553ff to
e0aeed0
Compare
e0aeed0 to
880ef0f
Compare
1adf3ea to
7beba56
Compare
880ef0f to
2304a11
Compare
7beba56 to
e1d30d6
Compare
2304a11 to
4c38e3f
Compare
e1d30d6 to
9160faa
Compare
4c38e3f to
471d42b
Compare
9160faa to
81106cc
Compare
471d42b to
d4b7c33
Compare
6a78e4d to
ff35a7d
Compare
OpenFeature Browser Entrypoint Bundle SizesMeasured from the Vite production output after installing packed
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 OpenFeature Browser Tracking Hook Bundle SizesSynthetic entrypoints import and call tracking hook factories from the packed
This report shows current PR artifact sizes only; it does not compare against the base branch. |
btthomas
left a comment
There was a problem hiding this comment.
Overall it looks close.
Found three things from AI.
Always run exposure and evaluation cleanup after a failed shutdown flush. Cover throwing proxies, repeated shutdown, and reinitialization. Environment: Datadog workspace
leoromanovsky
left a comment
There was a problem hiding this comment.
Minor improvement if you'd like to accept now or later: #391
1e1ba4b to
5f06704
Compare
5f06704 to
885716b
Compare
Preserve scoped Chrome storage reads with the updated Chrome types. Adapt telemetry cache scope to browser-core 7.1.0 and cover namespace-specific reads.
fix(browser): clean up tracking when the final flush fails
Summary
DatadogCoreProviderfrom@datadog/openfeature-browser/rules-basedas the minimal browser provider for application-managed flag configurationDatadogProviderBasesoDatadogCoreProvidercan be the concrete customer-facing providerDatadogOfflineProviderout of this PR; it can be considered later as a wrapper that auto-registers side effects if customer feedback calls for itcreateDatadogExposureLoggingHook(options)createDatadogEvaluationLoggingHook(options)createDatadogRumTrackingHook()composeDatadogTrackingHooks(...hooks)as a small controller composition helper:{ hooks, initialize, shutdown }DatadogProviderDatadogCoreProviderconfiguration identityWhy
DatadogCoreProvideris intended to be the smallest browser building block over@datadog/flagging-core: it evaluates a supplied flags configuration and does not fetch, poll, register hooks, or send telemetry by itself.The portable-configuration RFC prioritizes minimal, composable building blocks before provider-level convenience options. This PR therefore avoids adding tracking constructor options or an all-in-one offline convenience provider. Instead, Datadog side effects are exposed as OpenFeature hooks that the application registers explicitly.
The API uses separate hook factories instead of boolean opt-outs on one all-in-one factory. This makes the selected tracking integrations visible in imports and call sites, and avoids forcing apps that omit RUM/exposure/evaluation tracking to import those hook implementations through runtime boolean parameters.
The exposure hook is intended to be paired with
DatadogCoreProviderfrom the same@datadog/openfeature-browser/rules-basedentrypoint. The provider stamps exposure-eligible evaluations with an internal configuration identity, and the exposure hook uses that identity only for deduplication. This keepsresetExposureCache()out of the public API while still allowing exposures to be emitted again when the active configuration changes.A future
DatadogOfflineProvidercould layer on top of these primitives as a convenience provider that configures delivery and auto-registers standard Datadog side effects for a simpler developer experience. This PR keeps that as a follow-up design rather than committing to that API shape now.Jira: FFL-2926
API proposal
Minimal evaluation only:
Optional Datadog tracking hooks:
composeDatadogTrackingHooks()combines the supplied controllers' hooks and lifecycle methods. The name emphasizes composition: individual factories create the integrations, while this helper does not initialize resources or register hooks with OpenFeature automatically. Applications can also use individual controllers without this helper.To exclude an integration, omit that hook factory from both the import list and the
composeDatadogTrackingHooks()call.tracking.initialize()loads the exposure deduplication cache and starts the included hooks' transports, timers, and subscriptions. Exposure/evaluation factories do not start these resources at construction, and their hooks do not collect events until initialization completes. If only hooks without lifecycle are included, such as RUM tracking, initialization and shutdown are no-ops. Omitting the exposure factory creates no exposure hook or exposure cache.tracking.shutdown()flushes pending exposure/evaluation events, stops aggregation and batch timers, and removes subscriptions. Requests already handed to the browser transport may still complete or retry. Both methods are idempotent, serialize pending setup and teardown, and allow reinitialization without clearing persisted exposure deduplication. Individual exposure/evaluation controllers also expose both methods.Applications own manually registered hooks. Clearing client hooks or removing
DatadogCoreProviderdoes not dispose of their resources automatically; unregister them and explicitly calltracking.shutdown(). The regularDatadogProviderowns its built-in tracking and cleans it up through OpenFeature'sonClose()provider lifecycle. This preserves the core provider's evaluation-only responsibility.There is no public exposure-cache reset method: exposure deduplication is tied to the active
DatadogCoreProviderconfiguration identity, so replacing the provider configuration allows exposures for the new configuration to be emitted without clearing application-managed hook state.Configuration identity excludes retrieval metadata (
fetchedAtandetag). For rules-based configurations, it also excludes the server's per-buildcreatedAttimestamp, matching the backend's semanticFingerprint()behavior. Refetching otherwise unchanged configurations therefore keeps exposures deduplicated, while content changes allow new exposures. These fields are excluded only from deduplication identity; the configuration object and portable wire representation retain them.Persistent caches are scoped by a hash of telemetry site, client token, proxy URL, environment, application, service, and source. Both the standalone hook and regular provider use this scope, and clearing Chrome storage removes only that cache's keys. Raw tokens are not stored in cache keys. Legacy unscoped entries are not reused, which can cause a one-time repeat exposure after upgrading. Function-valued telemetry proxies use memory-only deduplication because callback identity cannot reliably identify the destination selected through closure state. The scope matches the browser-core 7.1.0 transport options now on
main; the removedinternalAnalyticsSubdomainoption is no longer included.Cross-SDK shape
The public shape is intentionally a small provider plus tracking controller rather than a browser-specific provider option:
This is meant to keep the customer-facing lifecycle portable across SDKs. Browser and React Native can expose the same initialization, hook-registration, and shutdown flow while using different platform implementations behind the scenes. A React Native implementation should release only resources owned by these hooks, not shut down a shared native SDK.
In the browser SDK, exposure/evaluation hook factories own browser tracking configuration, browser transport setup, and browser exposure deduplication storage. In React Native, the same controller shape could delegate tracking side effects to the existing native
DdFlagsbridge/native SDK configuration instead of browser transports. If React Native needs native exposure deduplication to account for configuration replacement, that can be handled behind the provider/hook implementation boundary rather than exposing a customer-facing cache reset API.Hook scope
OpenFeature gives applications several hook registration scopes. The Web SDK hooks docs say hooks can be registered globally, on a client, or for a single flag invocation. The Web SDK domains docs define domains as the mechanism for binding clients to providers, and the provider docs call out domain-scoped providers for applications that use multiple providers. The hooks specification also defines hook registration and ordering across the API, client, provider, and invocation levels.
Based on those docs, this PR recommends registering Datadog tracking hooks on the domain-scoped OpenFeature client when an application has multiple providers. Global hooks are OpenFeature-compliant, but they would run for every evaluation in the application. Invocation hooks are also valid, but too repetitive for telemetry that should accompany every evaluation from the Datadog client. Domain/client-scoped registration keeps Datadog telemetry attached to evaluations made through the Datadog provider without affecting other providers.
Behavior and compatibility
new DatadogCoreProvider()is the new rules-based entrypoint provider construction path for application-managed configurationDatadogCoreProviderevaluates both precomputed/static-context and rules-based/dynamic-context configurations supplied throughsetConfiguration()DatadogCoreProviderfetches nothing, polls nothing, registers no hooks, and sends no telemetry by itselfcreateDatadogExposureLoggingHook(),createDatadogEvaluationLoggingHook(), andcreateDatadogRumTrackingHook()can be imported and composed independentlylocalStorage, then memory; function-valued telemetry proxies use memory-only storageDatadogProviderDatadogCoreProviderconfiguration identity internally: changed configurations can emit fresh exposures, while refetching unchanged content remains dedupedDatadogProviderconstructor and configuration fetching remain unchanged; built-in tracking starts during initialization, shuts down throughonClose(), and uses scoped exposure cachesDatadogCoreProviderValidation
mainlockfileyarn workspace @datadog/openfeature-browser typecheckyarn workspace @datadog/openfeature-browser buildprettier --check packages/browser/README.md packages/browser/src packages/browser/test test-app/src/tracking*.ts scripts/report-entrypoint-bundle-sizes.js test-app/vite.config.ts test-app/*.htmlbiome check packages/browser/src packages/browser/test --diagnostic-level=errorgit diff --check