analytics-platform 0.3.6__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.6 → analytics_platform-0.3.7}/PKG-INFO +9 -2
  2. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/README.md +8 -1
  3. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/__init__.py +2 -2
  4. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/analytics.py +23 -4
  5. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/core.py +1 -1
  6. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform.egg-info/PKG-INFO +9 -2
  7. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/pyproject.toml +1 -1
  8. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_hardening.py +15 -0
  9. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/LICENSE +0 -0
  10. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/audit.py +0 -0
  11. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/calls.py +0 -0
  12. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/celery.py +0 -0
  13. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/challenge.py +0 -0
  14. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/crawlers.py +0 -0
  15. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/django.py +0 -0
  16. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/dramatiq.py +0 -0
  17. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/errors.py +0 -0
  18. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/firewall.py +0 -0
  19. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/flags.py +0 -0
  20. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/forms.py +0 -0
  21. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/inventory.py +0 -0
  22. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/maintenance.py +0 -0
  23. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/performance.py +0 -0
  24. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/posture.py +0 -0
  25. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/proxy.py +0 -0
  26. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/queries.py +0 -0
  27. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/queues.py +0 -0
  28. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/request_rules.py +0 -0
  29. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/resources.py +0 -0
  30. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/rq.py +0 -0
  31. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform.egg-info/SOURCES.txt +0 -0
  32. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform.egg-info/dependency_links.txt +0 -0
  33. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform.egg-info/top_level.txt +0 -0
  34. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/setup.cfg +0 -0
  35. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_calls_queues.py +0 -0
  36. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_celery.py +0 -0
  37. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_challenge.py +0 -0
  38. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_checkin.py +0 -0
  39. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_crawlers.py +0 -0
  40. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_errors.py +0 -0
  41. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_firewall.py +0 -0
  42. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_limits.py +0 -0
  43. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_page_limits.py +0 -0
  44. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_proxy.py +0 -0
  45. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_queries.py +0 -0
  46. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_redis.py +0 -0
  47. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_request_rules.py +0 -0
  48. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_resources.py +0 -0
  49. {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_rq_dramatiq.py +0 -0
  50. {analytics_platform-0.3.6 → 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.6
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
@@ -423,7 +423,7 @@ For what only your server knows for sure. Not pageviews: the tracker counts thos
423
423
  session = platform.session_id(request) # the visit, from the X-Analytics-Session header
424
424
 
425
425
  platform.track("purchase", {"numeric_value": "49.90", "currency": "EUR", "event_uid": f"order-{order.id}"}, session_id=session)
426
- 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
427
427
  platform.lead({"email": "ana@example.com", "name": "Ana", "status": "qualified"})
428
428
  ```
429
429
 
@@ -433,6 +433,13 @@ answer (up to 5 seconds). What you pass is taken as JSON when you pass it
433
433
  a set a list, `NaN` null and anything else its `str()`, so one odd value can't
434
434
  fail the batch it is sent in.
435
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
+
436
443
  **Link to the visit:** send the tracker's session ID with your site's requests:
437
444
 
438
445
  ```js
@@ -397,7 +397,7 @@ For what only your server knows for sure. Not pageviews: the tracker counts thos
397
397
  session = platform.session_id(request) # the visit, from the X-Analytics-Session header
398
398
 
399
399
  platform.track("purchase", {"numeric_value": "49.90", "currency": "EUR", "event_uid": f"order-{order.id}"}, session_id=session)
400
- 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
401
401
  platform.lead({"email": "ana@example.com", "name": "Ana", "status": "qualified"})
402
402
  ```
403
403
 
@@ -407,6 +407,13 @@ answer (up to 5 seconds). What you pass is taken as JSON when you pass it
407
407
  a set a list, `NaN` null and anything else its `str()`, so one odd value can't
408
408
  fail the batch it is sent in.
409
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
+
410
417
  **Link to the visit:** send the tracker's session ID with your site's requests:
411
418
 
412
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")
@@ -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.6"
26
+ VERSION = "0.3.7"
27
27
  TIMEOUT = 5
28
28
  MIN_SYNC = 30
29
29
  RETRY = 60
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: analytics-platform
3
- Version: 0.3.6
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
@@ -423,7 +423,7 @@ For what only your server knows for sure. Not pageviews: the tracker counts thos
423
423
  session = platform.session_id(request) # the visit, from the X-Analytics-Session header
424
424
 
425
425
  platform.track("purchase", {"numeric_value": "49.90", "currency": "EUR", "event_uid": f"order-{order.id}"}, session_id=session)
426
- 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
427
427
  platform.lead({"email": "ana@example.com", "name": "Ana", "status": "qualified"})
428
428
  ```
429
429
 
@@ -433,6 +433,13 @@ answer (up to 5 seconds). What you pass is taken as JSON when you pass it
433
433
  a set a list, `NaN` null and anything else its `str()`, so one odd value can't
434
434
  fail the batch it is sent in.
435
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
+
436
443
  **Link to the visit:** send the tracker's session ID with your site's requests:
437
444
 
438
445
  ```js
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "analytics-platform"
7
- version = "0.3.6"
7
+ version = "0.3.7"
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"
@@ -394,6 +394,21 @@ class Payloads(unittest.TestCase):
394
394
  self.assertEqual(event["at"], "2026-11-03")
395
395
  self.assertIsNone(event["bad"])
396
396
 
397
+ def test_identify_passes_scalar_traits_through(self):
398
+ core = Quiet()
399
+ sent = []
400
+ core.send = lambda module, path, body=None, **_: sent.append(body) or mock.Mock(ok=True)
401
+ analytics = Analytics(core)
402
+ self.assertTrue(analytics.identify("u1", "s1"))
403
+ self.assertTrue(analytics.identify("u1", "s1", traits={"plan": "pro", "Seats": 5, "ratio": 0.5, "trial": False,
404
+ "tags": ["a"], "meta": {"a": 1}, "gone": None, "bad": float("nan")}))
405
+ self.assertTrue(analytics.identify("u1", "s1", {"tags": ["a"]}))
406
+ self.assertEqual(sent, [
407
+ {"user_id": "u1", "session_id": "s1"},
408
+ {"user_id": "u1", "session_id": "s1", "traits": {"plan": "pro", "Seats": 5, "ratio": 0.5, "trial": False}},
409
+ {"user_id": "u1", "session_id": "s1"},
410
+ ])
411
+
397
412
  def test_plain_handles_deep_and_odd_values(self):
398
413
  deep = []
399
414
  cur = deep