envelope.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import io
  2. import json
  3. import mimetypes
  4. from sentry_sdk.session import Session
  5. from sentry_sdk.utils import json_dumps, capture_internal_exceptions
  6. from typing import TYPE_CHECKING
  7. if TYPE_CHECKING:
  8. from typing import Any
  9. from typing import Optional
  10. from typing import Union
  11. from typing import Dict
  12. from typing import List
  13. from typing import Iterator
  14. from sentry_sdk._types import Event, EventDataCategory
  15. def parse_json(data):
  16. # type: (Union[bytes, str]) -> Any
  17. # on some python 3 versions this needs to be bytes
  18. if isinstance(data, bytes):
  19. data = data.decode("utf-8", "replace")
  20. return json.loads(data)
  21. class Envelope:
  22. """
  23. Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints
  24. documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular,
  25. each envelope may have at most one Item with type "event" or "transaction" (but not both).
  26. """
  27. def __init__(
  28. self,
  29. headers=None, # type: Optional[Dict[str, Any]]
  30. items=None, # type: Optional[List[Item]]
  31. ):
  32. # type: (...) -> None
  33. if headers is not None:
  34. headers = dict(headers)
  35. self.headers = headers or {}
  36. if items is None:
  37. items = []
  38. else:
  39. items = list(items)
  40. self.items = items
  41. @property
  42. def description(self):
  43. # type: (...) -> str
  44. return "envelope with %s items (%s)" % (
  45. len(self.items),
  46. ", ".join(x.data_category for x in self.items),
  47. )
  48. def add_event(
  49. self,
  50. event, # type: Event
  51. ):
  52. # type: (...) -> None
  53. self.add_item(Item(payload=PayloadRef(json=event), type="event"))
  54. def add_transaction(
  55. self,
  56. transaction, # type: Event
  57. ):
  58. # type: (...) -> None
  59. self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction"))
  60. def add_profile(
  61. self,
  62. profile, # type: Any
  63. ):
  64. # type: (...) -> None
  65. self.add_item(Item(payload=PayloadRef(json=profile), type="profile"))
  66. def add_profile_chunk(
  67. self,
  68. profile_chunk, # type: Any
  69. ):
  70. # type: (...) -> None
  71. self.add_item(
  72. Item(
  73. payload=PayloadRef(json=profile_chunk),
  74. type="profile_chunk",
  75. headers={"platform": profile_chunk.get("platform", "python")},
  76. )
  77. )
  78. def add_checkin(
  79. self,
  80. checkin, # type: Any
  81. ):
  82. # type: (...) -> None
  83. self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in"))
  84. def add_session(
  85. self,
  86. session, # type: Union[Session, Any]
  87. ):
  88. # type: (...) -> None
  89. if isinstance(session, Session):
  90. session = session.to_json()
  91. self.add_item(Item(payload=PayloadRef(json=session), type="session"))
  92. def add_sessions(
  93. self,
  94. sessions, # type: Any
  95. ):
  96. # type: (...) -> None
  97. self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
  98. def add_item(
  99. self,
  100. item, # type: Item
  101. ):
  102. # type: (...) -> None
  103. self.items.append(item)
  104. def get_event(self):
  105. # type: (...) -> Optional[Event]
  106. for items in self.items:
  107. event = items.get_event()
  108. if event is not None:
  109. return event
  110. return None
  111. def get_transaction_event(self):
  112. # type: (...) -> Optional[Event]
  113. for item in self.items:
  114. event = item.get_transaction_event()
  115. if event is not None:
  116. return event
  117. return None
  118. def __iter__(self):
  119. # type: (...) -> Iterator[Item]
  120. return iter(self.items)
  121. def serialize_into(
  122. self,
  123. f, # type: Any
  124. ):
  125. # type: (...) -> None
  126. f.write(json_dumps(self.headers))
  127. f.write(b"\n")
  128. for item in self.items:
  129. item.serialize_into(f)
  130. def serialize(self):
  131. # type: (...) -> bytes
  132. out = io.BytesIO()
  133. self.serialize_into(out)
  134. return out.getvalue()
  135. @classmethod
  136. def deserialize_from(
  137. cls,
  138. f, # type: Any
  139. ):
  140. # type: (...) -> Envelope
  141. headers = parse_json(f.readline())
  142. items = []
  143. while 1:
  144. item = Item.deserialize_from(f)
  145. if item is None:
  146. break
  147. items.append(item)
  148. return cls(headers=headers, items=items)
  149. @classmethod
  150. def deserialize(
  151. cls,
  152. bytes, # type: bytes
  153. ):
  154. # type: (...) -> Envelope
  155. return cls.deserialize_from(io.BytesIO(bytes))
  156. def __repr__(self):
  157. # type: (...) -> str
  158. return "<Envelope headers=%r items=%r>" % (self.headers, self.items)
  159. class PayloadRef:
  160. def __init__(
  161. self,
  162. bytes=None, # type: Optional[bytes]
  163. path=None, # type: Optional[Union[bytes, str]]
  164. json=None, # type: Optional[Any]
  165. ):
  166. # type: (...) -> None
  167. self.json = json
  168. self.bytes = bytes
  169. self.path = path
  170. def get_bytes(self):
  171. # type: (...) -> bytes
  172. if self.bytes is None:
  173. if self.path is not None:
  174. with capture_internal_exceptions():
  175. with open(self.path, "rb") as f:
  176. self.bytes = f.read()
  177. elif self.json is not None:
  178. self.bytes = json_dumps(self.json)
  179. return self.bytes or b""
  180. @property
  181. def inferred_content_type(self):
  182. # type: (...) -> str
  183. if self.json is not None:
  184. return "application/json"
  185. elif self.path is not None:
  186. path = self.path
  187. if isinstance(path, bytes):
  188. path = path.decode("utf-8", "replace")
  189. ty = mimetypes.guess_type(path)[0]
  190. if ty:
  191. return ty
  192. return "application/octet-stream"
  193. def __repr__(self):
  194. # type: (...) -> str
  195. return "<Payload %r>" % (self.inferred_content_type,)
  196. class Item:
  197. def __init__(
  198. self,
  199. payload, # type: Union[bytes, str, PayloadRef]
  200. headers=None, # type: Optional[Dict[str, Any]]
  201. type=None, # type: Optional[str]
  202. content_type=None, # type: Optional[str]
  203. filename=None, # type: Optional[str]
  204. ):
  205. if headers is not None:
  206. headers = dict(headers)
  207. elif headers is None:
  208. headers = {}
  209. self.headers = headers
  210. if isinstance(payload, bytes):
  211. payload = PayloadRef(bytes=payload)
  212. elif isinstance(payload, str):
  213. payload = PayloadRef(bytes=payload.encode("utf-8"))
  214. else:
  215. payload = payload
  216. if filename is not None:
  217. headers["filename"] = filename
  218. if type is not None:
  219. headers["type"] = type
  220. if content_type is not None:
  221. headers["content_type"] = content_type
  222. elif "content_type" not in headers:
  223. headers["content_type"] = payload.inferred_content_type
  224. self.payload = payload
  225. def __repr__(self):
  226. # type: (...) -> str
  227. return "<Item headers=%r payload=%r data_category=%r>" % (
  228. self.headers,
  229. self.payload,
  230. self.data_category,
  231. )
  232. @property
  233. def type(self):
  234. # type: (...) -> Optional[str]
  235. return self.headers.get("type")
  236. @property
  237. def data_category(self):
  238. # type: (...) -> EventDataCategory
  239. ty = self.headers.get("type")
  240. if ty == "session" or ty == "sessions":
  241. return "session"
  242. elif ty == "attachment":
  243. return "attachment"
  244. elif ty == "transaction":
  245. return "transaction"
  246. elif ty == "event":
  247. return "error"
  248. elif ty == "log":
  249. return "log_item"
  250. elif ty == "trace_metric":
  251. return "trace_metric"
  252. elif ty == "client_report":
  253. return "internal"
  254. elif ty == "profile":
  255. return "profile"
  256. elif ty == "profile_chunk":
  257. return "profile_chunk"
  258. elif ty == "check_in":
  259. return "monitor"
  260. else:
  261. return "default"
  262. def get_bytes(self):
  263. # type: (...) -> bytes
  264. return self.payload.get_bytes()
  265. def get_event(self):
  266. # type: (...) -> Optional[Event]
  267. """
  268. Returns an error event if there is one.
  269. """
  270. if self.type == "event" and self.payload.json is not None:
  271. return self.payload.json
  272. return None
  273. def get_transaction_event(self):
  274. # type: (...) -> Optional[Event]
  275. if self.type == "transaction" and self.payload.json is not None:
  276. return self.payload.json
  277. return None
  278. def serialize_into(
  279. self,
  280. f, # type: Any
  281. ):
  282. # type: (...) -> None
  283. headers = dict(self.headers)
  284. bytes = self.get_bytes()
  285. headers["length"] = len(bytes)
  286. f.write(json_dumps(headers))
  287. f.write(b"\n")
  288. f.write(bytes)
  289. f.write(b"\n")
  290. def serialize(self):
  291. # type: (...) -> bytes
  292. out = io.BytesIO()
  293. self.serialize_into(out)
  294. return out.getvalue()
  295. @classmethod
  296. def deserialize_from(
  297. cls,
  298. f, # type: Any
  299. ):
  300. # type: (...) -> Optional[Item]
  301. line = f.readline().rstrip()
  302. if not line:
  303. return None
  304. headers = parse_json(line)
  305. length = headers.get("length")
  306. if length is not None:
  307. payload = f.read(length)
  308. f.readline()
  309. else:
  310. # if no length was specified we need to read up to the end of line
  311. # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope)
  312. payload = f.readline().rstrip(b"\n")
  313. if headers.get("type") in ("event", "transaction"):
  314. rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload)))
  315. else:
  316. rv = cls(headers=headers, payload=payload)
  317. return rv
  318. @classmethod
  319. def deserialize(
  320. cls,
  321. bytes, # type: bytes
  322. ):
  323. # type: (...) -> Optional[Item]
  324. return cls.deserialize_from(io.BytesIO(bytes))