Skip to content

Commit 427ddb0

Browse files
authored
feat: Add envelope abstraction and session tracking (getsentry#627)
1 parent e680a75 commit 427ddb0

File tree

15 files changed

+930
-57
lines changed

15 files changed

+930
-57
lines changed

sentry_sdk/_types.py

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Optional
1313
from typing import Tuple
1414
from typing import Type
15+
from typing_extensions import Literal
1516

1617
ExcInfo = Tuple[
1718
Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]
@@ -29,3 +30,8 @@
2930

3031
# https://github.com/python/mypy/issues/5710
3132
NotImplementedType = Any
33+
34+
EventDataCategory = Literal[
35+
"default", "error", "crash", "transaction", "security", "attachment", "session"
36+
]
37+
SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]

sentry_sdk/client.py

+75
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@
1818
from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO, ClientConstructor
1919
from sentry_sdk.integrations import setup_integrations
2020
from sentry_sdk.utils import ContextVar
21+
from sentry_sdk.sessions import SessionFlusher
22+
from sentry_sdk.envelope import Envelope
2123

2224
from sentry_sdk._types import MYPY
2325

2426
if MYPY:
2527
from typing import Any
2628
from typing import Callable
2729
from typing import Dict
30+
from typing import List
2831
from typing import Optional
2932

3033
from sentry_sdk.scope import Scope
3134
from sentry_sdk._types import Event, Hint
35+
from sentry_sdk.sessions import Session
3236

3337

3438
_client_init_debug = ContextVar("client_init_debug")
@@ -91,9 +95,20 @@ def __setstate__(self, state):
9195
def _init_impl(self):
9296
# type: () -> None
9397
old_debug = _client_init_debug.get(False)
98+
99+
def _send_sessions(sessions):
100+
# type: (List[Any]) -> None
101+
transport = self.transport
102+
if sessions and transport:
103+
envelope = Envelope()
104+
for session in sessions:
105+
envelope.add_session(session)
106+
transport.capture_envelope(envelope)
107+
94108
try:
95109
_client_init_debug.set(self.options["debug"])
96110
self.transport = make_transport(self.options)
111+
self.session_flusher = SessionFlusher(flush_func=_send_sessions)
97112

98113
request_bodies = ("always", "never", "small", "medium")
99114
if self.options["request_bodies"] not in request_bodies:
@@ -230,6 +245,48 @@ def _should_capture(
230245

231246
return True
232247

248+
def _update_session_from_event(
249+
self,
250+
session, # type: Session
251+
event, # type: Event
252+
):
253+
# type: (...) -> None
254+
255+
crashed = False
256+
errored = False
257+
user_agent = None
258+
259+
# Figure out if this counts as an error and if we should mark the
260+
# session as crashed.
261+
level = event.get("level")
262+
if level == "fatal":
263+
crashed = True
264+
if not crashed:
265+
exceptions = (event.get("exception") or {}).get("values")
266+
if exceptions:
267+
errored = True
268+
for error in exceptions:
269+
mechanism = error.get("mechanism")
270+
if mechanism and mechanism.get("handled") is False:
271+
crashed = True
272+
break
273+
274+
user = event.get("user")
275+
276+
if session.user_agent is None:
277+
headers = (event.get("request") or {}).get("headers")
278+
for (k, v) in iteritems(headers or {}):
279+
if k.lower() == "user-agent":
280+
user_agent = v
281+
break
282+
283+
session.update(
284+
status="crashed" if crashed else None,
285+
user=user,
286+
user_agent=user_agent,
287+
errors=session.errors + (errored or crashed),
288+
)
289+
233290
def capture_event(
234291
self,
235292
event, # type: Event
@@ -260,9 +317,25 @@ def capture_event(
260317
event_opt = self._prepare_event(event, hint, scope)
261318
if event_opt is None:
262319
return None
320+
321+
# whenever we capture an event we also check if the session needs
322+
# to be updated based on that information.
323+
session = scope.session if scope else None
324+
if session:
325+
self._update_session_from_event(session, event)
326+
263327
self.transport.capture_event(event_opt)
264328
return event_id
265329

330+
def capture_session(
331+
self, session # type: Session
332+
):
333+
# type: (...) -> None
334+
if not session.release:
335+
logger.info("Discarded session update because of missing release")
336+
else:
337+
self.session_flusher.add_session(session)
338+
266339
def close(
267340
self,
268341
timeout=None, # type: Optional[float]
@@ -275,6 +348,7 @@ def close(
275348
"""
276349
if self.transport is not None:
277350
self.flush(timeout=timeout, callback=callback)
351+
self.session_flusher.kill()
278352
self.transport.kill()
279353
self.transport = None
280354

@@ -294,6 +368,7 @@ def flush(
294368
if self.transport is not None:
295369
if timeout is None:
296370
timeout = self.options["shutdown_timeout"]
371+
self.session_flusher.flush()
297372
self.transport.flush(timeout=timeout, callback=callback)
298373

299374
def __enter__(self):

sentry_sdk/consts.py

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"max_spans": Optional[int],
2727
"record_sql_params": Optional[bool],
2828
"auto_enabling_integrations": Optional[bool],
29+
"auto_session_tracking": Optional[bool],
2930
},
3031
total=False,
3132
)

0 commit comments

Comments
 (0)