mbuzz 0.2.0__py3-none-any.whl → 0.7.0__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 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.2.0"
12
+ __version__ = "0.7.0"
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",
@@ -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/session/user IDs and ip/user_agent from context if not provided."""
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
- # Add ip and user_agent only if provided (for server-side session resolution)
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
- session_id: str
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,12 @@
1
1
  """Flask middleware for mbuzz tracking."""
2
+ # NOTE: Session cookie removed in 0.7.0 - server handles session resolution
2
3
 
3
- import threading
4
4
  from flask import Flask, request, g, Response
5
- from typing import Optional
6
5
 
7
6
  from ..config import config
8
7
  from ..context import RequestContext, set_context, clear_context
9
- from ..cookies import VISITOR_COOKIE, SESSION_COOKIE, VISITOR_MAX_AGE, SESSION_MAX_AGE
8
+ from ..cookies import VISITOR_COOKIE, VISITOR_MAX_AGE
10
9
  from ..utils.identifier import generate_id
11
- from ..utils.session_id import generate_deterministic, generate_from_fingerprint
12
- from ..client.session import create_session
13
10
 
14
11
 
15
12
  def init_app(app: Flask) -> None:
@@ -21,12 +18,11 @@ def init_app(app: Flask) -> None:
21
18
  return
22
19
 
23
20
  visitor_id = _get_or_create_visitor_id()
24
- session_id = _get_or_create_session_id()
25
- is_new_session = SESSION_COOKIE not in request.cookies
21
+ ip = _get_client_ip()
22
+ user_agent = _get_user_agent()
26
23
 
27
- _set_request_context(visitor_id, session_id)
28
- _store_in_g(visitor_id, session_id, is_new_session)
29
- _create_session_if_new(visitor_id, session_id, is_new_session)
24
+ _set_request_context(visitor_id, ip, user_agent)
25
+ _store_in_g(visitor_id)
30
26
 
31
27
  @app.after_request
32
28
  def after_request(response: Response) -> Response:
@@ -55,19 +51,6 @@ def _get_or_create_visitor_id() -> str:
55
51
  return request.cookies.get(VISITOR_COOKIE) or generate_id()
56
52
 
57
53
 
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
54
  def _get_client_ip() -> str:
72
55
  """Get client IP from request headers."""
73
56
  forwarded = request.headers.get("X-Forwarded-For", "")
@@ -81,11 +64,12 @@ def _get_user_agent() -> str:
81
64
  return request.headers.get("User-Agent", "unknown")
82
65
 
83
66
 
84
- def _set_request_context(visitor_id: str, session_id: str) -> None:
67
+ def _set_request_context(visitor_id: str, ip: str, user_agent: str) -> None:
85
68
  """Set request context for tracking calls."""
86
69
  ctx = RequestContext(
87
70
  visitor_id=visitor_id,
88
- session_id=session_id,
71
+ ip=ip,
72
+ user_agent=user_agent,
89
73
  user_id=None,
90
74
  url=request.url,
91
75
  referrer=request.referrer,
@@ -93,34 +77,14 @@ def _set_request_context(visitor_id: str, session_id: str) -> None:
93
77
  set_context(ctx)
94
78
 
95
79
 
96
- def _store_in_g(visitor_id: str, session_id: str, is_new_session: bool) -> None:
80
+ def _store_in_g(visitor_id: str) -> None:
97
81
  """Store tracking IDs in Flask g object for after_request."""
98
82
  g.mbuzz_visitor_id = visitor_id
99
- g.mbuzz_session_id = session_id
100
83
  g.mbuzz_is_new_visitor = VISITOR_COOKIE not in request.cookies
101
- g.mbuzz_is_new_session = is_new_session
102
-
103
-
104
- def _create_session_if_new(visitor_id: str, session_id: str, is_new_session: bool) -> None:
105
- """Create session asynchronously if new session."""
106
- if not is_new_session:
107
- return
108
-
109
- ctx = RequestContext(
110
- visitor_id=visitor_id,
111
- session_id=session_id,
112
- url=request.url,
113
- referrer=request.referrer,
114
- )
115
- threading.Thread(
116
- target=create_session,
117
- args=(visitor_id, session_id, ctx.url, ctx.referrer),
118
- daemon=True
119
- ).start()
120
84
 
121
85
 
122
86
  def _set_cookies(response: Response) -> None:
123
- """Set visitor and session cookies on response."""
87
+ """Set visitor cookie on response."""
124
88
  secure = request.is_secure
125
89
 
126
90
  response.set_cookie(
@@ -131,11 +95,3 @@ def _set_cookies(response: Response) -> None:
131
95
  samesite="Lax",
132
96
  secure=secure,
133
97
  )
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
- )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbuzz
3
- Version: 0.2.0
3
+ Version: 0.7.0
4
4
  Summary: Multi-touch attribution SDK for Python
5
5
  Project-URL: Homepage, https://mbuzz.co
6
6
  Project-URL: Documentation, https://mbuzz.co/docs/getting-started
@@ -0,0 +1,16 @@
1
+ mbuzz/__init__.py,sha256=tIBoql6_TTtWXErayliL98YGa-EIFMq9rE-X2vgfVHk,2150
2
+ mbuzz/api.py,sha256=BITkW6TQEGmPA0OmYFFC611X6JWiY6C1dY-_wsKDT2U,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=ccotvyzS4PB5-cfaplpwGJJOQzR90hy5iz6sTrhwXXc,2687
12
+ mbuzz/utils/__init__.py,sha256=-ejtRN6CTkV3D8uujDtAPDoMT0DAG-ULpWOF-Ae55dY,103
13
+ mbuzz/utils/identifier.py,sha256=iAYmd4he2RTtTNlmszb6KQmD9McZbSMgZZdkMuoGOCE,174
14
+ mbuzz-0.7.0.dist-info/METADATA,sha256=phnoKfsZaw8nA4u3UTE9uF6n9TkXxzGSAOBGKi35J_U,1849
15
+ mbuzz-0.7.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
+ mbuzz-0.7.0.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)
@@ -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