66
77import sentry_sdk
88from sentry_sdk import continue_trace
9- from sentry_sdk .consts import OP
9+ from sentry_sdk .consts import OP , SPANDATA
1010from sentry_sdk .integrations import DidNotEnable , Integration , _check_minimum_version
1111from sentry_sdk .integrations ._wsgi_common import RequestExtractor , _filter_headers
1212from sentry_sdk .integrations .logging import ignore_logger
13+ from sentry_sdk .scope import should_send_default_pii
14+ from sentry_sdk .traces import SegmentSource , StreamedSpan
1315from sentry_sdk .tracing import TransactionSource
16+ from sentry_sdk .tracing_utils import has_span_streaming_enabled
1417from sentry_sdk .utils import (
1518 CONTEXTVARS_ERROR_MESSAGE ,
1619 HAS_REAL_CONTEXTVARS ,
@@ -165,23 +168,43 @@ async def _context_enter(request: "Request") -> None:
165168 if not request .ctx ._sentry_do_integration :
166169 return
167170
171+ client = sentry_sdk .get_client ()
172+ is_span_streaming_enabled = has_span_streaming_enabled (client .options )
173+
168174 weak_request = weakref .ref (request )
169175 request .ctx ._sentry_scope = sentry_sdk .isolation_scope ()
170176 scope = request .ctx ._sentry_scope .__enter__ ()
171177 scope .clear_breadcrumbs ()
172178 scope .add_event_processor (_make_request_processor (weak_request ))
173179
174- transaction = continue_trace (
175- dict (request .headers ),
176- op = OP .HTTP_SERVER ,
177- # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction
178- name = request .path ,
179- source = TransactionSource .URL ,
180- origin = SanicIntegration .origin ,
181- )
182- request .ctx ._sentry_transaction = sentry_sdk .start_transaction (
183- transaction
184- ).__enter__ ()
180+ if is_span_streaming_enabled :
181+ sentry_sdk .traces .continue_trace (dict (request .headers ))
182+ scope .set_custom_sampling_context ({"sanic_request" : request })
183+
184+ span = sentry_sdk .traces .start_span (
185+ # Unless the request results in a 404 error, the name and source
186+ # will get overwritten in _set_transaction
187+ name = request .path ,
188+ attributes = {
189+ "sentry.op" : OP .HTTP_SERVER ,
190+ "sentry.origin" : SanicIntegration .origin ,
191+ "sentry.span.source" : SegmentSource .URL .value ,
192+ },
193+ parent_span = None ,
194+ )
195+ request .ctx ._sentry_root_span = span
196+ else :
197+ transaction = continue_trace (
198+ dict (request .headers ),
199+ op = OP .HTTP_SERVER ,
200+ # Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction
201+ name = request .path ,
202+ source = TransactionSource .URL ,
203+ origin = SanicIntegration .origin ,
204+ )
205+ request .ctx ._sentry_root_span = sentry_sdk .start_transaction (
206+ transaction
207+ ).__enter__ ()
185208
186209
187210async def _context_exit (
@@ -198,12 +221,24 @@ async def _context_exit(
198221 # This capture_internal_exceptions block has been intentionally nested here, so that in case an exception
199222 # happens while trying to end the transaction, we still attempt to exit the hub.
200223 with capture_internal_exceptions ():
201- request .ctx ._sentry_transaction .set_http_status (response_status )
202- request .ctx ._sentry_transaction .sampled &= (
203- isinstance (integration , SanicIntegration )
204- and response_status not in integration ._unsampled_statuses
205- )
206- request .ctx ._sentry_transaction .__exit__ (None , None , None )
224+ span = request .ctx ._sentry_root_span
225+ if isinstance (span , StreamedSpan ):
226+ with capture_internal_exceptions ():
227+ for attr , value in _get_request_attributes (request ).items ():
228+ span .set_attribute (attr , value )
229+ if response_status is not None :
230+ span .set_attribute (SPANDATA .HTTP_STATUS_CODE , response_status )
231+ span .status = "error" if response_status >= 400 else "ok"
232+
233+ span .end ()
234+ else :
235+ span .set_http_status (response_status )
236+ span .sampled &= (
237+ isinstance (integration , SanicIntegration )
238+ and response_status not in integration ._unsampled_statuses
239+ )
240+
241+ span .__exit__ (None , None , None )
207242
208243 request .ctx ._sentry_scope .__exit__ (None , None , None )
209244
@@ -315,6 +350,36 @@ def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None:
315350 sentry_sdk .capture_event (event , hint = hint )
316351
317352
353+ def _get_request_attributes (request : "Request" ) -> "Dict[str, Any]" :
354+ """
355+ Return span attributes related to the HTTP request from a Sanic request.
356+ """
357+ attributes = {} # type: Dict[str, Any]
358+
359+ if request .method :
360+ attributes [SPANDATA .HTTP_REQUEST_METHOD ] = request .method .upper ()
361+
362+ headers = _filter_headers (dict (request .headers ), use_annotated_value = False )
363+ for header , value in headers .items ():
364+ attributes [f"{ SPANDATA .HTTP_REQUEST_HEADER } .{ header .lower ()} " ] = value
365+
366+ urlparts = urlsplit (request .url )
367+
368+ if urlparts .query :
369+ attributes [SPANDATA .HTTP_QUERY ] = urlparts .query
370+
371+ attributes [SPANDATA .URL_FULL ] = request .url
372+
373+ if urlparts .scheme :
374+ attributes [SPANDATA .NETWORK_PROTOCOL_NAME ] = urlparts .scheme
375+
376+ if should_send_default_pii () and request .remote_addr :
377+ attributes [SPANDATA .CLIENT_ADDRESS ] = request .remote_addr
378+ attributes [SPANDATA .USER_IP_ADDRESS ] = request .remote_addr
379+
380+ return attributes
381+
382+
318383def _make_request_processor (weak_request : "Callable[[], Request]" ) -> "EventProcessor" :
319384 def sanic_processor (event : "Event" , hint : "Optional[Hint]" ) -> "Optional[Event]" :
320385 try :
0 commit comments