Skip to content

Commit be4f716

Browse files
crdantclaude
andcommitted
Use machine fingerprint as cluster ID in telemetry headers
- Add _machine_id property to ReplicatedClient and AsyncReplicatedClient - Initialize _machine_id from get_machine_fingerprint() at client creation - Instance and AsyncInstance now store _machine_id from client - Use _machine_id as X-Replicated-ClusterID in all telemetry requests - Add tests verifying machine_id propagation and header usage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent b604bcd commit be4f716

File tree

4 files changed

+143
-6
lines changed

4 files changed

+143
-6
lines changed

replicated/async_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any, Dict, Optional
22

3+
from .fingerprint import get_machine_fingerprint
34
from .http_client import AsyncHTTPClient
45
from .services import AsyncCustomerService
56
from .state import StateManager
@@ -21,6 +22,7 @@ def __init__(
2122
self.base_url = base_url
2223
self.timeout = timeout
2324
self.state_directory = state_directory
25+
self._machine_id = get_machine_fingerprint()
2426

2527
self.http_client = AsyncHTTPClient(
2628
base_url=base_url,

replicated/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any, Dict, Optional
22

3+
from .fingerprint import get_machine_fingerprint
34
from .http_client import SyncHTTPClient
45
from .services import CustomerService
56
from .state import StateManager
@@ -21,6 +22,7 @@ def __init__(
2122
self.base_url = base_url
2223
self.timeout = timeout
2324
self.state_directory = state_directory
25+
self._machine_id = get_machine_fingerprint()
2426

2527
self.http_client = SyncHTTPClient(
2628
base_url=base_url,

replicated/resources.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def __init__(
6464
self._client = client
6565
self.customer_id = customer_id
6666
self.instance_id = instance_id
67+
self._machine_id = client._machine_id
6768
self._data = kwargs
6869
self._status = "ready"
6970
self._metrics: dict[str, Union[int, float, str]] = {}
@@ -80,7 +81,7 @@ def send_metric(self, name: str, value: Union[int, float, str]) -> None:
8081
headers = {
8182
**self._client._get_auth_headers(),
8283
"X-Replicated-InstanceID": self.instance_id,
83-
"X-Replicated-ClusterID": self.instance_id,
84+
"X-Replicated-ClusterID": self._machine_id,
8485
"X-Replicated-AppStatus": self._status,
8586
}
8687

@@ -148,11 +149,10 @@ def _report_instance(self) -> None:
148149
json.dumps(instance_tags).encode()
149150
).decode()
150151

151-
# cluster_id is same as instance_id for non-K8s environments
152152
headers = {
153153
**self._client._get_auth_headers(),
154154
"X-Replicated-InstanceID": self.instance_id,
155-
"X-Replicated-ClusterID": self.instance_id,
155+
"X-Replicated-ClusterID": self._machine_id,
156156
"X-Replicated-AppStatus": self._status,
157157
"X-Replicated-InstanceTagData": instance_tags_b64,
158158
}
@@ -185,6 +185,7 @@ def __init__(
185185
self._client = client
186186
self.customer_id = customer_id
187187
self.instance_id = instance_id
188+
self._machine_id = client._machine_id
188189
self._data = kwargs
189190
self._status = "ready"
190191
self._metrics: dict[str, Union[int, float, str]] = {}
@@ -201,7 +202,7 @@ async def send_metric(self, name: str, value: Union[int, float, str]) -> None:
201202
headers = {
202203
**self._client._get_auth_headers(),
203204
"X-Replicated-InstanceID": self.instance_id,
204-
"X-Replicated-ClusterID": self.instance_id,
205+
"X-Replicated-ClusterID": self._machine_id,
205206
"X-Replicated-AppStatus": self._status,
206207
}
207208

@@ -269,11 +270,10 @@ async def _report_instance(self) -> None:
269270
json.dumps(instance_tags).encode()
270271
).decode()
271272

272-
# cluster_id is same as instance_id for non-K8s environments
273273
headers = {
274274
**self._client._get_auth_headers(),
275275
"X-Replicated-InstanceID": self.instance_id,
276-
"X-Replicated-ClusterID": self.instance_id,
276+
"X-Replicated-ClusterID": self._machine_id,
277277
"X-Replicated-AppStatus": self._status,
278278
"X-Replicated-InstanceTagData": instance_tags_b64,
279279
}

tests/test_client.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,68 @@ def test_default_state_directory_unchanged(self):
9898
assert "my-app" in state_dir_str
9999
assert "Replicated" in state_dir_str
100100

101+
def test_client_has_machine_id(self):
102+
"""Test that client initializes with a machine_id."""
103+
client = ReplicatedClient(publishable_key="pk_test_123", app_slug="my-app")
104+
assert hasattr(client, "_machine_id")
105+
assert client._machine_id is not None
106+
assert isinstance(client._machine_id, str)
107+
assert len(client._machine_id) == 64 # SHA256 hash
108+
109+
@patch("replicated.http_client.httpx.Client")
110+
def test_instance_has_machine_id_from_client(self, mock_httpx):
111+
"""Test that instances created from client have the client's machine_id."""
112+
from replicated.resources import Instance
113+
114+
mock_response = Mock()
115+
mock_response.is_success = True
116+
mock_response.json.return_value = {
117+
"customer": {
118+
"id": "customer_123",
119+
"email": "[email protected]",
120+
"name": "test user",
121+
"serviceToken": "service_token_123",
122+
"instanceId": "instance_123",
123+
}
124+
}
125+
126+
mock_client = Mock()
127+
mock_client.request.return_value = mock_response
128+
mock_httpx.return_value = mock_client
129+
130+
client = ReplicatedClient(publishable_key="pk_test_123", app_slug="my-app")
131+
customer = client.customer.get_or_create("[email protected]")
132+
instance = customer.get_or_create_instance()
133+
134+
assert isinstance(instance, Instance)
135+
assert hasattr(instance, "_machine_id")
136+
assert instance._machine_id == client._machine_id
137+
138+
@patch("replicated.http_client.httpx.Client")
139+
def test_instance_uses_machine_id_in_headers(self, mock_httpx):
140+
"""Test that instance methods use machine_id as cluster ID in headers."""
141+
from replicated.resources import Instance
142+
143+
mock_response = Mock()
144+
mock_response.is_success = True
145+
mock_response.json.return_value = {}
146+
147+
mock_client = Mock()
148+
mock_client.request.return_value = mock_response
149+
mock_httpx.return_value = mock_client
150+
151+
client = ReplicatedClient(publishable_key="pk_test_123", app_slug="my-app")
152+
instance = Instance(client, "customer_123", "instance_123")
153+
154+
# Send a metric
155+
instance.send_metric("test_metric", 42)
156+
157+
# Verify the request was made with correct headers
158+
call_args = mock_client.request.call_args
159+
headers = call_args[1]["headers"]
160+
assert "X-Replicated-ClusterID" in headers
161+
assert headers["X-Replicated-ClusterID"] == client._machine_id
162+
101163

102164
class TestAsyncReplicatedClient:
103165
@pytest.mark.asyncio
@@ -168,3 +230,74 @@ async def test_default_state_directory_unchanged(self):
168230
state_dir_str = str(client.state_manager._state_dir)
169231
assert "my-app" in state_dir_str
170232
assert "Replicated" in state_dir_str
233+
234+
@pytest.mark.asyncio
235+
async def test_client_has_machine_id(self):
236+
"""Test that async client initializes with a machine_id."""
237+
client = AsyncReplicatedClient(publishable_key="pk_test_123", app_slug="my-app")
238+
assert hasattr(client, "_machine_id")
239+
assert client._machine_id is not None
240+
assert isinstance(client._machine_id, str)
241+
assert len(client._machine_id) == 64 # SHA256 hash
242+
243+
@pytest.mark.asyncio
244+
async def test_instance_has_machine_id_from_client(self):
245+
"""Test that async instances have the client's machine_id."""
246+
from replicated.resources import AsyncInstance
247+
248+
with patch("replicated.http_client.httpx.AsyncClient") as mock_httpx:
249+
mock_response = Mock()
250+
mock_response.is_success = True
251+
mock_response.json.return_value = {
252+
"customer": {
253+
"id": "customer_123",
254+
"email": "[email protected]",
255+
"name": "test user",
256+
"serviceToken": "service_token_123",
257+
"instanceId": "instance_123",
258+
}
259+
}
260+
261+
mock_client = Mock()
262+
mock_client.request.return_value = mock_response
263+
mock_httpx.return_value = mock_client
264+
265+
client = AsyncReplicatedClient(
266+
publishable_key="pk_test_123", app_slug="my-app"
267+
)
268+
customer = await client.customer.get_or_create("[email protected]")
269+
instance = await customer.get_or_create_instance()
270+
271+
assert isinstance(instance, AsyncInstance)
272+
assert hasattr(instance, "_machine_id")
273+
assert instance._machine_id == client._machine_id
274+
275+
@pytest.mark.asyncio
276+
async def test_instance_uses_machine_id_in_headers(self):
277+
"""Test that async instance methods use machine_id as cluster ID in headers."""
278+
from unittest.mock import AsyncMock
279+
280+
from replicated.resources import AsyncInstance
281+
282+
with patch("replicated.http_client.httpx.AsyncClient") as mock_httpx:
283+
mock_response = Mock()
284+
mock_response.is_success = True
285+
mock_response.json.return_value = {}
286+
287+
mock_client = Mock()
288+
mock_client.request = AsyncMock(return_value=mock_response)
289+
mock_httpx.return_value = mock_client
290+
291+
client = AsyncReplicatedClient(
292+
publishable_key="pk_test_123", app_slug="my-app"
293+
)
294+
instance = AsyncInstance(client, "customer_123", "instance_123")
295+
296+
# Send a metric
297+
await instance.send_metric("test_metric", 42)
298+
299+
# Verify the request was made with correct headers
300+
call_args = mock_client.request.call_args
301+
headers = call_args[1]["headers"]
302+
assert "X-Replicated-ClusterID" in headers
303+
assert headers["X-Replicated-ClusterID"] == client._machine_id

0 commit comments

Comments
 (0)