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

Commit 8e2ad9f

Browse files
authored
Merge pull request #19 from apilytics/send-geo-ip
Send user's IP address with metrics
2 parents dbf8336 + 5c29d99 commit 8e2ad9f

7 files changed

Lines changed: 55 additions & 0 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- Send user's IP address with metrics. Used for visualization aggregate geolocation data.
13+
The IP is never stored, and it is never sent to 3rd parties.
14+
1015
## [1.4.0] - 2022-02-20
1116

1217
### Added

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def my_apilytics_middleware(request, get_response):
8282
method=request.method,
8383
request_size=len(request.body),
8484
user_agent=request.headers.get("user-agent"),
85+
ip=request.headers.get("x-forwarded-for", "").split(",")[0].strip(),
8586
) as sender:
8687
response = get_response(request)
8788
sender.set_response_info(

apilytics/core.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ApilyticsSender:
3333
method=request.method,
3434
request_size=len(request.body),
3535
user_agent=request.headers.get("user-agent"),
36+
ip=request.headers.get("x-forwarded-for", "").split(",")[0].strip(),
3637
) as sender:
3738
response = get_response(request)
3839
sender.set_response_info(
@@ -56,6 +57,7 @@ def __init__(
5657
query: Optional[str] = None,
5758
request_size: Optional[int] = None,
5859
user_agent: Optional[str] = None,
60+
ip: Optional[str] = None,
5961
apilytics_integration: Optional[str] = None,
6062
integrated_library: Optional[str] = None,
6163
) -> None:
@@ -70,6 +72,9 @@ def __init__(
7072
An empty string and None are treated equally. Can have an optional "?" at the start.
7173
request_size: Size of the user's HTTP request's body in bytes.
7274
user_agent: Value of the `User-Agent` header from the user's HTTP request.
75+
An empty string and None are treated equally.
76+
ip: User's IP address (used for geolocation, never stored nor sent to 3rd parties).
77+
An empty string and None are treated equally.
7378
apilytics_integration: Name of the Apilytics integration that's calling this,
7479
e.g. "apilytics-python-django". No need to pass this when calling from user code.
7580
integrated_library: Name and version of the integration that this is used in,
@@ -81,6 +86,7 @@ def __init__(
8186
self._query = query
8287
self._request_size = request_size
8388
self._user_agent = user_agent
89+
self._ip = ip
8490

8591
self._response_size: Optional[int] = None
8692
self._status_code: Optional[int] = None
@@ -147,6 +153,7 @@ def _send_metrics(self) -> None:
147153
# Don't send empty strings.
148154
**({"query": self._query} if self._query else {}),
149155
**({"userAgent": self._user_agent} if self._user_agent else {}),
156+
**({"ip": self._ip} if self._ip else {}),
150157
**(
151158
{"statusCode": self._status_code}
152159
if self._status_code is not None

apilytics/django.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def __call__(self, request: django.http.HttpRequest) -> django.http.HttpResponse
4949
method=request.method or "", # Typed as Optional, should never be None.
5050
request_size=int(request.META.get("CONTENT_LENGTH", 0)),
5151
user_agent=request.headers.get("user-agent"),
52+
ip=request.headers.get("x-forwarded-for", "").split(",")[0].strip(),
5253
apilytics_integration="apilytics-python-django",
5354
integrated_library=f"django/{django.__version__}",
5455
) as sender:

apilytics/fastapi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ async def dispatch(
5757
method=request.method,
5858
request_size=len(await request.body()),
5959
user_agent=request.headers.get("user-agent"),
60+
ip=request.headers.get("x-forwarded-for", "").split(",")[0].strip(),
6061
apilytics_integration="apilytics-python-fastapi",
6162
integrated_library=f"fastapi/{fastapi.__version__}",
6263
) as sender:

tests/django/test_django.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,26 @@ def test_middleware_should_send_user_agent(
8787
assert data["userAgent"] == "some agent"
8888

8989

90+
def test_middleware_should_send_ip(
91+
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
92+
) -> None:
93+
response = client.get("/dummy", HTTP_X_FORWARDED_FOR="127.0.0.1")
94+
assert response.status_code == 200
95+
96+
assert mocked_urlopen.call_count == 1
97+
__, call_kwargs = mocked_urlopen.call_args
98+
data = tests.conftest.decode_request_data(call_kwargs["data"])
99+
assert data["ip"] == "127.0.0.1"
100+
101+
response = client.get("/dummy", HTTP_X_FORWARDED_FOR="127.0.0.2,127.0.0.3")
102+
assert response.status_code == 200
103+
104+
assert mocked_urlopen.call_count == 2
105+
__, call_kwargs = mocked_urlopen.call_args
106+
data = tests.conftest.decode_request_data(call_kwargs["data"])
107+
assert data["ip"] == "127.0.0.2"
108+
109+
90110
def test_middleware_should_handle_zero_request_and_response_sizes(
91111
mocked_urlopen: unittest.mock.MagicMock, client: django.test.client.Client
92112
) -> None:

tests/fastapi/test_fastapi.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,26 @@ def test_middleware_should_send_user_agent(
9494
assert data["userAgent"] == "some agent"
9595

9696

97+
def test_middleware_should_send_ip(
98+
mocked_urlopen: unittest.mock.MagicMock,
99+
) -> None:
100+
response = client.get("/dummy", headers={"X-Forwarded-For": "127.0.0.1"})
101+
assert response.status_code == 200
102+
103+
assert mocked_urlopen.call_count == 1
104+
__, call_kwargs = mocked_urlopen.call_args
105+
data = tests.conftest.decode_request_data(call_kwargs["data"])
106+
assert data["ip"] == "127.0.0.1"
107+
108+
response = client.get("/dummy", headers={"X-Forwarded-For": "127.0.0.2,127.0.0.3"})
109+
assert response.status_code == 200
110+
111+
assert mocked_urlopen.call_count == 2
112+
__, call_kwargs = mocked_urlopen.call_args
113+
data = tests.conftest.decode_request_data(call_kwargs["data"])
114+
assert data["ip"] == "127.0.0.2"
115+
116+
97117
def test_middleware_should_handle_zero_request_and_response_sizes(
98118
mocked_urlopen: unittest.mock.MagicMock,
99119
) -> None:

0 commit comments

Comments
 (0)