analytics-platform 0.3.5__tar.gz → 0.3.7__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.5 → analytics_platform-0.3.7}/PKG-INFO +28 -2
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/README.md +27 -1
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/__init__.py +10 -4
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/analytics.py +23 -4
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/core.py +1 -1
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/queries.py +416 -4
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/queues.py +5 -1
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform.egg-info/PKG-INFO +28 -2
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform.egg-info/SOURCES.txt +1 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/pyproject.toml +1 -1
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_hardening.py +15 -0
- analytics_platform-0.3.7/tests/test_redis.py +428 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/LICENSE +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/audit.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/calls.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/celery.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/challenge.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/crawlers.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/django.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/dramatiq.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/errors.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/firewall.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/flags.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/forms.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/inventory.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/maintenance.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/performance.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/posture.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/proxy.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/request_rules.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/resources.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/rq.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform.egg-info/dependency_links.txt +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform.egg-info/top_level.txt +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/setup.cfg +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_calls_queues.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_celery.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_challenge.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_checkin.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_crawlers.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_errors.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_firewall.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_limits.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_page_limits.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_proxy.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_queries.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_request_rules.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_resources.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_rq_dramatiq.py +0 -0
- {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_static_late.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.7
|
|
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
|
|
@@ -317,6 +317,8 @@ read, so no data leaves it.
|
|
|
317
317
|
| pymongo | nothing, but **create the platform before your `MongoClient`**: pymongo only reports to clients made after the package is listening |
|
|
318
318
|
| Any other DB-API driver (psycopg, psycopg2, mysqlclient, PyMySQL, sqlite3...) | `conn = platform.watch_database(psycopg.connect(...))`, and use `conn` as before |
|
|
319
319
|
| ClickHouse: clickhouse-connect, clickhouse-driver | `client = platform.watch_database(clickhouse_connect.get_client(...))` (or a `clickhouse_driver.Client`), and use `client` as before |
|
|
320
|
+
| Redis: redis-py (`Redis`, `RedisCluster`, sync or `redis.asyncio`) | `platform.watch_database(r)`: the client is watched in place, so keep using `r` |
|
|
321
|
+
| Django's cache on Redis (Django's own `RedisCache`, or django-redis) | `platform.watch_database(caches["default"])`, once at start-up (in `AppConfig.ready()`) |
|
|
320
322
|
|
|
321
323
|
A query outside any request or job is counted as `JOB (background)`. Each
|
|
322
324
|
query is counted once: when SQLAlchemy or Django runs it over a watched
|
|
@@ -331,6 +333,23 @@ they answer. clickhouse-driver's `execute()` likewise. The streaming
|
|
|
331
333
|
queries (`query_rows_stream()`, `execute_iter()`) aren't timed. Only the
|
|
332
334
|
client you pass is wrapped; the library itself is left alone.
|
|
333
335
|
|
|
336
|
+
Redis: each command is counted by its name and the shape of its key (`GET
|
|
337
|
+
user:?:cart`); no other argument or value is read. A pipeline or transaction
|
|
338
|
+
is one call (`PIPELINE GET user:?`, `MULTI SET cart:?`), timed as a whole.
|
|
339
|
+
Commands that wait (`BLPOP`, `XREAD BLOCK`, subscribing) aren't timed, and
|
|
340
|
+
nor are this package's own reads of your RQ queues. An error reply counts
|
|
341
|
+
as failed; a nil reply doesn't.
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
r = redis.Redis.from_url(REDIS_URL)
|
|
345
|
+
platform.watch_database(r)
|
|
346
|
+
|
|
347
|
+
# Django (settings: CACHES backend django.core.cache.backends.redis.RedisCache
|
|
348
|
+
# or django_redis.cache.RedisCache), in your AppConfig.ready():
|
|
349
|
+
from django.core.cache import caches
|
|
350
|
+
platform.watch_database(caches["default"])
|
|
351
|
+
```
|
|
352
|
+
|
|
334
353
|
## Outgoing calls
|
|
335
354
|
|
|
336
355
|
Health → Server speed → Calls shows how long each route waited on other
|
|
@@ -404,7 +423,7 @@ For what only your server knows for sure. Not pageviews: the tracker counts thos
|
|
|
404
423
|
session = platform.session_id(request) # the visit, from the X-Analytics-Session header
|
|
405
424
|
|
|
406
425
|
platform.track("purchase", {"numeric_value": "49.90", "currency": "EUR", "event_uid": f"order-{order.id}"}, session_id=session)
|
|
407
|
-
platform.identify(user.id, session_id=session)
|
|
426
|
+
platform.identify(user.id, session_id=session, traits={"plan": "pro", "seats": 5, "trial": False}) # traits are optional
|
|
408
427
|
platform.lead({"email": "ana@example.com", "name": "Ana", "status": "qualified"})
|
|
409
428
|
```
|
|
410
429
|
|
|
@@ -414,6 +433,13 @@ answer (up to 5 seconds). What you pass is taken as JSON when you pass it
|
|
|
414
433
|
a set a list, `NaN` null and anything else its `str()`, so one odd value can't
|
|
415
434
|
fail the batch it is sent in.
|
|
416
435
|
|
|
436
|
+
**User traits:** `identify()` takes optional `traits`, a dict of name to
|
|
437
|
+
text, number or `True`/`False`. Lists, dicts and other values are left out
|
|
438
|
+
(and `traits` is left off when nothing is left). The collector does the rest:
|
|
439
|
+
names are lowercased with spaces and dashes as `_` (up to 40 letters, digits
|
|
440
|
+
and `_`), text is cut at 200 characters, values that look like an email
|
|
441
|
+
address are dropped, and at most 20 are kept.
|
|
442
|
+
|
|
417
443
|
**Link to the visit:** send the tracker's session ID with your site's requests:
|
|
418
444
|
|
|
419
445
|
```js
|
|
@@ -291,6 +291,8 @@ read, so no data leaves it.
|
|
|
291
291
|
| pymongo | nothing, but **create the platform before your `MongoClient`**: pymongo only reports to clients made after the package is listening |
|
|
292
292
|
| Any other DB-API driver (psycopg, psycopg2, mysqlclient, PyMySQL, sqlite3...) | `conn = platform.watch_database(psycopg.connect(...))`, and use `conn` as before |
|
|
293
293
|
| ClickHouse: clickhouse-connect, clickhouse-driver | `client = platform.watch_database(clickhouse_connect.get_client(...))` (or a `clickhouse_driver.Client`), and use `client` as before |
|
|
294
|
+
| Redis: redis-py (`Redis`, `RedisCluster`, sync or `redis.asyncio`) | `platform.watch_database(r)`: the client is watched in place, so keep using `r` |
|
|
295
|
+
| Django's cache on Redis (Django's own `RedisCache`, or django-redis) | `platform.watch_database(caches["default"])`, once at start-up (in `AppConfig.ready()`) |
|
|
294
296
|
|
|
295
297
|
A query outside any request or job is counted as `JOB (background)`. Each
|
|
296
298
|
query is counted once: when SQLAlchemy or Django runs it over a watched
|
|
@@ -305,6 +307,23 @@ they answer. clickhouse-driver's `execute()` likewise. The streaming
|
|
|
305
307
|
queries (`query_rows_stream()`, `execute_iter()`) aren't timed. Only the
|
|
306
308
|
client you pass is wrapped; the library itself is left alone.
|
|
307
309
|
|
|
310
|
+
Redis: each command is counted by its name and the shape of its key (`GET
|
|
311
|
+
user:?:cart`); no other argument or value is read. A pipeline or transaction
|
|
312
|
+
is one call (`PIPELINE GET user:?`, `MULTI SET cart:?`), timed as a whole.
|
|
313
|
+
Commands that wait (`BLPOP`, `XREAD BLOCK`, subscribing) aren't timed, and
|
|
314
|
+
nor are this package's own reads of your RQ queues. An error reply counts
|
|
315
|
+
as failed; a nil reply doesn't.
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
r = redis.Redis.from_url(REDIS_URL)
|
|
319
|
+
platform.watch_database(r)
|
|
320
|
+
|
|
321
|
+
# Django (settings: CACHES backend django.core.cache.backends.redis.RedisCache
|
|
322
|
+
# or django_redis.cache.RedisCache), in your AppConfig.ready():
|
|
323
|
+
from django.core.cache import caches
|
|
324
|
+
platform.watch_database(caches["default"])
|
|
325
|
+
```
|
|
326
|
+
|
|
308
327
|
## Outgoing calls
|
|
309
328
|
|
|
310
329
|
Health → Server speed → Calls shows how long each route waited on other
|
|
@@ -378,7 +397,7 @@ For what only your server knows for sure. Not pageviews: the tracker counts thos
|
|
|
378
397
|
session = platform.session_id(request) # the visit, from the X-Analytics-Session header
|
|
379
398
|
|
|
380
399
|
platform.track("purchase", {"numeric_value": "49.90", "currency": "EUR", "event_uid": f"order-{order.id}"}, session_id=session)
|
|
381
|
-
platform.identify(user.id, session_id=session)
|
|
400
|
+
platform.identify(user.id, session_id=session, traits={"plan": "pro", "seats": 5, "trial": False}) # traits are optional
|
|
382
401
|
platform.lead({"email": "ana@example.com", "name": "Ana", "status": "qualified"})
|
|
383
402
|
```
|
|
384
403
|
|
|
@@ -388,6 +407,13 @@ answer (up to 5 seconds). What you pass is taken as JSON when you pass it
|
|
|
388
407
|
a set a list, `NaN` null and anything else its `str()`, so one odd value can't
|
|
389
408
|
fail the batch it is sent in.
|
|
390
409
|
|
|
410
|
+
**User traits:** `identify()` takes optional `traits`, a dict of name to
|
|
411
|
+
text, number or `True`/`False`. Lists, dicts and other values are left out
|
|
412
|
+
(and `traits` is left off when nothing is left). The collector does the rest:
|
|
413
|
+
names are lowercased with spaces and dashes as `_` (up to 40 letters, digits
|
|
414
|
+
and `_`), text is cut at 200 characters, values that look like an email
|
|
415
|
+
address are dropped, and at most 20 are kept.
|
|
416
|
+
|
|
391
417
|
**Link to the visit:** send the tracker's session ID with your site's requests:
|
|
392
418
|
|
|
393
419
|
```js
|
|
@@ -308,9 +308,9 @@ class Platform:
|
|
|
308
308
|
analytics = self._modules.get("analytics")
|
|
309
309
|
return analytics.track(name, fields, session_id) if analytics else ""
|
|
310
310
|
|
|
311
|
-
def identify(self, user_id, session_id=""):
|
|
311
|
+
def identify(self, user_id, session_id="", traits=None):
|
|
312
312
|
analytics = self._modules.get("analytics")
|
|
313
|
-
return analytics.identify(user_id, session_id) if analytics else False
|
|
313
|
+
return analytics.identify(user_id, session_id, traits) if analytics else False
|
|
314
314
|
|
|
315
315
|
def lead(self, fields, session_id=""):
|
|
316
316
|
analytics = self._modules.get("analytics")
|
|
@@ -342,12 +342,18 @@ class Platform:
|
|
|
342
342
|
sqlite3...) or a ClickHouse client (clickhouse-connect,
|
|
343
343
|
clickhouse-driver) whose queries are timed for Server speed: use what
|
|
344
344
|
it returns in its place. db: "postgresql", "mysql"... when the driver
|
|
345
|
-
doesn't say. Django, SQLAlchemy and pymongo need nothing of this.
|
|
345
|
+
doesn't say. Django, SQLAlchemy and pymongo need nothing of this.
|
|
346
|
+
A redis-py client (Redis, RedisCluster, sync or asyncio) or a Django
|
|
347
|
+
cache on Redis is watched in place and handed back as itself."""
|
|
346
348
|
queries = self._queries()
|
|
347
349
|
if queries is None:
|
|
348
350
|
return connection
|
|
349
351
|
from .queries import watch_database
|
|
350
|
-
|
|
352
|
+
try:
|
|
353
|
+
return watch_database(connection, db)
|
|
354
|
+
except Exception as error:
|
|
355
|
+
self.core.log("performance", "watch_database: its queries aren't timed:", error)
|
|
356
|
+
return connection
|
|
351
357
|
|
|
352
358
|
def watch_http(self, client):
|
|
353
359
|
"""A requests Session, an httpx Client or AsyncClient, or an httpx
|
|
@@ -4,13 +4,15 @@ tracker counts those, and counting them here too would count them twice.
|
|
|
4
4
|
|
|
5
5
|
- track(): events, batched to POST /events/server-events in the background.
|
|
6
6
|
Each gets an event_uid, so a retried batch can be told apart from new events.
|
|
7
|
-
- identify(): links the browser's visit to your user ID (/events/identify)
|
|
7
|
+
- identify(): links the browser's visit to your user ID (/events/identify),
|
|
8
|
+
with optional traits about the user (plan, seats...).
|
|
8
9
|
- lead(): adds or updates a lead (/events/lead).
|
|
9
10
|
identify() and lead() wait for the answer (up to 5 seconds).
|
|
10
11
|
Pass session_id to link an event to the visit: the page sends
|
|
11
12
|
window.analyticsSession.getId() in the X-Analytics-Session header, and the
|
|
12
13
|
framework integrations give you platform.session_id(request).
|
|
13
14
|
"""
|
|
15
|
+
import math
|
|
14
16
|
import threading
|
|
15
17
|
import uuid
|
|
16
18
|
from datetime import datetime, timezone
|
|
@@ -23,6 +25,16 @@ FLUSH_EVERY = 2
|
|
|
23
25
|
RETRY = 15
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def scalar_traits(traits):
|
|
29
|
+
"""The traits that are text, a finite number or True/False; {} when none is."""
|
|
30
|
+
if not isinstance(traits, dict):
|
|
31
|
+
return {}
|
|
32
|
+
return {
|
|
33
|
+
str(name): value for name, value in traits.items()
|
|
34
|
+
if isinstance(value, (str, bool, int)) or (isinstance(value, float) and math.isfinite(value))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
26
38
|
class Analytics:
|
|
27
39
|
def __init__(self, core, **_):
|
|
28
40
|
self.core = core
|
|
@@ -79,11 +91,18 @@ class Analytics:
|
|
|
79
91
|
def _flush_job(self):
|
|
80
92
|
return FLUSH_EVERY if self.flush() else RETRY
|
|
81
93
|
|
|
82
|
-
def identify(self, user_id, session_id=""):
|
|
83
|
-
"""Links the visit to your user ID. True when stored.
|
|
94
|
+
def identify(self, user_id, session_id="", traits=None):
|
|
95
|
+
"""Links the visit to your user ID. True when stored. traits: what you
|
|
96
|
+
know about the user, name -> text, number or True/False
|
|
97
|
+
({"plan": "pro", "seats": 5}); anything else is left out, and the
|
|
98
|
+
collector cleans the names and values."""
|
|
84
99
|
if not user_id or not session_id or not self.core.is_on("analytics"):
|
|
85
100
|
return False
|
|
86
|
-
|
|
101
|
+
body = {"user_id": str(user_id), "session_id": session_id}
|
|
102
|
+
kept = scalar_traits(traits)
|
|
103
|
+
if kept:
|
|
104
|
+
body["traits"] = kept
|
|
105
|
+
res = self.core.send("analytics", "/events/identify", body=body)
|
|
87
106
|
return bool(res and res.ok)
|
|
88
107
|
|
|
89
108
|
def lead(self, fields, session_id=""):
|