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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devinspector
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: SDK Python de monitoramento e auditoria em tempo real | DevInspector
5
5
  Author-email: DevInspector <suporte@devinspector.com.br>
6
6
  License: MIT
@@ -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
- return {
175
- str(k): self._truncate(v, max_length) for k, v in value.items()
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devinspector
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: SDK Python de monitoramento e auditoria em tempo real | DevInspector
5
5
  Author-email: DevInspector <suporte@devinspector.com.br>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devinspector"
7
- version = "2.1.0"
7
+ version = "2.3.0"
8
8
  description = "SDK Python de monitoramento e auditoria em tempo real | DevInspector"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
File without changes
File without changes