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.
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/PKG-INFO +9 -2
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/README.md +8 -1
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/__init__.py +2 -2
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/analytics.py +23 -4
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/core.py +1 -1
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform.egg-info/PKG-INFO +9 -2
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/pyproject.toml +1 -1
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_hardening.py +15 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/LICENSE +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/audit.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/calls.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/celery.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/challenge.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/crawlers.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/django.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/dramatiq.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/errors.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/firewall.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/flags.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/forms.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/inventory.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/maintenance.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/performance.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/posture.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/proxy.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/queries.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/queues.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/request_rules.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/resources.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform/rq.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform.egg-info/SOURCES.txt +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform.egg-info/dependency_links.txt +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/analytics_platform.egg-info/top_level.txt +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/setup.cfg +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_calls_queues.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_celery.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_challenge.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_checkin.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_crawlers.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_errors.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_firewall.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_limits.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_page_limits.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_proxy.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_queries.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_redis.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_request_rules.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_resources.py +0 -0
- {analytics_platform-0.3.6 → analytics_platform-0.3.7}/tests/test_rq_dramatiq.py +0 -0
- {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.
|
|
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
|
-
|
|
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=""):
|
|
@@ -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
|
|
@@ -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.
|
|
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
|
|
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
|
|
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.6 → analytics_platform-0.3.7}/analytics_platform.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{analytics_platform-0.3.6 → analytics_platform-0.3.7}/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
|
|
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
|