mbuzz 0.1.0__tar.gz → 0.2.0__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.1.0 → mbuzz-0.2.0}/PKG-INFO +1 -1
- {mbuzz-0.1.0 → mbuzz-0.2.0}/pyproject.toml +1 -1
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/__init__.py +1 -1
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/client/track.py +25 -12
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/context.py +2 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/middleware/flask.py +24 -2
- mbuzz-0.2.0/src/mbuzz/utils/session_id.py +39 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/tests/test_client.py +94 -0
- mbuzz-0.2.0/tests/test_session_id.py +146 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/.gitignore +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/README.md +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/api.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/client/__init__.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/client/conversion.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/client/identify.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/client/session.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/config.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/cookies.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/middleware/__init__.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/utils/__init__.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/src/mbuzz/utils/identifier.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/tests/__init__.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/tests/test_api.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/tests/test_config.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/tests/test_context.py +0 -0
- {mbuzz-0.1.0 → mbuzz-0.2.0}/tests/test_middleware.py +0 -0
|
@@ -28,10 +28,12 @@ class TrackOptions:
|
|
|
28
28
|
session_id: Optional[str] = None
|
|
29
29
|
user_id: Optional[str] = None
|
|
30
30
|
properties: Optional[Dict[str, Any]] = None
|
|
31
|
+
ip: Optional[str] = None
|
|
32
|
+
user_agent: Optional[str] = None
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
def _resolve_ids(options: TrackOptions) -> TrackOptions:
|
|
34
|
-
"""Resolve visitor/session/user IDs from context if not provided."""
|
|
36
|
+
"""Resolve visitor/session/user IDs and ip/user_agent from context if not provided."""
|
|
35
37
|
ctx = get_context()
|
|
36
38
|
if not ctx:
|
|
37
39
|
return options
|
|
@@ -42,6 +44,8 @@ def _resolve_ids(options: TrackOptions) -> TrackOptions:
|
|
|
42
44
|
session_id=options.session_id or ctx.session_id,
|
|
43
45
|
user_id=options.user_id or ctx.user_id,
|
|
44
46
|
properties=options.properties,
|
|
47
|
+
ip=options.ip or ctx.ip,
|
|
48
|
+
user_agent=options.user_agent or ctx.user_agent,
|
|
45
49
|
)
|
|
46
50
|
|
|
47
51
|
|
|
@@ -62,18 +66,21 @@ def _validate(options: TrackOptions) -> bool:
|
|
|
62
66
|
|
|
63
67
|
def _build_payload(options: TrackOptions, properties: Dict[str, Any]) -> Dict[str, Any]:
|
|
64
68
|
"""Build API payload from options."""
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"properties": properties,
|
|
73
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
74
|
-
}
|
|
75
|
-
]
|
|
69
|
+
event = {
|
|
70
|
+
"event_type": options.event_type,
|
|
71
|
+
"visitor_id": options.visitor_id,
|
|
72
|
+
"session_id": options.session_id,
|
|
73
|
+
"user_id": options.user_id,
|
|
74
|
+
"properties": properties,
|
|
75
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
76
76
|
}
|
|
77
|
+
# Add ip and user_agent only if provided (for server-side session resolution)
|
|
78
|
+
if options.ip:
|
|
79
|
+
event["ip"] = options.ip
|
|
80
|
+
if options.user_agent:
|
|
81
|
+
event["user_agent"] = options.user_agent
|
|
82
|
+
|
|
83
|
+
return {"events": [event]}
|
|
77
84
|
|
|
78
85
|
|
|
79
86
|
def _parse_response(response: Optional[Dict[str, Any]], options: TrackOptions) -> TrackResult:
|
|
@@ -97,6 +104,8 @@ def track(
|
|
|
97
104
|
session_id: Optional[str] = None,
|
|
98
105
|
user_id: Optional[str] = None,
|
|
99
106
|
properties: Optional[Dict[str, Any]] = None,
|
|
107
|
+
ip: Optional[str] = None,
|
|
108
|
+
user_agent: Optional[str] = None,
|
|
100
109
|
) -> TrackResult:
|
|
101
110
|
"""Track an event.
|
|
102
111
|
|
|
@@ -106,6 +115,8 @@ def track(
|
|
|
106
115
|
session_id: Session ID (uses context if not provided)
|
|
107
116
|
user_id: User ID (uses context if not provided)
|
|
108
117
|
properties: Additional event properties
|
|
118
|
+
ip: Client IP address for server-side session resolution
|
|
119
|
+
user_agent: Client user agent for server-side session resolution
|
|
109
120
|
|
|
110
121
|
Returns:
|
|
111
122
|
TrackResult with success status and event details
|
|
@@ -116,6 +127,8 @@ def track(
|
|
|
116
127
|
session_id=session_id,
|
|
117
128
|
user_id=user_id,
|
|
118
129
|
properties=properties,
|
|
130
|
+
ip=ip,
|
|
131
|
+
user_agent=user_agent,
|
|
119
132
|
)
|
|
120
133
|
|
|
121
134
|
options = _resolve_ids(options)
|
|
@@ -14,6 +14,8 @@ class RequestContext:
|
|
|
14
14
|
user_id: Optional[str] = None
|
|
15
15
|
url: Optional[str] = None
|
|
16
16
|
referrer: Optional[str] = None
|
|
17
|
+
ip: Optional[str] = None
|
|
18
|
+
user_agent: Optional[str] = None
|
|
17
19
|
|
|
18
20
|
def enrich_properties(self, properties: Dict[str, Any]) -> Dict[str, Any]:
|
|
19
21
|
"""Add url and referrer to properties if not already present."""
|
|
@@ -8,6 +8,7 @@ from ..config import config
|
|
|
8
8
|
from ..context import RequestContext, set_context, clear_context
|
|
9
9
|
from ..cookies import VISITOR_COOKIE, SESSION_COOKIE, VISITOR_MAX_AGE, SESSION_MAX_AGE
|
|
10
10
|
from ..utils.identifier import generate_id
|
|
11
|
+
from ..utils.session_id import generate_deterministic, generate_from_fingerprint
|
|
11
12
|
from ..client.session import create_session
|
|
12
13
|
|
|
13
14
|
|
|
@@ -55,8 +56,29 @@ def _get_or_create_visitor_id() -> str:
|
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
def _get_or_create_session_id() -> str:
|
|
58
|
-
"""Get session ID from cookie or generate
|
|
59
|
-
|
|
59
|
+
"""Get session ID from cookie or generate deterministic one."""
|
|
60
|
+
existing = request.cookies.get(SESSION_COOKIE)
|
|
61
|
+
if existing:
|
|
62
|
+
return existing
|
|
63
|
+
|
|
64
|
+
existing_visitor_id = request.cookies.get(VISITOR_COOKIE)
|
|
65
|
+
if existing_visitor_id:
|
|
66
|
+
return generate_deterministic(existing_visitor_id)
|
|
67
|
+
else:
|
|
68
|
+
return generate_from_fingerprint(_get_client_ip(), _get_user_agent())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_client_ip() -> str:
|
|
72
|
+
"""Get client IP from request headers."""
|
|
73
|
+
forwarded = request.headers.get("X-Forwarded-For", "")
|
|
74
|
+
if forwarded:
|
|
75
|
+
return forwarded.split(",")[0].strip()
|
|
76
|
+
return request.remote_addr or "unknown"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_user_agent() -> str:
|
|
80
|
+
"""Get user agent from request."""
|
|
81
|
+
return request.headers.get("User-Agent", "unknown")
|
|
60
82
|
|
|
61
83
|
|
|
62
84
|
def _set_request_context(visitor_id: str, session_id: str) -> None:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Deterministic session ID generation."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import secrets
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
SESSION_TIMEOUT_SECONDS = 1800
|
|
8
|
+
SESSION_ID_LENGTH = 64
|
|
9
|
+
FINGERPRINT_LENGTH = 32
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_deterministic(visitor_id: str, timestamp: int | None = None) -> str:
|
|
13
|
+
"""Generate session ID for returning visitors."""
|
|
14
|
+
if timestamp is None:
|
|
15
|
+
timestamp = int(time.time())
|
|
16
|
+
time_bucket = timestamp // SESSION_TIMEOUT_SECONDS
|
|
17
|
+
raw = f"{visitor_id}_{time_bucket}"
|
|
18
|
+
return hashlib.sha256(raw.encode()).hexdigest()[:SESSION_ID_LENGTH]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_from_fingerprint(
|
|
22
|
+
client_ip: str,
|
|
23
|
+
user_agent: str,
|
|
24
|
+
timestamp: int | None = None
|
|
25
|
+
) -> str:
|
|
26
|
+
"""Generate session ID for new visitors using IP+UA fingerprint."""
|
|
27
|
+
if timestamp is None:
|
|
28
|
+
timestamp = int(time.time())
|
|
29
|
+
fingerprint = hashlib.sha256(
|
|
30
|
+
f"{client_ip}|{user_agent}".encode()
|
|
31
|
+
).hexdigest()[:FINGERPRINT_LENGTH]
|
|
32
|
+
time_bucket = timestamp // SESSION_TIMEOUT_SECONDS
|
|
33
|
+
raw = f"{fingerprint}_{time_bucket}"
|
|
34
|
+
return hashlib.sha256(raw.encode()).hexdigest()[:SESSION_ID_LENGTH]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def generate_random() -> str:
|
|
38
|
+
"""Generate random session ID (fallback)."""
|
|
39
|
+
return secrets.token_hex(32)
|
|
@@ -112,6 +112,100 @@ class TestTrack:
|
|
|
112
112
|
result = track(event_type="page_view", visitor_id="vid_123")
|
|
113
113
|
assert result.success is False
|
|
114
114
|
|
|
115
|
+
# Server-side session resolution tests (v0.2.0+)
|
|
116
|
+
|
|
117
|
+
@patch("mbuzz.client.track.post_with_response")
|
|
118
|
+
def test_accepts_ip_parameter(self, mock_post):
|
|
119
|
+
"""Should accept and forward ip parameter."""
|
|
120
|
+
mock_post.return_value = {"events": [{"id": "evt_123"}]}
|
|
121
|
+
|
|
122
|
+
result = track(
|
|
123
|
+
event_type="page_view",
|
|
124
|
+
visitor_id="vid_123",
|
|
125
|
+
ip="192.168.1.100"
|
|
126
|
+
)
|
|
127
|
+
assert result.success is True
|
|
128
|
+
|
|
129
|
+
call_args = mock_post.call_args[0]
|
|
130
|
+
payload = call_args[1]
|
|
131
|
+
assert payload["events"][0]["ip"] == "192.168.1.100"
|
|
132
|
+
|
|
133
|
+
@patch("mbuzz.client.track.post_with_response")
|
|
134
|
+
def test_accepts_user_agent_parameter(self, mock_post):
|
|
135
|
+
"""Should accept and forward user_agent parameter."""
|
|
136
|
+
mock_post.return_value = {"events": [{"id": "evt_123"}]}
|
|
137
|
+
|
|
138
|
+
result = track(
|
|
139
|
+
event_type="page_view",
|
|
140
|
+
visitor_id="vid_123",
|
|
141
|
+
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
|
|
142
|
+
)
|
|
143
|
+
assert result.success is True
|
|
144
|
+
|
|
145
|
+
call_args = mock_post.call_args[0]
|
|
146
|
+
payload = call_args[1]
|
|
147
|
+
assert payload["events"][0]["user_agent"] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
|
|
148
|
+
|
|
149
|
+
@patch("mbuzz.client.track.post_with_response")
|
|
150
|
+
def test_accepts_both_ip_and_user_agent(self, mock_post):
|
|
151
|
+
"""Should accept both ip and user_agent parameters."""
|
|
152
|
+
mock_post.return_value = {"events": [{"id": "evt_123"}]}
|
|
153
|
+
|
|
154
|
+
result = track(
|
|
155
|
+
event_type="page_view",
|
|
156
|
+
visitor_id="vid_123",
|
|
157
|
+
ip="10.0.0.1",
|
|
158
|
+
user_agent="Chrome/120"
|
|
159
|
+
)
|
|
160
|
+
assert result.success is True
|
|
161
|
+
|
|
162
|
+
call_args = mock_post.call_args[0]
|
|
163
|
+
payload = call_args[1]
|
|
164
|
+
assert payload["events"][0]["ip"] == "10.0.0.1"
|
|
165
|
+
assert payload["events"][0]["user_agent"] == "Chrome/120"
|
|
166
|
+
|
|
167
|
+
@patch("mbuzz.client.track.post_with_response")
|
|
168
|
+
def test_uses_context_ip_and_user_agent(self, mock_post):
|
|
169
|
+
"""Should use ip and user_agent from context if not explicitly provided."""
|
|
170
|
+
mock_post.return_value = {"events": [{"id": "evt_123"}]}
|
|
171
|
+
set_context(RequestContext(
|
|
172
|
+
visitor_id="ctx_vid",
|
|
173
|
+
session_id="ctx_sid",
|
|
174
|
+
ip="203.0.113.50",
|
|
175
|
+
user_agent="Safari/17.0"
|
|
176
|
+
))
|
|
177
|
+
|
|
178
|
+
result = track(event_type="page_view")
|
|
179
|
+
assert result.success is True
|
|
180
|
+
|
|
181
|
+
call_args = mock_post.call_args[0]
|
|
182
|
+
payload = call_args[1]
|
|
183
|
+
assert payload["events"][0]["ip"] == "203.0.113.50"
|
|
184
|
+
assert payload["events"][0]["user_agent"] == "Safari/17.0"
|
|
185
|
+
|
|
186
|
+
@patch("mbuzz.client.track.post_with_response")
|
|
187
|
+
def test_explicit_ip_overrides_context(self, mock_post):
|
|
188
|
+
"""Should use explicit ip/user_agent over context."""
|
|
189
|
+
mock_post.return_value = {"events": [{"id": "evt_123"}]}
|
|
190
|
+
set_context(RequestContext(
|
|
191
|
+
visitor_id="ctx_vid",
|
|
192
|
+
session_id="ctx_sid",
|
|
193
|
+
ip="context_ip",
|
|
194
|
+
user_agent="context_ua"
|
|
195
|
+
))
|
|
196
|
+
|
|
197
|
+
result = track(
|
|
198
|
+
event_type="page_view",
|
|
199
|
+
ip="explicit_ip",
|
|
200
|
+
user_agent="explicit_ua"
|
|
201
|
+
)
|
|
202
|
+
assert result.success is True
|
|
203
|
+
|
|
204
|
+
call_args = mock_post.call_args[0]
|
|
205
|
+
payload = call_args[1]
|
|
206
|
+
assert payload["events"][0]["ip"] == "explicit_ip"
|
|
207
|
+
assert payload["events"][0]["user_agent"] == "explicit_ua"
|
|
208
|
+
|
|
115
209
|
|
|
116
210
|
class TestIdentify:
|
|
117
211
|
"""Test identify function."""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Tests for deterministic session ID generation."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from mbuzz.utils.session_id import (
|
|
5
|
+
generate_deterministic,
|
|
6
|
+
generate_from_fingerprint,
|
|
7
|
+
generate_random,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestGenerateDeterministic:
|
|
12
|
+
sample_visitor_id = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
|
13
|
+
sample_timestamp = 1735500000
|
|
14
|
+
|
|
15
|
+
def test_returns_64_char_hex_string(self):
|
|
16
|
+
result = generate_deterministic(self.sample_visitor_id, self.sample_timestamp)
|
|
17
|
+
assert len(result) == 64
|
|
18
|
+
assert all(c in "0123456789abcdef" for c in result)
|
|
19
|
+
|
|
20
|
+
def test_is_consistent(self):
|
|
21
|
+
result1 = generate_deterministic(self.sample_visitor_id, self.sample_timestamp)
|
|
22
|
+
result2 = generate_deterministic(self.sample_visitor_id, self.sample_timestamp)
|
|
23
|
+
assert result1 == result2
|
|
24
|
+
|
|
25
|
+
def test_same_within_time_bucket(self):
|
|
26
|
+
# bucket = timestamp / 1800
|
|
27
|
+
# 1735500000 / 1800 = 964166
|
|
28
|
+
# 1735500599 / 1800 = 964166 (last second of bucket)
|
|
29
|
+
timestamp1 = 1735500000
|
|
30
|
+
timestamp2 = 1735500001
|
|
31
|
+
timestamp3 = 1735500599
|
|
32
|
+
|
|
33
|
+
result1 = generate_deterministic(self.sample_visitor_id, timestamp1)
|
|
34
|
+
result2 = generate_deterministic(self.sample_visitor_id, timestamp2)
|
|
35
|
+
result3 = generate_deterministic(self.sample_visitor_id, timestamp3)
|
|
36
|
+
|
|
37
|
+
assert result1 == result2
|
|
38
|
+
assert result1 == result3
|
|
39
|
+
|
|
40
|
+
def test_different_across_time_buckets(self):
|
|
41
|
+
timestamp1 = 1735500000
|
|
42
|
+
timestamp2 = 1735501800 # Next bucket
|
|
43
|
+
|
|
44
|
+
result1 = generate_deterministic(self.sample_visitor_id, timestamp1)
|
|
45
|
+
result2 = generate_deterministic(self.sample_visitor_id, timestamp2)
|
|
46
|
+
|
|
47
|
+
assert result1 != result2
|
|
48
|
+
|
|
49
|
+
def test_different_for_different_visitors(self):
|
|
50
|
+
result1 = generate_deterministic("visitor_a", self.sample_timestamp)
|
|
51
|
+
result2 = generate_deterministic("visitor_b", self.sample_timestamp)
|
|
52
|
+
|
|
53
|
+
assert result1 != result2
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestGenerateFromFingerprint:
|
|
57
|
+
sample_ip = "203.0.113.42"
|
|
58
|
+
sample_user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
|
59
|
+
sample_timestamp = 1735500000
|
|
60
|
+
|
|
61
|
+
def test_returns_64_char_hex_string(self):
|
|
62
|
+
result = generate_from_fingerprint(
|
|
63
|
+
self.sample_ip, self.sample_user_agent, self.sample_timestamp
|
|
64
|
+
)
|
|
65
|
+
assert len(result) == 64
|
|
66
|
+
assert all(c in "0123456789abcdef" for c in result)
|
|
67
|
+
|
|
68
|
+
def test_is_consistent(self):
|
|
69
|
+
result1 = generate_from_fingerprint(
|
|
70
|
+
self.sample_ip, self.sample_user_agent, self.sample_timestamp
|
|
71
|
+
)
|
|
72
|
+
result2 = generate_from_fingerprint(
|
|
73
|
+
self.sample_ip, self.sample_user_agent, self.sample_timestamp
|
|
74
|
+
)
|
|
75
|
+
assert result1 == result2
|
|
76
|
+
|
|
77
|
+
def test_same_within_time_bucket(self):
|
|
78
|
+
timestamp1 = 1735500000
|
|
79
|
+
timestamp2 = 1735500001
|
|
80
|
+
|
|
81
|
+
result1 = generate_from_fingerprint(
|
|
82
|
+
self.sample_ip, self.sample_user_agent, timestamp1
|
|
83
|
+
)
|
|
84
|
+
result2 = generate_from_fingerprint(
|
|
85
|
+
self.sample_ip, self.sample_user_agent, timestamp2
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert result1 == result2
|
|
89
|
+
|
|
90
|
+
def test_different_across_time_buckets(self):
|
|
91
|
+
timestamp1 = 1735500000
|
|
92
|
+
timestamp2 = 1735501800
|
|
93
|
+
|
|
94
|
+
result1 = generate_from_fingerprint(
|
|
95
|
+
self.sample_ip, self.sample_user_agent, timestamp1
|
|
96
|
+
)
|
|
97
|
+
result2 = generate_from_fingerprint(
|
|
98
|
+
self.sample_ip, self.sample_user_agent, timestamp2
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assert result1 != result2
|
|
102
|
+
|
|
103
|
+
def test_different_for_different_ips(self):
|
|
104
|
+
result1 = generate_from_fingerprint(
|
|
105
|
+
"192.168.1.1", self.sample_user_agent, self.sample_timestamp
|
|
106
|
+
)
|
|
107
|
+
result2 = generate_from_fingerprint(
|
|
108
|
+
"192.168.1.2", self.sample_user_agent, self.sample_timestamp
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
assert result1 != result2
|
|
112
|
+
|
|
113
|
+
def test_different_for_different_user_agents(self):
|
|
114
|
+
result1 = generate_from_fingerprint(
|
|
115
|
+
self.sample_ip, "Mozilla/5.0 Chrome", self.sample_timestamp
|
|
116
|
+
)
|
|
117
|
+
result2 = generate_from_fingerprint(
|
|
118
|
+
self.sample_ip, "Mozilla/5.0 Safari", self.sample_timestamp
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert result1 != result2
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestGenerateRandom:
|
|
125
|
+
def test_returns_64_char_hex_string(self):
|
|
126
|
+
result = generate_random()
|
|
127
|
+
assert len(result) == 64
|
|
128
|
+
assert all(c in "0123456789abcdef" for c in result)
|
|
129
|
+
|
|
130
|
+
def test_returns_unique_ids(self):
|
|
131
|
+
result1 = generate_random()
|
|
132
|
+
result2 = generate_random()
|
|
133
|
+
assert result1 != result2
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestCrossMethod:
|
|
137
|
+
def test_deterministic_and_fingerprint_produce_different_ids(self):
|
|
138
|
+
visitor_id = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
|
139
|
+
ip = "203.0.113.42"
|
|
140
|
+
user_agent = "Mozilla/5.0"
|
|
141
|
+
timestamp = 1735500000
|
|
142
|
+
|
|
143
|
+
deterministic = generate_deterministic(visitor_id, timestamp)
|
|
144
|
+
fingerprint = generate_from_fingerprint(ip, user_agent, timestamp)
|
|
145
|
+
|
|
146
|
+
assert deterministic != fingerprint
|
|
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
|