You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

356 lines
10 KiB

  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, event # type: Event
  50. ):
  51. # type: (...) -> None
  52. self.add_item(Item(payload=PayloadRef(json=event), type="event"))
  53. def add_transaction(
  54. self, transaction # type: Event
  55. ):
  56. # type: (...) -> None
  57. self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction"))
  58. def add_profile(
  59. self, profile # type: Any
  60. ):
  61. # type: (...) -> None
  62. self.add_item(Item(payload=PayloadRef(json=profile), type="profile"))
  63. def add_profile_chunk(
  64. self, profile_chunk # type: Any
  65. ):
  66. # type: (...) -> None
  67. self.add_item(
  68. Item(
  69. payload=PayloadRef(json=profile_chunk),
  70. type="profile_chunk",
  71. headers={"platform": profile_chunk.get("platform", "python")},
  72. )
  73. )
  74. def add_checkin(
  75. self, checkin # type: Any
  76. ):
  77. # type: (...) -> None
  78. self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in"))
  79. def add_session(
  80. self, session # type: Union[Session, Any]
  81. ):
  82. # type: (...) -> None
  83. if isinstance(session, Session):
  84. session = session.to_json()
  85. self.add_item(Item(payload=PayloadRef(json=session), type="session"))
  86. def add_sessions(
  87. self, sessions # type: Any
  88. ):
  89. # type: (...) -> None
  90. self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
  91. def add_item(
  92. self, item # type: Item
  93. ):
  94. # type: (...) -> None
  95. self.items.append(item)
  96. def get_event(self):
  97. # type: (...) -> Optional[Event]
  98. for items in self.items:
  99. event = items.get_event()
  100. if event is not None:
  101. return event
  102. return None
  103. def get_transaction_event(self):
  104. # type: (...) -> Optional[Event]
  105. for item in self.items:
  106. event = item.get_transaction_event()
  107. if event is not None:
  108. return event
  109. return None
  110. def __iter__(self):
  111. # type: (...) -> Iterator[Item]
  112. return iter(self.items)
  113. def serialize_into(
  114. self, f # type: Any
  115. ):
  116. # type: (...) -> None
  117. f.write(json_dumps(self.headers))
  118. f.write(b"\n")
  119. for item in self.items:
  120. item.serialize_into(f)
  121. def serialize(self):
  122. # type: (...) -> bytes
  123. out = io.BytesIO()
  124. self.serialize_into(out)
  125. return out.getvalue()
  126. @classmethod
  127. def deserialize_from(
  128. cls, f # type: Any
  129. ):
  130. # type: (...) -> Envelope
  131. headers = parse_json(f.readline())
  132. items = []
  133. while 1:
  134. item = Item.deserialize_from(f)
  135. if item is None:
  136. break
  137. items.append(item)
  138. return cls(headers=headers, items=items)
  139. @classmethod
  140. def deserialize(
  141. cls, bytes # type: bytes
  142. ):
  143. # type: (...) -> Envelope
  144. return cls.deserialize_from(io.BytesIO(bytes))
  145. def __repr__(self):
  146. # type: (...) -> str
  147. return "<Envelope headers=%r items=%r>" % (self.headers, self.items)
  148. class PayloadRef:
  149. def __init__(
  150. self,
  151. bytes=None, # type: Optional[bytes]
  152. path=None, # type: Optional[Union[bytes, str]]
  153. json=None, # type: Optional[Any]
  154. ):
  155. # type: (...) -> None
  156. self.json = json
  157. self.bytes = bytes
  158. self.path = path
  159. def get_bytes(self):
  160. # type: (...) -> bytes
  161. if self.bytes is None:
  162. if self.path is not None:
  163. with capture_internal_exceptions():
  164. with open(self.path, "rb") as f:
  165. self.bytes = f.read()
  166. elif self.json is not None:
  167. self.bytes = json_dumps(self.json)
  168. return self.bytes or b""
  169. @property
  170. def inferred_content_type(self):
  171. # type: (...) -> str
  172. if self.json is not None:
  173. return "application/json"
  174. elif self.path is not None:
  175. path = self.path
  176. if isinstance(path, bytes):
  177. path = path.decode("utf-8", "replace")
  178. ty = mimetypes.guess_type(path)[0]
  179. if ty:
  180. return ty
  181. return "application/octet-stream"
  182. def __repr__(self):
  183. # type: (...) -> str
  184. return "<Payload %r>" % (self.inferred_content_type,)
  185. class Item:
  186. def __init__(
  187. self,
  188. payload, # type: Union[bytes, str, PayloadRef]
  189. headers=None, # type: Optional[Dict[str, Any]]
  190. type=None, # type: Optional[str]
  191. content_type=None, # type: Optional[str]
  192. filename=None, # type: Optional[str]
  193. ):
  194. if headers is not None:
  195. headers = dict(headers)
  196. elif headers is None:
  197. headers = {}
  198. self.headers = headers
  199. if isinstance(payload, bytes):
  200. payload = PayloadRef(bytes=payload)
  201. elif isinstance(payload, str):
  202. payload = PayloadRef(bytes=payload.encode("utf-8"))
  203. else:
  204. payload = payload
  205. if filename is not None:
  206. headers["filename"] = filename
  207. if type is not None:
  208. headers["type"] = type
  209. if content_type is not None:
  210. headers["content_type"] = content_type
  211. elif "content_type" not in headers:
  212. headers["content_type"] = payload.inferred_content_type
  213. self.payload = payload
  214. def __repr__(self):
  215. # type: (...) -> str
  216. return "<Item headers=%r payload=%r data_category=%r>" % (
  217. self.headers,
  218. self.payload,
  219. self.data_category,
  220. )
  221. @property
  222. def type(self):
  223. # type: (...) -> Optional[str]
  224. return self.headers.get("type")
  225. @property
  226. def data_category(self):
  227. # type: (...) -> EventDataCategory
  228. ty = self.headers.get("type")
  229. if ty == "session" or ty == "sessions":
  230. return "session"
  231. elif ty == "attachment":
  232. return "attachment"
  233. elif ty == "transaction":
  234. return "transaction"
  235. elif ty == "event":
  236. return "error"
  237. elif ty == "log":
  238. return "log"
  239. elif ty == "client_report":
  240. return "internal"
  241. elif ty == "profile":
  242. return "profile"
  243. elif ty == "profile_chunk":
  244. return "profile_chunk"
  245. elif ty == "statsd":
  246. return "metric_bucket"
  247. elif ty == "check_in":
  248. return "monitor"
  249. else:
  250. return "default"
  251. def get_bytes(self):
  252. # type: (...) -> bytes
  253. return self.payload.get_bytes()
  254. def get_event(self):
  255. # type: (...) -> Optional[Event]
  256. """
  257. Returns an error event if there is one.
  258. """
  259. if self.type == "event" and self.payload.json is not None:
  260. return self.payload.json
  261. return None
  262. def get_transaction_event(self):
  263. # type: (...) -> Optional[Event]
  264. if self.type == "transaction" and self.payload.json is not None:
  265. return self.payload.json
  266. return None
  267. def serialize_into(
  268. self, f # type: Any
  269. ):
  270. # type: (...) -> None
  271. headers = dict(self.headers)
  272. bytes = self.get_bytes()
  273. headers["length"] = len(bytes)
  274. f.write(json_dumps(headers))
  275. f.write(b"\n")
  276. f.write(bytes)
  277. f.write(b"\n")
  278. def serialize(self):
  279. # type: (...) -> bytes
  280. out = io.BytesIO()
  281. self.serialize_into(out)
  282. return out.getvalue()
  283. @classmethod
  284. def deserialize_from(
  285. cls, f # type: Any
  286. ):
  287. # type: (...) -> Optional[Item]
  288. line = f.readline().rstrip()
  289. if not line:
  290. return None
  291. headers = parse_json(line)
  292. length = headers.get("length")
  293. if length is not None:
  294. payload = f.read(length)
  295. f.readline()
  296. else:
  297. # if no length was specified we need to read up to the end of line
  298. # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope)
  299. payload = f.readline().rstrip(b"\n")
  300. if headers.get("type") in ("event", "transaction", "metric_buckets"):
  301. rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload)))
  302. else:
  303. rv = cls(headers=headers, payload=payload)
  304. return rv
  305. @classmethod
  306. def deserialize(
  307. cls, bytes # type: bytes
  308. ):
  309. # type: (...) -> Optional[Item]
  310. return cls.deserialize_from(io.BytesIO(bytes))