Skip to content
This repository was archived by the owner on Sep 2, 2022. It is now read-only.

Commit 8b531cb

Browse files
committed
Send User-Agent information with metrics
1 parent b2d3332 commit 8b531cb

8 files changed

Lines changed: 45 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Send `User-Agent` information with metrics.
13+
1014
## [1.2.1] - 2022-02-01
1115

1216
### Fixed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def my_apilytics_middleware(request, get_response):
8181
query=request.query_string,
8282
method=request.method,
8383
request_size=len(request.body),
84+
user_agent=request.headers.get("user-agent"),
8485
) as sender:
8586
response = get_response(request)
8687
sender.set_response_info(

apilytics/core.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class ApilyticsSender:
3030
query=request.query_string,
3131
method=request.method,
3232
request_size=len(request.body),
33+
user_agent=request.headers.get("user-agent"),
3334
) as sender:
3435
response = get_response(request)
3536
sender.set_response_info(
@@ -52,6 +53,7 @@ def __init__(
5253
method: str,
5354
query: Optional[str] = None,
5455
request_size: Optional[int] = None,
56+
user_agent: Optional[str] = None,
5557
apilytics_integration: Optional[str] = None,
5658
integrated_library: Optional[str] = None,
5759
) -> None:
@@ -65,6 +67,7 @@ def __init__(
6567
query: Optional query string of the user's HTTP request e.g. "key=val&other=123".
6668
An empty string and None are treated equally. Can have an optional "?" at the start.
6769
request_size: Size of the user's HTTP request's body in bytes.
70+
user_agent: Value of the `User-Agent` header from the user's HTTP request.
6871
apilytics_integration: Name of the Apilytics integration that's calling this,
6972
e.g. "apilytics-python-django". No need to pass this when calling from user code.
7073
integrated_library: Name and version of the integration that this is used in,
@@ -75,6 +78,7 @@ def __init__(
7578
self._method = method
7679
self._query = query
7780
self._request_size = request_size
81+
self._user_agent = user_agent
7882

7983
self._response_size: Optional[int] = None
8084
self._status_code: Optional[int] = None
@@ -136,7 +140,9 @@ def _send_metrics(self) -> None:
136140
"path": self._path,
137141
"method": self._method,
138142
"timeMillis": (self._end_time_ns - self._start_time_ns) // 1_000_000,
139-
**({"query": self._query} if self._query else {}), # Don't send empty str.
143+
# Don't send empty strings.
144+
**({"query": self._query} if self._query else {}),
145+
**({"userAgent": self._user_agent} if self._user_agent else {}),
140146
**(
141147
{"statusCode": self._status_code}
142148
if self._status_code is not None

apilytics/django.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def __call__(self, request: django.http.HttpRequest) -> django.http.HttpResponse
4848
query=request.META.get("QUERY_STRING"),
4949
method=request.method or "", # Typed as Optional, should never be None.
5050
request_size=int(request.META.get("CONTENT_LENGTH", 0)),
51+
user_agent=request.headers.get("user-agent"),
5152
apilytics_integration="apilytics-python-django",
5253
integrated_library=f"django/{django.__version__}",
5354
) as sender:

apilytics/fastapi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ async def dispatch(
5656
query=request.url.query,
5757
method=request.method,
5858
request_size=len(await request.body()),
59+
user_agent=request.headers.get("user-agent"),
5960
apilytics_integration="apilytics-python-fastapi",
6061
integrated_library=f"fastapi/{fastapi.__version__}",
6162
) as sender:

tests/django/test_django.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ def test_middleware_should_send_query_params(
6565
assert isinstance(data["timeMillis"], int)
6666

6767

68+
def test_middleware_should_send_user_agent(
69+
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
70+
) -> None:
71+
response = client.get("/dummy", HTTP_USER_AGENT="some agent")
72+
assert response.status_code == 200
73+
74+
assert mocked_urlopen.call_count == 1
75+
__, call_kwargs = mocked_urlopen.call_args
76+
data = tests.conftest.decode_request_data(call_kwargs["data"])
77+
assert data["userAgent"] == "some agent"
78+
79+
6880
def test_middleware_should_handle_zero_request_and_response_sizes(
6981
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
7082
) -> None:

tests/fastapi/test_fastapi.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ def test_middleware_should_call_apilytics_api(
4242
"statusCode",
4343
"requestSize",
4444
"responseSize",
45+
"userAgent",
4546
"timeMillis",
4647
}
4748
assert data["path"] == "/"
4849
assert data["method"] == "GET"
4950
assert data["statusCode"] == 200
5051
assert data["requestSize"] == 0
5152
assert data["responseSize"] > 0
53+
assert data["userAgent"] == "testclient"
5254
assert isinstance(data["timeMillis"], int)
5355

5456

@@ -70,6 +72,18 @@ def test_middleware_should_send_query_params(
7072
assert isinstance(data["timeMillis"], int)
7173

7274

75+
def test_middleware_should_send_user_agent(
76+
mocked_urlopen: unittest.mock.MagicMock,
77+
) -> None:
78+
response = client.get("/dummy", headers={"User-Agent": "some agent"})
79+
assert response.status_code == 200
80+
81+
assert mocked_urlopen.call_count == 1
82+
__, call_kwargs = mocked_urlopen.call_args
83+
data = tests.conftest.decode_request_data(call_kwargs["data"])
84+
assert data["userAgent"] == "some agent"
85+
86+
7387
def test_middleware_should_handle_zero_request_and_response_sizes(
7488
mocked_urlopen: unittest.mock.MagicMock,
7589
) -> None:
@@ -112,12 +126,14 @@ def test_middleware_should_work_with_streaming_response(
112126
"method",
113127
"statusCode",
114128
"requestSize",
129+
"userAgent",
115130
"timeMillis",
116131
}
117132
assert data["path"] == "/streaming"
118133
assert data["method"] == "GET"
119134
assert data["statusCode"] == 200
120135
assert data["requestSize"] == 0
136+
assert data["userAgent"] == "testclient"
121137
assert isinstance(data["timeMillis"], int)
122138

123139

@@ -150,8 +166,9 @@ def test_middleware_should_send_data_even_on_errors(
150166

151167
__, call_kwargs = mocked_urlopen.call_args
152168
data = tests.conftest.decode_request_data(call_kwargs["data"])
153-
assert data.keys() == {"method", "path", "timeMillis", "requestSize"}
169+
assert data.keys() == {"method", "path", "timeMillis", "userAgent", "requestSize"}
154170
assert data["method"] == "GET"
155171
assert data["path"] == "/error"
156172
assert data["requestSize"] == 0
173+
assert data["userAgent"] == "testclient"
157174
assert isinstance(data["timeMillis"], int)

tests/test_core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def test_apilytics_sender_should_handle_empty_values_correctly(
9898
method="",
9999
query="",
100100
request_size=None,
101+
user_agent=None,
101102
apilytics_integration=None,
102103
integrated_library=None,
103104
) as sender:

0 commit comments

Comments
 (0)