Skip to content

Commit 7ba60bd

Browse files
Swatinemmitsuhiko
andauthored
feat: Support pre-aggregated sessions (getsentry#985)
This changes the SessionFlusher to pre-aggregate sessions according to https://develop.sentry.dev/sdk/sessions/#session-aggregates-payload instead of sending individual session updates. Co-authored-by: Armin Ronacher <armin.ronacher@active-4.com>
1 parent 123f7af commit 7ba60bd

File tree

8 files changed

+326
-179
lines changed

8 files changed

+326
-179
lines changed

sentry_sdk/client.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import uuid
33
import random
44
from datetime import datetime
5-
from itertools import islice
65
import socket
76

87
from sentry_sdk._compat import string_types, text_type, iteritems
@@ -30,12 +29,11 @@
3029
from typing import Any
3130
from typing import Callable
3231
from typing import Dict
33-
from typing import List
3432
from typing import Optional
3533

3634
from sentry_sdk.scope import Scope
3735
from sentry_sdk._types import Event, Hint
38-
from sentry_sdk.sessions import Session
36+
from sentry_sdk.session import Session
3937

4038

4139
_client_init_debug = ContextVar("client_init_debug")
@@ -99,24 +97,20 @@ def _init_impl(self):
9997
# type: () -> None
10098
old_debug = _client_init_debug.get(False)
10199

102-
def _send_sessions(sessions):
103-
# type: (List[Any]) -> None
104-
transport = self.transport
105-
if not transport or not sessions:
106-
return
107-
sessions_iter = iter(sessions)
108-
while True:
109-
envelope = Envelope()
110-
for session in islice(sessions_iter, 100):
111-
envelope.add_session(session)
112-
if not envelope.items:
113-
break
114-
transport.capture_envelope(envelope)
100+
def _capture_envelope(envelope):
101+
# type: (Envelope) -> None
102+
if self.transport is not None:
103+
self.transport.capture_envelope(envelope)
115104

116105
try:
117106
_client_init_debug.set(self.options["debug"])
118107
self.transport = make_transport(self.options)
119-
self.session_flusher = SessionFlusher(flush_func=_send_sessions)
108+
session_mode = self.options["_experiments"].get(
109+
"session_mode", "application"
110+
)
111+
self.session_flusher = SessionFlusher(
112+
capture_func=_capture_envelope, session_mode=session_mode
113+
)
120114

121115
request_bodies = ("always", "never", "small", "medium")
122116
if self.options["request_bodies"] not in request_bodies:

sentry_sdk/envelope.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from sentry_sdk._compat import text_type
66
from sentry_sdk._types import MYPY
7-
from sentry_sdk.sessions import Session
7+
from sentry_sdk.session import Session
88
from sentry_sdk.utils import json_dumps, capture_internal_exceptions
99

1010
if MYPY:
@@ -62,6 +62,12 @@ def add_session(
6262
session = session.to_json()
6363
self.add_item(Item(payload=PayloadRef(json=session), type="session"))
6464

65+
def add_sessions(
66+
self, sessions # type: Any
67+
):
68+
# type: (...) -> None
69+
self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
70+
6571
def add_item(
6672
self, item # type: Item
6773
):

sentry_sdk/hub.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from sentry_sdk.scope import Scope
99
from sentry_sdk.client import Client
1010
from sentry_sdk.tracing import Span, Transaction
11-
from sentry_sdk.sessions import Session
11+
from sentry_sdk.session import Session
1212
from sentry_sdk.utils import (
1313
exc_info_from_error,
1414
event_from_exception,
@@ -639,11 +639,12 @@ def end_session(self):
639639
"""Ends the current session if there is one."""
640640
client, scope = self._stack[-1]
641641
session = scope._session
642+
self.scope._session = None
643+
642644
if session is not None:
643645
session.close()
644646
if client is not None:
645647
client.capture_session(session)
646-
self.scope._session = None
647648

648649
def stop_auto_session_tracking(self):
649650
# type: (...) -> None

sentry_sdk/scope.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
)
2929

3030
from sentry_sdk.tracing import Span
31-
from sentry_sdk.sessions import Session
31+
from sentry_sdk.session import Session
3232

3333
F = TypeVar("F", bound=Callable[..., Any])
3434
T = TypeVar("T")

sentry_sdk/session.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import uuid
2+
from datetime import datetime
3+
4+
from sentry_sdk._types import MYPY
5+
from sentry_sdk.utils import format_timestamp
6+
7+
if MYPY:
8+
from typing import Optional
9+
from typing import Union
10+
from typing import Any
11+
from typing import Dict
12+
13+
from sentry_sdk._types import SessionStatus
14+
15+
16+
def _minute_trunc(ts):
17+
# type: (datetime) -> datetime
18+
return ts.replace(second=0, microsecond=0)
19+
20+
21+
def _make_uuid(
22+
val, # type: Union[str, uuid.UUID]
23+
):
24+
# type: (...) -> uuid.UUID
25+
if isinstance(val, uuid.UUID):
26+
return val
27+
return uuid.UUID(val)
28+
29+
30+
class Session(object):
31+
def __init__(
32+
self,
33+
sid=None, # type: Optional[Union[str, uuid.UUID]]
34+
did=None, # type: Optional[str]
35+
timestamp=None, # type: Optional[datetime]
36+
started=None, # type: Optional[datetime]
37+
duration=None, # type: Optional[float]
38+
status=None, # type: Optional[SessionStatus]
39+
release=None, # type: Optional[str]
40+
environment=None, # type: Optional[str]
41+
user_agent=None, # type: Optional[str]
42+
ip_address=None, # type: Optional[str]
43+
errors=None, # type: Optional[int]
44+
user=None, # type: Optional[Any]
45+
):
46+
# type: (...) -> None
47+
if sid is None:
48+
sid = uuid.uuid4()
49+
if started is None:
50+
started = datetime.utcnow()
51+
if status is None:
52+
status = "ok"
53+
self.status = status
54+
self.did = None # type: Optional[str]
55+
self.started = started
56+
self.release = None # type: Optional[str]
57+
self.environment = None # type: Optional[str]
58+
self.duration = None # type: Optional[float]
59+
self.user_agent = None # type: Optional[str]
60+
self.ip_address = None # type: Optional[str]
61+
self.errors = 0
62+
63+
self.update(
64+
sid=sid,
65+
did=did,
66+
timestamp=timestamp,
67+
duration=duration,
68+
release=release,
69+
environment=environment,
70+
user_agent=user_agent,
71+
ip_address=ip_address,
72+
errors=errors,
73+
user=user,
74+
)
75+
76+
@property
77+
def truncated_started(self):
78+
# type: (...) -> datetime
79+
return _minute_trunc(self.started)
80+
81+
def update(
82+
self,
83+
sid=None, # type: Optional[Union[str, uuid.UUID]]
84+
did=None, # type: Optional[str]
85+
timestamp=None, # type: Optional[datetime]
86+
started=None, # type: Optional[datetime]
87+
duration=None, # type: Optional[float]
88+
status=None, # type: Optional[SessionStatus]
89+
release=None, # type: Optional[str]
90+
environment=None, # type: Optional[str]
91+
user_agent=None, # type: Optional[str]
92+
ip_address=None, # type: Optional[str]
93+
errors=None, # type: Optional[int]
94+
user=None, # type: Optional[Any]
95+
):
96+
# type: (...) -> None
97+
# If a user is supplied we pull some data form it
98+
if user:
99+
if ip_address is None:
100+
ip_address = user.get("ip_address")
101+
if did is None:
102+
did = user.get("id") or user.get("email") or user.get("username")
103+
104+
if sid is not None:
105+
self.sid = _make_uuid(sid)
106+
if did is not None:
107+
self.did = str(did)
108+
if timestamp is None:
109+
timestamp = datetime.utcnow()
110+
self.timestamp = timestamp
111+
if started is not None:
112+
self.started = started
113+
if duration is not None:
114+
self.duration = duration
115+
if release is not None:
116+
self.release = release
117+
if environment is not None:
118+
self.environment = environment
119+
if ip_address is not None:
120+
self.ip_address = ip_address
121+
if user_agent is not None:
122+
self.user_agent = user_agent
123+
if errors is not None:
124+
self.errors = errors
125+
126+
if status is not None:
127+
self.status = status
128+
129+
def close(
130+
self, status=None # type: Optional[SessionStatus]
131+
):
132+
# type: (...) -> Any
133+
if status is None and self.status == "ok":
134+
status = "exited"
135+
if status is not None:
136+
self.update(status=status)
137+
138+
def get_json_attrs(
139+
self, with_user_info=True # type: Optional[bool]
140+
):
141+
# type: (...) -> Any
142+
attrs = {}
143+
if self.release is not None:
144+
attrs["release"] = self.release
145+
if self.environment is not None:
146+
attrs["environment"] = self.environment
147+
if with_user_info:
148+
if self.ip_address is not None:
149+
attrs["ip_address"] = self.ip_address
150+
if self.user_agent is not None:
151+
attrs["user_agent"] = self.user_agent
152+
return attrs
153+
154+
def to_json(self):
155+
# type: (...) -> Any
156+
rv = {
157+
"sid": str(self.sid),
158+
"init": True,
159+
"started": format_timestamp(self.started),
160+
"timestamp": format_timestamp(self.timestamp),
161+
"status": self.status,
162+
} # type: Dict[str, Any]
163+
if self.errors:
164+
rv["errors"] = self.errors
165+
if self.did is not None:
166+
rv["did"] = self.did
167+
if self.duration is not None:
168+
rv["duration"] = self.duration
169+
attrs = self.get_json_attrs()
170+
if attrs:
171+
rv["attrs"] = attrs
172+
return rv

0 commit comments

Comments
 (0)