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.
Files changed (50) hide show
  1. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/PKG-INFO +28 -2
  2. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/README.md +27 -1
  3. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/__init__.py +10 -4
  4. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/analytics.py +23 -4
  5. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/core.py +1 -1
  6. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/queries.py +416 -4
  7. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/queues.py +5 -1
  8. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform.egg-info/PKG-INFO +28 -2
  9. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform.egg-info/SOURCES.txt +1 -0
  10. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/pyproject.toml +1 -1
  11. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_hardening.py +15 -0
  12. analytics_platform-0.3.7/tests/test_redis.py +428 -0
  13. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/LICENSE +0 -0
  14. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/audit.py +0 -0
  15. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/calls.py +0 -0
  16. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/celery.py +0 -0
  17. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/challenge.py +0 -0
  18. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/crawlers.py +0 -0
  19. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/django.py +0 -0
  20. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/dramatiq.py +0 -0
  21. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/errors.py +0 -0
  22. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/firewall.py +0 -0
  23. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/flags.py +0 -0
  24. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/forms.py +0 -0
  25. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/inventory.py +0 -0
  26. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/maintenance.py +0 -0
  27. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/performance.py +0 -0
  28. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/posture.py +0 -0
  29. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/proxy.py +0 -0
  30. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/request_rules.py +0 -0
  31. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/resources.py +0 -0
  32. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform/rq.py +0 -0
  33. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform.egg-info/dependency_links.txt +0 -0
  34. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/analytics_platform.egg-info/top_level.txt +0 -0
  35. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/setup.cfg +0 -0
  36. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_calls_queues.py +0 -0
  37. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_celery.py +0 -0
  38. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_challenge.py +0 -0
  39. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_checkin.py +0 -0
  40. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_crawlers.py +0 -0
  41. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_errors.py +0 -0
  42. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_firewall.py +0 -0
  43. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_limits.py +0 -0
  44. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_page_limits.py +0 -0
  45. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_proxy.py +0 -0
  46. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_queries.py +0 -0
  47. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_request_rules.py +0 -0
  48. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_resources.py +0 -0
  49. {analytics_platform-0.3.5 → analytics_platform-0.3.7}/tests/test_rq_dramatiq.py +0 -0
  50. {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.5
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
- return watch_database(connection, db)
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
- res = self.core.send("analytics", "/events/identify", body={"user_id": str(user_id), "session_id": session_id})
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=""):
@@ -23,7 +23,7 @@ import urllib.error
23
23
  import urllib.parse
24
24
  import urllib.request
25
25
 
26
- VERSION = "0.3.5"
26
+ VERSION = "0.3.7"
27
27
  TIMEOUT = 5
28
28
  MIN_SYNC = 30
29
29
  RETRY = 60