mbuzz 0.1.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/.gitignore +53 -0
- mbuzz-0.1.0/PKG-INFO +66 -0
- mbuzz-0.1.0/README.md +31 -0
- mbuzz-0.1.0/pyproject.toml +54 -0
- mbuzz-0.1.0/src/mbuzz/__init__.py +87 -0
- mbuzz-0.1.0/src/mbuzz/api.py +71 -0
- mbuzz-0.1.0/src/mbuzz/client/__init__.py +1 -0
- mbuzz-0.1.0/src/mbuzz/client/conversion.py +74 -0
- mbuzz-0.1.0/src/mbuzz/client/identify.py +36 -0
- mbuzz-0.1.0/src/mbuzz/client/session.py +38 -0
- mbuzz-0.1.0/src/mbuzz/client/track.py +130 -0
- mbuzz-0.1.0/src/mbuzz/config.py +96 -0
- mbuzz-0.1.0/src/mbuzz/context.py +45 -0
- mbuzz-0.1.0/src/mbuzz/cookies.py +7 -0
- mbuzz-0.1.0/src/mbuzz/middleware/__init__.py +1 -0
- mbuzz-0.1.0/src/mbuzz/middleware/flask.py +119 -0
- mbuzz-0.1.0/src/mbuzz/utils/__init__.py +5 -0
- mbuzz-0.1.0/src/mbuzz/utils/identifier.py +8 -0
- mbuzz-0.1.0/tests/__init__.py +1 -0
- mbuzz-0.1.0/tests/test_api.py +232 -0
- mbuzz-0.1.0/tests/test_client.py +322 -0
- mbuzz-0.1.0/tests/test_config.py +153 -0
- mbuzz-0.1.0/tests/test_context.py +130 -0
- mbuzz-0.1.0/tests/test_middleware.py +394 -0
mbuzz-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# Virtual environments
|
|
28
|
+
venv/
|
|
29
|
+
ENV/
|
|
30
|
+
env/
|
|
31
|
+
.venv/
|
|
32
|
+
|
|
33
|
+
# pytest
|
|
34
|
+
.pytest_cache/
|
|
35
|
+
.coverage
|
|
36
|
+
htmlcov/
|
|
37
|
+
|
|
38
|
+
# mypy
|
|
39
|
+
.mypy_cache/
|
|
40
|
+
|
|
41
|
+
# IDE
|
|
42
|
+
.idea/
|
|
43
|
+
.vscode/
|
|
44
|
+
*.swp
|
|
45
|
+
*.swo
|
|
46
|
+
|
|
47
|
+
# OS
|
|
48
|
+
.DS_Store
|
|
49
|
+
Thumbs.db
|
|
50
|
+
|
|
51
|
+
# Local development
|
|
52
|
+
.env
|
|
53
|
+
.env.local
|
mbuzz-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mbuzz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-touch attribution SDK for Python
|
|
5
|
+
Project-URL: Homepage, https://mbuzz.co
|
|
6
|
+
Project-URL: Documentation, https://mbuzz.co/docs/getting-started
|
|
7
|
+
Project-URL: Repository, https://github.com/mbuzzco/mbuzz-python
|
|
8
|
+
Author-email: Mbuzz <support@mbuzz.co>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: analytics,attribution,marketing,tracking
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Framework :: Flask
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: black; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
30
|
+
Provides-Extra: django
|
|
31
|
+
Provides-Extra: fastapi
|
|
32
|
+
Requires-Dist: starlette; extra == 'fastapi'
|
|
33
|
+
Provides-Extra: flask
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# mbuzz-python
|
|
37
|
+
|
|
38
|
+
Python SDK for mbuzz multi-touch attribution.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install mbuzz
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import mbuzz
|
|
50
|
+
|
|
51
|
+
# Initialize (once on app start)
|
|
52
|
+
mbuzz.init(api_key="sk_live_...")
|
|
53
|
+
|
|
54
|
+
# Track events
|
|
55
|
+
mbuzz.event("page_view", url="/pricing")
|
|
56
|
+
|
|
57
|
+
# Track conversions
|
|
58
|
+
mbuzz.conversion("purchase", revenue=99.99)
|
|
59
|
+
|
|
60
|
+
# Identify users
|
|
61
|
+
mbuzz.identify("user_123", traits={"email": "user@example.com"})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
See [mbuzz.co/docs/getting-started](https://mbuzz.co/docs/getting-started) for full documentation.
|
mbuzz-0.1.0/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# mbuzz-python
|
|
2
|
+
|
|
3
|
+
Python SDK for mbuzz multi-touch attribution.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mbuzz
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import mbuzz
|
|
15
|
+
|
|
16
|
+
# Initialize (once on app start)
|
|
17
|
+
mbuzz.init(api_key="sk_live_...")
|
|
18
|
+
|
|
19
|
+
# Track events
|
|
20
|
+
mbuzz.event("page_view", url="/pricing")
|
|
21
|
+
|
|
22
|
+
# Track conversions
|
|
23
|
+
mbuzz.conversion("purchase", revenue=99.99)
|
|
24
|
+
|
|
25
|
+
# Identify users
|
|
26
|
+
mbuzz.identify("user_123", traits={"email": "user@example.com"})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Documentation
|
|
30
|
+
|
|
31
|
+
See [mbuzz.co/docs/getting-started](https://mbuzz.co/docs/getting-started) for full documentation.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mbuzz"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Multi-touch attribution SDK for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Mbuzz", email = "support@mbuzz.co" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Framework :: Django",
|
|
18
|
+
"Framework :: Flask",
|
|
19
|
+
"Framework :: FastAPI",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
]
|
|
29
|
+
keywords = ["analytics", "attribution", "marketing", "tracking"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://mbuzz.co"
|
|
33
|
+
Documentation = "https://mbuzz.co/docs/getting-started"
|
|
34
|
+
Repository = "https://github.com/mbuzzco/mbuzz-python"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
django = []
|
|
38
|
+
flask = []
|
|
39
|
+
fastapi = ["starlette"]
|
|
40
|
+
dev = ["pytest", "pytest-cov", "black", "ruff", "mypy"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/mbuzz"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 100
|
|
47
|
+
select = ["E", "F", "I", "N", "W"]
|
|
48
|
+
|
|
49
|
+
[tool.mypy]
|
|
50
|
+
python_version = "3.8"
|
|
51
|
+
strict = true
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Mbuzz - Multi-touch attribution SDK for Python."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Union
|
|
4
|
+
|
|
5
|
+
from .config import config
|
|
6
|
+
from .context import get_context
|
|
7
|
+
from .client.track import track, TrackResult
|
|
8
|
+
from .client.identify import identify
|
|
9
|
+
from .client.conversion import conversion, ConversionResult
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def init(
|
|
15
|
+
api_key: str,
|
|
16
|
+
api_url: Optional[str] = None,
|
|
17
|
+
enabled: bool = True,
|
|
18
|
+
debug: bool = False,
|
|
19
|
+
timeout: Optional[float] = None,
|
|
20
|
+
skip_paths: Optional[list] = None,
|
|
21
|
+
skip_extensions: Optional[list] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Initialize the mbuzz SDK.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api_key: Your mbuzz API key (required)
|
|
27
|
+
api_url: API endpoint URL (default: https://mbuzz.co/api/v1)
|
|
28
|
+
enabled: Enable/disable tracking (default: True)
|
|
29
|
+
debug: Enable debug logging (default: False)
|
|
30
|
+
timeout: Request timeout in seconds (default: 5.0)
|
|
31
|
+
skip_paths: Additional paths to skip tracking
|
|
32
|
+
skip_extensions: Additional file extensions to skip
|
|
33
|
+
"""
|
|
34
|
+
config.init(
|
|
35
|
+
api_key=api_key,
|
|
36
|
+
api_url=api_url,
|
|
37
|
+
enabled=enabled,
|
|
38
|
+
debug=debug,
|
|
39
|
+
timeout=timeout,
|
|
40
|
+
skip_paths=skip_paths or [],
|
|
41
|
+
skip_extensions=skip_extensions or [],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def event(event_type: str, **properties: Any) -> TrackResult:
|
|
46
|
+
"""Track an event.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
event_type: Type of event (e.g., "page_view", "button_click")
|
|
50
|
+
**properties: Additional event properties
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
TrackResult with success status and event details
|
|
54
|
+
"""
|
|
55
|
+
return track(event_type=event_type, properties=properties)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def visitor_id() -> Optional[str]:
|
|
59
|
+
"""Get current visitor ID from context."""
|
|
60
|
+
ctx = get_context()
|
|
61
|
+
return ctx.visitor_id if ctx else None
|
|
62
|
+
|
|
63
|
+
|
|
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
|
+
def user_id() -> Optional[str]:
|
|
71
|
+
"""Get current user ID from context."""
|
|
72
|
+
ctx = get_context()
|
|
73
|
+
return ctx.user_id if ctx else None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"init",
|
|
78
|
+
"event",
|
|
79
|
+
"conversion",
|
|
80
|
+
"identify",
|
|
81
|
+
"visitor_id",
|
|
82
|
+
"session_id",
|
|
83
|
+
"user_id",
|
|
84
|
+
"TrackResult",
|
|
85
|
+
"ConversionResult",
|
|
86
|
+
"__version__",
|
|
87
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""HTTP client for mbuzz API communication."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
from urllib.request import Request, urlopen
|
|
7
|
+
from urllib.error import URLError, HTTPError
|
|
8
|
+
|
|
9
|
+
from .config import config
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("mbuzz")
|
|
12
|
+
|
|
13
|
+
VERSION = "0.1.0"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def post(path: str, payload: Dict[str, Any]) -> bool:
|
|
17
|
+
"""POST to API, return True on success, False on any failure.
|
|
18
|
+
|
|
19
|
+
Never raises exceptions - all errors are caught and logged.
|
|
20
|
+
"""
|
|
21
|
+
if not config._initialized or not config.enabled:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
response = _make_request(path, payload)
|
|
26
|
+
return 200 <= response.status < 300
|
|
27
|
+
except Exception as e:
|
|
28
|
+
if config.debug:
|
|
29
|
+
logger.error(f"mbuzz API error: {e}")
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def post_with_response(path: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
34
|
+
"""POST to API, return parsed JSON on success, None on failure.
|
|
35
|
+
|
|
36
|
+
Never raises exceptions - all errors are caught and logged.
|
|
37
|
+
"""
|
|
38
|
+
if not config._initialized or not config.enabled:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
response = _make_request(path, payload)
|
|
43
|
+
if 200 <= response.status < 300:
|
|
44
|
+
body = response.read().decode("utf-8")
|
|
45
|
+
if config.debug:
|
|
46
|
+
logger.debug(f"mbuzz response: {body}")
|
|
47
|
+
return json.loads(body)
|
|
48
|
+
return None
|
|
49
|
+
except Exception as e:
|
|
50
|
+
if config.debug:
|
|
51
|
+
logger.error(f"mbuzz API error: {e}")
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _make_request(path: str, payload: Dict[str, Any]):
|
|
56
|
+
"""Make HTTP request to API."""
|
|
57
|
+
base_url = config.api_url.rstrip("/")
|
|
58
|
+
clean_path = path.lstrip("/")
|
|
59
|
+
url = f"{base_url}/{clean_path}"
|
|
60
|
+
|
|
61
|
+
data = json.dumps(payload).encode("utf-8")
|
|
62
|
+
|
|
63
|
+
req = Request(url, data=data, method="POST")
|
|
64
|
+
req.add_header("Authorization", f"Bearer {config.api_key}")
|
|
65
|
+
req.add_header("Content-Type", "application/json")
|
|
66
|
+
req.add_header("User-Agent", f"mbuzz-python/{VERSION}")
|
|
67
|
+
|
|
68
|
+
if config.debug:
|
|
69
|
+
logger.debug(f"mbuzz POST {url}: {payload}")
|
|
70
|
+
|
|
71
|
+
return urlopen(req, timeout=config.timeout)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Client module - stub for TDD."""
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Conversion request for tracking conversions."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
from ..api import post_with_response
|
|
7
|
+
from ..context import get_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ConversionResult:
|
|
12
|
+
"""Result of tracking a conversion."""
|
|
13
|
+
|
|
14
|
+
success: bool
|
|
15
|
+
conversion_id: Optional[str] = None
|
|
16
|
+
attribution: Optional[Dict[str, Any]] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def conversion(
|
|
20
|
+
conversion_type: str,
|
|
21
|
+
visitor_id: Optional[str] = None,
|
|
22
|
+
user_id: Optional[Union[str, int]] = None,
|
|
23
|
+
event_id: Optional[str] = None,
|
|
24
|
+
revenue: Optional[float] = None,
|
|
25
|
+
currency: str = "USD",
|
|
26
|
+
is_acquisition: bool = False,
|
|
27
|
+
inherit_acquisition: bool = False,
|
|
28
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
29
|
+
) -> ConversionResult:
|
|
30
|
+
"""Track a conversion.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
conversion_type: Type of conversion (e.g., "purchase", "signup")
|
|
34
|
+
visitor_id: Visitor ID (uses context if not provided)
|
|
35
|
+
user_id: User ID (uses context if not provided)
|
|
36
|
+
event_id: Optional event ID to link conversion to
|
|
37
|
+
revenue: Revenue amount
|
|
38
|
+
currency: Currency code (default: USD)
|
|
39
|
+
is_acquisition: Whether this is a customer acquisition
|
|
40
|
+
inherit_acquisition: Whether to inherit acquisition from previous conversion
|
|
41
|
+
properties: Additional conversion properties
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
ConversionResult with success status, conversion ID, and attribution data
|
|
45
|
+
"""
|
|
46
|
+
ctx = get_context()
|
|
47
|
+
|
|
48
|
+
visitor_id = visitor_id or (ctx.visitor_id if ctx else None)
|
|
49
|
+
user_id = user_id or (ctx.user_id if ctx else None)
|
|
50
|
+
|
|
51
|
+
if not visitor_id and not user_id:
|
|
52
|
+
return ConversionResult(success=False)
|
|
53
|
+
|
|
54
|
+
payload = {
|
|
55
|
+
"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
|
+
"currency": currency,
|
|
61
|
+
"is_acquisition": is_acquisition,
|
|
62
|
+
"inherit_acquisition": inherit_acquisition,
|
|
63
|
+
"properties": properties or {},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
response = post_with_response("/conversions", payload)
|
|
67
|
+
if not response:
|
|
68
|
+
return ConversionResult(success=False)
|
|
69
|
+
|
|
70
|
+
return ConversionResult(
|
|
71
|
+
success=True,
|
|
72
|
+
conversion_id=response.get("conversion", {}).get("id"),
|
|
73
|
+
attribution=response.get("attribution"),
|
|
74
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Identify request for user identification."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Union
|
|
4
|
+
|
|
5
|
+
from ..api import post
|
|
6
|
+
from ..context import get_context
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def identify(
|
|
10
|
+
user_id: Union[str, int, None],
|
|
11
|
+
visitor_id: Optional[str] = None,
|
|
12
|
+
traits: Optional[Dict[str, Any]] = None,
|
|
13
|
+
) -> bool:
|
|
14
|
+
"""Identify a user and link to visitor.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
user_id: User ID to identify (required)
|
|
18
|
+
visitor_id: Visitor ID (uses context if not provided)
|
|
19
|
+
traits: User traits/attributes
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
True on success, False on failure
|
|
23
|
+
"""
|
|
24
|
+
if not user_id:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
ctx = get_context()
|
|
28
|
+
visitor_id = visitor_id or (ctx.visitor_id if ctx else None)
|
|
29
|
+
|
|
30
|
+
payload = {
|
|
31
|
+
"user_id": str(user_id),
|
|
32
|
+
"visitor_id": visitor_id,
|
|
33
|
+
"traits": traits or {},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return post("/identify", payload)
|
|
@@ -0,0 +1,38 @@
|
|
|
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)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Track request for event tracking."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from ..api import post_with_response
|
|
8
|
+
from ..context import get_context
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class TrackResult:
|
|
13
|
+
"""Result of tracking an event."""
|
|
14
|
+
|
|
15
|
+
success: bool
|
|
16
|
+
event_id: Optional[str] = None
|
|
17
|
+
event_type: Optional[str] = None
|
|
18
|
+
visitor_id: Optional[str] = None
|
|
19
|
+
session_id: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class TrackOptions:
|
|
24
|
+
"""Options for tracking an event."""
|
|
25
|
+
|
|
26
|
+
event_type: str
|
|
27
|
+
visitor_id: Optional[str] = None
|
|
28
|
+
session_id: Optional[str] = None
|
|
29
|
+
user_id: Optional[str] = None
|
|
30
|
+
properties: Optional[Dict[str, Any]] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_ids(options: TrackOptions) -> TrackOptions:
|
|
34
|
+
"""Resolve visitor/session/user IDs from context if not provided."""
|
|
35
|
+
ctx = get_context()
|
|
36
|
+
if not ctx:
|
|
37
|
+
return options
|
|
38
|
+
|
|
39
|
+
return TrackOptions(
|
|
40
|
+
event_type=options.event_type,
|
|
41
|
+
visitor_id=options.visitor_id or ctx.visitor_id,
|
|
42
|
+
session_id=options.session_id or ctx.session_id,
|
|
43
|
+
user_id=options.user_id or ctx.user_id,
|
|
44
|
+
properties=options.properties,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _enrich_properties(options: TrackOptions) -> Dict[str, Any]:
|
|
49
|
+
"""Enrich properties with url/referrer from context."""
|
|
50
|
+
ctx = get_context()
|
|
51
|
+
props = options.properties or {}
|
|
52
|
+
|
|
53
|
+
if ctx:
|
|
54
|
+
return ctx.enrich_properties(props)
|
|
55
|
+
return props
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _validate(options: TrackOptions) -> bool:
|
|
59
|
+
"""Validate track options. Must have visitor_id or user_id."""
|
|
60
|
+
return bool(options.visitor_id or options.user_id)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_payload(options: TrackOptions, properties: Dict[str, Any]) -> Dict[str, Any]:
|
|
64
|
+
"""Build API payload from options."""
|
|
65
|
+
return {
|
|
66
|
+
"events": [
|
|
67
|
+
{
|
|
68
|
+
"event_type": options.event_type,
|
|
69
|
+
"visitor_id": options.visitor_id,
|
|
70
|
+
"session_id": options.session_id,
|
|
71
|
+
"user_id": options.user_id,
|
|
72
|
+
"properties": properties,
|
|
73
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_response(response: Optional[Dict[str, Any]], options: TrackOptions) -> TrackResult:
|
|
80
|
+
"""Parse API response into TrackResult."""
|
|
81
|
+
if not response or not response.get("events"):
|
|
82
|
+
return TrackResult(success=False)
|
|
83
|
+
|
|
84
|
+
event = response["events"][0]
|
|
85
|
+
return TrackResult(
|
|
86
|
+
success=True,
|
|
87
|
+
event_id=event.get("id"),
|
|
88
|
+
event_type=options.event_type,
|
|
89
|
+
visitor_id=options.visitor_id,
|
|
90
|
+
session_id=options.session_id,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def track(
|
|
95
|
+
event_type: str,
|
|
96
|
+
visitor_id: Optional[str] = None,
|
|
97
|
+
session_id: Optional[str] = None,
|
|
98
|
+
user_id: Optional[str] = None,
|
|
99
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
100
|
+
) -> TrackResult:
|
|
101
|
+
"""Track an event.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
event_type: Type of event (e.g., "page_view", "button_click")
|
|
105
|
+
visitor_id: Visitor ID (uses context if not provided)
|
|
106
|
+
session_id: Session ID (uses context if not provided)
|
|
107
|
+
user_id: User ID (uses context if not provided)
|
|
108
|
+
properties: Additional event properties
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
TrackResult with success status and event details
|
|
112
|
+
"""
|
|
113
|
+
options = TrackOptions(
|
|
114
|
+
event_type=event_type,
|
|
115
|
+
visitor_id=visitor_id,
|
|
116
|
+
session_id=session_id,
|
|
117
|
+
user_id=user_id,
|
|
118
|
+
properties=properties,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
options = _resolve_ids(options)
|
|
122
|
+
|
|
123
|
+
if not _validate(options):
|
|
124
|
+
return TrackResult(success=False)
|
|
125
|
+
|
|
126
|
+
enriched_props = _enrich_properties(options)
|
|
127
|
+
payload = _build_payload(options, enriched_props)
|
|
128
|
+
response = post_with_response("/events", payload)
|
|
129
|
+
|
|
130
|
+
return _parse_response(response, options)
|