diff --git a/datadog/api/http_client.py b/datadog/api/http_client.py index fa5be4280..6d1f599d9 100644 --- a/datadog/api/http_client.py +++ b/datadog/api/http_client.py @@ -9,7 +9,9 @@ 2. `urlfetch` 3p module - Google App Engine only """ # stdlib +import copy import logging +import platform import urllib from threading import Lock @@ -32,6 +34,16 @@ log = logging.getLogger('datadog.api') +def _get_user_agent_header(): + from datadog import version + return 'datadogpy/{version} (python {pyver}; os {os}; arch {arch})'.format( + version=version.__version__, + pyver=platform.python_version(), + os=platform.system().lower(), + arch=platform.machine().lower(), + ) + + def _remove_context(exc): """Python3: remove context from chained exceptions to prevent leaking API keys in tracebacks.""" exc.__cause__ = None @@ -79,6 +91,7 @@ def request(cls, method, url, headers, params, data, timeout, proxies, verify, m cls._session = requests.Session() http_adapter = requests.adapters.HTTPAdapter(max_retries=max_retries) cls._session.mount('https://', http_adapter) + cls._session.headers.update({'User-Agent': _get_user_agent_header()}) result = cls._session.request( method, url, @@ -130,12 +143,14 @@ def request(cls, method, url, headers, params, data, timeout, proxies, verify, m url=url, params=urllib.urlencode(params) ) + newheaders = copy.deepcopy(headers) + newheaders['User-Agent'] = _get_user_agent_header() try: result = urlfetch.fetch( url=url_with_params, method=method, - headers=headers, + headers=newheaders, validate_certificate=validate_certificate, deadline=timeout, payload=data, diff --git a/tests/integration/api/cassettes/TestDatadog.test_user_agent.yaml b/tests/integration/api/cassettes/TestDatadog.test_user_agent.yaml new file mode 100644 index 000000000..6990a10a9 --- /dev/null +++ b/tests/integration/api/cassettes/TestDatadog.test_user_agent.yaml @@ -0,0 +1,51 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - datadogpy/0.38.0.dev (python 3.7.7; os darwin; arch x86_64) + method: GET + uri: https://api.datadoghq.com/api/v1/validate + response: + body: + string: '{"valid":true}' + headers: + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Length: + - '14' + Content-Security-Policy: + - frame-ancestors 'self'; report-uri https://api.datadoghq.com/csp-report + Content-Type: + - application/json + DD-POOL: + - dogweb + Date: + - Wed, 01 Jul 2020 17:22:12 GMT + Pragma: + - no-cache + Set-Cookie: + - DD-PSHARD=89; Max-Age=604800; Path=/; expires=Wed, 08-Jul-2020 17:22:12 GMT; + secure; HttpOnly + Strict-Transport-Security: + - max-age=15724800; + X-Content-Type-Options: + - nosniff + X-DD-Debug: + - SaHvyR/hQzhMjBxXmmuM76vwlwfocpgL0LhX3u6R0CFONYqUGm7Xe/7/HyTliTFX + X-DD-VERSION: + - '35.2688709' + X-Frame-Options: + - SAMEORIGIN + status: + code: 200 + message: OK +version: 1 diff --git a/tests/integration/api/test_api.py b/tests/integration/api/test_api.py index 8bce7a605..b69c7a434 100644 --- a/tests/integration/api/test_api.py +++ b/tests/integration/api/test_api.py @@ -4,7 +4,9 @@ # python import datetime import json +import mock import os +import re import time import requests @@ -959,3 +961,8 @@ def test_roles_crud(self, dog): # check if new role is deleted successfully res = dog.Roles.get(role_uuid) assert "errors" in res + + @mock.patch('datadog.api._return_raw_response', True) + def test_user_agent(self, dog): + _, resp = dog.api_client.APIClient.submit('GET', 'validate') + assert re.match(r'^datadogpy\/[^\s]+ \(python [^\s]+; os [^\s]+; arch [^\s]+\)$', resp.request.headers['User-Agent']) diff --git a/tox.ini b/tox.ini index 31e054a44..37ddc062c 100644 --- a/tox.ini +++ b/tox.ini @@ -11,9 +11,9 @@ envlist = passenv = DD_TEST_CLIENT* usedevelop = true deps = - !integration: mock click freezegun + mock pytest pytest-vcr python-dateutil @@ -28,6 +28,7 @@ usedevelop = true deps = click freezegun + mock pytest pytest-vcr python-dateutil