Repository: Unleash/unleash-client-python
Pull request #346 (2025-02-27 12:07:08)

feat: new headers and payload ( connection id and interval)
## Request updates

- Fetch flag calls include the `unleash-interval` header.
- Registration calls now have `connectionId` added to the payload (previously, it only had the `unleash-connection-id` header).

## Metrics updates

- Send metrics calls include the `unleash-interval` header.
- Send metrics now have `connectionId` added to the payload (previously, it only had the `unleash-connection-id` header).


coveralls: 
[![Coverage Status](https://coveralls.io/builds/72535827/badge)](https://coveralls.io/builds/72535827)

coverage: 95.455% (+0.1%) from 95.327%
when pulling **c1eb4b4193dd6f77f5d27ffa772b3926badfca87 on new-headers**
into **9fb8d444e27f39aabe36dc1ab312079b31a8c3fb on main**.


Patch:
diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py
index 4b005e5..6b449e4 100644
--- a/UnleashClient/__init__.py
+++ b/UnleashClient/__init__.py
@@ -110,6 +110,7 @@ class UnleashClient:
         self.unleash_app_name = app_name
         self.unleash_environment = environment
         self.unleash_instance_id = instance_id
+        self._connection_id = str(uuid.uuid4())
         self.unleash_refresh_interval = refresh_interval
         self.unleash_request_timeout = request_timeout
         self.unleash_request_retries = request_retries
@@ -183,6 +184,18 @@ class UnleashClient:
                 engine=self.engine,
             )
 
+    @property
+    def unleash_refresh_interval_str_millis(self) -> str:
+        return str(self.unleash_refresh_interval * 1000)
+
+    @property
+    def unleash_metrics_interval_str_millis(self) -> str:
+        return str(self.unleash_metrics_interval * 1000)
+
+    @property
+    def connection_id(self):
+        return self._connection_id
+
     def initialize_client(self, fetch_toggles: bool = True) -> None:
         """
         Initializes client and starts communication with central unleash server(s).
@@ -213,19 +226,25 @@ class UnleashClient:
         if not self.is_initialized:
             # pylint: disable=no-else-raise
             try:
-                headers = {
+                base_headers = {
                     **self.unleash_custom_headers,
-                    "unleash-connection-id": str(uuid.uuid4()),
+                    "unleash-connection-id": self.connection_id,
                     "unleash-appname": self.unleash_app_name,
                     "unleash-sdk": f"{SDK_NAME}:{SDK_VERSION}",
                 }
 
+                metrics_headers = {
+                    **base_headers,
+                    "unleash-interval": self.unleash_metrics_interval_str_millis,
+                }
+
                 # Setup
                 metrics_args = {
                     "url": self.unleash_url,
                     "app_name": self.unleash_app_name,
+                    "connection_id": self.connection_id,
                     "instance_id": self.unleash_instance_id,
-                    "headers": headers,
+                    "headers": metrics_headers,
                     "custom_options": self.unleash_custom_options,
                     "request_timeout": self.unleash_request_timeout,
                     "engine": self.engine,
@@ -237,19 +256,25 @@ class UnleashClient:
                         self.unleash_url,
                         self.unleash_app_name,
                         self.unleash_instance_id,
+                        self.connection_id,
                         self.unleash_metrics_interval,
-                        headers,
+                        base_headers,
                         self.unleash_custom_options,
                         self.strategy_mapping,
                         self.unleash_request_timeout,
                     )
 
                 if fetch_toggles:
+                    fetch_headers = {
+                        **base_headers,
+                        "unleash-interval": self.unleash_refresh_interval_str_millis,
+                    }
+
                     job_args = {
                         "url": self.unleash_url,
                         "app_name": self.unleash_app_name,
                         "instance_id": self.unleash_instance_id,
-                        "headers": headers,
+                        "headers": fetch_headers,
                         "custom_options": self.unleash_custom_options,
                         "cache": self.cache,
                         "engine": self.engine,
@@ -277,7 +302,6 @@ class UnleashClient:
                     executor=self.unleash_executor_name,
                     kwargs=job_args,
                 )
-
                 if not self.unleash_disable_metrics:
                     self.metric_job = self.unleash_scheduler.add_job(
                         aggregate_and_send_metrics,
diff --git a/UnleashClient/api/register.py b/UnleashClient/api/register.py
index 24c0d6e..1802448 100644
--- a/UnleashClient/api/register.py
+++ b/UnleashClient/api/register.py
@@ -21,6 +21,7 @@ def register_client(
     url: str,
     app_name: str,
     instance_id: str,
+    connection_id: str,
     metrics_interval: int,
     headers: dict,
     custom_options: dict,
@@ -47,6 +48,7 @@ def register_client(
     registration_request = {
         "appName": app_name,
         "instanceId": instance_id,
+        "connectionId": connection_id,
         "sdkVersion": f"{SDK_NAME}:{SDK_VERSION}",
         "strategies": [*supported_strategies],
         "started": datetime.now(timezone.utc).isoformat(),
diff --git a/UnleashClient/periodic_tasks/send_metrics.py b/UnleashClient/periodic_tasks/send_metrics.py
index ef532ca..55fed20 100644
--- a/UnleashClient/periodic_tasks/send_metrics.py
+++ b/UnleashClient/periodic_tasks/send_metrics.py
@@ -12,6 +12,7 @@ def aggregate_and_send_metrics(
     url: str,
     app_name: str,
     instance_id: str,
+    connection_id: str,
     headers: dict,
     custom_options: dict,
     request_timeout: int,
@@ -22,6 +23,7 @@ def aggregate_and_send_metrics(
     metrics_request = {
         "appName": app_name,
         "instanceId": instance_id,
+        "connectionId": connection_id,
         "bucket": metrics_bucket,
         "platformName": python_implementation(),
         "platformVersion": python_version(),


Test patch:
diff --git a/tests/unit_tests/api/test_metrics.py b/tests/unit_tests/api/test_metrics.py
index 2e3d356..35ff98d 100644
--- a/tests/unit_tests/api/test_metrics.py
+++ b/tests/unit_tests/api/test_metrics.py
@@ -1,3 +1,5 @@
+import json
+
 import responses
 from pytest import mark, param
 from requests import ConnectionError
@@ -36,5 +38,9 @@ def test_send_metrics(payload, status, expected):
         URL, MOCK_METRICS_REQUEST, CUSTOM_HEADERS, CUSTOM_OPTIONS, REQUEST_TIMEOUT
     )
 
+    request = json.loads(responses.calls[0].request.body)
+
     assert len(responses.calls) == 1
     assert expected(result)
+
+    assert request["connectionId"] == MOCK_METRICS_REQUEST.get("connectionId")
diff --git a/tests/unit_tests/api/test_register.py b/tests/unit_tests/api/test_register.py
index 3cfc6e6..972f56a 100644
--- a/tests/unit_tests/api/test_register.py
+++ b/tests/unit_tests/api/test_register.py
@@ -6,6 +6,7 @@ from requests import ConnectionError
 
 from tests.utilities.testing_constants import (
     APP_NAME,
+    CONNECTION_ID,
     CUSTOM_HEADERS,
     CUSTOM_OPTIONS,
     INSTANCE_ID,
@@ -40,6 +41,7 @@ def test_register_client(payload, status, expected):
         URL,
         APP_NAME,
         INSTANCE_ID,
+        CONNECTION_ID,
         METRICS_INTERVAL,
         CUSTOM_HEADERS,
         CUSTOM_OPTIONS,
@@ -59,6 +61,7 @@ def test_register_includes_metadata():
         URL,
         APP_NAME,
         INSTANCE_ID,
+        CONNECTION_ID,
         METRICS_INTERVAL,
         CUSTOM_HEADERS,
         CUSTOM_OPTIONS,
@@ -71,5 +74,6 @@ def test_register_includes_metadata():
 
     assert request["yggdrasilVersion"] is not None
     assert request["specVersion"] == CLIENT_SPEC_VERSION
+    assert request["connectionId"] == CONNECTION_ID
     assert request["platformName"] is not None
     assert request["platformVersion"] is not None
diff --git a/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py b/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py
index 8fd4807..8621d94 100644
--- a/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py
+++ b/tests/unit_tests/periodic/test_aggregate_and_send_metrics.py
@@ -5,6 +5,7 @@ from yggdrasil_engine.engine import UnleashEngine
 
 from tests.utilities.testing_constants import (
     APP_NAME,
+    CONNECTION_ID,
     CUSTOM_HEADERS,
     CUSTOM_OPTIONS,
     INSTANCE_ID,
@@ -31,6 +32,7 @@ def test_no_metrics():
         URL,
         APP_NAME,
         INSTANCE_ID,
+        CONNECTION_ID,
         CUSTOM_HEADERS,
         CUSTOM_OPTIONS,
         REQUEST_TIMEOUT,
@@ -51,6 +53,7 @@ def test_metrics_metadata_is_sent():
         URL,
         APP_NAME,
         INSTANCE_ID,
+        CONNECTION_ID,
         CUSTOM_HEADERS,
         CUSTOM_OPTIONS,
         REQUEST_TIMEOUT,
diff --git a/tests/unit_tests/test_client.py b/tests/unit_tests/test_client.py
index c114eb4..807245a 100644
--- a/tests/unit_tests/test_client.py
+++ b/tests/unit_tests/test_client.py
@@ -1,4 +1,6 @@
+import json
 import time
+import uuid
 import warnings
 from datetime import datetime, timezone
 from pathlib import Path
@@ -1078,3 +1080,79 @@ def test_identification_headers_unique_connection_id():
         "UNLEASH-CONNECTION-ID"
     ]
     assert connection_id_first_client != connection_id_second_client
+
+
+@responses.activate
+def test_identification_values_are_passed_in():
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(
+        responses.GET, URL + FEATURES_URL, json=MOCK_FEATURE_RESPONSE, status=200
+    )
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    refresh_interval = 1
+    metrics_interval = 2
+    unleash_client = UnleashClient(
+        URL,
+        APP_NAME,
+        refresh_interval=refresh_interval,
+        metrics_interval=metrics_interval,
+    )
+
+    expected_refresh_interval = str(refresh_interval * 1000)
+    expected_metrics_interval = str(metrics_interval * 1000)
+
+    unleash_client.initialize_client()
+    register_request = responses.calls[0].request
+    register_body = json.loads(register_request.body)
+
+    assert "connectionId" in register_body, "Key missing: connectionId"
+    try:
+        uuid.UUID(register_body["connectionId"])
+    except ValueError:
+        assert False, "Invalid UUID format in connectionId"
+
+    assert (
+        "UNLEASH-CONNECTION-ID" in register_request.headers
+    ), "Header missing: UNLEASH-CONNECTION-ID"
+    try:
+        uuid.UUID(register_request.headers["UNLEASH-CONNECTION-ID"])
+    except ValueError:
+        assert False, "Invalid UUID format in UNLEASH-CONNECTION-ID"
+
+    unleash_client.is_enabled("registerMetricsFlag")
+
+    features_request = responses.calls[1].request
+
+    assert features_request.headers["UNLEASH-INTERVAL"] == expected_refresh_interval
+
+    assert (
+        "UNLEASH-CONNECTION-ID" in features_request.headers
+    ), "Header missing: UNLEASH-CONNECTION-ID"
+
+    try:
+        uuid.UUID(features_request.headers["UNLEASH-CONNECTION-ID"])
+    except ValueError:
+        assert False, "Invalid UUID format in UNLEASH-CONNECTION-ID"
+
+    time.sleep(3)
+    metrics_request = [
+        call for call in responses.calls if METRICS_URL in call.request.url
+    ][0].request
+    metrics_body = json.loads(metrics_request.body)
+
+    assert metrics_request.headers["UNLEASH-INTERVAL"] == expected_metrics_interval
+
+    assert "connectionId" in metrics_body, "Key missing: connectionId"
+    try:
+        uuid.UUID(metrics_body["connectionId"])
+    except ValueError:
+        assert False, "Invalid UUID format in connectionId"
+
+    assert (
+        "UNLEASH-CONNECTION-ID" in metrics_request.headers
+    ), "Header missing: UNLEASH-CONNECTION-ID"
+    try:
+        uuid.UUID(metrics_request.headers["UNLEASH-CONNECTION-ID"])
+    except ValueError:
+        assert False, "Invalid UUID format in UNLEASH-CONNECTION-ID"
diff --git a/tests/utilities/mocks/mock_metrics.py b/tests/utilities/mocks/mock_metrics.py
index ceaa8ab..66ab96e 100644
--- a/tests/utilities/mocks/mock_metrics.py
+++ b/tests/utilities/mocks/mock_metrics.py
@@ -1,6 +1,7 @@
 MOCK_METRICS_REQUEST = {
     "appName": "appName",
     "instanceId": "instanceId",
+    "connectionId": "connectionId",
     "bucket": {
         "start": "2016-11-03T07:16:43.572Z",
         "stop": "2016-11-03T07:16:53.572Z",
diff --git a/tests/utilities/testing_constants.py b/tests/utilities/testing_constants.py
index ca05b6a..1ece2f4 100644
--- a/tests/utilities/testing_constants.py
+++ b/tests/utilities/testing_constants.py
@@ -4,6 +4,7 @@ from UnleashClient.constants import FEATURES_URL
 APP_NAME = "pytest"
 ENVIRONMENT = "unit"
 INSTANCE_ID = "123"
+CONNECTION_ID = "test-connection-id"
 REFRESH_INTERVAL = 3
 REFRESH_JITTER = None
 METRICS_INTERVAL = 2