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

Commit 1022f8a

Browse files
authored
Merge pull request #13 from apilytics/user-agent
Send `User-Agent` information with metrics
2 parents e759f01 + 8b531cb commit 1022f8a

8 files changed

Lines changed: 75 additions & 20 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: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@
66
import apilytics
77
import tests.conftest
88

9-
client = django.test.client.Client()
10-
119

1210
def test_middleware_should_call_apilytics_api(
13-
mocked_urlopen: unittest.mock.MagicMock,
11+
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
1412
) -> None:
15-
# Refresh middleware to call `__init__` again making sure `MiddlewareNotUsed`
16-
# is ether called or not called properly.
17-
client.handler.load_middleware()
1813
response = client.get("/")
1914
assert response.status_code == 200
2015

@@ -53,9 +48,8 @@ def test_middleware_should_call_apilytics_api(
5348

5449

5550
def test_middleware_should_send_query_params(
56-
mocked_urlopen: unittest.mock.MagicMock,
51+
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
5752
) -> None:
58-
client.handler.load_middleware()
5953
response = client.post("/dummy/123/path/?param=foo&param2=bar")
6054
assert response.status_code == 201
6155

@@ -71,10 +65,21 @@ def test_middleware_should_send_query_params(
7165
assert isinstance(data["timeMillis"], int)
7266

7367

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+
7480
def test_middleware_should_handle_zero_request_and_response_sizes(
75-
mocked_urlopen: unittest.mock.MagicMock,
81+
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
7682
) -> None:
77-
client.handler.load_middleware()
7883
response = client.post("/empty?some=query", content_type="application/json")
7984
assert response.status_code == 200
8085

@@ -86,9 +91,8 @@ def test_middleware_should_handle_zero_request_and_response_sizes(
8691

8792

8893
def test_middleware_should_handle_non_zero_request_and_response_sizes(
89-
mocked_urlopen: unittest.mock.MagicMock,
94+
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
9095
) -> None:
91-
client.handler.load_middleware()
9296
response = client.post(
9397
"/dummy?some=query", data={"hello": "world"}, content_type="application/json"
9498
)
@@ -102,9 +106,8 @@ def test_middleware_should_handle_non_zero_request_and_response_sizes(
102106

103107

104108
def test_middleware_should_work_with_streaming_response(
105-
mocked_urlopen: unittest.mock.MagicMock,
109+
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
106110
) -> None:
107-
client.handler.load_middleware()
108111
response = client.get("/streaming")
109112
assert response.status_code == 200
110113
# Ignore: The attribute *does* exist on StreamingHTTPResponse.
@@ -130,19 +133,17 @@ def test_middleware_should_work_with_streaming_response(
130133

131134
@django.test.override_settings(APILYTICS_API_KEY=None)
132135
def test_middleware_should_be_disabled_if_api_key_is_unset(
133-
mocked_urlopen: unittest.mock.MagicMock,
136+
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
134137
) -> None:
135-
client.handler.load_middleware()
136138
response = client.get("/")
137139
assert response.status_code == 200
138140

139141
assert mocked_urlopen.call_count == 0
140142

141143

142144
def test_middleware_should_send_data_even_on_errors(
143-
mocked_urlopen: unittest.mock.MagicMock,
145+
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
144146
) -> None:
145-
client.handler.load_middleware()
146147
try:
147148
client.get("/error")
148149
except RuntimeError:

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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,30 @@ def test_middleware_should_not_send_empty_query_params(
8989
assert "query" not in data
9090

9191

92+
def test_apilytics_sender_should_handle_empty_values_correctly(
93+
mocked_urlopen: unittest.mock.MagicMock,
94+
) -> None:
95+
with apilytics.core.ApilyticsSender(
96+
api_key="dummy-key",
97+
path="",
98+
method="",
99+
query="",
100+
request_size=None,
101+
user_agent=None,
102+
apilytics_integration=None,
103+
integrated_library=None,
104+
) as sender:
105+
sender.set_response_info(status_code=None, response_size=None)
106+
107+
assert mocked_urlopen.call_count == 1
108+
__, call_kwargs = mocked_urlopen.call_args
109+
data = tests.conftest.decode_request_data(call_kwargs["data"])
110+
assert data.keys() == {"path", "method", "timeMillis"}
111+
assert data["path"] == ""
112+
assert data["method"] == ""
113+
assert isinstance(data["timeMillis"], int)
114+
115+
92116
@unittest.mock.patch(
93117
"apilytics.core.urllib.request.urlopen",
94118
side_effect=urllib.error.URLError("testing"),

0 commit comments

Comments
 (0)