analytics-platform 0.2.1__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 (30) hide show
  1. analytics_platform-0.2.1/LICENSE +21 -0
  2. analytics_platform-0.2.1/PKG-INFO +227 -0
  3. analytics_platform-0.2.1/README.md +201 -0
  4. analytics_platform-0.2.1/analytics_platform/__init__.py +483 -0
  5. analytics_platform-0.2.1/analytics_platform/analytics.py +101 -0
  6. analytics_platform-0.2.1/analytics_platform/audit.py +53 -0
  7. analytics_platform-0.2.1/analytics_platform/core.py +199 -0
  8. analytics_platform-0.2.1/analytics_platform/crawlers.py +191 -0
  9. analytics_platform-0.2.1/analytics_platform/django.py +112 -0
  10. analytics_platform-0.2.1/analytics_platform/errors.py +113 -0
  11. analytics_platform-0.2.1/analytics_platform/firewall.py +315 -0
  12. analytics_platform-0.2.1/analytics_platform/flags.py +46 -0
  13. analytics_platform-0.2.1/analytics_platform/forms.py +58 -0
  14. analytics_platform-0.2.1/analytics_platform/inventory.py +72 -0
  15. analytics_platform-0.2.1/analytics_platform/maintenance.py +61 -0
  16. analytics_platform-0.2.1/analytics_platform/performance.py +98 -0
  17. analytics_platform-0.2.1/analytics_platform/posture.py +264 -0
  18. analytics_platform-0.2.1/analytics_platform/proxy.py +170 -0
  19. analytics_platform-0.2.1/analytics_platform/request_rules.py +175 -0
  20. analytics_platform-0.2.1/analytics_platform/resources.py +471 -0
  21. analytics_platform-0.2.1/analytics_platform.egg-info/PKG-INFO +227 -0
  22. analytics_platform-0.2.1/analytics_platform.egg-info/SOURCES.txt +28 -0
  23. analytics_platform-0.2.1/analytics_platform.egg-info/dependency_links.txt +1 -0
  24. analytics_platform-0.2.1/analytics_platform.egg-info/top_level.txt +1 -0
  25. analytics_platform-0.2.1/pyproject.toml +46 -0
  26. analytics_platform-0.2.1/setup.cfg +4 -0
  27. analytics_platform-0.2.1/tests/test_crawlers.py +111 -0
  28. analytics_platform-0.2.1/tests/test_proxy.py +141 -0
  29. analytics_platform-0.2.1/tests/test_request_rules.py +78 -0
  30. analytics_platform-0.2.1/tests/test_resources.py +299 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 steviec87
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.4
2
+ Name: analytics-platform
3
+ Version: 0.2.1
4
+ Summary: Firewall, error monitoring and server analytics for your Python server, run from your analytics dashboard.
5
+ Author: steviec87
6
+ License-Expression: MIT
7
+ Keywords: firewall,waf,rate-limit,error-monitoring,analytics,server-monitoring,django,flask,fastapi,wsgi,asgi
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: Flask
17
+ Classifier: Framework :: FastAPI
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Internet :: Log Analysis
20
+ Classifier: Topic :: Security
21
+ Classifier: Topic :: System :: Monitoring
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # analytics-platform (Python)
28
+
29
+ One package for your Python server, run from your analytics dashboard: a
30
+ **firewall**, **error monitoring** and **server analytics** (payments,
31
+ sign-ups, leads). Django, Flask, FastAPI and any WSGI or ASGI app. No
32
+ dependencies; Python 3.8 or later.
33
+
34
+ ```bash
35
+ pip install analytics-platform
36
+ ```
37
+
38
+ ## Django
39
+
40
+ ```python
41
+ # settings.py
42
+ MIDDLEWARE = [
43
+ "analytics_platform.django.PlatformMiddleware", # first
44
+ # ...
45
+ ]
46
+
47
+ ANALYTICS_PLATFORM = {
48
+ "site_id": "YOUR_SITE_ID",
49
+ "secret": os.environ["ANALYTICS_SITE_SECRET"], # site settings; keep it on the server
50
+ "endpoint": "https://YOUR_COLLECTOR_URL",
51
+ # "trust_proxy": 1, # behind Nginx or a load balancer
52
+ # "trust_proxy": "cloudflare", # every request through Cloudflare
53
+ }
54
+ ```
55
+
56
+ In your code: `from analytics_platform.django import get_platform`.
57
+
58
+ ## FastAPI / Starlette
59
+
60
+ ```python
61
+ from analytics_platform import create_platform, PlatformMiddleware
62
+
63
+ platform = create_platform(site_id="YOUR_SITE_ID", secret=os.environ["ANALYTICS_SITE_SECRET"],
64
+ endpoint="https://YOUR_COLLECTOR_URL")
65
+ app.add_middleware(PlatformMiddleware, platform=platform)
66
+ ```
67
+
68
+ ## Flask
69
+
70
+ ```python
71
+ platform = create_platform(site_id="YOUR_SITE_ID", secret=os.environ["ANALYTICS_SITE_SECRET"],
72
+ endpoint="https://YOUR_COLLECTOR_URL")
73
+ platform.init_flask(app)
74
+ ```
75
+
76
+ Other WSGI apps: `app = platform.wsgi(app)`; other ASGI apps: `app = platform.asgi(app)`.
77
+
78
+ ## Modules
79
+
80
+ | Module | What it does | Default |
81
+ |---|---|---|
82
+ | `firewall` | Blocks addresses, known criminal networks, shared blocks and floods (Security → Firewall) | on |
83
+ | `firewall`'s attack patterns | Rules that match the *request* (its path, query string and user agent), not just the address, so an attack is turned away before your code runs — a package with a known hole stays safe until you update it. They come with the firewall's rules and are managed on the Security page (Firewall → Attack patterns); a pattern a real visitor might trip only watches until you promote it. Nothing to configure (protocol: `sdk/protocols/firewall.md`) | with the firewall |
84
+ | `proxy` | Serves the tracker and takes visits under your own domain, so ad blockers can't hide those visitors | on |
85
+ | `errors` | Sends your server's errors to the Errors report | on |
86
+ | `analytics` | `track()`, `identify()` and `lead()` from your server | on |
87
+ | `audit` | `platform.audit("user.role_changed", actor=..., target=..., request=...)`; Django logins and logouts on their own | on |
88
+ | `flags` | `platform.is_enabled("new-checkout", user=user.id)`: features on or off from the dashboard | on |
89
+ | `maintenance` | A "back soon" page while maintenance mode is on in the dashboard | on |
90
+ | `headers` | Adds the security headers you pick on the Security page; your own headers win | on |
91
+ | `forms` | Checks forms before you accept them: `platform.check_form(request)` | on |
92
+ | `performance` | Response times and errors of every route (Health → Server speed): Django, Flask and FastAPI route patterns | on |
93
+ | `crawlers` | Which search engines and AI bots fetch your pages (Health → Crawlers). Django counts them for you; elsewhere call `platform.record_crawler(...)` | on |
94
+ | `inventory` | The installed packages and Python version, once a day, so the Security check can flag known security holes (`{"host": ...}`: the server's name) | on |
95
+ | `posture` | How the server is set up, once a day, for the Security check: Django's `DEBUG`, `SECRET_KEY`, `ALLOWED_HOSTS` and cookie settings (when it's a Django site), whether the app runs as root, code anyone on the machine can write, and files in the folders you serve to the web that shouldn't be public (a dump, a `.env`, a `.bak`). The dashboard then asks your site for each of those files and tells you whether it really is downloadable. Reads only, and sends no file's contents. `{"roots": ["public", {"dir": "uploads", "prefix": "files"}]}` (default: Django's `STATIC_ROOT` and `MEDIA_ROOT`, reported at their `STATIC_URL` / `MEDIA_URL`), `{"checks": fn}` for checks of your own (protocol: `sdk/protocols/posture.md`) | on |
96
+
97
+ Each takes `True`, `False` or its options as a dict:
98
+
99
+ ```python
100
+ create_platform(site_id=..., secret=..., endpoint=...,
101
+ firewall={"mode": "monitor", "cache_file": "/tmp/firewall-rules.json"},
102
+ errors={"capture_uncaught": False},
103
+ analytics=False)
104
+ ```
105
+
106
+ **Switch modules off in the dashboard** (Security → Settings → Server package) without a
107
+ deploy: the package checks about once a minute. Modules are independent: one
108
+ failing to start, or the collector being down, never stops the others, and
109
+ never slows your requests. Background work runs in daemon threads, which start
110
+ again in each worker after a fork (gunicorn, uWSGI).
111
+
112
+ ## Firewall
113
+
114
+ Rules, monitor or block mode and reports are in the dashboard (Security →
115
+ Firewall). New sites start in monitor mode.
116
+
117
+ Options: `mode` (`"monitor"` or `"block"`, overrides the dashboard),
118
+ `cache_file` (keep rules between restarts), `trust_proxy`, `counter`.
119
+
120
+ **The visitor's address:** behind a proxy, every request comes from the proxy
121
+ unless you set `trust_proxy`: `1` for one proxy or load balancer, `2` for
122
+ Cloudflare in front of a load balancer, `"cloudflare"` when every request comes
123
+ through Cloudflare. (Uvicorn with `--proxy-headers` already does this: leave it unset.)
124
+
125
+ **Rate limit with several workers:** each worker counts on its own, so the
126
+ limit is per worker. For one count, pass a shared counter:
127
+
128
+ ```python
129
+ def counter(key, ttl):
130
+ hits = redis.incr(key)
131
+ if hits == 1:
132
+ redis.expire(key, ttl)
133
+ return hits
134
+
135
+ create_platform(..., firewall={"counter": counter})
136
+ ```
137
+
138
+ **Bot protection:** Django's failed logins are counted on their own; elsewhere
139
+ call `platform.login_failed(request)` when a login fails. Past the site's limit
140
+ the address is banned here straight away and on your other servers within a
141
+ minute or two. Floods (over the rate limit) are banned the same way.
142
+
143
+
144
+ **Probes.** Requests for files no visitor ever asks for (`/.env`, `/.git/…`,
145
+ backups: the list comes with the rules) are rule `probe`; in block mode the
146
+ address is also banned on this server, and the collector bans it on the
147
+ site's other servers.
148
+ ## Error monitoring
149
+
150
+ Uncaught exceptions are sent on their own; request errors through the
151
+ middleware (Django, Flask, FastAPI). Errors you catch:
152
+
153
+ ```python
154
+ try:
155
+ charge_card(order)
156
+ except PaymentError as error:
157
+ platform.capture_exception(error, status=502)
158
+ raise
159
+ ```
160
+
161
+ Options: `server_name`, `root` (traceback paths relative to it),
162
+ `capture_uncaught`, `before_send(event, error)` (change an error, or return
163
+ `None` to drop it).
164
+
165
+ ## Server analytics
166
+
167
+ For what only your server knows for sure. Not pageviews: the tracker counts those.
168
+
169
+ ```python
170
+ session = platform.session_id(request) # the visit, from the X-Analytics-Session header
171
+
172
+ platform.track("purchase", {"numeric_value": "49.90", "currency": "EUR", "event_uid": f"order-{order.id}"}, session_id=session)
173
+ platform.identify(user.id, session_id=session)
174
+ platform.lead({"email": "ana@example.com", "name": "Ana", "status": "qualified"})
175
+ ```
176
+
177
+ `track()` sends in the background; `identify()` and `lead()` wait for the
178
+ answer (up to 5 seconds).
179
+
180
+ **Link to the visit:** send the tracker's session ID with your site's requests:
181
+
182
+ ```js
183
+ fetch("/api/orders", { headers: { "x-analytics-session": window.analyticsSession?.getId() || "" }, ... });
184
+ ```
185
+
186
+ ## Ad blockers
187
+
188
+ Many visitors use an ad blocker, and most blockers stop analytics. They
189
+ recognise **the address a script comes from** and **what it is called**, so a
190
+ tracker loaded from us is simply never counted.
191
+
192
+ This package already runs on your own domain, so it serves the tracker and
193
+ takes the visits itself. Nothing to set up: use the two addresses it gives you
194
+ in your page's template.
195
+
196
+ ```python
197
+ platform = get_platform() # Django; elsewhere your own platform
198
+ context = {
199
+ "script_url": platform.proxy.script_url(), # /3f9a2c1b/js/v1/s.js
200
+ "collect_url": platform.proxy.collect_url(), # /3f9a2c1b/api/e
201
+ }
202
+ ```
203
+
204
+ ```html
205
+ <script async src="{{ script_url }}" data-siteid="..." data-domain="example.com"
206
+ data-collect="{{ collect_url }}"></script>
207
+ ```
208
+
209
+ - The path comes from your secret, so it isn't in the page and isn't a name
210
+ any blocker lists. Pick your own with `proxy={"prefix": "..."}`.
211
+ - Tracker files are fetched once and then held in memory; if we can't be
212
+ reached, the copy already held is served rather than nothing.
213
+ - The visitor's address goes on as `X-Forwarded-For` (your collector needs it
214
+ for the country, the daily visitor ID and the rate limit) and cookies never
215
+ leave your server.
216
+ - Beacons are answered before the firewall, the crawler count and the timing,
217
+ so your own analytics traffic doesn't appear as pages of your site. On ASGI
218
+ the send runs in a thread, so the event loop keeps turning.
219
+ - Works through the Django, WSGI (Flask, Bottle, Pyramid) and ASGI (FastAPI,
220
+ Starlette) middleware alike.
221
+ - Switch it off with `proxy=False`, or in the dashboard (Site settings →
222
+ Server). Then load the tracker from us as before.
223
+
224
+ ## Shutting down and serverless
225
+
226
+ `platform.flush()` sends everything waiting (on Lambda, before returning).
227
+ `platform.close()` also stops the background threads.
@@ -0,0 +1,201 @@
1
+ # analytics-platform (Python)
2
+
3
+ One package for your Python server, run from your analytics dashboard: a
4
+ **firewall**, **error monitoring** and **server analytics** (payments,
5
+ sign-ups, leads). Django, Flask, FastAPI and any WSGI or ASGI app. No
6
+ dependencies; Python 3.8 or later.
7
+
8
+ ```bash
9
+ pip install analytics-platform
10
+ ```
11
+
12
+ ## Django
13
+
14
+ ```python
15
+ # settings.py
16
+ MIDDLEWARE = [
17
+ "analytics_platform.django.PlatformMiddleware", # first
18
+ # ...
19
+ ]
20
+
21
+ ANALYTICS_PLATFORM = {
22
+ "site_id": "YOUR_SITE_ID",
23
+ "secret": os.environ["ANALYTICS_SITE_SECRET"], # site settings; keep it on the server
24
+ "endpoint": "https://YOUR_COLLECTOR_URL",
25
+ # "trust_proxy": 1, # behind Nginx or a load balancer
26
+ # "trust_proxy": "cloudflare", # every request through Cloudflare
27
+ }
28
+ ```
29
+
30
+ In your code: `from analytics_platform.django import get_platform`.
31
+
32
+ ## FastAPI / Starlette
33
+
34
+ ```python
35
+ from analytics_platform import create_platform, PlatformMiddleware
36
+
37
+ platform = create_platform(site_id="YOUR_SITE_ID", secret=os.environ["ANALYTICS_SITE_SECRET"],
38
+ endpoint="https://YOUR_COLLECTOR_URL")
39
+ app.add_middleware(PlatformMiddleware, platform=platform)
40
+ ```
41
+
42
+ ## Flask
43
+
44
+ ```python
45
+ platform = create_platform(site_id="YOUR_SITE_ID", secret=os.environ["ANALYTICS_SITE_SECRET"],
46
+ endpoint="https://YOUR_COLLECTOR_URL")
47
+ platform.init_flask(app)
48
+ ```
49
+
50
+ Other WSGI apps: `app = platform.wsgi(app)`; other ASGI apps: `app = platform.asgi(app)`.
51
+
52
+ ## Modules
53
+
54
+ | Module | What it does | Default |
55
+ |---|---|---|
56
+ | `firewall` | Blocks addresses, known criminal networks, shared blocks and floods (Security → Firewall) | on |
57
+ | `firewall`'s attack patterns | Rules that match the *request* (its path, query string and user agent), not just the address, so an attack is turned away before your code runs — a package with a known hole stays safe until you update it. They come with the firewall's rules and are managed on the Security page (Firewall → Attack patterns); a pattern a real visitor might trip only watches until you promote it. Nothing to configure (protocol: `sdk/protocols/firewall.md`) | with the firewall |
58
+ | `proxy` | Serves the tracker and takes visits under your own domain, so ad blockers can't hide those visitors | on |
59
+ | `errors` | Sends your server's errors to the Errors report | on |
60
+ | `analytics` | `track()`, `identify()` and `lead()` from your server | on |
61
+ | `audit` | `platform.audit("user.role_changed", actor=..., target=..., request=...)`; Django logins and logouts on their own | on |
62
+ | `flags` | `platform.is_enabled("new-checkout", user=user.id)`: features on or off from the dashboard | on |
63
+ | `maintenance` | A "back soon" page while maintenance mode is on in the dashboard | on |
64
+ | `headers` | Adds the security headers you pick on the Security page; your own headers win | on |
65
+ | `forms` | Checks forms before you accept them: `platform.check_form(request)` | on |
66
+ | `performance` | Response times and errors of every route (Health → Server speed): Django, Flask and FastAPI route patterns | on |
67
+ | `crawlers` | Which search engines and AI bots fetch your pages (Health → Crawlers). Django counts them for you; elsewhere call `platform.record_crawler(...)` | on |
68
+ | `inventory` | The installed packages and Python version, once a day, so the Security check can flag known security holes (`{"host": ...}`: the server's name) | on |
69
+ | `posture` | How the server is set up, once a day, for the Security check: Django's `DEBUG`, `SECRET_KEY`, `ALLOWED_HOSTS` and cookie settings (when it's a Django site), whether the app runs as root, code anyone on the machine can write, and files in the folders you serve to the web that shouldn't be public (a dump, a `.env`, a `.bak`). The dashboard then asks your site for each of those files and tells you whether it really is downloadable. Reads only, and sends no file's contents. `{"roots": ["public", {"dir": "uploads", "prefix": "files"}]}` (default: Django's `STATIC_ROOT` and `MEDIA_ROOT`, reported at their `STATIC_URL` / `MEDIA_URL`), `{"checks": fn}` for checks of your own (protocol: `sdk/protocols/posture.md`) | on |
70
+
71
+ Each takes `True`, `False` or its options as a dict:
72
+
73
+ ```python
74
+ create_platform(site_id=..., secret=..., endpoint=...,
75
+ firewall={"mode": "monitor", "cache_file": "/tmp/firewall-rules.json"},
76
+ errors={"capture_uncaught": False},
77
+ analytics=False)
78
+ ```
79
+
80
+ **Switch modules off in the dashboard** (Security → Settings → Server package) without a
81
+ deploy: the package checks about once a minute. Modules are independent: one
82
+ failing to start, or the collector being down, never stops the others, and
83
+ never slows your requests. Background work runs in daemon threads, which start
84
+ again in each worker after a fork (gunicorn, uWSGI).
85
+
86
+ ## Firewall
87
+
88
+ Rules, monitor or block mode and reports are in the dashboard (Security →
89
+ Firewall). New sites start in monitor mode.
90
+
91
+ Options: `mode` (`"monitor"` or `"block"`, overrides the dashboard),
92
+ `cache_file` (keep rules between restarts), `trust_proxy`, `counter`.
93
+
94
+ **The visitor's address:** behind a proxy, every request comes from the proxy
95
+ unless you set `trust_proxy`: `1` for one proxy or load balancer, `2` for
96
+ Cloudflare in front of a load balancer, `"cloudflare"` when every request comes
97
+ through Cloudflare. (Uvicorn with `--proxy-headers` already does this: leave it unset.)
98
+
99
+ **Rate limit with several workers:** each worker counts on its own, so the
100
+ limit is per worker. For one count, pass a shared counter:
101
+
102
+ ```python
103
+ def counter(key, ttl):
104
+ hits = redis.incr(key)
105
+ if hits == 1:
106
+ redis.expire(key, ttl)
107
+ return hits
108
+
109
+ create_platform(..., firewall={"counter": counter})
110
+ ```
111
+
112
+ **Bot protection:** Django's failed logins are counted on their own; elsewhere
113
+ call `platform.login_failed(request)` when a login fails. Past the site's limit
114
+ the address is banned here straight away and on your other servers within a
115
+ minute or two. Floods (over the rate limit) are banned the same way.
116
+
117
+
118
+ **Probes.** Requests for files no visitor ever asks for (`/.env`, `/.git/…`,
119
+ backups: the list comes with the rules) are rule `probe`; in block mode the
120
+ address is also banned on this server, and the collector bans it on the
121
+ site's other servers.
122
+ ## Error monitoring
123
+
124
+ Uncaught exceptions are sent on their own; request errors through the
125
+ middleware (Django, Flask, FastAPI). Errors you catch:
126
+
127
+ ```python
128
+ try:
129
+ charge_card(order)
130
+ except PaymentError as error:
131
+ platform.capture_exception(error, status=502)
132
+ raise
133
+ ```
134
+
135
+ Options: `server_name`, `root` (traceback paths relative to it),
136
+ `capture_uncaught`, `before_send(event, error)` (change an error, or return
137
+ `None` to drop it).
138
+
139
+ ## Server analytics
140
+
141
+ For what only your server knows for sure. Not pageviews: the tracker counts those.
142
+
143
+ ```python
144
+ session = platform.session_id(request) # the visit, from the X-Analytics-Session header
145
+
146
+ platform.track("purchase", {"numeric_value": "49.90", "currency": "EUR", "event_uid": f"order-{order.id}"}, session_id=session)
147
+ platform.identify(user.id, session_id=session)
148
+ platform.lead({"email": "ana@example.com", "name": "Ana", "status": "qualified"})
149
+ ```
150
+
151
+ `track()` sends in the background; `identify()` and `lead()` wait for the
152
+ answer (up to 5 seconds).
153
+
154
+ **Link to the visit:** send the tracker's session ID with your site's requests:
155
+
156
+ ```js
157
+ fetch("/api/orders", { headers: { "x-analytics-session": window.analyticsSession?.getId() || "" }, ... });
158
+ ```
159
+
160
+ ## Ad blockers
161
+
162
+ Many visitors use an ad blocker, and most blockers stop analytics. They
163
+ recognise **the address a script comes from** and **what it is called**, so a
164
+ tracker loaded from us is simply never counted.
165
+
166
+ This package already runs on your own domain, so it serves the tracker and
167
+ takes the visits itself. Nothing to set up: use the two addresses it gives you
168
+ in your page's template.
169
+
170
+ ```python
171
+ platform = get_platform() # Django; elsewhere your own platform
172
+ context = {
173
+ "script_url": platform.proxy.script_url(), # /3f9a2c1b/js/v1/s.js
174
+ "collect_url": platform.proxy.collect_url(), # /3f9a2c1b/api/e
175
+ }
176
+ ```
177
+
178
+ ```html
179
+ <script async src="{{ script_url }}" data-siteid="..." data-domain="example.com"
180
+ data-collect="{{ collect_url }}"></script>
181
+ ```
182
+
183
+ - The path comes from your secret, so it isn't in the page and isn't a name
184
+ any blocker lists. Pick your own with `proxy={"prefix": "..."}`.
185
+ - Tracker files are fetched once and then held in memory; if we can't be
186
+ reached, the copy already held is served rather than nothing.
187
+ - The visitor's address goes on as `X-Forwarded-For` (your collector needs it
188
+ for the country, the daily visitor ID and the rate limit) and cookies never
189
+ leave your server.
190
+ - Beacons are answered before the firewall, the crawler count and the timing,
191
+ so your own analytics traffic doesn't appear as pages of your site. On ASGI
192
+ the send runs in a thread, so the event loop keeps turning.
193
+ - Works through the Django, WSGI (Flask, Bottle, Pyramid) and ASGI (FastAPI,
194
+ Starlette) middleware alike.
195
+ - Switch it off with `proxy=False`, or in the dashboard (Site settings →
196
+ Server). Then load the tracker from us as before.
197
+
198
+ ## Shutting down and serverless
199
+
200
+ `platform.flush()` sends everything waiting (on Lambda, before returning).
201
+ `platform.close()` also stops the background threads.