| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- import io
- import json
- import mimetypes
- from sentry_sdk.session import Session
- from sentry_sdk.utils import json_dumps, capture_internal_exceptions
- from typing import TYPE_CHECKING
- if TYPE_CHECKING:
- from typing import Any
- from typing import Optional
- from typing import Union
- from typing import Dict
- from typing import List
- from typing import Iterator
- from sentry_sdk._types import Event, EventDataCategory
- def parse_json(data):
- # type: (Union[bytes, str]) -> Any
- # on some python 3 versions this needs to be bytes
- if isinstance(data, bytes):
- data = data.decode("utf-8", "replace")
- return json.loads(data)
- class Envelope:
- """
- Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints
- documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular,
- each envelope may have at most one Item with type "event" or "transaction" (but not both).
- """
- def __init__(
- self,
- headers=None, # type: Optional[Dict[str, Any]]
- items=None, # type: Optional[List[Item]]
- ):
- # type: (...) -> None
- if headers is not None:
- headers = dict(headers)
- self.headers = headers or {}
- if items is None:
- items = []
- else:
- items = list(items)
- self.items = items
- @property
- def description(self):
- # type: (...) -> str
- return "envelope with %s items (%s)" % (
- len(self.items),
- ", ".join(x.data_category for x in self.items),
- )
- def add_event(
- self,
- event, # type: Event
- ):
- # type: (...) -> None
- self.add_item(Item(payload=PayloadRef(json=event), type="event"))
- def add_transaction(
- self,
- transaction, # type: Event
- ):
- # type: (...) -> None
- self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction"))
- def add_profile(
- self,
- profile, # type: Any
- ):
- # type: (...) -> None
- self.add_item(Item(payload=PayloadRef(json=profile), type="profile"))
- def add_profile_chunk(
- self,
- profile_chunk, # type: Any
- ):
- # type: (...) -> None
- self.add_item(
- Item(
- payload=PayloadRef(json=profile_chunk),
- type="profile_chunk",
- headers={"platform": profile_chunk.get("platform", "python")},
- )
- )
- def add_checkin(
- self,
- checkin, # type: Any
- ):
- # type: (...) -> None
- self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in"))
- def add_session(
- self,
- session, # type: Union[Session, Any]
- ):
- # type: (...) -> None
- if isinstance(session, Session):
- session = session.to_json()
- self.add_item(Item(payload=PayloadRef(json=session), type="session"))
- def add_sessions(
- self,
- sessions, # type: Any
- ):
- # type: (...) -> None
- self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
- def add_item(
- self,
- item, # type: Item
- ):
- # type: (...) -> None
- self.items.append(item)
- def get_event(self):
- # type: (...) -> Optional[Event]
- for items in self.items:
- event = items.get_event()
- if event is not None:
- return event
- return None
- def get_transaction_event(self):
- # type: (...) -> Optional[Event]
- for item in self.items:
- event = item.get_transaction_event()
- if event is not None:
- return event
- return None
- def __iter__(self):
- # type: (...) -> Iterator[Item]
- return iter(self.items)
- def serialize_into(
- self,
- f, # type: Any
- ):
- # type: (...) -> None
- f.write(json_dumps(self.headers))
- f.write(b"\n")
- for item in self.items:
- item.serialize_into(f)
- def serialize(self):
- # type: (...) -> bytes
- out = io.BytesIO()
- self.serialize_into(out)
- return out.getvalue()
- @classmethod
- def deserialize_from(
- cls,
- f, # type: Any
- ):
- # type: (...) -> Envelope
- headers = parse_json(f.readline())
- items = []
- while 1:
- item = Item.deserialize_from(f)
- if item is None:
- break
- items.append(item)
- return cls(headers=headers, items=items)
- @classmethod
- def deserialize(
- cls,
- bytes, # type: bytes
- ):
- # type: (...) -> Envelope
- return cls.deserialize_from(io.BytesIO(bytes))
- def __repr__(self):
- # type: (...) -> str
- return "<Envelope headers=%r items=%r>" % (self.headers, self.items)
- class PayloadRef:
- def __init__(
- self,
- bytes=None, # type: Optional[bytes]
- path=None, # type: Optional[Union[bytes, str]]
- json=None, # type: Optional[Any]
- ):
- # type: (...) -> None
- self.json = json
- self.bytes = bytes
- self.path = path
- def get_bytes(self):
- # type: (...) -> bytes
- if self.bytes is None:
- if self.path is not None:
- with capture_internal_exceptions():
- with open(self.path, "rb") as f:
- self.bytes = f.read()
- elif self.json is not None:
- self.bytes = json_dumps(self.json)
- return self.bytes or b""
- @property
- def inferred_content_type(self):
- # type: (...) -> str
- if self.json is not None:
- return "application/json"
- elif self.path is not None:
- path = self.path
- if isinstance(path, bytes):
- path = path.decode("utf-8", "replace")
- ty = mimetypes.guess_type(path)[0]
- if ty:
- return ty
- return "application/octet-stream"
- def __repr__(self):
- # type: (...) -> str
- return "<Payload %r>" % (self.inferred_content_type,)
- class Item:
- def __init__(
- self,
- payload, # type: Union[bytes, str, PayloadRef]
- headers=None, # type: Optional[Dict[str, Any]]
- type=None, # type: Optional[str]
- content_type=None, # type: Optional[str]
- filename=None, # type: Optional[str]
- ):
- if headers is not None:
- headers = dict(headers)
- elif headers is None:
- headers = {}
- self.headers = headers
- if isinstance(payload, bytes):
- payload = PayloadRef(bytes=payload)
- elif isinstance(payload, str):
- payload = PayloadRef(bytes=payload.encode("utf-8"))
- else:
- payload = payload
- if filename is not None:
- headers["filename"] = filename
- if type is not None:
- headers["type"] = type
- if content_type is not None:
- headers["content_type"] = content_type
- elif "content_type" not in headers:
- headers["content_type"] = payload.inferred_content_type
- self.payload = payload
- def __repr__(self):
- # type: (...) -> str
- return "<Item headers=%r payload=%r data_category=%r>" % (
- self.headers,
- self.payload,
- self.data_category,
- )
- @property
- def type(self):
- # type: (...) -> Optional[str]
- return self.headers.get("type")
- @property
- def data_category(self):
- # type: (...) -> EventDataCategory
- ty = self.headers.get("type")
- if ty == "session" or ty == "sessions":
- return "session"
- elif ty == "attachment":
- return "attachment"
- elif ty == "transaction":
- return "transaction"
- elif ty == "event":
- return "error"
- elif ty == "log":
- return "log_item"
- elif ty == "trace_metric":
- return "trace_metric"
- elif ty == "client_report":
- return "internal"
- elif ty == "profile":
- return "profile"
- elif ty == "profile_chunk":
- return "profile_chunk"
- elif ty == "check_in":
- return "monitor"
- else:
- return "default"
- def get_bytes(self):
- # type: (...) -> bytes
- return self.payload.get_bytes()
- def get_event(self):
- # type: (...) -> Optional[Event]
- """
- Returns an error event if there is one.
- """
- if self.type == "event" and self.payload.json is not None:
- return self.payload.json
- return None
- def get_transaction_event(self):
- # type: (...) -> Optional[Event]
- if self.type == "transaction" and self.payload.json is not None:
- return self.payload.json
- return None
- def serialize_into(
- self,
- f, # type: Any
- ):
- # type: (...) -> None
- headers = dict(self.headers)
- bytes = self.get_bytes()
- headers["length"] = len(bytes)
- f.write(json_dumps(headers))
- f.write(b"\n")
- f.write(bytes)
- f.write(b"\n")
- def serialize(self):
- # type: (...) -> bytes
- out = io.BytesIO()
- self.serialize_into(out)
- return out.getvalue()
- @classmethod
- def deserialize_from(
- cls,
- f, # type: Any
- ):
- # type: (...) -> Optional[Item]
- line = f.readline().rstrip()
- if not line:
- return None
- headers = parse_json(line)
- length = headers.get("length")
- if length is not None:
- payload = f.read(length)
- f.readline()
- else:
- # if no length was specified we need to read up to the end of line
- # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope)
- payload = f.readline().rstrip(b"\n")
- if headers.get("type") in ("event", "transaction"):
- rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload)))
- else:
- rv = cls(headers=headers, payload=payload)
- return rv
- @classmethod
- def deserialize(
- cls,
- bytes, # type: bytes
- ):
- # type: (...) -> Optional[Item]
- return cls.deserialize_from(io.BytesIO(bytes))
|