Skip to content

Commit d0ef55f

Browse files
committed
Address comment + some doc about using feature flags
1 parent 826b998 commit d0ef55f

File tree

11 files changed

+110
-44
lines changed

11 files changed

+110
-44
lines changed

docs/tutorials/cli/create-command.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,62 @@ $ dda agent-release data
134134
│ │ └───┴────────┘ │
135135
...
136136
```
137+
138+
## Using feature flags
139+
140+
You can guard behavior behind feature flags using the `app.features` manager. It evaluates a remote flag for the current user/machine with a default fallback.
141+
142+
143+
Update the command to check a flag before performing the network request:
144+
145+
/// tab | :octicons-file-code-16: src/dda/cli/agent_release/data/__init__.py
146+
```python hl_lines="15 23-28 31-36"
147+
from __future__ import annotations
148+
149+
from typing import TYPE_CHECKING
150+
151+
from dda.cli.base import dynamic_command, pass_app
152+
153+
if TYPE_CHECKING:
154+
from dda.cli.application import Application
155+
156+
157+
@dynamic_command(
158+
short_help="Show Agent release data",
159+
features=["http"],
160+
)
161+
@pass_app
162+
def cmd(app: Application) -> None:
163+
"""
164+
Show Agent release data.
165+
"""
166+
# Check a feature flag to enable this command's behavior
167+
# Replace "agent-release-enabled" with your flag key
168+
enabled = app.features.enabled(
169+
"agent-release-enabled",
170+
default_value=True,
171+
extra_attributes={"module": "agent-release"},
172+
)
173+
if not enabled:
174+
app.display_warning("This command is currently disabled by feature flag.")
175+
return
176+
177+
import httpx
178+
179+
base = "https://raw.githubusercontent.com"
180+
repo = "DataDog/datadog-agent"
181+
branch = "main"
182+
path = "release.json"
183+
with app.status("Fetching Agent release data"):
184+
response = httpx.get(f"{base}/{repo}/{branch}/{path}")
185+
186+
response.raise_for_status()
187+
app.display_table(response.json())
188+
```
189+
///
190+
191+
/// note
192+
`app.features.enabled(key, default_value, extra_attributes)` returns the evaluated value for `key`, or `default_value` if the flag is not found. The client automatically includes base context like platform, CI, environment, and user; you can add `extra_attributes` to refine targeting.
193+
///
194+
195+

src/dda/cli/application.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def telemetry(self) -> TelemetryManager:
128128
return TelemetryManager(self)
129129

130130
@cached_property
131-
def ff(self) -> FeatureFlagManager:
131+
def features(self) -> FeatureFlagManager:
132132
from dda.feature_flags.manager import FeatureFlagManager
133133

134134
return FeatureFlagManager(self)

src/dda/feature_flags/client.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# SPDX-FileCopyrightText: 2025-present Datadog, Inc. <[email protected]>
22
#
33
# SPDX-License-Identifier: MIT
4+
from __future__ import annotations
5+
46
import json
5-
from typing import Any, Optional
7+
from typing import TYPE_CHECKING, Any, Optional
68

7-
from dda.utils.network.http.client import get_http_client
9+
if TYPE_CHECKING:
10+
from dda.cli.application import Application
811

912

1013
class DatadogFeatureFlag:
@@ -15,10 +18,7 @@ class DatadogFeatureFlag:
1518
/Users/kevin.fairise/dd/openfeature-js-client/packages/browser/src/transport/fetchConfiguration.ts
1619
"""
1720

18-
def __init__(
19-
self,
20-
client_token: str,
21-
):
21+
def __init__(self, client_token: str | None, app: Application):
2222
"""
2323
Initialize the Datadog Feature Flag client
2424
@@ -32,11 +32,11 @@ def __init__(
3232
flagging_proxy: Optional proxy URL for flagging configuration requests
3333
custom_headers: Optional custom headers to add to requests
3434
"""
35-
self.client_token = client_token
36-
self.env = "Production"
37-
self.endpoint_url = f"https://preview.ff-cdn.datadoghq.com/precompute-assignments?dd_env={self.env}"
38-
self.application_id = "dda"
39-
self.__client = get_http_client()
35+
self.__client_token = client_token
36+
self.__env = "Production"
37+
self.__endpoint_url = f"https://preview.ff-cdn.datadoghq.com/precompute-assignments?dd_env={self.__env}"
38+
self.__application_id = "dda"
39+
self.__app = app
4040

4141
def _fetch_flags(
4242
self, targeting_key: str = "", targeting_attributes: Optional[dict[str, Any]] = None
@@ -54,14 +54,17 @@ def _fetch_flags(
5454
Raises:
5555
requests.HTTPError: If the API request fails
5656
"""
57+
if not self.__client_token:
58+
return {}
59+
5760
# Build headers
5861
headers = {
5962
"Content-Type": "application/vnd.api+json",
60-
"dd-client-token": self.client_token,
63+
"dd-client-token": self.__client_token,
6164
}
6265

63-
if self.application_id:
64-
headers["dd-application-id"] = self.application_id
66+
if self.__application_id:
67+
headers["dd-application-id"] = self.__application_id
6568

6669
# Stringify all targeting attributes
6770
stringified_attributes = {}
@@ -78,7 +81,7 @@ def _fetch_flags(
7881
"type": "precompute-assignments-request",
7982
"attributes": {
8083
"env": {
81-
"dd_env": self.env,
84+
"dd_env": self.__env,
8285
},
8386
"sdk": {
8487
"name": "python-example",
@@ -94,8 +97,9 @@ def _fetch_flags(
9497

9598
try:
9699
# Make the request
97-
response = self.__client.post(self.endpoint_url, headers=headers, json=payload, timeout=10)
98-
except Exception: # noqa: BLE001
100+
response = self.__app.http.client().post(self.__endpoint_url, headers=headers, json=payload, timeout=10)
101+
except Exception as e: # noqa: BLE001
102+
self.__app.display_warning(f"Error fetching flags: {e}")
99103
return {}
100104

101105
return response.json()

src/dda/feature_flags/manager.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from typing import TYPE_CHECKING, Any, Optional
88

99
from dda.feature_flags.client import DatadogFeatureFlag
10+
from dda.user.datadog import User
1011
from dda.utils.ci import running_in_ci
11-
from dda.utils.user import User
1212

1313
if TYPE_CHECKING:
1414
from dda.cli.application import Application
@@ -23,25 +23,20 @@ def __init__(self, config: RootConfig) -> None:
2323
class FeatureFlagManager:
2424
"""
2525
A class for querying feature flags. This is available as the
26-
[`Application.ff`][dda.cli.application.Application.ff] property.
26+
[`Application.features`][dda.cli.application.Application.features] property.
2727
"""
2828

2929
def __init__(self, app: Application) -> None:
3030
self.__app = app
3131

32-
self.__started = False
33-
34-
if self.client_token is not None:
35-
self.__ff_client = DatadogFeatureFlag(self.client_token)
36-
self.__started = True
37-
32+
self.__ff_client = DatadogFeatureFlag(self.__client_token, self.__app)
3833
self.__cache: dict[tuple[str, str, tuple[tuple[str, str], ...]], Any] = {}
3934

4035
@cached_property
41-
def client_token(self) -> str | None:
36+
def __client_token(self) -> str | None:
4237
from contextlib import suppress
4338

44-
from dda.utils.secrets.secrets import fetch_client_token, read_client_token, save_client_token
39+
from dda.secrets.api import fetch_client_token, read_client_token, save_client_token
4540

4641
client_token: str | None = None
4742
with suppress(Exception):
@@ -53,23 +48,26 @@ def client_token(self) -> str | None:
5348
return client_token
5449

5550
@property
56-
def user(self) -> FeatureFlagUser:
51+
def __user(self) -> FeatureFlagUser:
5752
return FeatureFlagUser(self.__app.config)
5853

59-
def get_targeting_key(self) -> str:
54+
def __get_targeting_key(self) -> str:
6055
if running_in_ci():
6156
import os
6257

6358
return os.getenv("CI_JOB_ID", "default_job_id")
6459

65-
return self.user.machine_id
60+
return self.__user.machine_id
6661

67-
def check_flag(self, flag_key: str, default_value: Any, extra_attributes: Optional[dict[str, str]] = None) -> bool:
68-
if not self.__started:
62+
def enabled(
63+
self, flag_key: str, *, default_value: bool = False, extra_attributes: Optional[dict[str, str]] = None
64+
) -> bool:
65+
if not self.__client_token:
66+
self.__app.display_debug("No client token found")
6967
return default_value
7068

71-
targeting_key = self.get_targeting_key()
72-
targeting_attributes = self._get_base_context()
69+
targeting_key = self.__get_targeting_key()
70+
targeting_attributes = self.__get_base_context()
7371
if extra_attributes is not None:
7472
targeting_attributes.update(extra_attributes)
7573

@@ -79,12 +77,14 @@ def check_flag(self, flag_key: str, default_value: Any, extra_attributes: Option
7977
self.__app.display_debug(
8078
f"Checking flag {flag_key} with targeting key {targeting_key} and targeting attributes {tuple_attributes}"
8179
)
82-
flag_value = self._check_flag(flag_key, targeting_key, tuple_attributes)
80+
flag_value = self.__check_flag(flag_key, targeting_key, tuple_attributes)
8381
if flag_value is None:
8482
return default_value
8583
return flag_value
8684

87-
def _check_flag(self, flag_key: str, targeting_key: str, targeting_attributes: tuple[tuple[str, str], ...]) -> bool:
85+
def __check_flag(
86+
self, flag_key: str, targeting_key: str, targeting_attributes: tuple[tuple[str, str], ...]
87+
) -> bool:
8888
cache_key = (flag_key, targeting_key, targeting_attributes)
8989
if cache_key in self.__cache:
9090
return self.__cache[cache_key]
@@ -98,10 +98,10 @@ def _check_flag(self, flag_key: str, targeting_key: str, targeting_attributes: t
9898
self.__cache[cache_key] = flag_value
9999
return flag_value
100100

101-
def _get_base_context(self) -> dict[str, str]:
101+
def __get_base_context(self) -> dict[str, str]:
102102
return {
103103
"platform": "toto",
104104
"ci": "true" if running_in_ci() else "false",
105105
"env": "prod",
106-
"user": self.user.email,
106+
"user": self.__user.email,
107107
}
File renamed without changes.

src/dda/utils/secrets/secrets.py renamed to src/dda/secrets/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ def save_client_token(client_token: str) -> None:
3737

3838

3939
def fetch_api_key() -> str:
40-
from dda.utils.secrets.vault import fetch_secret
40+
from dda.secrets.vault import fetch_secret
4141

4242
return fetch_secret("group/subproduct-agent/deva", "telemetry-api-key")
4343

4444

4545
def fetch_client_token() -> str:
46-
from dda.utils.secrets.vault import fetch_secret
46+
from dda.secrets.vault import fetch_secret
4747

4848
return fetch_secret("group/subproduct-agent/deva", "feature-flags-client-token")
File renamed without changes.

src/dda/telemetry/daemon/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
import psutil
1515
import watchfiles
1616

17+
from dda.secrets.api import fetch_api_key, read_api_key, save_api_key
1718
from dda.telemetry.constants import DaemonEnvVars
1819
from dda.telemetry.daemon.handler import finalize_error
1920
from dda.utils.fs import Path
20-
from dda.utils.secrets.secrets import fetch_api_key, read_api_key, save_api_key
2121

2222
if TYPE_CHECKING:
2323
from collections.abc import AsyncIterator

src/dda/telemetry/manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from functools import cached_property
88
from typing import TYPE_CHECKING
99

10-
from dda.utils.user import User
10+
from dda.user.datadog import User
1111

1212
if TYPE_CHECKING:
1313
from dda.cli.application import Application
@@ -78,7 +78,7 @@ def api_key(self) -> str | None:
7878

7979
from contextlib import suppress
8080

81-
from dda.utils.secrets.secrets import fetch_api_key, read_api_key, save_api_key
81+
from dda.secrets.api import fetch_api_key, read_api_key, save_api_key
8282

8383
api_key: str | None = None
8484
with suppress(Exception):

src/dda/user/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# SPDX-FileCopyrightText: 2025-present Datadog, Inc. <[email protected]>
2+
#
3+
# SPDX-License-Identifier: MIT

0 commit comments

Comments
 (0)