mbuzz 0.7.0__tar.gz → 0.7.3__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.
- mbuzz-0.7.3/CHANGELOG.md +18 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/PKG-INFO +1 -1
- {mbuzz-0.7.0 → mbuzz-0.7.3}/pyproject.toml +1 -1
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/__init__.py +1 -1
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/api.py +1 -1
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/middleware/flask.py +65 -0
- mbuzz-0.7.3/src/mbuzz/utils/__init__.py +6 -0
- mbuzz-0.7.3/src/mbuzz/utils/fingerprint.py +12 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/tests/test_client.py +40 -0
- mbuzz-0.7.3/tests/test_fingerprint.py +33 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/tests/test_middleware.py +205 -1
- mbuzz-0.7.3/uv.lock +1227 -0
- mbuzz-0.7.0/src/mbuzz/utils/__init__.py +0 -5
- {mbuzz-0.7.0 → mbuzz-0.7.3}/.gitignore +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/README.md +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/client/__init__.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/client/conversion.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/client/identify.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/client/track.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/config.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/context.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/cookies.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/middleware/__init__.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/src/mbuzz/utils/identifier.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/tests/__init__.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/tests/test_api.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/tests/test_config.py +0 -0
- {mbuzz-0.7.0 → mbuzz-0.7.3}/tests/test_context.py +0 -0
mbuzz-0.7.3/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.7.3 (2026-02-03)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Navigation-aware session creation** — middleware now only creates server-side sessions for real page navigations, filtering out Turbo frames, htmx partials, fetch/XHR, prefetch, and other sub-requests. Uses browser-enforced `Sec-Fetch-*` headers as the primary signal with a framework-specific blacklist fallback for old browsers.
|
|
8
|
+
- `device_fingerprint()` utility — computes `SHA256(ip|user_agent)[0:32]`, matching the server-side fingerprint for session deduplication.
|
|
9
|
+
- Async session creation via `POST /sessions` — fire-and-forget background thread on real navigations.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **5x visit count inflation** caused by concurrent sub-requests (Turbo frames, htmx) each creating separate sessions on first page load.
|
|
14
|
+
|
|
15
|
+
## 0.7.0 (2026-01-15)
|
|
16
|
+
|
|
17
|
+
- Initial release with Flask middleware, visitor cookie management, event tracking, user identification, and conversion tracking.
|
|
18
|
+
- Session cookie removed — server handles session resolution via device fingerprint.
|
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
"""Flask middleware for mbuzz tracking."""
|
|
2
2
|
# NOTE: Session cookie removed in 0.7.0 - server handles session resolution
|
|
3
3
|
|
|
4
|
+
import threading
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
4
9
|
from flask import Flask, request, g, Response
|
|
5
10
|
|
|
11
|
+
from ..api import post
|
|
6
12
|
from ..config import config
|
|
7
13
|
from ..context import RequestContext, set_context, clear_context
|
|
8
14
|
from ..cookies import VISITOR_COOKIE, VISITOR_MAX_AGE
|
|
15
|
+
from ..utils.fingerprint import device_fingerprint
|
|
9
16
|
from ..utils.identifier import generate_id
|
|
10
17
|
|
|
11
18
|
|
|
19
|
+
def should_create_session() -> bool:
|
|
20
|
+
"""Determine whether this request is a real page navigation.
|
|
21
|
+
|
|
22
|
+
Primary signal: Sec-Fetch-* headers (modern browsers, unforgeable).
|
|
23
|
+
Fallback: blacklist known sub-request framework headers (old browsers/bots).
|
|
24
|
+
"""
|
|
25
|
+
mode = request.headers.get("Sec-Fetch-Mode")
|
|
26
|
+
dest = request.headers.get("Sec-Fetch-Dest")
|
|
27
|
+
|
|
28
|
+
if mode:
|
|
29
|
+
return (
|
|
30
|
+
mode == "navigate"
|
|
31
|
+
and dest == "document"
|
|
32
|
+
and not request.headers.get("Sec-Purpose")
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Fallback for old browsers / bots: blacklist known sub-requests
|
|
36
|
+
return (
|
|
37
|
+
not request.headers.get("Turbo-Frame")
|
|
38
|
+
and not request.headers.get("HX-Request")
|
|
39
|
+
and not request.headers.get("X-Up-Version")
|
|
40
|
+
and request.headers.get("X-Requested-With") != "XMLHttpRequest"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
12
44
|
def init_app(app: Flask) -> None:
|
|
13
45
|
"""Initialize mbuzz tracking for Flask app."""
|
|
14
46
|
|
|
@@ -24,6 +56,11 @@ def init_app(app: Flask) -> None:
|
|
|
24
56
|
_set_request_context(visitor_id, ip, user_agent)
|
|
25
57
|
_store_in_g(visitor_id)
|
|
26
58
|
|
|
59
|
+
if should_create_session():
|
|
60
|
+
_create_session_async(
|
|
61
|
+
visitor_id, request.url, request.referrer, ip, user_agent
|
|
62
|
+
)
|
|
63
|
+
|
|
27
64
|
@app.after_request
|
|
28
65
|
def after_request(response: Response) -> Response:
|
|
29
66
|
if not hasattr(g, "mbuzz_visitor_id"):
|
|
@@ -83,6 +120,34 @@ def _store_in_g(visitor_id: str) -> None:
|
|
|
83
120
|
g.mbuzz_is_new_visitor = VISITOR_COOKIE not in request.cookies
|
|
84
121
|
|
|
85
122
|
|
|
123
|
+
def _create_session_async(
|
|
124
|
+
visitor_id: str,
|
|
125
|
+
url: str,
|
|
126
|
+
referrer: Optional[str],
|
|
127
|
+
ip: str,
|
|
128
|
+
user_agent: str,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Fire-and-forget session creation via background thread.
|
|
131
|
+
|
|
132
|
+
All data is captured before the thread starts — no request-object
|
|
133
|
+
access inside the thread (it would be invalid after the response).
|
|
134
|
+
"""
|
|
135
|
+
payload = {
|
|
136
|
+
"session": {
|
|
137
|
+
"visitor_id": visitor_id,
|
|
138
|
+
"session_id": str(uuid.uuid4()),
|
|
139
|
+
"url": url,
|
|
140
|
+
"referrer": referrer,
|
|
141
|
+
"device_fingerprint": device_fingerprint(ip, user_agent),
|
|
142
|
+
"started_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
threading.Thread(
|
|
147
|
+
target=post, args=("/sessions", payload), daemon=True
|
|
148
|
+
).start()
|
|
149
|
+
|
|
150
|
+
|
|
86
151
|
def _set_cookies(response: Response) -> None:
|
|
87
152
|
"""Set visitor cookie on response."""
|
|
88
153
|
secure = request.is_secure
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Device fingerprint generation — matches server-side SHA256(ip|user_agent)[0:32]."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def device_fingerprint(ip: str, user_agent: str) -> str:
|
|
7
|
+
"""Compute a device fingerprint from IP and User-Agent.
|
|
8
|
+
|
|
9
|
+
Produces a 32-char hex string identical to the server-side computation
|
|
10
|
+
and the Ruby/Node SDKs.
|
|
11
|
+
"""
|
|
12
|
+
return hashlib.sha256(f"{ip}|{user_agent}".encode()).hexdigest()[:32]
|
|
@@ -411,3 +411,43 @@ class TestConversion:
|
|
|
411
411
|
call_args = mock_post.call_args[0]
|
|
412
412
|
payload = call_args[1]
|
|
413
413
|
assert payload["identifier"] == {"email": "test@example.com"}
|
|
414
|
+
|
|
415
|
+
@patch("mbuzz.client.conversion.post_with_response")
|
|
416
|
+
def test_uses_context_ip_and_user_agent(self, mock_post):
|
|
417
|
+
"""Should use ip and user_agent from context if not explicitly provided."""
|
|
418
|
+
mock_post.return_value = {"conversion": {"id": "conv_123"}}
|
|
419
|
+
set_context(RequestContext(
|
|
420
|
+
visitor_id="ctx_vid",
|
|
421
|
+
ip="203.0.113.50",
|
|
422
|
+
user_agent="Safari/17.0",
|
|
423
|
+
))
|
|
424
|
+
|
|
425
|
+
result = conversion(conversion_type="purchase")
|
|
426
|
+
assert result.success is True
|
|
427
|
+
|
|
428
|
+
call_args = mock_post.call_args[0]
|
|
429
|
+
payload = call_args[1]
|
|
430
|
+
assert payload["ip"] == "203.0.113.50"
|
|
431
|
+
assert payload["user_agent"] == "Safari/17.0"
|
|
432
|
+
|
|
433
|
+
@patch("mbuzz.client.conversion.post_with_response")
|
|
434
|
+
def test_explicit_ip_overrides_context(self, mock_post):
|
|
435
|
+
"""Should use explicit ip/user_agent over context."""
|
|
436
|
+
mock_post.return_value = {"conversion": {"id": "conv_123"}}
|
|
437
|
+
set_context(RequestContext(
|
|
438
|
+
visitor_id="ctx_vid",
|
|
439
|
+
ip="context_ip",
|
|
440
|
+
user_agent="context_ua",
|
|
441
|
+
))
|
|
442
|
+
|
|
443
|
+
result = conversion(
|
|
444
|
+
conversion_type="purchase",
|
|
445
|
+
ip="explicit_ip",
|
|
446
|
+
user_agent="explicit_ua"
|
|
447
|
+
)
|
|
448
|
+
assert result.success is True
|
|
449
|
+
|
|
450
|
+
call_args = mock_post.call_args[0]
|
|
451
|
+
payload = call_args[1]
|
|
452
|
+
assert payload["ip"] == "explicit_ip"
|
|
453
|
+
assert payload["user_agent"] == "explicit_ua"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Tests for device fingerprint utility."""
|
|
2
|
+
|
|
3
|
+
from mbuzz.utils.fingerprint import device_fingerprint
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestDeviceFingerprint:
|
|
7
|
+
"""Test device_fingerprint matches server-side SHA256(ip|user_agent)[0:32]."""
|
|
8
|
+
|
|
9
|
+
def test_ruby_parity(self):
|
|
10
|
+
"""Must produce identical output to Ruby: Digest::SHA256.hexdigest('127.0.0.1|Mozilla/5.0')[0,32]."""
|
|
11
|
+
result = device_fingerprint("127.0.0.1", "Mozilla/5.0")
|
|
12
|
+
assert result == "ea687534a507e203bdef87cee3cc60c5"
|
|
13
|
+
|
|
14
|
+
def test_deterministic(self):
|
|
15
|
+
"""Same input must always produce same output."""
|
|
16
|
+
a = device_fingerprint("10.0.0.1", "TestAgent/1.0")
|
|
17
|
+
b = device_fingerprint("10.0.0.1", "TestAgent/1.0")
|
|
18
|
+
assert a == b
|
|
19
|
+
|
|
20
|
+
def test_unique_for_different_inputs(self):
|
|
21
|
+
"""Different inputs must produce different outputs."""
|
|
22
|
+
a = device_fingerprint("10.0.0.1", "Agent-A")
|
|
23
|
+
b = device_fingerprint("10.0.0.2", "Agent-A")
|
|
24
|
+
c = device_fingerprint("10.0.0.1", "Agent-B")
|
|
25
|
+
assert a != b
|
|
26
|
+
assert a != c
|
|
27
|
+
assert b != c
|
|
28
|
+
|
|
29
|
+
def test_returns_32_char_hex(self):
|
|
30
|
+
"""Must return exactly 32 lowercase hex characters."""
|
|
31
|
+
result = device_fingerprint("192.168.1.1", "Chrome/120")
|
|
32
|
+
assert len(result) == 32
|
|
33
|
+
assert all(c in "0123456789abcdef" for c in result)
|
|
@@ -4,7 +4,7 @@ import pytest
|
|
|
4
4
|
from unittest.mock import patch, MagicMock
|
|
5
5
|
from flask import Flask, g
|
|
6
6
|
|
|
7
|
-
from mbuzz.middleware.flask import init_app
|
|
7
|
+
from mbuzz.middleware.flask import init_app, should_create_session
|
|
8
8
|
from mbuzz.config import config
|
|
9
9
|
from mbuzz.context import get_context, clear_context
|
|
10
10
|
from mbuzz.cookies import VISITOR_COOKIE, VISITOR_MAX_AGE
|
|
@@ -342,3 +342,207 @@ class TestFlaskMiddleware:
|
|
|
342
342
|
assert response.status_code == 500
|
|
343
343
|
# Context should still be cleared
|
|
344
344
|
assert get_context() is None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class TestNavigationDetection:
|
|
348
|
+
"""Test should_create_session() — Sec-Fetch-* whitelist + framework blacklist fallback.
|
|
349
|
+
|
|
350
|
+
Mirrors the e2e test at sdk_integration_tests/scenarios/navigation_detection_test.rb.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def setup_method(self):
|
|
354
|
+
config.reset()
|
|
355
|
+
clear_context()
|
|
356
|
+
self.app = Flask(__name__)
|
|
357
|
+
self.app.config["TESTING"] = True
|
|
358
|
+
|
|
359
|
+
@self.app.route("/")
|
|
360
|
+
def index():
|
|
361
|
+
return "OK"
|
|
362
|
+
|
|
363
|
+
def teardown_method(self):
|
|
364
|
+
config.reset()
|
|
365
|
+
clear_context()
|
|
366
|
+
|
|
367
|
+
# -----------------------------------------------------------------
|
|
368
|
+
# Whitelist path: modern browsers with Sec-Fetch-* headers
|
|
369
|
+
# -----------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
def test_real_navigation_returns_true(self):
|
|
372
|
+
"""navigate + document = real page navigation → create session."""
|
|
373
|
+
with self.app.test_request_context(
|
|
374
|
+
"/", headers={
|
|
375
|
+
"Sec-Fetch-Mode": "navigate",
|
|
376
|
+
"Sec-Fetch-Dest": "document",
|
|
377
|
+
}
|
|
378
|
+
):
|
|
379
|
+
assert should_create_session() is True
|
|
380
|
+
|
|
381
|
+
def test_turbo_frame_returns_false(self):
|
|
382
|
+
"""same-origin + empty + Turbo-Frame → sub-request → skip."""
|
|
383
|
+
with self.app.test_request_context(
|
|
384
|
+
"/", headers={
|
|
385
|
+
"Sec-Fetch-Mode": "same-origin",
|
|
386
|
+
"Sec-Fetch-Dest": "empty",
|
|
387
|
+
"Turbo-Frame": "content_frame",
|
|
388
|
+
}
|
|
389
|
+
):
|
|
390
|
+
assert should_create_session() is False
|
|
391
|
+
|
|
392
|
+
def test_htmx_returns_false(self):
|
|
393
|
+
"""same-origin + empty + HX-Request → sub-request → skip."""
|
|
394
|
+
with self.app.test_request_context(
|
|
395
|
+
"/", headers={
|
|
396
|
+
"Sec-Fetch-Mode": "same-origin",
|
|
397
|
+
"Sec-Fetch-Dest": "empty",
|
|
398
|
+
"HX-Request": "true",
|
|
399
|
+
}
|
|
400
|
+
):
|
|
401
|
+
assert should_create_session() is False
|
|
402
|
+
|
|
403
|
+
def test_fetch_xhr_returns_false(self):
|
|
404
|
+
"""cors + empty → fetch/XHR → skip."""
|
|
405
|
+
with self.app.test_request_context(
|
|
406
|
+
"/", headers={
|
|
407
|
+
"Sec-Fetch-Mode": "cors",
|
|
408
|
+
"Sec-Fetch-Dest": "empty",
|
|
409
|
+
}
|
|
410
|
+
):
|
|
411
|
+
assert should_create_session() is False
|
|
412
|
+
|
|
413
|
+
def test_prefetch_returns_false(self):
|
|
414
|
+
"""navigate + document + Sec-Purpose: prefetch → skip."""
|
|
415
|
+
with self.app.test_request_context(
|
|
416
|
+
"/", headers={
|
|
417
|
+
"Sec-Fetch-Mode": "navigate",
|
|
418
|
+
"Sec-Fetch-Dest": "document",
|
|
419
|
+
"Sec-Purpose": "prefetch",
|
|
420
|
+
}
|
|
421
|
+
):
|
|
422
|
+
assert should_create_session() is False
|
|
423
|
+
|
|
424
|
+
def test_iframe_returns_false(self):
|
|
425
|
+
"""navigate + iframe → not a document navigation → skip."""
|
|
426
|
+
with self.app.test_request_context(
|
|
427
|
+
"/", headers={
|
|
428
|
+
"Sec-Fetch-Mode": "navigate",
|
|
429
|
+
"Sec-Fetch-Dest": "iframe",
|
|
430
|
+
}
|
|
431
|
+
):
|
|
432
|
+
assert should_create_session() is False
|
|
433
|
+
|
|
434
|
+
# -----------------------------------------------------------------
|
|
435
|
+
# Blacklist fallback: old browsers without Sec-Fetch-* headers
|
|
436
|
+
# -----------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
def test_old_browser_no_framework_headers_returns_true(self):
|
|
439
|
+
"""No Sec-Fetch, no framework headers → allow (legacy browser)."""
|
|
440
|
+
with self.app.test_request_context("/"):
|
|
441
|
+
assert should_create_session() is True
|
|
442
|
+
|
|
443
|
+
def test_old_browser_turbo_frame_returns_false(self):
|
|
444
|
+
"""No Sec-Fetch + Turbo-Frame → blacklist catches it."""
|
|
445
|
+
with self.app.test_request_context(
|
|
446
|
+
"/", headers={"Turbo-Frame": "lazy_banner"}
|
|
447
|
+
):
|
|
448
|
+
assert should_create_session() is False
|
|
449
|
+
|
|
450
|
+
def test_old_browser_hx_request_returns_false(self):
|
|
451
|
+
"""No Sec-Fetch + HX-Request → blacklist catches it."""
|
|
452
|
+
with self.app.test_request_context(
|
|
453
|
+
"/", headers={"HX-Request": "true"}
|
|
454
|
+
):
|
|
455
|
+
assert should_create_session() is False
|
|
456
|
+
|
|
457
|
+
def test_old_browser_xhr_returns_false(self):
|
|
458
|
+
"""No Sec-Fetch + X-Requested-With: XMLHttpRequest → blacklist catches it."""
|
|
459
|
+
with self.app.test_request_context(
|
|
460
|
+
"/", headers={"X-Requested-With": "XMLHttpRequest"}
|
|
461
|
+
):
|
|
462
|
+
assert should_create_session() is False
|
|
463
|
+
|
|
464
|
+
def test_old_browser_unpoly_returns_false(self):
|
|
465
|
+
"""No Sec-Fetch + X-Up-Version → blacklist catches it."""
|
|
466
|
+
with self.app.test_request_context(
|
|
467
|
+
"/", headers={"X-Up-Version": "3.0.0"}
|
|
468
|
+
):
|
|
469
|
+
assert should_create_session() is False
|
|
470
|
+
|
|
471
|
+
# -----------------------------------------------------------------
|
|
472
|
+
# Middleware integration: session creation + cookie behavior
|
|
473
|
+
# -----------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
@patch("mbuzz.middleware.flask.post")
|
|
476
|
+
def test_navigation_calls_post_sessions(self, mock_post):
|
|
477
|
+
"""Real navigation must fire POST /sessions."""
|
|
478
|
+
config.init(api_key="sk_test_123")
|
|
479
|
+
init_app(self.app)
|
|
480
|
+
|
|
481
|
+
with self.app.test_client() as client:
|
|
482
|
+
client.get("/", headers={
|
|
483
|
+
"Sec-Fetch-Mode": "navigate",
|
|
484
|
+
"Sec-Fetch-Dest": "document",
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
mock_post.assert_called_once()
|
|
488
|
+
args = mock_post.call_args
|
|
489
|
+
assert args[0][0] == "/sessions"
|
|
490
|
+
payload = args[0][1]
|
|
491
|
+
assert "session" in payload
|
|
492
|
+
assert "visitor_id" in payload["session"]
|
|
493
|
+
assert "session_id" in payload["session"]
|
|
494
|
+
assert "device_fingerprint" in payload["session"]
|
|
495
|
+
assert len(payload["session"]["device_fingerprint"]) == 32
|
|
496
|
+
|
|
497
|
+
@patch("mbuzz.middleware.flask.post")
|
|
498
|
+
def test_turbo_frame_does_not_call_post(self, mock_post):
|
|
499
|
+
"""Turbo frame request must NOT fire POST /sessions."""
|
|
500
|
+
config.init(api_key="sk_test_123")
|
|
501
|
+
init_app(self.app)
|
|
502
|
+
|
|
503
|
+
with self.app.test_client() as client:
|
|
504
|
+
client.get("/", headers={
|
|
505
|
+
"Sec-Fetch-Mode": "same-origin",
|
|
506
|
+
"Sec-Fetch-Dest": "empty",
|
|
507
|
+
"Turbo-Frame": "banner",
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
mock_post.assert_not_called()
|
|
511
|
+
|
|
512
|
+
@patch("mbuzz.middleware.flask.post")
|
|
513
|
+
def test_visitor_cookie_set_on_sub_request(self, mock_post):
|
|
514
|
+
"""Visitor cookie must be set even when session creation is skipped."""
|
|
515
|
+
config.init(api_key="sk_test_123")
|
|
516
|
+
init_app(self.app)
|
|
517
|
+
|
|
518
|
+
with self.app.test_client() as client:
|
|
519
|
+
response = client.get("/", headers={
|
|
520
|
+
"Sec-Fetch-Mode": "same-origin",
|
|
521
|
+
"Sec-Fetch-Dest": "empty",
|
|
522
|
+
"Turbo-Frame": "banner",
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
cookies = response.headers.getlist("Set-Cookie")
|
|
526
|
+
visitor_cookie = next(
|
|
527
|
+
(c for c in cookies if VISITOR_COOKIE in c), None
|
|
528
|
+
)
|
|
529
|
+
assert visitor_cookie is not None
|
|
530
|
+
|
|
531
|
+
@patch("mbuzz.middleware.flask.post")
|
|
532
|
+
def test_response_always_succeeds(self, mock_post):
|
|
533
|
+
"""Request completes normally regardless of navigation detection outcome."""
|
|
534
|
+
config.init(api_key="sk_test_123")
|
|
535
|
+
init_app(self.app)
|
|
536
|
+
|
|
537
|
+
with self.app.test_client() as client:
|
|
538
|
+
nav = client.get("/", headers={
|
|
539
|
+
"Sec-Fetch-Mode": "navigate",
|
|
540
|
+
"Sec-Fetch-Dest": "document",
|
|
541
|
+
})
|
|
542
|
+
sub = client.get("/", headers={
|
|
543
|
+
"Sec-Fetch-Mode": "same-origin",
|
|
544
|
+
"Sec-Fetch-Dest": "empty",
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
assert nav.status_code == 200
|
|
548
|
+
assert sub.status_code == 200
|