scope-analytics 0.1.0__tar.gz → 0.1.1__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.
- {scope_analytics-0.1.0/scope_analytics.egg-info → scope_analytics-0.1.1}/PKG-INFO +1 -1
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/context.py +69 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/events.py +13 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/middleware.py +51 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1/scope_analytics.egg-info}/PKG-INFO +1 -1
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/setup.py +1 -1
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/LICENSE +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/MANIFEST.in +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/README.md +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/__init__.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/auto.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/cli.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/client.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/config.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/patches/__init__.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/patches/anthropic_patch.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/patches/gemini_patch.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/patches/openai_patch.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics/queue.py +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics.egg-info/SOURCES.txt +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics.egg-info/dependency_links.txt +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics.egg-info/entry_points.txt +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics.egg-info/requires.txt +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics.egg-info/top_level.txt +0 -0
- {scope_analytics-0.1.0 → scope_analytics-0.1.1}/setup.cfg +0 -0
|
@@ -17,6 +17,23 @@ user_id_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
|
17
17
|
'scope_user_id', default=None
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
+
# Request context for LLM event correlation
|
|
21
|
+
url_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
22
|
+
'scope_url', default=None
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
referrer_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
26
|
+
'scope_referrer', default=None
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
thread_id_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
30
|
+
'scope_thread_id', default=None
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
user_agent_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
34
|
+
'scope_user_agent', default=None
|
|
35
|
+
)
|
|
36
|
+
|
|
20
37
|
# Recursion guard - prevents infinite loops when Scope tracks its own LLM calls
|
|
21
38
|
# This is critical when Scope AI backend uses Scope SDK to track itself
|
|
22
39
|
_in_scope_context: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
|
@@ -72,6 +89,50 @@ class ScopeContext:
|
|
|
72
89
|
"""
|
|
73
90
|
return user_id_context.get()
|
|
74
91
|
|
|
92
|
+
# =========================================================================
|
|
93
|
+
# Request Context - URL, Referrer, Thread ID, User Agent
|
|
94
|
+
# =========================================================================
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def set_url(url: str) -> None:
|
|
98
|
+
"""Set the request URL for the current context"""
|
|
99
|
+
url_context.set(url)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def get_url() -> Optional[str]:
|
|
103
|
+
"""Get the request URL from current context"""
|
|
104
|
+
return url_context.get()
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def set_referrer(referrer: str) -> None:
|
|
108
|
+
"""Set the referrer for the current context"""
|
|
109
|
+
referrer_context.set(referrer)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def get_referrer() -> Optional[str]:
|
|
113
|
+
"""Get the referrer from current context"""
|
|
114
|
+
return referrer_context.get()
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def set_thread_id(thread_id: str) -> None:
|
|
118
|
+
"""Set the thread ID for the current context (for ChatGPT-style apps)"""
|
|
119
|
+
thread_id_context.set(thread_id)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def get_thread_id() -> Optional[str]:
|
|
123
|
+
"""Get the thread ID from current context"""
|
|
124
|
+
return thread_id_context.get()
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def set_user_agent(user_agent: str) -> None:
|
|
128
|
+
"""Set the user agent for the current context"""
|
|
129
|
+
user_agent_context.set(user_agent)
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def get_user_agent() -> Optional[str]:
|
|
133
|
+
"""Get the user agent from current context"""
|
|
134
|
+
return user_agent_context.get()
|
|
135
|
+
|
|
75
136
|
@staticmethod
|
|
76
137
|
def clear() -> None:
|
|
77
138
|
"""
|
|
@@ -80,6 +141,10 @@ class ScopeContext:
|
|
|
80
141
|
"""
|
|
81
142
|
session_id_context.set(None)
|
|
82
143
|
user_id_context.set(None)
|
|
144
|
+
url_context.set(None)
|
|
145
|
+
referrer_context.set(None)
|
|
146
|
+
thread_id_context.set(None)
|
|
147
|
+
user_agent_context.set(None)
|
|
83
148
|
|
|
84
149
|
@staticmethod
|
|
85
150
|
def generate_temp_session_id() -> str:
|
|
@@ -116,6 +181,10 @@ class ScopeContext:
|
|
|
116
181
|
return {
|
|
117
182
|
"session_id": session_id_context.get(),
|
|
118
183
|
"user_id": user_id_context.get(),
|
|
184
|
+
"url": url_context.get(),
|
|
185
|
+
"referrer": referrer_context.get(),
|
|
186
|
+
"thread_id": thread_id_context.get(),
|
|
187
|
+
"user_agent": user_agent_context.get(),
|
|
119
188
|
}
|
|
120
189
|
|
|
121
190
|
# =========================================================================
|
|
@@ -46,6 +46,12 @@ class EventFormatter:
|
|
|
46
46
|
session_id = ScopeContext.get_session_id()
|
|
47
47
|
user_id = ScopeContext.get_user_id()
|
|
48
48
|
|
|
49
|
+
# Get request context for conversation correlation
|
|
50
|
+
url = ScopeContext.get_url()
|
|
51
|
+
referrer = ScopeContext.get_referrer()
|
|
52
|
+
thread_id = ScopeContext.get_thread_id()
|
|
53
|
+
user_agent = ScopeContext.get_user_agent()
|
|
54
|
+
|
|
49
55
|
# Determine if this is a user-facing LLM call
|
|
50
56
|
# If we have a session_id from frontend (not temp_), it's user-facing
|
|
51
57
|
is_user_facing = session_id and not session_id.startswith("temp_")
|
|
@@ -64,6 +70,13 @@ class EventFormatter:
|
|
|
64
70
|
"session_id": session_id or ScopeContext.ensure_session_id(),
|
|
65
71
|
"user_id": user_id,
|
|
66
72
|
|
|
73
|
+
# Request context for conversation correlation
|
|
74
|
+
# This enables the fallback chain: thread_id → session_id+url → session_id
|
|
75
|
+
"url": url,
|
|
76
|
+
"referrer": referrer,
|
|
77
|
+
"thread_id": thread_id, # For ChatGPT-style apps with multiple threads
|
|
78
|
+
"user_agent": user_agent,
|
|
79
|
+
|
|
67
80
|
# LLM call specific fields
|
|
68
81
|
"provider": provider,
|
|
69
82
|
"model": model,
|
|
@@ -199,6 +199,48 @@ def extract_session_from_headers(headers: dict) -> Optional[str]:
|
|
|
199
199
|
return None
|
|
200
200
|
|
|
201
201
|
|
|
202
|
+
def _set_request_context(headers: dict, path: str = None) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Set request context variables from HTTP headers.
|
|
205
|
+
This context is used to enrich LLM events with request information.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
headers: Dictionary of HTTP headers
|
|
209
|
+
path: Request path (URL)
|
|
210
|
+
"""
|
|
211
|
+
# Set URL (request path)
|
|
212
|
+
if path:
|
|
213
|
+
ScopeContext.set_url(path)
|
|
214
|
+
|
|
215
|
+
# Extract and set referrer (case-insensitive)
|
|
216
|
+
referrer = None
|
|
217
|
+
for key, value in headers.items():
|
|
218
|
+
key_lower = key.lower()
|
|
219
|
+
if key_lower == "referer" or key_lower == "referrer":
|
|
220
|
+
referrer = value
|
|
221
|
+
break
|
|
222
|
+
if referrer:
|
|
223
|
+
ScopeContext.set_referrer(referrer)
|
|
224
|
+
|
|
225
|
+
# Extract and set thread_id (X-Scope-Thread-ID header for ChatGPT-style apps)
|
|
226
|
+
thread_id = None
|
|
227
|
+
for key, value in headers.items():
|
|
228
|
+
if key.lower() == "x-scope-thread-id":
|
|
229
|
+
thread_id = value
|
|
230
|
+
break
|
|
231
|
+
if thread_id:
|
|
232
|
+
ScopeContext.set_thread_id(thread_id)
|
|
233
|
+
|
|
234
|
+
# Extract and set user_agent
|
|
235
|
+
user_agent = None
|
|
236
|
+
for key, value in headers.items():
|
|
237
|
+
if key.lower() == "user-agent":
|
|
238
|
+
user_agent = value
|
|
239
|
+
break
|
|
240
|
+
if user_agent:
|
|
241
|
+
ScopeContext.set_user_agent(user_agent)
|
|
242
|
+
|
|
243
|
+
|
|
202
244
|
# =============================================================================
|
|
203
245
|
# FastAPI / Starlette Middleware (ASGI)
|
|
204
246
|
# =============================================================================
|
|
@@ -278,6 +320,9 @@ class ScopeSessionMiddleware:
|
|
|
278
320
|
# Extract request details for tracking
|
|
279
321
|
method = scope.get("method", "GET")
|
|
280
322
|
path = scope.get("path", "/")
|
|
323
|
+
|
|
324
|
+
# Set request context (URL, referrer, thread_id, user_agent) for LLM events
|
|
325
|
+
_set_request_context(headers, path)
|
|
281
326
|
query_string = scope.get("query_string", b"").decode("utf-8", errors="ignore")
|
|
282
327
|
query_params = dict(pair.split("=", 1) for pair in query_string.split("&") if "=" in pair) if query_string else {}
|
|
283
328
|
|
|
@@ -407,6 +452,9 @@ class FlaskScopeMiddleware:
|
|
|
407
452
|
# Extract request details for tracking
|
|
408
453
|
method = environ.get("REQUEST_METHOD", "GET")
|
|
409
454
|
path = environ.get("PATH_INFO", "/")
|
|
455
|
+
|
|
456
|
+
# Set request context (URL, referrer, thread_id, user_agent) for LLM events
|
|
457
|
+
_set_request_context(headers, path)
|
|
410
458
|
query_string = environ.get("QUERY_STRING", "")
|
|
411
459
|
query_params = dict(pair.split("=", 1) for pair in query_string.split("&") if "=" in pair) if query_string else {}
|
|
412
460
|
|
|
@@ -631,6 +679,9 @@ class DjangoScopeMiddleware:
|
|
|
631
679
|
# Extract request details for tracking
|
|
632
680
|
method = request.method
|
|
633
681
|
path = request.path
|
|
682
|
+
|
|
683
|
+
# Set request context (URL, referrer, thread_id, user_agent) for LLM events
|
|
684
|
+
_set_request_context(django_headers, path)
|
|
634
685
|
query_params = dict(request.GET.items()) if hasattr(request, 'GET') else {}
|
|
635
686
|
|
|
636
687
|
# Extract useful headers
|
|
@@ -11,7 +11,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
11
11
|
|
|
12
12
|
setup(
|
|
13
13
|
name="scope-analytics",
|
|
14
|
-
version="0.1.
|
|
14
|
+
version="0.1.1",
|
|
15
15
|
author="Scope AI",
|
|
16
16
|
author_email="support@scopeai.dev",
|
|
17
17
|
description="AI-powered analytics SDK for backend applications with automatic LLM tracking",
|
|
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
|
{scope_analytics-0.1.0 → scope_analytics-0.1.1}/scope_analytics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|