analytics-platform 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/PKG-INFO +16 -1
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/README.md +15 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/__init__.py +26 -9
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/core.py +1 -1
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/django.py +3 -1
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/errors.py +24 -1
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/proxy.py +10 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform.egg-info/PKG-INFO +16 -1
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/pyproject.toml +1 -1
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/tests/test_errors.py +47 -1
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/tests/test_proxy.py +6 -1
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/LICENSE +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/analytics.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/audit.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/crawlers.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/firewall.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/flags.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/forms.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/inventory.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/maintenance.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/performance.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/posture.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/request_rules.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform/resources.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform.egg-info/SOURCES.txt +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform.egg-info/dependency_links.txt +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform.egg-info/top_level.txt +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/setup.cfg +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/tests/test_crawlers.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/tests/test_firewall.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/tests/test_request_rules.py +0 -0
- {analytics_platform-0.3.0 → analytics_platform-0.3.2}/tests/test_resources.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: analytics-platform
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Firewall, error monitoring and server analytics for your Python server, run from your analytics dashboard.
|
|
5
5
|
Author: steviec87
|
|
6
6
|
License-Expression: MIT
|
|
@@ -168,6 +168,11 @@ a few seconds later.
|
|
|
168
168
|
What still can't be sent waits in a file in the temp folder and is sent when
|
|
169
169
|
it can be, after a restart too; each error keeps the time it happened.
|
|
170
170
|
|
|
171
|
+
An error in a request or job also shows the steps before it. Add them with
|
|
172
|
+
`platform.add_breadcrumb("Order saved")`; a step added outside a request or job
|
|
173
|
+
is dropped. Outgoing calls aren't recorded on their own, since Python has no
|
|
174
|
+
public way to see them.
|
|
175
|
+
|
|
171
176
|
Background jobs, as a decorator or a `with` block (sync or async):
|
|
172
177
|
|
|
173
178
|
```python
|
|
@@ -179,6 +184,16 @@ Their errors carry the job's name as the route, a job that raises is sent
|
|
|
179
184
|
(status 500) and raised on, and each run is timed (Server speed → Jobs).
|
|
180
185
|
Several services on one site: `Platform(..., server_name="worker")`.
|
|
181
186
|
|
|
187
|
+
**Several apps on one site** (microservices, a worker, a Node service next to
|
|
188
|
+
this one): install the package in each, with the same `site_id`, `secret` and
|
|
189
|
+
`endpoint`, and a `server_name` of its own. Errors, Server speed, Crawlers and
|
|
190
|
+
Security → Updates then tell the apps apart. Without one, each app is named
|
|
191
|
+
after its machine: two apps on one machine are reported as one, and only the
|
|
192
|
+
last package list sent is kept. A service only your other apps call takes
|
|
193
|
+
`firewall=False, proxy=False, maintenance=False` (its requests all come from
|
|
194
|
+
your own apps, so the rate limit would see one visitor); on a machine shared
|
|
195
|
+
by several apps, keep `resources` on in only one of them.
|
|
196
|
+
|
|
182
197
|
Options: `server_name`, `root` (traceback paths relative to it),
|
|
183
198
|
`capture_uncaught`, `capture_logging`, `spool_dir` (where unsent errors wait;
|
|
184
199
|
`False` for none), `before_send(event, error)` (change an error, or return
|
|
@@ -142,6 +142,11 @@ a few seconds later.
|
|
|
142
142
|
What still can't be sent waits in a file in the temp folder and is sent when
|
|
143
143
|
it can be, after a restart too; each error keeps the time it happened.
|
|
144
144
|
|
|
145
|
+
An error in a request or job also shows the steps before it. Add them with
|
|
146
|
+
`platform.add_breadcrumb("Order saved")`; a step added outside a request or job
|
|
147
|
+
is dropped. Outgoing calls aren't recorded on their own, since Python has no
|
|
148
|
+
public way to see them.
|
|
149
|
+
|
|
145
150
|
Background jobs, as a decorator or a `with` block (sync or async):
|
|
146
151
|
|
|
147
152
|
```python
|
|
@@ -153,6 +158,16 @@ Their errors carry the job's name as the route, a job that raises is sent
|
|
|
153
158
|
(status 500) and raised on, and each run is timed (Server speed → Jobs).
|
|
154
159
|
Several services on one site: `Platform(..., server_name="worker")`.
|
|
155
160
|
|
|
161
|
+
**Several apps on one site** (microservices, a worker, a Node service next to
|
|
162
|
+
this one): install the package in each, with the same `site_id`, `secret` and
|
|
163
|
+
`endpoint`, and a `server_name` of its own. Errors, Server speed, Crawlers and
|
|
164
|
+
Security → Updates then tell the apps apart. Without one, each app is named
|
|
165
|
+
after its machine: two apps on one machine are reported as one, and only the
|
|
166
|
+
last package list sent is kept. A service only your other apps call takes
|
|
167
|
+
`firewall=False, proxy=False, maintenance=False` (its requests all come from
|
|
168
|
+
your own apps, so the rate limit would see one visitor); on a machine shared
|
|
169
|
+
by several apps, keep `resources` on in only one of them.
|
|
170
|
+
|
|
156
171
|
Options: `server_name`, `root` (traceback paths relative to it),
|
|
157
172
|
`capture_uncaught`, `capture_logging`, `spool_dir` (where unsent errors wait;
|
|
158
173
|
`False` for none), `before_send(event, error)` (change an error, or return
|
|
@@ -18,7 +18,7 @@ import functools
|
|
|
18
18
|
import time
|
|
19
19
|
|
|
20
20
|
from .core import Core, SESSION_HEADER, VERSION, header, request_target
|
|
21
|
-
from .errors import current_request
|
|
21
|
+
from .errors import add_breadcrumb, current_request, current_trail
|
|
22
22
|
|
|
23
23
|
__all__ = ["create_platform", "Platform", "PlatformMiddleware", "VERSION"]
|
|
24
24
|
|
|
@@ -100,6 +100,10 @@ class Platform:
|
|
|
100
100
|
# so its errors, timings and resources can be told apart.
|
|
101
101
|
if server_name and name in ("errors", "performance", "crawlers", "resources"):
|
|
102
102
|
opts.setdefault("server_name", server_name)
|
|
103
|
+
# Inventory and posture call it host, and are stored one per site
|
|
104
|
+
# and host: without it two services on one machine overwrite each other's.
|
|
105
|
+
if server_name and name in ("inventory", "posture"):
|
|
106
|
+
opts.setdefault("host", server_name)
|
|
103
107
|
try:
|
|
104
108
|
self._modules[name] = _load(name, self.core, opts)
|
|
105
109
|
except Exception as error:
|
|
@@ -150,6 +154,12 @@ class Platform:
|
|
|
150
154
|
if errors:
|
|
151
155
|
errors.capture_exception(error, request=request, status=status, kind=kind)
|
|
152
156
|
|
|
157
|
+
def add_breadcrumb(self, text):
|
|
158
|
+
"""A step of your own in the trail shown before an error in this
|
|
159
|
+
request or job."""
|
|
160
|
+
if self._modules.get("errors"):
|
|
161
|
+
add_breadcrumb(text)
|
|
162
|
+
|
|
153
163
|
def job(self, name):
|
|
154
164
|
"""A background job (a queue consumer, a cron task), as a decorator or
|
|
155
165
|
a with block: its errors are sent with the job's name as their route,
|
|
@@ -361,6 +371,7 @@ class Platform:
|
|
|
361
371
|
response_headers = list(response_headers) + platform.security_headers(n for n, _ in response_headers) + remember
|
|
362
372
|
return start_response(status, response_headers, exc_info) if exc_info else start_response(status, response_headers)
|
|
363
373
|
|
|
374
|
+
trail = current_trail.set([])
|
|
364
375
|
try:
|
|
365
376
|
return app(environ, timed_start_response)
|
|
366
377
|
except Exception as error:
|
|
@@ -370,6 +381,7 @@ class Platform:
|
|
|
370
381
|
})
|
|
371
382
|
raise
|
|
372
383
|
finally:
|
|
384
|
+
current_trail.reset(trail)
|
|
373
385
|
if timing:
|
|
374
386
|
platform.time_request(environ.get("REQUEST_METHOD", ""), "", seen["status"], started, environ.get("PATH_INFO", ""))
|
|
375
387
|
|
|
@@ -449,18 +461,21 @@ class _Job:
|
|
|
449
461
|
return {"method": "JOB", "path": self.route, "route": self.route, "session_id": ""}
|
|
450
462
|
|
|
451
463
|
def __enter__(self):
|
|
452
|
-
self._runs.append((time.perf_counter(), current_request.set(self._fields)))
|
|
464
|
+
self._runs.append((time.perf_counter(), current_request.set(self._fields), current_trail.set([])))
|
|
453
465
|
return self
|
|
454
466
|
|
|
455
467
|
def __exit__(self, kind, value, tb):
|
|
456
|
-
started, token = self._runs.pop()
|
|
468
|
+
started, token, trail = self._runs.pop()
|
|
457
469
|
current_request.reset(token)
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
470
|
+
try:
|
|
471
|
+
if value is not None and not isinstance(value, Exception):
|
|
472
|
+
return False # cancelled or interrupted (CancelledError, KeyboardInterrupt): not a run that failed
|
|
473
|
+
if value is not None:
|
|
474
|
+
self.platform.capture_exception(value, status=500, kind="error", request=self._fields())
|
|
475
|
+
self.platform.time_request("JOB", self.route, 500 if value is not None else 200, started, self.route)
|
|
476
|
+
return False
|
|
477
|
+
finally:
|
|
478
|
+
current_trail.reset(trail)
|
|
464
479
|
|
|
465
480
|
async def __aenter__(self):
|
|
466
481
|
return self.__enter__()
|
|
@@ -552,6 +567,7 @@ class PlatformMiddleware:
|
|
|
552
567
|
"method": scope.get("method", ""), "path": scope.get("path", ""),
|
|
553
568
|
"route": getattr(scope.get("route"), "path", ""), "session_id": headers.get(SESSION_HEADER, ""),
|
|
554
569
|
})
|
|
570
|
+
trail = current_trail.set([])
|
|
555
571
|
try:
|
|
556
572
|
await self.app(scope, receive, timed_send)
|
|
557
573
|
except Exception as error:
|
|
@@ -562,6 +578,7 @@ class PlatformMiddleware:
|
|
|
562
578
|
raise
|
|
563
579
|
finally:
|
|
564
580
|
current_request.reset(token)
|
|
581
|
+
current_trail.reset(trail)
|
|
565
582
|
# The router sets scope["route"] (FastAPI, Starlette) on the way in.
|
|
566
583
|
self.platform.time_request(scope.get("method", ""), getattr(scope.get("route"), "path", "") or "",
|
|
567
584
|
seen["status"], started, scope.get("path", ""))
|
|
@@ -16,7 +16,7 @@ from django.conf import settings
|
|
|
16
16
|
from django.http import HttpResponse
|
|
17
17
|
|
|
18
18
|
from . import SESSION_HEADER, Platform
|
|
19
|
-
from .errors import current_request
|
|
19
|
+
from .errors import current_request, current_trail
|
|
20
20
|
|
|
21
21
|
_platform = None
|
|
22
22
|
|
|
@@ -96,10 +96,12 @@ class PlatformMiddleware:
|
|
|
96
96
|
# Errors captured during the request (logged, or caught without the
|
|
97
97
|
# request) still know which request they came from.
|
|
98
98
|
token = current_request.set(lambda: _request_fields(request))
|
|
99
|
+
trail = current_trail.set([])
|
|
99
100
|
try:
|
|
100
101
|
response = self.get_response(request)
|
|
101
102
|
finally:
|
|
102
103
|
current_request.reset(token)
|
|
104
|
+
current_trail.reset(trail)
|
|
103
105
|
for name, value in self.platform.security_headers(response.headers.keys()):
|
|
104
106
|
response[name] = value
|
|
105
107
|
if closed:
|
|
@@ -8,7 +8,8 @@ error is sent once however many of those see it. Errors are sent in small
|
|
|
8
8
|
batches, a batch the collector couldn't take is tried once more, and sending
|
|
9
9
|
never raises. What still can't be sent (the collector is away, or the process
|
|
10
10
|
is crashing) waits in a file and is sent once it can be, after a restart too;
|
|
11
|
-
each error carries when it happened
|
|
11
|
+
each error carries when it happened, and the trail of its request or job: the
|
|
12
|
+
steps you add with add_breadcrumb().
|
|
12
13
|
"""
|
|
13
14
|
import contextvars
|
|
14
15
|
import hashlib
|
|
@@ -44,6 +45,25 @@ SPOOL_BYTES = 1024 * 1024
|
|
|
44
45
|
# returning {"method", "path", "route", "session_id"}, so an error captured
|
|
45
46
|
# without a request (logged, or caught deep in your code) still gets its route.
|
|
46
47
|
current_request = contextvars.ContextVar("analytics_platform_request", default=None)
|
|
48
|
+
# The steps of the request or job being handled, oldest first, set to a new
|
|
49
|
+
# list wherever current_request starts a request. Outside one, steps are dropped.
|
|
50
|
+
current_trail = contextvars.ContextVar("analytics_platform_trail", default=None)
|
|
51
|
+
CRUMB_MAX = 15
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def add_breadcrumb(text):
|
|
55
|
+
trail = current_trail.get()
|
|
56
|
+
if trail is None:
|
|
57
|
+
return
|
|
58
|
+
trail.append({"type": "note", "text": str(text)[:200], "at": time.time()})
|
|
59
|
+
del trail[:-CRUMB_MAX]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _breadcrumbs():
|
|
63
|
+
"""The trail as the tracker sends it: how long before now each step happened."""
|
|
64
|
+
now = time.time()
|
|
65
|
+
return [{"type": crumb["type"], "text": crumb["text"], "ms": max(0, round((now - crumb["at"]) * 1000))}
|
|
66
|
+
for crumb in current_trail.get() or []]
|
|
47
67
|
|
|
48
68
|
|
|
49
69
|
def _request_fields():
|
|
@@ -172,6 +192,9 @@ class Errors:
|
|
|
172
192
|
# When it happened: a crash sent after a restart keeps its own time.
|
|
173
193
|
"at": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
|
|
174
194
|
}
|
|
195
|
+
trail = _breadcrumbs()
|
|
196
|
+
if trail:
|
|
197
|
+
event["breadcrumbs"] = trail
|
|
175
198
|
event.update({k: str(v or "") for k, v in (request or {}).items() if k in ("method", "path", "route", "session_id")})
|
|
176
199
|
if event.get("path"):
|
|
177
200
|
event["path"] = event["path"].split("?")[0]
|
|
@@ -47,6 +47,13 @@ def prefix_for(secret):
|
|
|
47
47
|
return hashlib.sha256(("proxy|" + str(secret)).encode("utf-8")).hexdigest()[:8]
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
# Sent as X-Proxy-Key, so the collector believes the visitor addresses this
|
|
51
|
+
# proxy passes on: anyone can write X-Forwarded-For. Worked out from the secret
|
|
52
|
+
# as the collector does (expresscollect/utils/proxyKey.js).
|
|
53
|
+
def key_for(secret):
|
|
54
|
+
return hashlib.sha256(("proxykey|" + str(secret)).encode("utf-8")).hexdigest()[:32]
|
|
55
|
+
|
|
56
|
+
|
|
50
57
|
class Proxy:
|
|
51
58
|
"""prefix: the path it answers on (default: from the secret)."""
|
|
52
59
|
|
|
@@ -54,6 +61,7 @@ class Proxy:
|
|
|
54
61
|
self.core = core
|
|
55
62
|
self.enabled = enabled
|
|
56
63
|
self.prefix = re.sub(r"[^A-Za-z0-9_-]", "", str(prefix or prefix_for(core.secret)))
|
|
64
|
+
self.key = key_for(core.secret) if core.secret else ""
|
|
57
65
|
# One copy of each tracker file, so a repeat visitor costs no upstream
|
|
58
66
|
# request. A handful of small files: it never grows.
|
|
59
67
|
self._files = {}
|
|
@@ -139,6 +147,8 @@ class Proxy:
|
|
|
139
147
|
# The visitor's address: the collector needs it for the country, the
|
|
140
148
|
# daily visitor ID and the per-visitor rate limit.
|
|
141
149
|
out = {"X-Forwarded-For": client_ip or ""}
|
|
150
|
+
if self.key:
|
|
151
|
+
out["X-Proxy-Key"] = self.key
|
|
142
152
|
for name in PASS_HEADERS:
|
|
143
153
|
value = headers.get(name)
|
|
144
154
|
if value:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: analytics-platform
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Firewall, error monitoring and server analytics for your Python server, run from your analytics dashboard.
|
|
5
5
|
Author: steviec87
|
|
6
6
|
License-Expression: MIT
|
|
@@ -168,6 +168,11 @@ a few seconds later.
|
|
|
168
168
|
What still can't be sent waits in a file in the temp folder and is sent when
|
|
169
169
|
it can be, after a restart too; each error keeps the time it happened.
|
|
170
170
|
|
|
171
|
+
An error in a request or job also shows the steps before it. Add them with
|
|
172
|
+
`platform.add_breadcrumb("Order saved")`; a step added outside a request or job
|
|
173
|
+
is dropped. Outgoing calls aren't recorded on their own, since Python has no
|
|
174
|
+
public way to see them.
|
|
175
|
+
|
|
171
176
|
Background jobs, as a decorator or a `with` block (sync or async):
|
|
172
177
|
|
|
173
178
|
```python
|
|
@@ -179,6 +184,16 @@ Their errors carry the job's name as the route, a job that raises is sent
|
|
|
179
184
|
(status 500) and raised on, and each run is timed (Server speed → Jobs).
|
|
180
185
|
Several services on one site: `Platform(..., server_name="worker")`.
|
|
181
186
|
|
|
187
|
+
**Several apps on one site** (microservices, a worker, a Node service next to
|
|
188
|
+
this one): install the package in each, with the same `site_id`, `secret` and
|
|
189
|
+
`endpoint`, and a `server_name` of its own. Errors, Server speed, Crawlers and
|
|
190
|
+
Security → Updates then tell the apps apart. Without one, each app is named
|
|
191
|
+
after its machine: two apps on one machine are reported as one, and only the
|
|
192
|
+
last package list sent is kept. A service only your other apps call takes
|
|
193
|
+
`firewall=False, proxy=False, maintenance=False` (its requests all come from
|
|
194
|
+
your own apps, so the rate limit would see one visitor); on a machine shared
|
|
195
|
+
by several apps, keep `resources` on in only one of them.
|
|
196
|
+
|
|
182
197
|
Options: `server_name`, `root` (traceback paths relative to it),
|
|
183
198
|
`capture_uncaught`, `capture_logging`, `spool_dir` (where unsent errors wait;
|
|
184
199
|
`False` for none), `before_send(event, error)` (change an error, or return
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "analytics-platform"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Firewall, error monitoring and server analytics for your Python server, run from your analytics dashboard."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -13,7 +13,7 @@ import unittest
|
|
|
13
13
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
14
14
|
|
|
15
15
|
from analytics_platform import errors as mod # noqa: E402
|
|
16
|
-
from analytics_platform.errors import Errors, current_request # noqa: E402
|
|
16
|
+
from analytics_platform.errors import Errors, add_breadcrumb, current_request, current_trail # noqa: E402
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class Answer:
|
|
@@ -249,5 +249,51 @@ class ErrorsTest(unittest.TestCase):
|
|
|
249
249
|
kept = open(errors.spool_file).read().splitlines()
|
|
250
250
|
self.assertEqual([json.loads(line)["message"] for line in kept], ["e10", "e11"], "once, and not what was sent")
|
|
251
251
|
|
|
252
|
+
|
|
253
|
+
class Breadcrumbs(unittest.TestCase):
|
|
254
|
+
def capture(self, errors, message):
|
|
255
|
+
errors.capture_exception(ValueError(message))
|
|
256
|
+
|
|
257
|
+
def test_an_error_carries_the_last_steps_of_its_request(self):
|
|
258
|
+
core = FakeCore()
|
|
259
|
+
errors = Errors(core, capture_uncaught=False)
|
|
260
|
+
add_breadcrumb("outside a request, kept nowhere")
|
|
261
|
+
self.capture(errors, "no trail")
|
|
262
|
+
token = current_trail.set([])
|
|
263
|
+
try:
|
|
264
|
+
for i in range(20):
|
|
265
|
+
add_breadcrumb("step %d" % i)
|
|
266
|
+
self.capture(errors, "after the steps")
|
|
267
|
+
finally:
|
|
268
|
+
current_trail.reset(token)
|
|
269
|
+
self.capture(errors, "no trail either")
|
|
270
|
+
errors.flush()
|
|
271
|
+
first, second, third = core.sent[0]
|
|
272
|
+
self.assertNotIn("breadcrumbs", first)
|
|
273
|
+
self.assertNotIn("breadcrumbs", third)
|
|
274
|
+
crumbs = second["breadcrumbs"]
|
|
275
|
+
self.assertEqual(len(crumbs), 15)
|
|
276
|
+
self.assertEqual(crumbs[0]["text"], "step 5")
|
|
277
|
+
self.assertEqual(crumbs[-1]["type"], "note")
|
|
278
|
+
self.assertNotIn("at", crumbs[0])
|
|
279
|
+
self.assertGreaterEqual(crumbs[0]["ms"], 0)
|
|
280
|
+
|
|
281
|
+
def test_a_job_has_its_own_trail_and_keeps_it_for_the_error_it_raises(self):
|
|
282
|
+
from analytics_platform import create_platform
|
|
283
|
+
platform = create_platform(site_id="a1b2c3d4e5f6a1b2c3d4e5f6", secret="s", endpoint="http://127.0.0.1:9",
|
|
284
|
+
firewall=False, analytics=False, flags=False, inventory=False, posture=False,
|
|
285
|
+
resources=False, proxy=False, maintenance=False, crawlers=False, forms=False,
|
|
286
|
+
audit=False, errors={"capture_uncaught": False, "spool_dir": False})
|
|
287
|
+
seen = []
|
|
288
|
+
platform._modules["errors"].before_send = lambda event, error: seen.append(event) or None
|
|
289
|
+
with self.assertRaises(ValueError):
|
|
290
|
+
with platform.job("send-receipts"):
|
|
291
|
+
platform.add_breadcrumb("Order saved")
|
|
292
|
+
raise ValueError("in a job")
|
|
293
|
+
self.assertEqual([crumb["text"] for crumb in seen[0]["breadcrumbs"]], ["Order saved"])
|
|
294
|
+
self.assertIsNone(current_trail.get(), "the trail ends with the job")
|
|
295
|
+
platform.close()
|
|
296
|
+
|
|
297
|
+
|
|
252
298
|
if __name__ == "__main__":
|
|
253
299
|
unittest.main()
|
|
@@ -12,7 +12,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
12
12
|
|
|
13
13
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
14
14
|
|
|
15
|
-
from analytics_platform.proxy import Proxy, prefix_for # noqa: E402
|
|
15
|
+
from analytics_platform.proxy import Proxy, key_for, prefix_for # noqa: E402
|
|
16
16
|
|
|
17
17
|
TRACKER = b"// a tracker" + b"x" * 600
|
|
18
18
|
|
|
@@ -77,6 +77,10 @@ class ProxyTest(unittest.TestCase):
|
|
|
77
77
|
Collector.seen = []
|
|
78
78
|
self.proxy = Proxy(FakeCore(self.endpoint))
|
|
79
79
|
|
|
80
|
+
def test_the_proxy_key_is_worked_out_as_the_collector_does(self):
|
|
81
|
+
# The same vector is pinned in the collector, the backend and the other packages.
|
|
82
|
+
self.assertEqual(key_for("0123456789abcdef01234567"), "8fc74dbbd4f088449862841cd29fcaee")
|
|
83
|
+
|
|
80
84
|
def test_the_address_comes_from_the_secret(self):
|
|
81
85
|
self.assertEqual(prefix_for("s3cret"), prefix_for("s3cret"))
|
|
82
86
|
self.assertNotEqual(prefix_for("s3cret"), prefix_for("other"))
|
|
@@ -123,6 +127,7 @@ class ProxyTest(unittest.TestCase):
|
|
|
123
127
|
self.assertEqual(got["path"], "/collectanalytics/e?v=1")
|
|
124
128
|
self.assertEqual(got["body"], b'{"event":"pageview"}')
|
|
125
129
|
self.assertEqual(got["headers"].get("X-Forwarded-For"), "198.51.100.4")
|
|
130
|
+
self.assertEqual(got["headers"].get("X-Proxy-Key"), key_for("s3cret"), "so the collector believes the address")
|
|
126
131
|
self.assertEqual(got["headers"].get("User-Agent"), "Mozilla/5.0")
|
|
127
132
|
self.assertIsNone(got["headers"].get("Cookie"), "the site's own cookies stay on the site")
|
|
128
133
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{analytics_platform-0.3.0 → analytics_platform-0.3.2}/analytics_platform.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|