Skip to content

Commit 53e8d6c

Browse files
committed
feat: Support SDK metrics
1 parent 9ac4dc7 commit 53e8d6c

File tree

5 files changed

+136
-7
lines changed

5 files changed

+136
-7
lines changed

flagsmith/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from . import webhooks
2-
from .flagsmith import Flagsmith
1+
from flagsmith import webhooks
2+
from flagsmith.flagsmith import Flagsmith
3+
from flagsmith.version import __version__
34

4-
__all__ = ("Flagsmith", "webhooks")
5+
__all__ = ("Flagsmith", "webhooks", "__version__")

flagsmith/flagsmith.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from flag_engine.identities.traits.types import TraitValue
1212
from flag_engine.segments.evaluator import get_identity_segments
1313
from requests.adapters import HTTPAdapter
14+
from requests.utils import default_user_agent
1415
from urllib3 import Retry
1516

1617
from flagsmith.analytics import AnalyticsProcessor
@@ -19,13 +20,15 @@
1920
from flagsmith.offline_handlers import BaseOfflineHandler
2021
from flagsmith.polling_manager import EnvironmentDataPollingManager
2122
from flagsmith.streaming_manager import EventStreamManager, StreamEvent
22-
from flagsmith.types import JsonType, TraitConfig, TraitMapping
23+
from flagsmith.types import ApplicationMetadata, JsonType, TraitConfig, TraitMapping
2324
from flagsmith.utils.identities import generate_identity_data
25+
from flagsmith.version import __version__
2426

2527
logger = logging.getLogger(__name__)
2628

2729
DEFAULT_API_URL = "https://edge.api.flagsmith.com/api/v1/"
2830
DEFAULT_REALTIME_API_URL = "https://realtime.flagsmith.com/"
31+
DEFAULT_USER_AGENT = f"flagsmith-python-client/{__version__} " + default_user_agent()
2932

3033

3134
class Flagsmith:
@@ -61,6 +64,7 @@ def __init__(
6164
offline_mode: bool = False,
6265
offline_handler: typing.Optional[BaseOfflineHandler] = None,
6366
enable_realtime_updates: bool = False,
67+
application_metadata: typing.Optional[ApplicationMetadata] = None,
6468
):
6569
"""
6670
:param environment_key: The environment key obtained from Flagsmith interface.
@@ -88,6 +92,7 @@ def __init__(
8892
document from another source when in offline_mode. Works in place of
8993
default_flag_handler if offline_mode is not set and using remote evaluation.
9094
:param enable_realtime_updates: Use real-time functionality via SSE as opposed to polling the API
95+
:param application_metadata: Optional metadata about the client application.
9196
"""
9297

9398
self.offline_mode = offline_mode
@@ -122,7 +127,11 @@ def __init__(
122127

123128
self.session = requests.Session()
124129
self.session.headers.update(
125-
**{"X-Environment-Key": environment_key}, **(custom_headers or {})
130+
self._get_headers(
131+
environment_key=environment_key,
132+
application_metadata=application_metadata,
133+
custom_headers=custom_headers,
134+
)
126135
)
127136
self.session.proxies.update(proxies or {})
128137
retries = retries or Retry(total=3, backoff_factor=0.1)
@@ -275,6 +284,24 @@ def update_environment(self) -> None:
275284
identity.identifier: identity for identity in overrides
276285
}
277286

287+
def _get_headers(
288+
self,
289+
environment_key: str,
290+
application_metadata: ApplicationMetadata,
291+
custom_headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
292+
) -> typing.Dict[str, str]:
293+
headers = {
294+
"X-Environment-Key": environment_key,
295+
"User-Agent": DEFAULT_USER_AGENT,
296+
}
297+
if application_metadata:
298+
if name := application_metadata.get("name"):
299+
headers["Flagsmith-Application-Name"] = name
300+
if version := application_metadata.get("version"):
301+
headers["Flagsmith-Application-Version"] = version
302+
headers.update(custom_headers or {})
303+
return headers
304+
278305
def _get_environment_from_api(self) -> EnvironmentModel:
279306
environment_data = self._get_json_response(self.environment_url, method="GET")
280307
return EnvironmentModel.model_validate(environment_data)

flagsmith/types.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import typing
22

