scope-analytics 0.1.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.
- scope_analytics/__init__.py +244 -0
- scope_analytics/auto.py +335 -0
- scope_analytics/cli.py +240 -0
- scope_analytics/client.py +121 -0
- scope_analytics/config.py +101 -0
- scope_analytics/context.py +173 -0
- scope_analytics/events.py +275 -0
- scope_analytics/middleware.py +669 -0
- scope_analytics/patches/__init__.py +9 -0
- scope_analytics/patches/anthropic_patch.py +430 -0
- scope_analytics/patches/gemini_patch.py +422 -0
- scope_analytics/patches/openai_patch.py +483 -0
- scope_analytics/queue.py +158 -0
- scope_analytics-0.1.0.dist-info/METADATA +253 -0
- scope_analytics-0.1.0.dist-info/RECORD +19 -0
- scope_analytics-0.1.0.dist-info/WHEEL +5 -0
- scope_analytics-0.1.0.dist-info/entry_points.txt +2 -0
- scope_analytics-0.1.0.dist-info/licenses/LICENSE +21 -0
- scope_analytics-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scope Analytics - Backend SDK
|
|
3
|
+
AI-powered analytics with automatic LLM conversation tracking
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import atexit
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .config import ScopeConfig
|
|
10
|
+
from .context import ScopeContext
|
|
11
|
+
from .events import EventFormatter
|
|
12
|
+
from .queue import EventQueue
|
|
13
|
+
from .client import ScopeAPIClient
|
|
14
|
+
from .patches.openai_patch import OpenAIPatcher
|
|
15
|
+
from .patches.anthropic_patch import AnthropicPatcher
|
|
16
|
+
from .patches.gemini_patch import GeminiPatcher
|
|
17
|
+
from .middleware import (
|
|
18
|
+
ScopeSessionMiddleware,
|
|
19
|
+
FlaskScopeMiddleware,
|
|
20
|
+
init_flask_session_tracking,
|
|
21
|
+
DjangoScopeMiddleware,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ScopeAnalytics",
|
|
27
|
+
"ScopeContext",
|
|
28
|
+
"get_sdk_instance",
|
|
29
|
+
# Middleware exports
|
|
30
|
+
"ScopeSessionMiddleware", # For FastAPI/Starlette
|
|
31
|
+
"FlaskScopeMiddleware", # For Flask (WSGI wrapper)
|
|
32
|
+
"init_flask_session_tracking", # For Flask (hooks approach)
|
|
33
|
+
"DjangoScopeMiddleware", # For Django
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Global SDK instance reference for middleware access
|
|
37
|
+
# This allows middleware to capture HTTP request events without requiring
|
|
38
|
+
# explicit SDK reference in user code
|
|
39
|
+
_global_sdk_instance: Optional["ScopeAnalytics"] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_sdk_instance() -> Optional["ScopeAnalytics"]:
|
|
43
|
+
"""
|
|
44
|
+
Get the global SDK instance.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
ScopeAnalytics instance if initialized, None otherwise
|
|
48
|
+
"""
|
|
49
|
+
return _global_sdk_instance
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _set_sdk_instance(instance: "ScopeAnalytics") -> None:
|
|
53
|
+
"""
|
|
54
|
+
Set the global SDK instance.
|
|
55
|
+
Called internally when ScopeAnalytics is initialized.
|
|
56
|
+
"""
|
|
57
|
+
global _global_sdk_instance
|
|
58
|
+
_global_sdk_instance = instance
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ScopeAnalytics:
|
|
62
|
+
"""
|
|
63
|
+
Main SDK class for Scope Analytics
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
scope = ScopeAnalytics(api_key="sk_live_...")
|
|
67
|
+
|
|
68
|
+
# SDK automatically patches OpenAI, Anthropic, LangChain
|
|
69
|
+
# All LLM calls are captured and sent to Scope AI
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
api_key: Optional[str] = None,
|
|
75
|
+
endpoint: Optional[str] = None,
|
|
76
|
+
auto_patch: bool = True,
|
|
77
|
+
batch_size: int = 10,
|
|
78
|
+
batch_timeout_seconds: int = 5,
|
|
79
|
+
max_queue_size: int = 1000,
|
|
80
|
+
debug: bool = False,
|
|
81
|
+
environment: Optional[str] = None,
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
Initialize Scope Analytics SDK
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
api_key: Secret API key (sk_live_... or sk_test_...)
|
|
88
|
+
endpoint: API endpoint URL (default: https://api.scopeai.dev)
|
|
89
|
+
auto_patch: Automatically patch LLM libraries (default: True)
|
|
90
|
+
batch_size: Events per batch (default: 10)
|
|
91
|
+
batch_timeout_seconds: Max wait before sending partial batch (default: 5)
|
|
92
|
+
max_queue_size: Max events to queue (default: 1000)
|
|
93
|
+
debug: Enable debug logging (default: False)
|
|
94
|
+
environment: Environment name (default: production)
|
|
95
|
+
"""
|
|
96
|
+
# Initialize configuration
|
|
97
|
+
self.config = ScopeConfig(
|
|
98
|
+
api_key=api_key,
|
|
99
|
+
endpoint=endpoint,
|
|
100
|
+
auto_patch=auto_patch,
|
|
101
|
+
batch_size=batch_size,
|
|
102
|
+
batch_timeout_seconds=batch_timeout_seconds,
|
|
103
|
+
max_queue_size=max_queue_size,
|
|
104
|
+
debug=debug,
|
|
105
|
+
environment=environment,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Initialize components
|
|
109
|
+
self.event_formatter = EventFormatter(self.config)
|
|
110
|
+
self.client = ScopeAPIClient(self.config)
|
|
111
|
+
self.queue = EventQueue(
|
|
112
|
+
batch_size=self.config.batch_size,
|
|
113
|
+
batch_timeout_seconds=self.config.batch_timeout_seconds,
|
|
114
|
+
max_queue_size=self.config.max_queue_size,
|
|
115
|
+
flush_callback=self._flush_events,
|
|
116
|
+
config=self.config,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Initialize patchers
|
|
120
|
+
self.openai_patcher = OpenAIPatcher(self)
|
|
121
|
+
self.anthropic_patcher = AnthropicPatcher(self)
|
|
122
|
+
self.gemini_patcher = GeminiPatcher(self)
|
|
123
|
+
self.patches = [] # List of successfully applied patches
|
|
124
|
+
|
|
125
|
+
# Start queue background thread
|
|
126
|
+
self.queue.start()
|
|
127
|
+
|
|
128
|
+
# Register shutdown hook
|
|
129
|
+
atexit.register(self.shutdown)
|
|
130
|
+
|
|
131
|
+
self.config.log("Scope Analytics SDK initialized")
|
|
132
|
+
self.config.log(f"Configuration: {self.config.to_dict()}")
|
|
133
|
+
|
|
134
|
+
# Register as global instance for middleware access
|
|
135
|
+
_set_sdk_instance(self)
|
|
136
|
+
|
|
137
|
+
# Auto-patch LLM libraries if enabled
|
|
138
|
+
if self.config.auto_patch:
|
|
139
|
+
self._apply_patches()
|
|
140
|
+
|
|
141
|
+
def _apply_patches(self):
|
|
142
|
+
"""Apply monkey patches to LLM libraries"""
|
|
143
|
+
self.config.log("Auto-patching enabled - will patch LLM libraries")
|
|
144
|
+
|
|
145
|
+
# Patch OpenAI
|
|
146
|
+
try:
|
|
147
|
+
import openai
|
|
148
|
+
self.config.log("OpenAI library detected - patching...")
|
|
149
|
+
if self.openai_patcher.patch():
|
|
150
|
+
self.patches.append('openai')
|
|
151
|
+
except ImportError:
|
|
152
|
+
self.config.log("OpenAI library not installed - skipping patch")
|
|
153
|
+
|
|
154
|
+
# Patch Anthropic
|
|
155
|
+
try:
|
|
156
|
+
import anthropic
|
|
157
|
+
self.config.log("Anthropic library detected - patching...")
|
|
158
|
+
if self.anthropic_patcher.patch():
|
|
159
|
+
self.patches.append('anthropic')
|
|
160
|
+
except ImportError:
|
|
161
|
+
self.config.log("Anthropic library not installed - skipping patch")
|
|
162
|
+
|
|
163
|
+
# Patch Google Gemini
|
|
164
|
+
try:
|
|
165
|
+
import google.generativeai
|
|
166
|
+
self.config.log("Google Generative AI library detected - patching...")
|
|
167
|
+
if self.gemini_patcher.patch():
|
|
168
|
+
self.patches.append('gemini')
|
|
169
|
+
except ImportError:
|
|
170
|
+
self.config.log("Google Generative AI library not installed - skipping patch")
|
|
171
|
+
|
|
172
|
+
def track_event(self, event_type: str, properties: dict):
|
|
173
|
+
"""
|
|
174
|
+
Manually track an event
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
event_type: Type of event (e.g., "llm_call", "external_api_call")
|
|
178
|
+
properties: Event properties
|
|
179
|
+
"""
|
|
180
|
+
self.config.log(f"Tracking event: {event_type}")
|
|
181
|
+
|
|
182
|
+
# Create event with standard fields
|
|
183
|
+
event = {
|
|
184
|
+
"event_type": event_type,
|
|
185
|
+
"source": self.config.sdk_source,
|
|
186
|
+
**properties
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Validate and enqueue
|
|
190
|
+
if self.event_formatter.validate_event(event):
|
|
191
|
+
self.queue.enqueue(event)
|
|
192
|
+
|
|
193
|
+
def identify(self, user_id: str, traits: Optional[dict] = None):
|
|
194
|
+
"""
|
|
195
|
+
Identify a user and optionally set traits
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
user_id: Unique user identifier
|
|
199
|
+
traits: Optional user traits (e.g., email, name, plan)
|
|
200
|
+
"""
|
|
201
|
+
ScopeContext.set_user_id(user_id)
|
|
202
|
+
self.config.log(f"Identified user: {user_id}")
|
|
203
|
+
|
|
204
|
+
# TODO: Send identify event to API
|
|
205
|
+
if traits:
|
|
206
|
+
self.config.log(f"User traits: {traits}")
|
|
207
|
+
|
|
208
|
+
def _flush_events(self, events: list):
|
|
209
|
+
"""
|
|
210
|
+
Callback for flushing events to API
|
|
211
|
+
Called by event queue when batch is ready
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
events: List of events to flush
|
|
215
|
+
"""
|
|
216
|
+
success = self.client.ship_events(events)
|
|
217
|
+
|
|
218
|
+
if not success:
|
|
219
|
+
self.config.log(f"⚠️ Failed to ship {len(events)} events")
|
|
220
|
+
|
|
221
|
+
def shutdown(self):
|
|
222
|
+
"""
|
|
223
|
+
Gracefully shutdown SDK
|
|
224
|
+
Flushes remaining events and cleans up resources
|
|
225
|
+
"""
|
|
226
|
+
self.config.log("Shutting down Scope Analytics SDK...")
|
|
227
|
+
|
|
228
|
+
# Stop queue and flush remaining events
|
|
229
|
+
if self.queue:
|
|
230
|
+
self.queue.stop()
|
|
231
|
+
|
|
232
|
+
# Close HTTP client
|
|
233
|
+
if self.client:
|
|
234
|
+
self.client.close()
|
|
235
|
+
|
|
236
|
+
# Remove patches
|
|
237
|
+
if 'openai' in self.patches:
|
|
238
|
+
self.openai_patcher.unpatch()
|
|
239
|
+
if 'anthropic' in self.patches:
|
|
240
|
+
self.anthropic_patcher.unpatch()
|
|
241
|
+
if 'gemini' in self.patches:
|
|
242
|
+
self.gemini_patcher.unpatch()
|
|
243
|
+
|
|
244
|
+
self.config.log("Scope Analytics SDK shutdown complete")
|
scope_analytics/auto.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-instrumentation module for Scope Analytics
|
|
3
|
+
|
|
4
|
+
Import this module to automatically initialize Scope Analytics
|
|
5
|
+
with configuration from environment variables.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
import scope_analytics.auto # That's it!
|
|
9
|
+
|
|
10
|
+
Or via CLI:
|
|
11
|
+
scope-run python app.py
|
|
12
|
+
|
|
13
|
+
Environment Variables:
|
|
14
|
+
SCOPE_API_KEY: Required. Your Scope Analytics API key
|
|
15
|
+
SCOPE_ENDPOINT: Optional. Custom API endpoint
|
|
16
|
+
SCOPE_DEBUG: Optional. Set to 'true' for debug logging
|
|
17
|
+
SCOPE_ENVIRONMENT: Optional. Environment name (default: production)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import warnings
|
|
23
|
+
import atexit
|
|
24
|
+
|
|
25
|
+
# Only initialize once
|
|
26
|
+
_initialized = False
|
|
27
|
+
_scope_instance = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _log(message: str) -> None:
|
|
31
|
+
"""Log a message if debug mode is enabled"""
|
|
32
|
+
if os.environ.get('SCOPE_DEBUG', '').lower() == 'true':
|
|
33
|
+
print(f"[Scope SDK] {message}", file=sys.stderr)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _auto_init():
|
|
37
|
+
"""
|
|
38
|
+
Automatically initialize Scope Analytics from environment variables.
|
|
39
|
+
|
|
40
|
+
This function:
|
|
41
|
+
1. Reads configuration from environment variables
|
|
42
|
+
2. Initializes ScopeAnalytics with auto-patching enabled
|
|
43
|
+
3. Sets up auto-middleware injection for known frameworks
|
|
44
|
+
"""
|
|
45
|
+
global _initialized, _scope_instance
|
|
46
|
+
|
|
47
|
+
if _initialized:
|
|
48
|
+
return _scope_instance
|
|
49
|
+
|
|
50
|
+
_initialized = True # Set early to prevent re-entry
|
|
51
|
+
|
|
52
|
+
api_key = os.environ.get('SCOPE_API_KEY')
|
|
53
|
+
|
|
54
|
+
if not api_key:
|
|
55
|
+
# No API key - skip initialization with a warning
|
|
56
|
+
warnings.warn(
|
|
57
|
+
"SCOPE_API_KEY not set - Scope Analytics disabled. "
|
|
58
|
+
"Set SCOPE_API_KEY environment variable to enable tracking.",
|
|
59
|
+
UserWarning,
|
|
60
|
+
stacklevel=2
|
|
61
|
+
)
|
|
62
|
+
_log("No API key found - instrumentation disabled")
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
from scope_analytics import ScopeAnalytics
|
|
67
|
+
|
|
68
|
+
_log("Initializing Scope Analytics...")
|
|
69
|
+
|
|
70
|
+
_scope_instance = ScopeAnalytics(
|
|
71
|
+
api_key=api_key,
|
|
72
|
+
endpoint=os.environ.get('SCOPE_ENDPOINT'),
|
|
73
|
+
debug=os.environ.get('SCOPE_DEBUG', '').lower() == 'true',
|
|
74
|
+
environment=os.environ.get('SCOPE_ENVIRONMENT', 'production'),
|
|
75
|
+
auto_patch=True, # Always auto-patch in auto mode
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
_log("Scope Analytics initialized successfully")
|
|
79
|
+
|
|
80
|
+
# Auto-inject session middleware for known frameworks
|
|
81
|
+
_auto_inject_middleware()
|
|
82
|
+
|
|
83
|
+
return _scope_instance
|
|
84
|
+
|
|
85
|
+
except ValueError as e:
|
|
86
|
+
# Invalid API key format or missing required config
|
|
87
|
+
warnings.warn(
|
|
88
|
+
f"Scope Analytics initialization failed: {e}",
|
|
89
|
+
UserWarning,
|
|
90
|
+
stacklevel=2
|
|
91
|
+
)
|
|
92
|
+
_log(f"Initialization failed: {e}")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
# Unexpected error - don't crash the user's app
|
|
97
|
+
warnings.warn(
|
|
98
|
+
f"Scope Analytics initialization failed unexpectedly: {e}",
|
|
99
|
+
UserWarning,
|
|
100
|
+
stacklevel=2
|
|
101
|
+
)
|
|
102
|
+
_log(f"Unexpected error during initialization: {e}")
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _auto_inject_middleware():
|
|
107
|
+
"""
|
|
108
|
+
Automatically inject session middleware for known frameworks.
|
|
109
|
+
This happens at RUNTIME - no user code changes required.
|
|
110
|
+
|
|
111
|
+
Supports: FastAPI/Starlette, Flask, Django
|
|
112
|
+
|
|
113
|
+
The middleware extracts X-Scope-Session-ID headers and makes
|
|
114
|
+
session correlation work automatically between frontend and backend.
|
|
115
|
+
"""
|
|
116
|
+
_log("Setting up auto-middleware injection...")
|
|
117
|
+
|
|
118
|
+
injected = False
|
|
119
|
+
|
|
120
|
+
# Try FastAPI/Starlette
|
|
121
|
+
if _try_inject_fastapi():
|
|
122
|
+
_log("Prepared session middleware injection for FastAPI/Starlette")
|
|
123
|
+
injected = True
|
|
124
|
+
|
|
125
|
+
# Try Flask
|
|
126
|
+
if _try_inject_flask():
|
|
127
|
+
_log("Prepared session hooks injection for Flask")
|
|
128
|
+
injected = True
|
|
129
|
+
|
|
130
|
+
# Try Django
|
|
131
|
+
if _try_inject_django():
|
|
132
|
+
_log("Prepared session middleware injection for Django")
|
|
133
|
+
injected = True
|
|
134
|
+
|
|
135
|
+
if not injected:
|
|
136
|
+
_log(
|
|
137
|
+
"No known framework detected - session middleware not auto-injected. "
|
|
138
|
+
"LLM tracking still works. For session correlation with custom frameworks, "
|
|
139
|
+
"see docs on manual ScopeContext.set_session_id()"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _try_inject_fastapi() -> bool:
|
|
144
|
+
"""
|
|
145
|
+
Inject session middleware into FastAPI/Starlette apps at runtime.
|
|
146
|
+
|
|
147
|
+
We patch the __init__ method of FastAPI and Starlette classes
|
|
148
|
+
so that when the user creates an app, our middleware is automatically added.
|
|
149
|
+
"""
|
|
150
|
+
patched = False
|
|
151
|
+
|
|
152
|
+
# Try patching Starlette first (FastAPI extends it)
|
|
153
|
+
try:
|
|
154
|
+
from starlette.applications import Starlette
|
|
155
|
+
|
|
156
|
+
if not hasattr(Starlette.__init__, '_scope_patched'):
|
|
157
|
+
original_starlette_init = Starlette.__init__
|
|
158
|
+
|
|
159
|
+
def patched_starlette_init(self, *args, **kwargs):
|
|
160
|
+
original_starlette_init(self, *args, **kwargs)
|
|
161
|
+
# Add our middleware after app is initialized
|
|
162
|
+
try:
|
|
163
|
+
from scope_analytics.middleware import ScopeSessionMiddleware
|
|
164
|
+
self.add_middleware(ScopeSessionMiddleware)
|
|
165
|
+
_log(f"Auto-injected ScopeSessionMiddleware into Starlette app")
|
|
166
|
+
except Exception as e:
|
|
167
|
+
_log(f"Failed to inject middleware into Starlette: {e}")
|
|
168
|
+
|
|
169
|
+
patched_starlette_init._scope_patched = True
|
|
170
|
+
Starlette.__init__ = patched_starlette_init
|
|
171
|
+
patched = True
|
|
172
|
+
_log("Patched Starlette.__init__ for auto-middleware injection")
|
|
173
|
+
|
|
174
|
+
except ImportError:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
# Also patch FastAPI specifically (more common)
|
|
178
|
+
try:
|
|
179
|
+
from fastapi import FastAPI
|
|
180
|
+
|
|
181
|
+
if not hasattr(FastAPI.__init__, '_scope_patched'):
|
|
182
|
+
original_fastapi_init = FastAPI.__init__
|
|
183
|
+
|
|
184
|
+
def patched_fastapi_init(self, *args, **kwargs):
|
|
185
|
+
original_fastapi_init(self, *args, **kwargs)
|
|
186
|
+
# Add our middleware after app is initialized
|
|
187
|
+
try:
|
|
188
|
+
from scope_analytics.middleware import ScopeSessionMiddleware
|
|
189
|
+
# Check if middleware already added (from Starlette patch)
|
|
190
|
+
has_scope_middleware = any(
|
|
191
|
+
getattr(m, 'cls', None) == ScopeSessionMiddleware
|
|
192
|
+
for m in getattr(self, 'user_middleware', [])
|
|
193
|
+
)
|
|
194
|
+
if not has_scope_middleware:
|
|
195
|
+
self.add_middleware(ScopeSessionMiddleware)
|
|
196
|
+
_log(f"Auto-injected ScopeSessionMiddleware into FastAPI app")
|
|
197
|
+
except Exception as e:
|
|
198
|
+
_log(f"Failed to inject middleware into FastAPI: {e}")
|
|
199
|
+
|
|
200
|
+
patched_fastapi_init._scope_patched = True
|
|
201
|
+
FastAPI.__init__ = patched_fastapi_init
|
|
202
|
+
patched = True
|
|
203
|
+
_log("Patched FastAPI.__init__ for auto-middleware injection")
|
|
204
|
+
|
|
205
|
+
except ImportError:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
return patched
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _try_inject_flask() -> bool:
|
|
212
|
+
"""
|
|
213
|
+
Inject session hooks into Flask apps at runtime.
|
|
214
|
+
|
|
215
|
+
We patch Flask's __init__ to register before/after request hooks
|
|
216
|
+
that handle session ID extraction.
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
from flask import Flask
|
|
220
|
+
|
|
221
|
+
if hasattr(Flask.__init__, '_scope_patched'):
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
original_flask_init = Flask.__init__
|
|
225
|
+
|
|
226
|
+
def patched_flask_init(self, *args, **kwargs):
|
|
227
|
+
original_flask_init(self, *args, **kwargs)
|
|
228
|
+
# Register session tracking hooks after app is created
|
|
229
|
+
try:
|
|
230
|
+
from scope_analytics.middleware import init_flask_session_tracking
|
|
231
|
+
init_flask_session_tracking(self)
|
|
232
|
+
_log(f"Auto-injected session hooks into Flask app")
|
|
233
|
+
except Exception as e:
|
|
234
|
+
_log(f"Failed to inject session hooks into Flask: {e}")
|
|
235
|
+
|
|
236
|
+
patched_flask_init._scope_patched = True
|
|
237
|
+
Flask.__init__ = patched_flask_init
|
|
238
|
+
_log("Patched Flask.__init__ for auto-middleware injection")
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
except ImportError:
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _try_inject_django() -> bool:
|
|
246
|
+
"""
|
|
247
|
+
Inject session middleware into Django apps at runtime.
|
|
248
|
+
|
|
249
|
+
Django is trickier - middleware is configured in settings.py.
|
|
250
|
+
We hook into Django's setup to add our middleware.
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
import django
|
|
254
|
+
from django.conf import settings
|
|
255
|
+
|
|
256
|
+
# Check if Django is configured
|
|
257
|
+
if not settings.configured:
|
|
258
|
+
# Django not configured yet - set up a hook for when it is
|
|
259
|
+
_log("Django detected but not configured - deferring middleware injection")
|
|
260
|
+
|
|
261
|
+
# We can hook into django.setup() to add middleware when called
|
|
262
|
+
if not hasattr(django.setup, '_scope_patched'):
|
|
263
|
+
original_setup = django.setup
|
|
264
|
+
|
|
265
|
+
def patched_setup(*args, **kwargs):
|
|
266
|
+
result = original_setup(*args, **kwargs)
|
|
267
|
+
_inject_django_middleware()
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
patched_setup._scope_patched = True
|
|
271
|
+
django.setup = patched_setup
|
|
272
|
+
_log("Patched django.setup() for deferred middleware injection")
|
|
273
|
+
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
# Django is already configured - inject middleware now
|
|
277
|
+
return _inject_django_middleware()
|
|
278
|
+
|
|
279
|
+
except ImportError:
|
|
280
|
+
return False
|
|
281
|
+
except Exception as e:
|
|
282
|
+
_log(f"Error setting up Django middleware injection: {e}")
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _inject_django_middleware() -> bool:
|
|
287
|
+
"""
|
|
288
|
+
Actually inject the middleware into Django's MIDDLEWARE setting.
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
from django.conf import settings
|
|
292
|
+
|
|
293
|
+
if not settings.configured:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
middleware_class = 'scope_analytics.middleware.DjangoScopeMiddleware'
|
|
297
|
+
|
|
298
|
+
if hasattr(settings, 'MIDDLEWARE'):
|
|
299
|
+
middleware_list = list(settings.MIDDLEWARE)
|
|
300
|
+
if middleware_class not in middleware_list:
|
|
301
|
+
# Insert at the beginning for earliest access to request
|
|
302
|
+
middleware_list.insert(0, middleware_class)
|
|
303
|
+
settings.MIDDLEWARE = middleware_list
|
|
304
|
+
_log(f"Auto-injected {middleware_class} into Django MIDDLEWARE")
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
_log(f"Failed to inject Django middleware: {e}")
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_instance():
|
|
315
|
+
"""
|
|
316
|
+
Get the auto-initialized ScopeAnalytics instance.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
ScopeAnalytics instance or None if not initialized
|
|
320
|
+
"""
|
|
321
|
+
return _scope_instance
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def is_initialized() -> bool:
|
|
325
|
+
"""
|
|
326
|
+
Check if auto-instrumentation has been initialized.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
True if initialized, False otherwise
|
|
330
|
+
"""
|
|
331
|
+
return _initialized and _scope_instance is not None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# Auto-initialize on import
|
|
335
|
+
_auto_init()
|