devinspector 2.1.0__tar.gz → 2.3.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.
- {devinspector-2.1.0 → devinspector-2.3.0}/PKG-INFO +1 -1
- {devinspector-2.1.0 → devinspector-2.3.0}/devinspector/core.py +123 -14
- {devinspector-2.1.0 → devinspector-2.3.0}/devinspector.egg-info/PKG-INFO +1 -1
- {devinspector-2.1.0 → devinspector-2.3.0}/pyproject.toml +1 -1
- {devinspector-2.1.0 → devinspector-2.3.0}/README.md +0 -0
- {devinspector-2.1.0 → devinspector-2.3.0}/devinspector/__init__.py +0 -0
- {devinspector-2.1.0 → devinspector-2.3.0}/devinspector/fastapi.py +0 -0
- {devinspector-2.1.0 → devinspector-2.3.0}/devinspector/flask.py +0 -0
- {devinspector-2.1.0 → devinspector-2.3.0}/devinspector.egg-info/SOURCES.txt +0 -0
- {devinspector-2.1.0 → devinspector-2.3.0}/devinspector.egg-info/dependency_links.txt +0 -0
- {devinspector-2.1.0 → devinspector-2.3.0}/devinspector.egg-info/requires.txt +0 -0
- {devinspector-2.1.0 → devinspector-2.3.0}/devinspector.egg-info/top_level.txt +0 -0
- {devinspector-2.1.0 → devinspector-2.3.0}/setup.cfg +0 -0
|
@@ -9,10 +9,9 @@ import asyncio
|
|
|
9
9
|
import functools
|
|
10
10
|
import contextvars
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
|
-
from typing import Any, Dict, Optional
|
|
12
|
+
from typing import Any, Dict, Optional, Union
|
|
13
13
|
|
|
14
14
|
# Variável de contexto para armazenar dados da requisição atual (APM)
|
|
15
|
-
# Substitui o papel do AsyncLocalStorage do Node.js
|
|
16
15
|
request_context = contextvars.ContextVar("request_context", default=None)
|
|
17
16
|
|
|
18
17
|
class AuditCore:
|
|
@@ -48,6 +47,29 @@ class AuditCore:
|
|
|
48
47
|
self.initialized = True
|
|
49
48
|
self._listen_global_errors()
|
|
50
49
|
|
|
50
|
+
def is_ingest_url(self, target_url: str) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Verifica dinamicamente se uma URL pertence ao servidor de ingestão do Dev Inspector.
|
|
53
|
+
Evita chamadas em loop em ambientes self-hosted (Coolify, VPS, etc).
|
|
54
|
+
"""
|
|
55
|
+
if not target_url:
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
if self.endpoint and target_url in self.endpoint or target_url in self.endpoint:
|
|
59
|
+
if target_url in self.endpoint or self.endpoint in target_url:
|
|
60
|
+
return True
|
|
61
|
+
from urllib.parse import urlparse
|
|
62
|
+
endpoint_obj = urlparse(self.endpoint)
|
|
63
|
+
return (
|
|
64
|
+
endpoint_obj.netloc in target_url or
|
|
65
|
+
endpoint_obj.path in target_url
|
|
66
|
+
)
|
|
67
|
+
except Exception:
|
|
68
|
+
return (
|
|
69
|
+
"devinspector.com.br" in target_url or
|
|
70
|
+
"/ingest/track" in target_url
|
|
71
|
+
)
|
|
72
|
+
|
|
51
73
|
def capture_request(
|
|
52
74
|
self,
|
|
53
75
|
method: str,
|
|
@@ -81,7 +103,7 @@ class AuditCore:
|
|
|
81
103
|
self, error: Exception, metadata: Optional[Dict[str, Any]] = None
|
|
82
104
|
) -> None:
|
|
83
105
|
raw_message = str(error) or "Erro Desconhecido"
|
|
84
|
-
raw_stack = "".join(traceback.format_tb(error.__traceback__))
|
|
106
|
+
raw_stack = "".join(traceback.format_tb(error.__traceback__)) if error.__traceback__ else str(error)
|
|
85
107
|
|
|
86
108
|
meta = metadata.copy() if metadata else {}
|
|
87
109
|
meta.update(
|
|
@@ -148,9 +170,7 @@ class AuditCore:
|
|
|
148
170
|
|
|
149
171
|
sys.excepthook = custom_excepthook
|
|
150
172
|
|
|
151
|
-
# Captura exceções em Threads secundárias (Python 3.8+)
|
|
152
173
|
if hasattr(threading, "excepthook"):
|
|
153
|
-
|
|
154
174
|
def custom_thread_excepthook(args):
|
|
155
175
|
self.capture_error(
|
|
156
176
|
args.exc_value,
|
|
@@ -162,7 +182,10 @@ class AuditCore:
|
|
|
162
182
|
|
|
163
183
|
threading.excepthook = custom_thread_excepthook
|
|
164
184
|
|
|
165
|
-
def _truncate(self, value: Any, max_length: int = 5000) -> Any:
|
|
185
|
+
def _truncate(self, value: Any, max_length: int = 5000, seen=None) -> Any:
|
|
186
|
+
if seen is None:
|
|
187
|
+
seen = set()
|
|
188
|
+
|
|
166
189
|
if isinstance(value, str):
|
|
167
190
|
return (
|
|
168
191
|
value[:max_length] + "... [truncated]"
|
|
@@ -171,9 +194,26 @@ class AuditCore:
|
|
|
171
194
|
)
|
|
172
195
|
|
|
173
196
|
if isinstance(value, dict):
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
197
|
+
obj_id = id(value)
|
|
198
|
+
if obj_id in seen:
|
|
199
|
+
return "[Circular Reference]"
|
|
200
|
+
seen.add(obj_id)
|
|
201
|
+
|
|
202
|
+
truncated_obj = {}
|
|
203
|
+
for k, v in value.items():
|
|
204
|
+
truncated_obj[str(k)] = self._truncate(v, max_length, seen)
|
|
205
|
+
seen.remove(obj_id)
|
|
206
|
+
return truncated_obj
|
|
207
|
+
|
|
208
|
+
if isinstance(value, (list, tuple, set)):
|
|
209
|
+
obj_id = id(value)
|
|
210
|
+
if obj_id in seen:
|
|
211
|
+
return "[Circular Reference]"
|
|
212
|
+
seen.add(obj_id)
|
|
213
|
+
|
|
214
|
+
truncated_list = [self._truncate(item, max_length, seen) for item in value]
|
|
215
|
+
seen.remove(obj_id)
|
|
216
|
+
return truncated_list
|
|
177
217
|
|
|
178
218
|
return value
|
|
179
219
|
|
|
@@ -214,10 +254,8 @@ class AuditCore:
|
|
|
214
254
|
with urllib.request.urlopen(req, timeout=5):
|
|
215
255
|
pass
|
|
216
256
|
except urllib.error.HTTPError as e:
|
|
217
|
-
# Mostra o erro detalhado que o painel devolveu
|
|
218
257
|
error_body = e.read().decode('utf-8', errors='ignore')
|
|
219
258
|
print(f"[DevInspector] HTTP {e.code} do painel: {e.reason} - Resposta: {error_body}")
|
|
220
|
-
print(f"[DevInspector] Payload enviado que gerou o erro: {payload}")
|
|
221
259
|
except Exception as err:
|
|
222
260
|
print(f"[DevInspector] Falha ao enviar requisição para o painel: {err}")
|
|
223
261
|
|
|
@@ -278,7 +316,78 @@ def audit_operation(name: str, threshold_ms: int = 300):
|
|
|
278
316
|
duration_ms = (time.perf_counter() - start) * 1000
|
|
279
317
|
audit.capture_exception(e, {"operationName": name, "durationMs": round(duration_ms)})
|
|
280
318
|
raise
|
|
281
|
-
|
|
282
|
-
# Retorna o wrapper correto dependendo se a função original é async ou não
|
|
319
|
+
|
|
283
320
|
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
|
|
284
|
-
return decorator
|
|
321
|
+
return decorator
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def core_audit_fetch(
|
|
325
|
+
url: str,
|
|
326
|
+
method: str = "GET",
|
|
327
|
+
headers: Optional[Dict[str, str]] = None,
|
|
328
|
+
data: Optional[Any] = None,
|
|
329
|
+
timeout: float = 10.0,
|
|
330
|
+
error_type: str = "network_error",
|
|
331
|
+
default_user_agent: str = "",
|
|
332
|
+
) -> Any:
|
|
333
|
+
"""
|
|
334
|
+
Helper centralizado de Intercepção de Fetch/Requisições HTTP para APM em Python.
|
|
335
|
+
Equivalente ao coreAuditFetch do SDK TypeScript.
|
|
336
|
+
"""
|
|
337
|
+
if url and audit.is_ingest_url(url):
|
|
338
|
+
# Execução direta sem rastreamento para evitar loop
|
|
339
|
+
req = urllib.request.Request(url, data=data, headers=headers or {}, method=method)
|
|
340
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
341
|
+
return response
|
|
342
|
+
|
|
343
|
+
start = time.perf_counter()
|
|
344
|
+
try:
|
|
345
|
+
# Exemplo usando urllib síncrono ou adaptado para chamadas
|
|
346
|
+
# (Para puro asyncio, pode-se integrar com httpx caso prefira)
|
|
347
|
+
req_headers = headers or {}
|
|
348
|
+
if default_user_agent and "User-Agent" not in req_headers:
|
|
349
|
+
req_headers["User-Agent"] = default_user_agent
|
|
350
|
+
|
|
351
|
+
req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
|
|
352
|
+
|
|
353
|
+
# Executa em executor separado para não travar o loop async se chamado em contexto assíncrono
|
|
354
|
+
loop = asyncio.get_running_loop()
|
|
355
|
+
response = await loop.run_in_executor(
|
|
356
|
+
None, lambda: urllib.request.urlopen(req, timeout=timeout)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
360
|
+
status_code = response.getcode()
|
|
361
|
+
|
|
362
|
+
if status_code >= 400 or duration_ms >= 300:
|
|
363
|
+
audit.capture_request(
|
|
364
|
+
method=method,
|
|
365
|
+
url=url,
|
|
366
|
+
status_code=status_code,
|
|
367
|
+
duration_ms=duration_ms,
|
|
368
|
+
route=url,
|
|
369
|
+
user_agent=req_headers.get("User-Agent", default_user_agent),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return response
|
|
373
|
+
except Exception as error:
|
|
374
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
375
|
+
|
|
376
|
+
status_code = 0
|
|
377
|
+
if hasattr(error, "code"):
|
|
378
|
+
status_code = error.code
|
|
379
|
+
|
|
380
|
+
audit.capture_request(
|
|
381
|
+
method=method,
|
|
382
|
+
url=url,
|
|
383
|
+
status_code=status_code,
|
|
384
|
+
duration_ms=duration_ms,
|
|
385
|
+
route=url,
|
|
386
|
+
user_agent=default_user_agent,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
audit.capture_exception(
|
|
390
|
+
error if isinstance(error, Exception) else Exception(str(error)),
|
|
391
|
+
{"url": url, "method": method, "type": error_type},
|
|
392
|
+
)
|
|
393
|
+
raise error
|
|
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
|