diff --git a/config/_default/menus/main.en.yaml b/config/_default/menus/main.en.yaml index 7357821a9a2..753746763e2 100644 --- a/config/_default/menus/main.en.yaml +++ b/config/_default/menus/main.en.yaml @@ -6493,6 +6493,11 @@ menu: parent: feature_flags identifier: feature_flags_implementation_patterns weight: 5 + - name: Local Flag Overrides + url: feature_flags/implementation_patterns/local_flag_overrides + parent: feature_flags_implementation_patterns + identifier: feature_flags_implementation_patterns_local_flag_overrides + weight: 500 - name: OpenTelemetry url: feature_flags/implementation_patterns/opentelemetry parent: feature_flags_implementation_patterns diff --git a/content/en/feature_flags/implementation_patterns/_index.md b/content/en/feature_flags/implementation_patterns/_index.md index 6a870e2d06d..14a5af7b180 100644 --- a/content/en/feature_flags/implementation_patterns/_index.md +++ b/content/en/feature_flags/implementation_patterns/_index.md @@ -6,6 +6,7 @@ description: Learn common patterns for integrating Datadog Feature Flags with yo Explore implementation patterns for integrating Datadog Feature Flags with your existing tooling, SDKs, and observability stack. {{< whatsnext desc=" " >}} + {{< nextlink href="/feature_flags/implementation_patterns/local_flag_overrides" >}}Local Flag Overrides with the Multi-Provider Pattern{{< /nextlink >}} {{< nextlink href="/feature_flags/implementation_patterns/opentelemetry" >}}Feature Flags with OpenTelemetry{{< /nextlink >}} {{< nextlink href="/feature_flags/implementation_patterns/serverless" >}}Serverless Environments{{< /nextlink >}} {{< /whatsnext >}} diff --git a/content/en/feature_flags/implementation_patterns/local_flag_overrides.md b/content/en/feature_flags/implementation_patterns/local_flag_overrides.md new file mode 100644 index 00000000000..b855ebee620 --- /dev/null +++ b/content/en/feature_flags/implementation_patterns/local_flag_overrides.md @@ -0,0 +1,391 @@ +--- +title: Local Flag Overrides with the Multi-Provider Pattern +description: Use OpenFeature's Multi-Provider pattern with an in-memory provider to override flag variants locally for testing. +further_reading: +- link: "/feature_flags/server/" + tag: "Documentation" + text: "Server-Side Feature Flags" +- link: "/feature_flags/client/javascript/" + tag: "Documentation" + text: "JavaScript Feature Flags" +- link: "https://openfeature.dev/specification/appendix-a/#multi-provider" + tag: "OpenFeature" + text: "Multi-Provider Specification" +- link: "https://openfeature.dev/blog/openfeature-multi-provider-release/" + tag: "OpenFeature" + text: "OpenFeature Multi-Provider Release" +--- + +## Overview + +When you test or demo a feature flag integration, you often need to force a specific variant without changing the flag configuration in Datadog. The [OpenFeature Multi-Provider][1] pattern combines an `InMemoryProvider` with your Datadog provider so local overrides take precedence while all other flags continue to resolve from Datadog. + +Typical use cases include: + +- QA and manual testing of specific flag variants +- Local development without editing flag configuration in the UI +- Demos and support workflows where a teammate needs a predictable variant on demand + +
MultiProvider is available, but the OpenFeature SDKs do not ship a built-in in-memory provider — implement a small custom FeatureProvider instead, as shown in the iOS and Android testing documentation.UserDefaults on iOS or SharedPreferences on Android).
+
+### URL query parameters
+
+Read overrides from the page URL at startup. This approach is useful when QA or support needs to share a link that forces specific variants:
+
+{{< code-block lang="javascript" >}}
+import { InMemoryProvider, MultiProvider, OpenFeature } from '@openfeature/web-sdk';
+import { DatadogProvider } from '@datadog/openfeature-browser';
+
+const OVERRIDE_QUERY_PREFIX = 'ff.';
+
+function buildInMemoryFlags(overrides) {
+ return Object.fromEntries(
+ Object.entries(overrides).map(([flagKey, value]) => [
+ flagKey,
+ {
+ variants: { forced: value },
+ defaultVariant: 'forced',
+ disabled: false,
+ },
+ ]),
+ );
+}
+
+function loadOverrides() {
+ const overrides = {};
+ const params = new URLSearchParams(window.location.search);
+
+ for (const [key, value] of params.entries()) {
+ if (!key.startsWith(OVERRIDE_QUERY_PREFIX)) continue;
+ const flagKey = key.slice(OVERRIDE_QUERY_PREFIX.length);
+ if (flagKey) overrides[flagKey] = value;
+ }
+
+ return overrides;
+}
+
+export async function initializeFeatureFlags({
+ clientToken,
+ applicationId,
+ env,
+ service,
+}) {
+ const datadogProvider = new DatadogProvider({
+ clientToken,
+ applicationId,
+ env,
+ service,
+ });
+
+ await OpenFeature.setProviderAndWait(
+ new MultiProvider([
+ { provider: new InMemoryProvider(buildInMemoryFlags(loadOverrides())) },
+ { provider: datadogProvider },
+ ]),
+ );
+}
+{{< /code-block >}}
+
+A tester could append `?ff.checkout_new=true&ff.ui_theme=dark` to the page URL to force those variants.
+
+### localStorage
+
+Persist overrides across page reloads by reading from `localStorage`. This approach is useful when a developer toggles flags from a local debug panel:
+
+{{< code-block lang="javascript" >}}
+import { InMemoryProvider, MultiProvider, OpenFeature } from '@openfeature/web-sdk';
+import { DatadogProvider } from '@datadog/openfeature-browser';
+
+const OVERRIDE_STORAGE_KEY = 'ff_overrides';
+
+function buildInMemoryFlags(overrides) {
+ return Object.fromEntries(
+ Object.entries(overrides).map(([flagKey, value]) => [
+ flagKey,
+ {
+ variants: { forced: value },
+ defaultVariant: 'forced',
+ disabled: false,
+ },
+ ]),
+ );
+}
+
+function loadOverrides() {
+ const raw = localStorage.getItem(OVERRIDE_STORAGE_KEY);
+ if (!raw) return {};
+
+ try {
+ return JSON.parse(raw);
+ } catch {
+ return {};
+ }
+}
+
+export async function initializeFeatureFlags({
+ clientToken,
+ applicationId,
+ env,
+ service,
+}) {
+ const datadogProvider = new DatadogProvider({
+ clientToken,
+ applicationId,
+ env,
+ service,
+ });
+
+ await OpenFeature.setProviderAndWait(
+ new MultiProvider([
+ { provider: new InMemoryProvider(buildInMemoryFlags(loadOverrides())) },
+ { provider: datadogProvider },
+ ]),
+ );
+}
+{{< /code-block >}}
+
+Set overrides from a debug panel or the browser console, then reload the page:
+
+{{< code-block lang="javascript" >}}
+localStorage.setItem('ff_overrides', JSON.stringify({
+ checkout_new: true,
+ ui_theme: 'dark',
+}));
+{{< /code-block >}}
+
+## Server override examples
+
+On the server, populate the in-memory provider at process startup from any local source. Environment variables and static configuration maps are two common approaches.
+
+
+
+### Environment variables
+
+Read overrides from environment variables at startup. This approach is useful for one-off local runs or CI jobs that need a specific variant:
+
+{{< tabs >}}
+{{% tab "Node.js" %}}
+
+{{< code-block lang="javascript" >}}
+import { MultiProvider, OpenFeature, TypedInMemoryProvider } from '@openfeature/server-sdk';
+import tracer from 'dd-trace';
+
+const OVERRIDE_ENV_PREFIX = 'FF_';
+
+function buildInMemoryFlags(overrides) {
+ return Object.fromEntries(
+ Object.entries(overrides).map(([flagKey, value]) => [
+ flagKey,
+ {
+ variants: { forced: value },
+ defaultVariant: 'forced',
+ disabled: false,
+ },
+ ]),
+ );
+}
+
+function loadOverrides() {
+ const overrides = {};
+
+ for (const [key, value] of Object.entries(process.env)) {
+ if (!key.startsWith(OVERRIDE_ENV_PREFIX) || value === undefined) continue;
+ const flagKey = key.slice(OVERRIDE_ENV_PREFIX.length);
+ if (flagKey) overrides[flagKey] = value;
+ }
+
+ return overrides;
+}
+
+export async function initializeFeatureFlags() {
+ tracer.init();
+
+ const overrideProvider = new TypedInMemoryProvider(
+ buildInMemoryFlags(loadOverrides()),
+ );
+
+ await OpenFeature.setProviderAndWait(
+ new MultiProvider([
+ { provider: overrideProvider },
+ { provider: tracer.openfeature },
+ ]),
+ );
+}
+{{< /code-block >}}
+{{% /tab %}}
+{{% tab "Go" %}}
+
+