33
from flag_engine.identities.traits.types import TraitValue
4-
from typing_extensions import TypeAlias
4+
from typing_extensions import TypeAlias, NotRequired
55

66
_JsonScalarType: TypeAlias = typing.Union[
77
int,
@@ -23,3 +23,8 @@ class TraitConfig(typing.TypedDict):
2323

2424

2525
TraitMapping: TypeAlias = typing.Mapping[str, typing.Union[TraitValue, TraitConfig]]
26+
27+
28+
class ApplicationMetadata(typing.TypedDict):
29+
name: NotRequired[str]
30+
version: NotRequired[str]

flagsmith/version.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from importlib.metadata import version
2+
3+
__version__ = version("flagsmith")

tests/test_flagsmith.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pytest_mock import MockerFixture
1212
from responses import matchers
1313

14-
from flagsmith import Flagsmith
14+
from flagsmith import Flagsmith, __version__
1515
from flagsmith.exceptions import (
1616
FlagsmithAPIError,
1717
FlagsmithFeatureDoesNotExistError,
@@ -717,3 +717,96 @@ def test_custom_feature_error_raised_when_invalid_feature(
717717
with pytest.raises(FlagsmithFeatureDoesNotExistError):
718718
# When
719719
flags.is_feature_enabled("non-existing-feature")
720+
721+
722+
@pytest.fixture
723+
def default_headers() -> typing.Dict[str, str]:
724+
return {
725+
"User-Agent": f"flagsmith-python-client/{__version__} python-requests/2.32.4",
726+
"Accept-Encoding": "gzip, deflate",
727+
"Accept": "*/*",
728+
"Connection": "keep-alive",
729+
}
730+
731+
732+
@pytest.mark.parametrize(
733+
"kwargs,expected_headers",
734+
[
735+
(
736+
{
737+
"environment_key": "test-key",
738+
"application_metadata": {"name": "test-app", "version": "1.0.0"},
739+
},
740+
{
741+
"Flagsmith-Application-Name": "test-app",
742+
"Flagsmith-Application-Version": "1.0.0",
743+
"X-Environment-Key": "test-key",
744+
},
745+
),
746+
(
747+
{
748+
"environment_key": "test-key",
749+
"application_metadata": {"name": "test-app"},
750+
},
751+
{
752+
"Flagsmith-Application-Name": "test-app",
753+
"X-Environment-Key": "test-key",
754+
},
755+
),
756+
(
757+
{
758+
"environment_key": "test-key",
759+
"application_metadata": {"version": "1.0.0"},
760+
},
761+
{
762+
"Flagsmith-Application-Version": "1.0.0",
763+
"X-Environment-Key": "test-key",
764+
},
765+
),
766+
(
767+
{
768+
"environment_key": "test-key",
769+
"application_metadata": {"version": "1.0.0"},
770+
"custom_headers": {"X-Custom-Header": "CustomValue"},
771+
},
772+
{
773+
"Flagsmith-Application-Version": "1.0.0",
774+
"X-Environment-Key": "test-key",
775+
"X-Custom-Header": "CustomValue",
776+
},
777+
),
778+
(
779+
{
780+
"environment_key": "test-key",
781+
"application_metadata": None,
782+
"custom_headers": {"X-Custom-Header": "CustomValue"},
783+
},
784+
{
785+
"X-Environment-Key": "test-key",
786+
"X-Custom-Header": "CustomValue",
787+
},
788+
),
789+
(
790+
{"environment_key": "test-key"},
791+
{
792+
"X-Environment-Key": "test-key",
793+
},
794+
),
795+
],
796+
)
797+
@responses.activate()
798+
def test_flagsmith__init__expected_headers_sent(
799+
default_headers: typing.Dict[str, str],
800+
kwargs: typing.Dict[str, typing.Any],
801+
expected_headers: typing.Dict[str, str],
802+
) -> None:
803+
# Given
804+
flagsmith = Flagsmith(**kwargs)
805+
responses.add(method="GET", url=flagsmith.environment_flags_url, body="{}")
806+
807+
# When
808+
flagsmith.get_environment_flags()
809+
810+
# Then
811+
headers = responses.calls[0].request.headers
812+
assert headers == {**default_headers, **expected_headers}

0 commit comments

Comments
 (0)