mbuzz 0.2.0__py3-none-any.whl → 0.7.3__py3-none-any.whl
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/__init__.py +2 -8
- mbuzz/api.py +1 -1
- mbuzz/client/conversion.py +26 -5
- mbuzz/client/track.py +16 -13
- mbuzz/context.py +3 -3
- mbuzz/cookies.py +1 -3
- mbuzz/middleware/flask.py +70 -49
- mbuzz/utils/__init__.py +2 -1
- mbuzz/utils/fingerprint.py +12 -0
- {mbuzz-0.2.0.dist-info → mbuzz-0.7.3.dist-info}/METADATA +1 -1
- mbuzz-0.7.3.dist-info/RECORD +17 -0
- mbuzz/client/session.py +0 -38
- mbuzz/utils/session_id.py +0 -39
- mbuzz-0.2.0.dist-info/RECORD +0 -18
- {mbuzz-0.2.0.dist-info → mbuzz-0.7.3.dist-info}/WHEEL +0 -0
mbuzz/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Mbuzz - Multi-touch attribution SDK for Python."""
|
|
2
|
+
# NOTE: Session ID removed in 0.7.0 - server handles session resolution
|
|
2
3
|
|
|
3
4
|
from typing import Any, Dict, Optional, Union
|
|
4
5
|
|
|
@@ -8,7 +9,7 @@ from .client.track import track, TrackResult
|
|
|
8
9
|
from .client.identify import identify
|
|
9
10
|
from .client.conversion import conversion, ConversionResult
|
|
10
11
|
|
|
11
|
-
__version__ = "0.
|
|
12
|
+
__version__ = "0.7.3"
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def init(
|
|
@@ -61,12 +62,6 @@ def visitor_id() -> Optional[str]:
|
|
|
61
62
|
return ctx.visitor_id if ctx else None
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
def session_id() -> Optional[str]:
|
|
65
|
-
"""Get current session ID from context."""
|
|
66
|
-
ctx = get_context()
|
|
67
|
-
return ctx.session_id if ctx else None
|
|
68
|
-
|
|
69
|
-
|
|
70
65
|
def user_id() -> Optional[str]:
|
|
71
66
|
"""Get current user ID from context."""
|
|
72
67
|
ctx = get_context()
|
|
@@ -79,7 +74,6 @@ __all__ = [
|
|
|
79
74
|
"conversion",
|
|
80
75
|
"identify",
|
|
81
76
|
"visitor_id",
|
|
82
|
-
"session_id",
|
|
83
77
|
"user_id",
|
|
84
78
|
"TrackResult",
|
|
85
79
|
"ConversionResult",
|
mbuzz/api.py
CHANGED
mbuzz/client/conversion.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Conversion request for tracking conversions."""
|
|
2
|
+
# NOTE: Session ID removed in 0.7.0 - server handles session resolution
|
|
2
3
|
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Any, Dict, Optional, Union
|
|
@@ -26,6 +27,9 @@ def conversion(
|
|
|
26
27
|
is_acquisition: bool = False,
|
|
27
28
|
inherit_acquisition: bool = False,
|
|
28
29
|
properties: Optional[Dict[str, Any]] = None,
|
|
30
|
+
ip: Optional[str] = None,
|
|
31
|
+
user_agent: Optional[str] = None,
|
|
32
|
+
identifier: Optional[Dict[str, str]] = None,
|
|
29
33
|
) -> ConversionResult:
|
|
30
34
|
"""Track a conversion.
|
|
31
35
|
|
|
@@ -39,6 +43,9 @@ def conversion(
|
|
|
39
43
|
is_acquisition: Whether this is a customer acquisition
|
|
40
44
|
inherit_acquisition: Whether to inherit acquisition from previous conversion
|
|
41
45
|
properties: Additional conversion properties
|
|
46
|
+
ip: Client IP address for server-side session resolution
|
|
47
|
+
user_agent: Client user agent for server-side session resolution
|
|
48
|
+
identifier: Cross-device identifier (email, user_id, etc.)
|
|
42
49
|
|
|
43
50
|
Returns:
|
|
44
51
|
ConversionResult with success status, conversion ID, and attribution data
|
|
@@ -47,22 +54,36 @@ def conversion(
|
|
|
47
54
|
|
|
48
55
|
visitor_id = visitor_id or (ctx.visitor_id if ctx else None)
|
|
49
56
|
user_id = user_id or (ctx.user_id if ctx else None)
|
|
57
|
+
ip = ip or (ctx.ip if ctx else None)
|
|
58
|
+
user_agent = user_agent or (ctx.user_agent if ctx else None)
|
|
50
59
|
|
|
51
60
|
if not visitor_id and not user_id:
|
|
52
61
|
return ConversionResult(success=False)
|
|
53
62
|
|
|
54
|
-
payload = {
|
|
63
|
+
payload: Dict[str, Any] = {
|
|
55
64
|
"conversion_type": conversion_type,
|
|
56
|
-
"visitor_id": visitor_id,
|
|
57
|
-
"user_id": str(user_id) if user_id else None,
|
|
58
|
-
"event_id": event_id,
|
|
59
|
-
"revenue": revenue,
|
|
60
65
|
"currency": currency,
|
|
61
66
|
"is_acquisition": is_acquisition,
|
|
62
67
|
"inherit_acquisition": inherit_acquisition,
|
|
63
68
|
"properties": properties or {},
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
# Only include non-None values
|
|
72
|
+
if visitor_id:
|
|
73
|
+
payload["visitor_id"] = visitor_id
|
|
74
|
+
if user_id:
|
|
75
|
+
payload["user_id"] = str(user_id)
|
|
76
|
+
if event_id:
|
|
77
|
+
payload["event_id"] = event_id
|
|
78
|
+
if revenue is not None:
|
|
79
|
+
payload["revenue"] = revenue
|
|
80
|
+
if ip:
|
|
81
|
+
payload["ip"] = ip
|
|
82
|
+
if user_agent:
|
|
83
|
+
payload["user_agent"] = user_agent
|
|
84
|
+
if identifier:
|
|
85
|
+
payload["identifier"] = identifier
|
|
86
|
+
|
|
66
87
|
response = post_with_response("/conversions", payload)
|
|
67
88
|
if not response:
|
|
68
89
|
return ConversionResult(success=False)
|
mbuzz/client/track.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Track request for event tracking."""
|
|
2
|
+
# NOTE: Session ID removed in 0.7.0 - server handles session resolution
|
|
2
3
|
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from datetime import datetime, timezone
|
|
@@ -16,7 +17,6 @@ class TrackResult:
|
|
|
16
17
|
event_id: Optional[str] = None
|
|
17
18
|
event_type: Optional[str] = None
|
|
18
19
|
visitor_id: Optional[str] = None
|
|
19
|
-
session_id: Optional[str] = None
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@dataclass
|
|
@@ -25,15 +25,15 @@ class TrackOptions:
|
|
|
25
25
|
|
|
26
26
|
event_type: str
|
|
27
27
|
visitor_id: Optional[str] = None
|
|
28
|
-
session_id: Optional[str] = None
|
|
29
28
|
user_id: Optional[str] = None
|
|
30
29
|
properties: Optional[Dict[str, Any]] = None
|
|
31
30
|
ip: Optional[str] = None
|
|
32
31
|
user_agent: Optional[str] = None
|
|
32
|
+
identifier: Optional[Dict[str, str]] = None
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def _resolve_ids(options: TrackOptions) -> TrackOptions:
|
|
36
|
-
"""Resolve visitor/
|
|
36
|
+
"""Resolve visitor/user IDs and ip/user_agent from context if not provided."""
|
|
37
37
|
ctx = get_context()
|
|
38
38
|
if not ctx:
|
|
39
39
|
return options
|
|
@@ -41,11 +41,11 @@ def _resolve_ids(options: TrackOptions) -> TrackOptions:
|
|
|
41
41
|
return TrackOptions(
|
|
42
42
|
event_type=options.event_type,
|
|
43
43
|
visitor_id=options.visitor_id or ctx.visitor_id,
|
|
44
|
-
session_id=options.session_id or ctx.session_id,
|
|
45
44
|
user_id=options.user_id or ctx.user_id,
|
|
46
45
|
properties=options.properties,
|
|
47
46
|
ip=options.ip or ctx.ip,
|
|
48
47
|
user_agent=options.user_agent or ctx.user_agent,
|
|
48
|
+
identifier=options.identifier,
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
|
|
@@ -66,19 +66,23 @@ def _validate(options: TrackOptions) -> bool:
|
|
|
66
66
|
|
|
67
67
|
def _build_payload(options: TrackOptions, properties: Dict[str, Any]) -> Dict[str, Any]:
|
|
68
68
|
"""Build API payload from options."""
|
|
69
|
-
event = {
|
|
69
|
+
event: Dict[str, Any] = {
|
|
70
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
71
|
"properties": properties,
|
|
75
72
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
76
73
|
}
|
|
77
|
-
|
|
74
|
+
|
|
75
|
+
# Only include non-None values
|
|
76
|
+
if options.visitor_id:
|
|
77
|
+
event["visitor_id"] = options.visitor_id
|
|
78
|
+
if options.user_id:
|
|
79
|
+
event["user_id"] = options.user_id
|
|
78
80
|
if options.ip:
|
|
79
81
|
event["ip"] = options.ip
|
|
80
82
|
if options.user_agent:
|
|
81
83
|
event["user_agent"] = options.user_agent
|
|
84
|
+
if options.identifier:
|
|
85
|
+
event["identifier"] = options.identifier
|
|
82
86
|
|
|
83
87
|
return {"events": [event]}
|
|
84
88
|
|
|
@@ -94,29 +98,28 @@ def _parse_response(response: Optional[Dict[str, Any]], options: TrackOptions) -
|
|
|
94
98
|
event_id=event.get("id"),
|
|
95
99
|
event_type=options.event_type,
|
|
96
100
|
visitor_id=options.visitor_id,
|
|
97
|
-
session_id=options.session_id,
|
|
98
101
|
)
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
def track(
|
|
102
105
|
event_type: str,
|
|
103
106
|
visitor_id: Optional[str] = None,
|
|
104
|
-
session_id: Optional[str] = None,
|
|
105
107
|
user_id: Optional[str] = None,
|
|
106
108
|
properties: Optional[Dict[str, Any]] = None,
|
|
107
109
|
ip: Optional[str] = None,
|
|
108
110
|
user_agent: Optional[str] = None,
|
|
111
|
+
identifier: Optional[Dict[str, str]] = None,
|
|
109
112
|
) -> TrackResult:
|
|
110
113
|
"""Track an event.
|
|
111
114
|
|
|
112
115
|
Args:
|
|
113
116
|
event_type: Type of event (e.g., "page_view", "button_click")
|
|
114
117
|
visitor_id: Visitor ID (uses context if not provided)
|
|
115
|
-
session_id: Session ID (uses context if not provided)
|
|
116
118
|
user_id: User ID (uses context if not provided)
|
|
117
119
|
properties: Additional event properties
|
|
118
120
|
ip: Client IP address for server-side session resolution
|
|
119
121
|
user_agent: Client user agent for server-side session resolution
|
|
122
|
+
identifier: Cross-device identifier (email, user_id, etc.)
|
|
120
123
|
|
|
121
124
|
Returns:
|
|
122
125
|
TrackResult with success status and event details
|
|
@@ -124,11 +127,11 @@ def track(
|
|
|
124
127
|
options = TrackOptions(
|
|
125
128
|
event_type=event_type,
|
|
126
129
|
visitor_id=visitor_id,
|
|
127
|
-
session_id=session_id,
|
|
128
130
|
user_id=user_id,
|
|
129
131
|
properties=properties,
|
|
130
132
|
ip=ip,
|
|
131
133
|
user_agent=user_agent,
|
|
134
|
+
identifier=identifier,
|
|
132
135
|
)
|
|
133
136
|
|
|
134
137
|
options = _resolve_ids(options)
|
mbuzz/context.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Request context management using contextvars."""
|
|
2
|
+
# NOTE: Session ID removed in 0.7.0 - server handles session resolution
|
|
2
3
|
|
|
3
4
|
from contextvars import ContextVar
|
|
4
5
|
from dataclasses import dataclass
|
|
@@ -10,12 +11,11 @@ class RequestContext:
|
|
|
10
11
|
"""Holds request-scoped data for tracking."""
|
|
11
12
|
|
|
12
13
|
visitor_id: str
|
|
13
|
-
|
|
14
|
+
ip: str
|
|
15
|
+
user_agent: str
|
|
14
16
|
user_id: Optional[str] = None
|
|
15
17
|
url: Optional[str] = None
|
|
16
18
|
referrer: Optional[str] = None
|
|
17
|
-
ip: Optional[str] = None
|
|
18
|
-
user_agent: Optional[str] = None
|
|
19
19
|
|
|
20
20
|
def enrich_properties(self, properties: Dict[str, Any]) -> Dict[str, Any]:
|
|
21
21
|
"""Add url and referrer to properties if not already present."""
|
mbuzz/cookies.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Cookie constants for mbuzz SDK."""
|
|
2
|
+
# NOTE: Session cookie removed in 0.7.0 - server handles session resolution
|
|
2
3
|
|
|
3
4
|
VISITOR_COOKIE = "_mbuzz_vid"
|
|
4
|
-
SESSION_COOKIE = "_mbuzz_sid"
|
|
5
|
-
|
|
6
5
|
VISITOR_MAX_AGE = 63072000 # 2 years in seconds
|
|
7
|
-
SESSION_MAX_AGE = 1800 # 30 minutes in seconds
|
mbuzz/middleware/flask.py
CHANGED
|
@@ -1,15 +1,44 @@
|
|
|
1
1
|
"""Flask middleware for mbuzz tracking."""
|
|
2
|
+
# NOTE: Session cookie removed in 0.7.0 - server handles session resolution
|
|
2
3
|
|
|
3
4
|
import threading
|
|
4
|
-
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime, timezone
|
|
5
7
|
from typing import Optional
|
|
6
8
|
|
|
9
|
+
from flask import Flask, request, g, Response
|
|
10
|
+
|
|
11
|
+
from ..api import post
|
|
7
12
|
from ..config import config
|
|
8
13
|
from ..context import RequestContext, set_context, clear_context
|
|
9
|
-
from ..cookies import VISITOR_COOKIE,
|
|
14
|
+
from ..cookies import VISITOR_COOKIE, VISITOR_MAX_AGE
|
|
15
|
+
from ..utils.fingerprint import device_fingerprint
|
|
10
16
|
from ..utils.identifier import generate_id
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
|
|
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
|
+
)
|
|
13
42
|
|
|
14
43
|
|
|
15
44
|
def init_app(app: Flask) -> None:
|
|
@@ -21,12 +50,16 @@ def init_app(app: Flask) -> None:
|
|
|
21
50
|
return
|
|
22
51
|
|
|
23
52
|
visitor_id = _get_or_create_visitor_id()
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
ip = _get_client_ip()
|
|
54
|
+
user_agent = _get_user_agent()
|
|
55
|
+
|
|
56
|
+
_set_request_context(visitor_id, ip, user_agent)
|
|
57
|
+
_store_in_g(visitor_id)
|
|
26
58
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
59
|
+
if should_create_session():
|
|
60
|
+
_create_session_async(
|
|
61
|
+
visitor_id, request.url, request.referrer, ip, user_agent
|
|
62
|
+
)
|
|
30
63
|
|
|
31
64
|
@app.after_request
|
|
32
65
|
def after_request(response: Response) -> Response:
|
|
@@ -55,19 +88,6 @@ def _get_or_create_visitor_id() -> str:
|
|
|
55
88
|
return request.cookies.get(VISITOR_COOKIE) or generate_id()
|
|
56
89
|
|
|
57
90
|
|
|
58
|
-
def _get_or_create_session_id() -> str:
|
|
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
91
|
def _get_client_ip() -> str:
|
|
72
92
|
"""Get client IP from request headers."""
|
|
73
93
|
forwarded = request.headers.get("X-Forwarded-For", "")
|
|
@@ -81,11 +101,12 @@ def _get_user_agent() -> str:
|
|
|
81
101
|
return request.headers.get("User-Agent", "unknown")
|
|
82
102
|
|
|
83
103
|
|
|
84
|
-
def _set_request_context(visitor_id: str,
|
|
104
|
+
def _set_request_context(visitor_id: str, ip: str, user_agent: str) -> None:
|
|
85
105
|
"""Set request context for tracking calls."""
|
|
86
106
|
ctx = RequestContext(
|
|
87
107
|
visitor_id=visitor_id,
|
|
88
|
-
|
|
108
|
+
ip=ip,
|
|
109
|
+
user_agent=user_agent,
|
|
89
110
|
user_id=None,
|
|
90
111
|
url=request.url,
|
|
91
112
|
referrer=request.referrer,
|
|
@@ -93,34 +114,42 @@ def _set_request_context(visitor_id: str, session_id: str) -> None:
|
|
|
93
114
|
set_context(ctx)
|
|
94
115
|
|
|
95
116
|
|
|
96
|
-
def _store_in_g(visitor_id: str
|
|
117
|
+
def _store_in_g(visitor_id: str) -> None:
|
|
97
118
|
"""Store tracking IDs in Flask g object for after_request."""
|
|
98
119
|
g.mbuzz_visitor_id = visitor_id
|
|
99
|
-
g.mbuzz_session_id = session_id
|
|
100
120
|
g.mbuzz_is_new_visitor = VISITOR_COOKIE not in request.cookies
|
|
101
|
-
g.mbuzz_is_new_session = is_new_session
|
|
102
121
|
|
|
103
122
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
145
|
|
|
109
|
-
ctx = RequestContext(
|
|
110
|
-
visitor_id=visitor_id,
|
|
111
|
-
session_id=session_id,
|
|
112
|
-
url=request.url,
|
|
113
|
-
referrer=request.referrer,
|
|
114
|
-
)
|
|
115
146
|
threading.Thread(
|
|
116
|
-
target=
|
|
117
|
-
args=(visitor_id, session_id, ctx.url, ctx.referrer),
|
|
118
|
-
daemon=True
|
|
147
|
+
target=post, args=("/sessions", payload), daemon=True
|
|
119
148
|
).start()
|
|
120
149
|
|
|
121
150
|
|
|
122
151
|
def _set_cookies(response: Response) -> None:
|
|
123
|
-
"""Set visitor
|
|
152
|
+
"""Set visitor cookie on response."""
|
|
124
153
|
secure = request.is_secure
|
|
125
154
|
|
|
126
155
|
response.set_cookie(
|
|
@@ -131,11 +160,3 @@ def _set_cookies(response: Response) -> None:
|
|
|
131
160
|
samesite="Lax",
|
|
132
161
|
secure=secure,
|
|
133
162
|
)
|
|
134
|
-
response.set_cookie(
|
|
135
|
-
SESSION_COOKIE,
|
|
136
|
-
g.mbuzz_session_id,
|
|
137
|
-
max_age=SESSION_MAX_AGE,
|
|
138
|
-
httponly=True,
|
|
139
|
-
samesite="Lax",
|
|
140
|
-
secure=secure,
|
|
141
|
-
)
|
mbuzz/utils/__init__.py
CHANGED
|
@@ -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]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
mbuzz/__init__.py,sha256=OQDjNSQTB6Yfc66NRpZPN8cWjWcVJopDysoB8tb3cbU,2150
|
|
2
|
+
mbuzz/api.py,sha256=bOLjQRgv9PbfGvNxYMtdyiBQvlLFHsHcVgZ8_lEWEHU,2107
|
|
3
|
+
mbuzz/config.py,sha256=u6v1WnMTgiqyDXHszlXw5hyNENgYRy7-z-IWKuYp0-o,2399
|
|
4
|
+
mbuzz/context.py,sha256=tcvtKaAbHzH8dHGkKviNgXF31IKgintqo0lhb6uU4dU,1308
|
|
5
|
+
mbuzz/cookies.py,sha256=pDcvSq0ulzKrFLf5UK408s9Lv2pVzLANVaF_-P7iG3c,194
|
|
6
|
+
mbuzz/client/__init__.py,sha256=oNOTpPKdt-b_VH_f848OQTbEEDFhlpbEjZ-WjKvh3JI,36
|
|
7
|
+
mbuzz/client/conversion.py,sha256=mUkxU8FgbmvvXnliTskawnDzbwn1SxutZ5E4rFkNt5o,3155
|
|
8
|
+
mbuzz/client/identify.py,sha256=duyd-OWdBo4w7-7Nv5X-GeG2IpMI-1G_kYwnDXup0FE,859
|
|
9
|
+
mbuzz/client/track.py,sha256=NReHqXWx24cPhIlc7UBQjj9QVdnXfrJsFzS5UmOUMtA,4354
|
|
10
|
+
mbuzz/middleware/__init__.py,sha256=gIjwTArToaQNB2NC0iPE_RcmzuHjH7-7jjd7_Dyq4Pw,37
|
|
11
|
+
mbuzz/middleware/flask.py,sha256=gkvwtgfLasTKq64lUvK_9I2PeOR9oBRbrWpQZbqnaAk,4656
|
|
12
|
+
mbuzz/utils/__init__.py,sha256=tq5wgr4Cy-PFl2tvpN5VYHzPd3M-e5qcxnv-McoTopI,169
|
|
13
|
+
mbuzz/utils/fingerprint.py,sha256=T2plrP9n70LkoCvGbBCh7nxZRfdQpCd_CdZBseBfzCg,410
|
|
14
|
+
mbuzz/utils/identifier.py,sha256=iAYmd4he2RTtTNlmszb6KQmD9McZbSMgZZdkMuoGOCE,174
|
|
15
|
+
mbuzz-0.7.3.dist-info/METADATA,sha256=Q0dHPns1Lk73_ziPwsknxzQMIkJFAYnqRo5drKkCD_0,1849
|
|
16
|
+
mbuzz-0.7.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
mbuzz-0.7.3.dist-info/RECORD,,
|
mbuzz/client/session.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""Session request for creating sessions."""
|
|
2
|
-
|
|
3
|
-
from datetime import datetime, timezone
|
|
4
|
-
from typing import Optional
|
|
5
|
-
|
|
6
|
-
from ..api import post
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def create_session(
|
|
10
|
-
visitor_id: str,
|
|
11
|
-
session_id: str,
|
|
12
|
-
url: str,
|
|
13
|
-
referrer: Optional[str] = None,
|
|
14
|
-
) -> bool:
|
|
15
|
-
"""Create a new session.
|
|
16
|
-
|
|
17
|
-
Called async from middleware on first request.
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
visitor_id: Visitor ID
|
|
21
|
-
session_id: Session ID
|
|
22
|
-
url: Current page URL
|
|
23
|
-
referrer: Referring URL
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
True on success, False on failure
|
|
27
|
-
"""
|
|
28
|
-
payload = {
|
|
29
|
-
"session": {
|
|
30
|
-
"visitor_id": visitor_id,
|
|
31
|
-
"session_id": session_id,
|
|
32
|
-
"url": url,
|
|
33
|
-
"referrer": referrer,
|
|
34
|
-
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return post("/sessions", payload)
|
mbuzz/utils/session_id.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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)
|
mbuzz-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
mbuzz/__init__.py,sha256=ip9lSCsNe7T5k1qOfGs4pttoTNGx5Ws-Zf9fHoWXFPg,2247
|
|
2
|
-
mbuzz/api.py,sha256=BITkW6TQEGmPA0OmYFFC611X6JWiY6C1dY-_wsKDT2U,2107
|
|
3
|
-
mbuzz/config.py,sha256=u6v1WnMTgiqyDXHszlXw5hyNENgYRy7-z-IWKuYp0-o,2399
|
|
4
|
-
mbuzz/context.py,sha256=mM9Hi8GnqTkAVCqB_VKGCXl7a5AFME4WTM-2DCIji_w,1290
|
|
5
|
-
mbuzz/cookies.py,sha256=MCpMljQXaqJ2bhEtTv1aKR2fuqLfGBFofjPRayAQGpw,197
|
|
6
|
-
mbuzz/client/__init__.py,sha256=oNOTpPKdt-b_VH_f848OQTbEEDFhlpbEjZ-WjKvh3JI,36
|
|
7
|
-
mbuzz/client/conversion.py,sha256=Ua3ngFWmH6ekm3ZanNf5up_jV8t5nf788ipM1usvZoM,2346
|
|
8
|
-
mbuzz/client/identify.py,sha256=duyd-OWdBo4w7-7Nv5X-GeG2IpMI-1G_kYwnDXup0FE,859
|
|
9
|
-
mbuzz/client/session.py,sha256=mk8v892bvpeW-VNKFQYDsW442w881yfXBiM4sr9a0SU,835
|
|
10
|
-
mbuzz/client/track.py,sha256=HWRYWDWJ_WW_k5IHtkAn4s_MmVwSVFxsxR76KG2h_Ps,4288
|
|
11
|
-
mbuzz/middleware/__init__.py,sha256=gIjwTArToaQNB2NC0iPE_RcmzuHjH7-7jjd7_Dyq4Pw,37
|
|
12
|
-
mbuzz/middleware/flask.py,sha256=s3U77A2986L-KvWUH9MmqSwJwQWzQTYlZPZXH-wzE9o,4182
|
|
13
|
-
mbuzz/utils/__init__.py,sha256=-ejtRN6CTkV3D8uujDtAPDoMT0DAG-ULpWOF-Ae55dY,103
|
|
14
|
-
mbuzz/utils/identifier.py,sha256=iAYmd4he2RTtTNlmszb6KQmD9McZbSMgZZdkMuoGOCE,174
|
|
15
|
-
mbuzz/utils/session_id.py,sha256=3XJkzPg5iUIbRYzz5LThfS8WTAmlmtn60WZTA_TlVhk,1185
|
|
16
|
-
mbuzz-0.2.0.dist-info/METADATA,sha256=3Z8snpq6fv0PmZUvSuAjsH1KaiZIpE9pRLVHTaw8cPE,1849
|
|
17
|
-
mbuzz-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
-
mbuzz-0.2.0.dist-info/RECORD,,
|
|
File without changes
|