Skip to content

Commit 2cb8cd4

Browse files
Merge pull request #290 from runpod/user-agent-tracking
User agent tracking
2 parents 65215bd + a99a67d commit 2cb8cd4

7 files changed

Lines changed: 109 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change Log
22

3+
## Release 1.6.1 (TBD)
4+
5+
### Added
6+
7+
- User-Agent for better analytics tracking.
8+
39
## Release 1.6.0 (1/29/24)
410

511
### Fixed

runpod/api/graphql.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,23 @@
88
import requests
99

1010
from runpod import error
11+
from runpod.user_agent import USER_AGENT
1112

1213
HTTP_STATUS_UNAUTHORIZED = 401
1314

15+
1416
def run_graphql_query(query: str) -> Dict[str, Any]:
1517
'''
1618
Run a GraphQL query
1719
'''
1820
from runpod import api_key # pylint: disable=import-outside-toplevel, cyclic-import
1921
url = f"https://api.runpod.io/graphql?api_key={api_key}"
22+
2023
headers = {
2124
"Content-Type": "application/json",
25+
"User-Agent": USER_AGENT,
2226
}
27+
2328
data = json.dumps({"query": query})
2429
response = requests.post(url, headers=headers, data=data, timeout=30)
2530

runpod/serverless/modules/rp_ping.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
import requests
1010
from urllib3.util.retry import Retry
1111

12+
from runpod.version import __version__ as runpod_version
1213
from runpod.serverless.modules.rp_logger import RunPodLogger
13-
from .worker_state import Jobs, WORKER_ID
14-
from ...version import __version__ as runpod_version
14+
from runpod.serverless.modules.worker_state import Jobs, WORKER_ID
15+
1516

1617
log = RunPodLogger()
17-
jobs = Jobs() # Contains the list of jobs that are currently running.
18+
jobs = Jobs() # Contains the list of jobs that are currently running.
1819

1920

2021
class Heartbeat:

runpod/serverless/worker.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import aiohttp
1010

11+
from runpod.user_agent import USER_AGENT
1112
from runpod.serverless.modules import (
1213
rp_logger, rp_local, rp_handler, rp_ping,
1314
rp_scale
@@ -24,7 +25,10 @@
2425

2526
def _get_auth_header() -> Dict[str, str]:
2627
""" Returns the authorization header with the API key. """
27-
return {"Authorization": f"{os.environ.get('RUNPOD_AI_API_KEY')}"}
28+
return {
29+
"Authorization": f"{os.environ.get('RUNPOD_AI_API_KEY')}",
30+
"User-Agent": USER_AGENT
31+
}
2832

2933

3034
def _is_local(config) -> bool:

runpod/user_agent.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
""" User-Agent for RunPod-Python-SDK """
2+
3+
import os
4+
import platform
5+
6+
from runpod.version import __version__ as runpod_version
7+
8+
9+
def construct_user_agent():
10+
""" Constructs the User-Agent string for the RunPod-Python-SDK
11+
12+
Example:
13+
RunPod-Python-SDK/0.1.0 (Linux 5.4.0-54-generic; x86_64) Language/Python 3.8.5
14+
"""
15+
os_info = f"{platform.system()} {platform.release()}; {platform.machine()}"
16+
python_version = platform.python_version()
17+
integration_method = os.getenv('RUNPOD_UA_INTEGRATION')
18+
19+
ua_components = [
20+
f"RunPod-Python-SDK/{runpod_version}",
21+
f"({os_info})",
22+
f"Language/Python {python_version}"
23+
]
24+
25+
if integration_method:
26+
ua_components.append(f"Integration/{integration_method}")
27+
28+
user_agent = " ".join(ua_components)
29+
return user_agent
30+
31+
32+
USER_AGENT = construct_user_agent()

tests/test_serverless/test_worker.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
# pylint: disable=protected-access
33

44
import os
5+
import platform
56
import argparse
67
from unittest.mock import patch, mock_open, Mock, MagicMock
78

89
from unittest import IsolatedAsyncioTestCase
910
import nest_asyncio
1011

1112
import runpod
13+
from runpod._version import __version__ as runpod_version
1214
from runpod.serverless.modules.rp_logger import RunPodLogger
1315
from runpod.serverless import _signal_handler
1416

@@ -34,10 +36,14 @@ def test_get_auth_header(self):
3436
'''
3537
Test _get_auth_header
3638
'''
39+
os_info = f"{platform.system()} {platform.release()}; {platform.machine()}"
3740
with patch("runpod.serverless.worker.os") as mock_os:
3841
mock_os.environ.get.return_value = "test"
3942
assert runpod.serverless.worker._get_auth_header(
40-
) == {'Authorization': 'test'}
43+
) == {
44+
'Authorization': 'test',
45+
'User-Agent': f'RunPod-Python-SDK/{runpod_version} ({os_info}) Language/Python {platform.python_version()}' # pylint: disable=line-too-long
46+
}
4147

4248
def test_is_local(self):
4349
'''

tests/test_user_agent.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
""" Tests for the user_agent module. """
2+
3+
import unittest
4+
from unittest.mock import patch
5+
import os
6+
7+
from runpod import __version__ as runpod_version
8+
from runpod.user_agent import construct_user_agent
9+
10+
11+
class TestConstructUserAgent(unittest.TestCase):
12+
"""Test the construct_user_agent function."""
13+
14+
@patch('runpod.user_agent.platform.system', return_value='Windows')
15+
@patch('runpod.user_agent.platform.release', return_value='10')
16+
@patch('runpod.user_agent.platform.machine', return_value='AMD64')
17+
@patch('runpod.user_agent.platform.python_version', return_value='3.8.10')
18+
def test_user_agent_without_integration(
19+
self, mock_python_version, mock_machine, mock_release, mock_system):
20+
"""Test the User-Agent string without specifying an integration method."""
21+
if 'RUNPOD_UA_INTEGRATION' in os.environ:
22+
del os.environ['RUNPOD_UA_INTEGRATION']
23+
24+
expected_ua = f"RunPod-Python-SDK/{runpod_version} (Windows 10; AMD64) Language/Python 3.8.10" # pylint: disable=line-too-long
25+
self.assertEqual(construct_user_agent(), expected_ua)
26+
27+
assert mock_python_version.called
28+
assert mock_machine.called
29+
assert mock_release.called
30+
assert mock_system.called
31+
32+
@patch('runpod.user_agent.platform.system', return_value='Linux')
33+
@patch('runpod.user_agent.platform.release', return_value='5.4')
34+
@patch('runpod.user_agent.platform.machine', return_value='x86_64')
35+
@patch('runpod.user_agent.platform.python_version', return_value='3.9.5')
36+
@patch.dict(os.environ, {'RUNPOD_UA_INTEGRATION': 'SkyPilot'})
37+
def test_user_agent_with_integration(
38+
self, mock_python_version, mock_machine, mock_release, mock_system):
39+
"""Test the User-Agent string with an integration method specified."""
40+
expected_ua = f"RunPod-Python-SDK/{runpod_version} (Linux 5.4; x86_64) Language/Python 3.9.5 Integration/SkyPilot" # pylint: disable=line-too-long
41+
self.assertEqual(construct_user_agent(), expected_ua)
42+
43+
assert mock_python_version.called
44+
assert mock_machine.called
45+
assert mock_release.called
46+
assert mock_system.called
47+
48+
49+
if __name__ == '__main__':
50+
unittest.main()

0 commit comments

Comments
 (0)