275 lines
9 KiB
Python
275 lines
9 KiB
Python
import os
|
|
import warnings
|
|
from threading import Thread, Lock, Event
|
|
from contextlib import contextmanager
|
|
|
|
import sentry_sdk
|
|
from sentry_sdk.envelope import Envelope
|
|
from sentry_sdk.session import Session
|
|
from sentry_sdk.utils import format_timestamp
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Any
|
|
from typing import Callable
|
|
from typing import Dict
|
|
from typing import Generator
|
|
from typing import List
|
|
from typing import Optional
|
|
from typing import Union
|
|
|
|
|
|
def is_auto_session_tracking_enabled(hub=None):
|
|
# type: (Optional[sentry_sdk.Hub]) -> Union[Any, bool, None]
|
|
"""DEPRECATED: Utility function to find out if session tracking is enabled."""
|
|
|
|
# Internal callers should use private _is_auto_session_tracking_enabled, instead.
|
|
warnings.warn(
|
|
"This function is deprecated and will be removed in the next major release. "
|
|
"There is no public API replacement.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
if hub is None:
|
|
hub = sentry_sdk.Hub.current
|
|
|
|
should_track = hub.scope._force_auto_session_tracking
|
|
|
|
if should_track is None:
|
|
client_options = hub.client.options if hub.client else {}
|
|
should_track = client_options.get("auto_session_tracking", False)
|
|
|
|
return should_track
|
|
|
|
|
|
@contextmanager
|
|
def auto_session_tracking(hub=None, session_mode="application"):
|
|
# type: (Optional[sentry_sdk.Hub], str) -> Generator[None, None, None]
|
|
"""DEPRECATED: Use track_session instead
|
|
Starts and stops a session automatically around a block.
|
|
"""
|
|
warnings.warn(
|
|
"This function is deprecated and will be removed in the next major release. "
|
|
"Use track_session instead.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
if hub is None:
|
|
hub = sentry_sdk.Hub.current
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore", DeprecationWarning)
|
|
should_track = is_auto_session_tracking_enabled(hub)
|
|
if should_track:
|
|
hub.start_session(session_mode=session_mode)
|
|
try:
|
|
yield
|
|
finally:
|
|
if should_track:
|
|
hub.end_session()
|
|
|
|
|
|
def is_auto_session_tracking_enabled_scope(scope):
|
|
# type: (sentry_sdk.Scope) -> bool
|
|
"""
|
|
DEPRECATED: Utility function to find out if session tracking is enabled.
|
|
"""
|
|
|
|
warnings.warn(
|
|
"This function is deprecated and will be removed in the next major release. "
|
|
"There is no public API replacement.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
# Internal callers should use private _is_auto_session_tracking_enabled, instead.
|
|
return _is_auto_session_tracking_enabled(scope)
|
|
|
|
|
|
def _is_auto_session_tracking_enabled(scope):
|
|
# type: (sentry_sdk.Scope) -> bool
|
|
"""
|
|
Utility function to find out if session tracking is enabled.
|
|
"""
|
|
|
|
should_track = scope._force_auto_session_tracking
|
|
if should_track is None:
|
|
client_options = sentry_sdk.get_client().options
|
|
should_track = client_options.get("auto_session_tracking", False)
|
|
|
|
return should_track
|
|
|
|
|
|
@contextmanager
|
|
def auto_session_tracking_scope(scope, session_mode="application"):
|
|
# type: (sentry_sdk.Scope, str) -> Generator[None, None, None]
|
|
"""DEPRECATED: This function is a deprecated alias for track_session.
|
|
Starts and stops a session automatically around a block.
|
|
"""
|
|
|
|
warnings.warn(
|
|
"This function is a deprecated alias for track_session and will be removed in the next major release.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
with track_session(scope, session_mode=session_mode):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def track_session(scope, session_mode="application"):
|
|
# type: (sentry_sdk.Scope, str) -> Generator[None, None, None]
|
|
"""
|
|
Start a new session in the provided scope, assuming session tracking is enabled.
|
|
This is a no-op context manager if session tracking is not enabled.
|
|
"""
|
|
|
|
should_track = _is_auto_session_tracking_enabled(scope)
|
|
if should_track:
|
|
scope.start_session(session_mode=session_mode)
|
|
try:
|
|
yield
|
|
finally:
|
|
if should_track:
|
|
scope.end_session()
|
|
|
|
|
|
TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed")
|
|
MAX_ENVELOPE_ITEMS = 100
|
|
|
|
|
|
def make_aggregate_envelope(aggregate_states, attrs):
|
|
# type: (Any, Any) -> Any
|
|
return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())}
|
|
|
|
|
|
class SessionFlusher:
|
|
def __init__(
|
|
self,
|
|
capture_func, # type: Callable[[Envelope], None]
|
|
flush_interval=60, # type: int
|
|
):
|
|
# type: (...) -> None
|
|
self.capture_func = capture_func
|
|
self.flush_interval = flush_interval
|
|
self.pending_sessions = [] # type: List[Any]
|
|
self.pending_aggregates = {} # type: Dict[Any, Any]
|
|
self._thread = None # type: Optional[Thread]
|
|
self._thread_lock = Lock()
|
|
self._aggregate_lock = Lock()
|
|
self._thread_for_pid = None # type: Optional[int]
|
|
self.__shutdown_requested = Event()
|
|
|
|
def flush(self):
|
|
# type: (...) -> None
|
|
pending_sessions = self.pending_sessions
|
|
self.pending_sessions = []
|
|
|
|
with self._aggregate_lock:
|
|
pending_aggregates = self.pending_aggregates
|
|
self.pending_aggregates = {}
|
|
|
|
envelope = Envelope()
|
|
for session in pending_sessions:
|
|
if len(envelope.items) == MAX_ENVELOPE_ITEMS:
|
|
self.capture_func(envelope)
|
|
envelope = Envelope()
|
|
|
|
envelope.add_session(session)
|
|
|
|
for attrs, states in pending_aggregates.items():
|
|
if len(envelope.items) == MAX_ENVELOPE_ITEMS:
|
|
self.capture_func(envelope)
|
|
envelope = Envelope()
|
|
|
|
envelope.add_sessions(make_aggregate_envelope(states, attrs))
|
|
|
|
if len(envelope.items) > 0:
|
|
self.capture_func(envelope)
|
|
|
|
def _ensure_running(self):
|
|
# type: (...) -> None
|
|
"""
|
|
Check that we have an active thread to run in, or create one if not.
|
|
|
|
Note that this might fail (e.g. in Python 3.12 it's not possible to
|
|
spawn new threads at interpreter shutdown). In that case self._running
|
|
will be False after running this function.
|
|
"""
|
|
if self._thread_for_pid == os.getpid() and self._thread is not None:
|
|
return None
|
|
with self._thread_lock:
|
|
if self._thread_for_pid == os.getpid() and self._thread is not None:
|
|
return None
|
|
|
|
def _thread():
|
|
# type: (...) -> None
|
|
running = True
|
|
while running:
|
|
running = not self.__shutdown_requested.wait(self.flush_interval)
|
|
self.flush()
|
|
|
|
thread = Thread(target=_thread)
|
|
thread.daemon = True
|
|
try:
|
|
thread.start()
|
|
except RuntimeError:
|
|
# Unfortunately at this point the interpreter is in a state that no
|
|
# longer allows us to spawn a thread and we have to bail.
|
|
self.__shutdown_requested.set()
|
|
return None
|
|
|
|
self._thread = thread
|
|
self._thread_for_pid = os.getpid()
|
|
|
|
return None
|
|
|
|
def add_aggregate_session(
|
|
self,
|
|
session, # type: Session
|
|
):
|
|
# type: (...) -> None
|
|
# NOTE on `session.did`:
|
|
# the protocol can deal with buckets that have a distinct-id, however
|
|
# in practice we expect the python SDK to have an extremely high cardinality
|
|
# here, effectively making aggregation useless, therefore we do not
|
|
# aggregate per-did.
|
|
|
|
# For this part we can get away with using the global interpreter lock
|
|
with self._aggregate_lock:
|
|
attrs = session.get_json_attrs(with_user_info=False)
|
|
primary_key = tuple(sorted(attrs.items()))
|
|
secondary_key = session.truncated_started # (, session.did)
|
|
states = self.pending_aggregates.setdefault(primary_key, {})
|
|
state = states.setdefault(secondary_key, {})
|
|
|
|
if "started" not in state:
|
|
state["started"] = format_timestamp(session.truncated_started)
|
|
# if session.did is not None:
|
|
# state["did"] = session.did
|
|
if session.status == "crashed":
|
|
state["crashed"] = state.get("crashed", 0) + 1
|
|
elif session.status == "abnormal":
|
|
state["abnormal"] = state.get("abnormal", 0) + 1
|
|
elif session.errors > 0:
|
|
state["errored"] = state.get("errored", 0) + 1
|
|
else:
|
|
state["exited"] = state.get("exited", 0) + 1
|
|
|
|
def add_session(
|
|
self,
|
|
session, # type: Session
|
|
):
|
|
# type: (...) -> None
|
|
if session.session_mode == "request":
|
|
self.add_aggregate_session(session)
|
|
else:
|
|
self.pending_sessions.append(session.to_json())
|
|
self._ensure_running()
|
|
|
|
def kill(self):
|
|
# type: (...) -> None
|
|
self.__shutdown_requested.set()
|