schema.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. from datetime import datetime
  2. from typing import Annotated, Any, Literal
  3. from ninja import Field, ModelSchema, Schema
  4. from pydantic import computed_field
  5. from apps.event_ingest.schema import CSPReportSchema, EventException
  6. from apps.projects.models import Project
  7. from apps.users.models import User
  8. from glitchtip.schema import CamelSchema
  9. from sentry.interfaces.stacktrace import get_context
  10. from ..shared.schema.contexts import Contexts
  11. from ..shared.schema.event import (
  12. BaseIssueEvent,
  13. BaseRequest,
  14. EventBreadcrumb,
  15. ListKeyValue,
  16. )
  17. from ..shared.schema.user import EventUser
  18. from .constants import IssueEventType
  19. from .models import Comment, Issue, IssueEvent, UserReport
  20. class ProjectReference(CamelSchema, ModelSchema):
  21. id: str
  22. class Config:
  23. model = Project
  24. model_fields = ["platform", "slug", "name"]
  25. populate_by_name = True
  26. @staticmethod
  27. def resolve_id(obj: Project):
  28. return str(obj.id)
  29. # For Sentry compatibility
  30. def to_camel_with_lower_id(string: str) -> str:
  31. return "".join(
  32. word if i == 0 else "Id" if word == "id" else word.capitalize()
  33. for i, word in enumerate(string.split("_"))
  34. )
  35. class IssueSchema(ModelSchema):
  36. id: str
  37. count: str
  38. type: str = Field(validation_alias="get_type_display")
  39. level: str = Field(validation_alias="get_level_display")
  40. status: str = Field(validation_alias="get_status_display")
  41. project: ProjectReference = Field(validation_alias="project")
  42. short_id: str = Field(validation_alias="short_id_display")
  43. num_comments: int
  44. stats: dict[str, list[list[float]]] | None = {"24h": []}
  45. share_id: int | None = None
  46. logger: str | None = None
  47. permalink: str | None = "Not implemented"
  48. status_details: dict[str, str] | None = {}
  49. subscription_details: str | None = None
  50. user_count: int | None = 0
  51. matching_event_id: str | None = Field(
  52. default=None, serialization_alias="matchingEventId"
  53. )
  54. @staticmethod
  55. def resolve_culprit(obj: Issue):
  56. return obj.culprit or ""
  57. @staticmethod
  58. def resolve_matching_event_id(obj: Issue, context):
  59. if event_id := context["request"].matching_event_id:
  60. return event_id.hex
  61. class Config:
  62. model = Issue
  63. model_fields = [
  64. "title",
  65. "metadata",
  66. "culprit",
  67. "first_seen",
  68. "last_seen",
  69. ]
  70. alias_generator = to_camel_with_lower_id
  71. coerce_numbers_to_str = True
  72. populate_by_name = True
  73. class IssueDetailSchema(IssueSchema):
  74. user_report_count: int
  75. class ExceptionEntryData(Schema):
  76. values: dict
  77. exc_omitted: None = None
  78. has_system_frames: bool
  79. class ExceptionEntry(Schema):
  80. type: Literal["exception"]
  81. data: dict
  82. class MessageEntry(Schema):
  83. type: Literal["message"]
  84. data: dict
  85. class CSPEntry(Schema):
  86. type: Literal["csp"]
  87. data: dict
  88. class APIEventBreadcrumb(EventBreadcrumb):
  89. """Slightly modified Breadcrumb for sentry api compatibility"""
  90. event_id: str | None = None
  91. class BreadcrumbsEntry(Schema):
  92. type: Literal["breadcrumbs"]
  93. data: dict[Literal["values"], list[APIEventBreadcrumb]]
  94. class Request(CamelSchema, BaseRequest):
  95. headers: ListKeyValue | None = None
  96. query_string: ListKeyValue | None = Field(default=None, serialization_alias="query")
  97. @computed_field
  98. @property
  99. def inferred_content_type(self) -> str | None:
  100. if self.headers:
  101. return next(
  102. (value for key, value in self.headers if key == "Content-Type"), None
  103. )
  104. return None
  105. class Config(CamelSchema.Config, BaseRequest.Config):
  106. pass
  107. class RequestEntry(Schema):
  108. type: Literal["request"]
  109. data: Request
  110. class IssueEventSchema(CamelSchema, ModelSchema, BaseIssueEvent):
  111. id: str = Field(validation_alias="id.hex")
  112. event_id: str
  113. project_id: int = Field(validation_alias="issue.project_id")
  114. group_id: str
  115. date_created: datetime = Field(validation_alias="timestamp")
  116. date_received: datetime = Field(validation_alias="received")
  117. dist: str | None = None
  118. culprit: str | None = Field(validation_alias="transaction", default=None)
  119. packages: dict[str, str | None] | None = Field(
  120. validation_alias="data.modules", default=None
  121. )
  122. type: str = Field(validation_alias="get_type_display")
  123. message: str
  124. metadata: dict[str, str] = Field(default_factory=dict)
  125. tags: list[dict[str, str | None]] = []
  126. entries: list[
  127. Annotated[
  128. BreadcrumbsEntry | CSPEntry | ExceptionEntry | MessageEntry | RequestEntry,
  129. Field(..., discriminator="type"),
  130. ]
  131. ] = Field(default_factory=list)
  132. contexts: Contexts | None = Field(validation_alias="data.contexts", default=None)
  133. context: dict[str, Any] | None = Field(validation_alias="data.extra", default=None)
  134. user: Any | None = Field(validation_alias="data.user", default=None)
  135. class Config:
  136. model = IssueEvent
  137. model_fields = ["id", "type", "title"]
  138. populate_by_name = True
  139. @staticmethod
  140. def resolve_group_id(obj: IssueEvent):
  141. return str(obj.issue_id)
  142. @staticmethod
  143. def resolve_tags(obj: IssueEvent):
  144. return [{"key": tag[0], "value": tag[1]} for tag in obj.tags.items()]
  145. @staticmethod
  146. def resolve_entries(obj: IssueEvent):
  147. entries = []
  148. data = obj.data
  149. if exception := data.get("exception"):
  150. exception = {"values": exception, "hasSystemFrames": False}
  151. # https://gitlab.com/glitchtip/sentry-open-source/sentry/-/blob/master/src/sentry/interfaces/stacktrace.py#L487
  152. # if any frame is "in_app" set this to True
  153. for value in exception["values"]:
  154. if (
  155. value.get("stacktrace", None) is not None
  156. and "frames" in value["stacktrace"]
  157. ):
  158. for frame in value["stacktrace"]["frames"]:
  159. if frame.get("in_app") is True:
  160. exception["hasSystemFrames"] = True
  161. if "in_app" in frame:
  162. frame["inApp"] = frame.pop("in_app")
  163. if "abs_path" in frame:
  164. frame["absPath"] = frame.pop("abs_path")
  165. if "colno" in frame:
  166. frame["colNo"] = frame.pop("colno")
  167. if "lineno" in frame:
  168. frame["lineNo"] = frame.pop("lineno")
  169. pre_context = frame.pop("pre_context", None)
  170. post_context = frame.pop("post_context", None)
  171. if "context" not in frame:
  172. frame["context"] = get_context(
  173. frame["lineNo"],
  174. frame.get("context_line"),
  175. pre_context,
  176. post_context,
  177. )
  178. entries.append({"type": "exception", "data": exception})
  179. if breadcrumbs := data.get("breadcrumbs"):
  180. entries.append({"type": "breadcrumbs", "data": {"values": breadcrumbs}})
  181. if logentry := data.get("logentry"):
  182. entries.append({"type": "message", "data": logentry})
  183. elif message := data.get("message"):
  184. entries.append({"type": "message", "data": {"formatted": message}})
  185. if request := data.get("request"):
  186. entries.append({"type": "request", "data": request})
  187. if csp := data.get("csp"):
  188. entries.append({"type": IssueEventType.CSP.label, "data": csp})
  189. return entries
  190. class UserReportSchema(CamelSchema, ModelSchema):
  191. event_id: str = Field(validation_alias="event_id.hex")
  192. event: dict[str, str]
  193. date_created: datetime = Field(validation_alias="created")
  194. user: str | None = None
  195. class Config:
  196. model = UserReport
  197. model_fields = ["id", "name", "email", "comments"]
  198. populate_by_name = True
  199. @staticmethod
  200. def resolve_event(obj):
  201. return {
  202. "eventId": obj.event_id.hex,
  203. }
  204. # TODO: Sentry includes a full user object with its nested comments,
  205. # so we should drop this schema once we create a full user schema
  206. class CommentUserSchema(CamelSchema, ModelSchema):
  207. id: str
  208. class Config:
  209. model = User
  210. model_fields = [
  211. "email",
  212. ]
  213. populate_by_name = True
  214. @staticmethod
  215. def resolve_id(obj: User):
  216. return str(obj.id)
  217. class CommentSchema(CamelSchema, ModelSchema):
  218. data: dict[str, str]
  219. type: str | None = "note"
  220. date_created: datetime = Field(validation_alias="created")
  221. user: CommentUserSchema | None
  222. class Config:
  223. model = Comment
  224. model_fields = ["id"]
  225. @staticmethod
  226. def resolve_data(obj: Comment):
  227. return {
  228. "text": obj.text,
  229. }
  230. class IssueEventDetailSchema(IssueEventSchema):
  231. user_report: UserReportSchema | None
  232. next_event_id: str | None = None
  233. previous_event_id: str | None = None
  234. @staticmethod
  235. def resolve_previous_event_id(obj):
  236. if event_id := obj.previous:
  237. return event_id.hex
  238. @staticmethod
  239. def resolve_next_event_id(obj):
  240. if event_id := obj.next:
  241. return event_id.hex
  242. class IssueEventJsonSchema(ModelSchema, BaseIssueEvent):
  243. """
  244. Represents a more raw view of the event, built with open source (legacy) Sentry compatibility
  245. """
  246. event_id: str = Field(validation_alias="id.hex")
  247. timestamp: float = Field()
  248. x_datetime: datetime = Field(
  249. validation_alias="timestamp", serialization_alias="datetime"
  250. )
  251. breadcrumbs: Any | None = Field(validation_alias="data.breadcrumbs", default=None)
  252. project: int = Field(validation_alias="issue.project_id")
  253. level: str | None = Field(validation_alias="get_level_display")
  254. exception: Any | None = Field(validation_alias="data.exception", default=None)
  255. modules: dict[str, str] | None = Field(
  256. validation_alias="data.modules", default_factory=dict
  257. )
  258. contexts: dict | None = Field(validation_alias="data.contexts", default=None)
  259. sdk: dict | None = Field(validation_alias="data.sdk", default_factory=dict)
  260. type: str | None = Field(validation_alias="get_type_display")
  261. request: Any | None = Field(validation_alias="data.request", default=None)
  262. environment: str | None = Field(validation_alias="data.environment", default=None)
  263. extra: dict[str, Any] | None = Field(validation_alias="data.extra", default=None)
  264. user: EventUser | None = Field(validation_alias="data.user", default=None)
  265. class Config:
  266. model = IssueEvent
  267. model_fields = ["title", "transaction", "tags"]
  268. @staticmethod
  269. def resolve_timestamp(obj):
  270. return obj.timestamp.timestamp()
  271. class IssueEventDataSchema(Schema):
  272. """IssueEvent model data json schema"""
  273. metadata: dict[str, Any] | None = None
  274. breadcrumbs: list[EventBreadcrumb] | None = None
  275. exception: list[EventException] | None = None
  276. class CSPIssueEventDataSchema(IssueEventDataSchema):
  277. csp: CSPReportSchema
  278. class IssueTagTopValue(CamelSchema):
  279. name: str
  280. value: str
  281. count: int
  282. key: str
  283. class IssueTagSchema(CamelSchema):
  284. top_values: list[IssueTagTopValue]
  285. unique_values: int
  286. key: str
  287. name: str
  288. total_values: int