models.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import uuid
  2. from django.conf import settings
  3. from django.contrib.postgres.indexes import GinIndex
  4. from django.contrib.postgres.search import SearchVectorField
  5. from django.db import models
  6. from django.utils import timezone
  7. from glitchtip.base_models import AggregationModel, CreatedModel, SoftDeleteModel
  8. from psqlextra.models import PostgresPartitionedModel
  9. from psqlextra.types import PostgresPartitioningMethod
  10. from sentry.constants import MAX_CULPRIT_LENGTH
  11. from .constants import EventStatus, IssueEventType, LogLevel
  12. from .utils import base32_encode
  13. class DeferedFieldManager(models.Manager):
  14. def __init__(self, defered_fields=[]):
  15. super().__init__()
  16. self.defered_fields = defered_fields
  17. def get_queryset(self, *args, **kwargs):
  18. return super().get_queryset(*args, **kwargs).defer(*self.defered_fields)
  19. class TagKey(models.Model):
  20. id = models.AutoField(primary_key=True)
  21. key = models.CharField(max_length=255, unique=True)
  22. class TagValue(models.Model):
  23. value = models.CharField(max_length=255, unique=True)
  24. class IssueTag(AggregationModel):
  25. """
  26. This model is a aggregate of event tags for an issue.
  27. It is denormalized data that powers fast search results.
  28. """
  29. issue = models.ForeignKey("Issue", on_delete=models.CASCADE)
  30. date = models.DateTimeField()
  31. tag_key = models.ForeignKey(TagKey, on_delete=models.CASCADE)
  32. tag_value = models.ForeignKey(TagValue, on_delete=models.CASCADE)
  33. count = models.PositiveIntegerField(default=1)
  34. class Meta:
  35. constraints = [
  36. models.UniqueConstraint(
  37. fields=["issue", "date", "tag_key", "tag_value"],
  38. name="issue_tag_key_value_unique",
  39. )
  40. ]
  41. class PartitioningMeta(AggregationModel.PartitioningMeta):
  42. pass
  43. class Issue(SoftDeleteModel):
  44. culprit = models.CharField(max_length=1024, blank=True, null=True)
  45. is_public = models.BooleanField(default=False)
  46. level = models.PositiveSmallIntegerField(
  47. choices=LogLevel.choices, default=LogLevel.ERROR
  48. )
  49. metadata = models.JSONField()
  50. project = models.ForeignKey(
  51. "projects.Project", on_delete=models.CASCADE, related_name="issues"
  52. )
  53. title = models.CharField(max_length=255)
  54. type = models.PositiveSmallIntegerField(
  55. choices=IssueEventType.choices, default=IssueEventType.DEFAULT
  56. )
  57. status = models.PositiveSmallIntegerField(
  58. choices=EventStatus.choices, default=EventStatus.UNRESOLVED
  59. )
  60. short_id = models.PositiveIntegerField(null=True)
  61. search_vector = SearchVectorField(editable=False, default="")
  62. count = models.PositiveIntegerField(default=1, editable=False)
  63. first_seen = models.DateTimeField(default=timezone.now, db_index=True)
  64. last_seen = models.DateTimeField(default=timezone.now, db_index=True)
  65. objects = DeferedFieldManager(["search_vector"])
  66. class Meta:
  67. base_manager_name = "objects"
  68. constraints = [
  69. models.UniqueConstraint(
  70. fields=["project", "short_id"],
  71. name="project_short_id_unique",
  72. )
  73. ]
  74. indexes = [
  75. GinIndex(fields=["search_vector"]),
  76. ]
  77. def __str__(self):
  78. return self.title
  79. def get_detail_url(self):
  80. return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/issues/{self.pk}"
  81. def get_hex_color(self):
  82. if self.level == LogLevel.INFO:
  83. return "#4b60b4"
  84. elif self.level is LogLevel.WARNING:
  85. return "#e9b949"
  86. elif self.level in [LogLevel.ERROR, LogLevel.FATAL]:
  87. return "#e52b50"
  88. @property
  89. def short_id_display(self):
  90. """
  91. Short IDs are per project issue counters. They show as PROJECT_SLUG-ID_BASE32
  92. The intention is to be human readable identifiers that can reference an issue.
  93. """
  94. if self.short_id is not None:
  95. return f"{self.project.slug.upper()}-{base32_encode(self.short_id)}"
  96. return ""
  97. class IssueHash(models.Model):
  98. issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="hashes")
  99. # Redundant project allows for unique constraint
  100. project = models.ForeignKey(
  101. "projects.Project", on_delete=models.CASCADE, related_name="+"
  102. )
  103. value = models.UUIDField(db_index=True)
  104. class Meta:
  105. constraints = [
  106. models.UniqueConstraint(
  107. fields=["project", "value"], name="issue hash project"
  108. )
  109. ]
  110. class Comment(models.Model):
  111. created = models.DateTimeField(auto_now_add=True)
  112. issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="comments")
  113. user = models.ForeignKey(
  114. "users.User", null=True, on_delete=models.SET_NULL, related_name="+"
  115. )
  116. text = models.TextField(blank=True, null=True)
  117. class Meta:
  118. ordering = ("-created",)
  119. class UserReport(CreatedModel):
  120. project = models.ForeignKey(
  121. "projects.Project", on_delete=models.CASCADE, related_name="+"
  122. )
  123. issue = models.ForeignKey(Issue, null=True, on_delete=models.CASCADE)
  124. event_id = models.UUIDField()
  125. name = models.CharField(max_length=128)
  126. email = models.EmailField()
  127. comments = models.TextField()
  128. class Meta:
  129. constraints = [
  130. models.UniqueConstraint(
  131. fields=["project", "event_id"],
  132. name="project_event_unique",
  133. )
  134. ]
  135. class IssueEvent(PostgresPartitionedModel, models.Model):
  136. id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  137. issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
  138. type = models.PositiveSmallIntegerField(default=0, choices=IssueEventType.choices)
  139. timestamp = models.DateTimeField(help_text="Time at which event happened")
  140. received = models.DateTimeField(help_text="Time at which GlitchTip accepted event")
  141. title = models.CharField(max_length=255)
  142. transaction = models.CharField(max_length=MAX_CULPRIT_LENGTH)
  143. level = models.PositiveSmallIntegerField(
  144. choices=LogLevel.choices, default=LogLevel.ERROR
  145. )
  146. data = models.JSONField()
  147. # This could be HStore, but jsonb is just as good and removes need for
  148. # 'django.contrib.postgres' which makes several unnecessary SQL calls
  149. tags = models.JSONField()
  150. release = models.ForeignKey(
  151. "releases.Release", blank=True, null=True, on_delete=models.SET_NULL
  152. )
  153. class Meta:
  154. indexes = [models.Index(fields=["issue", "-received"])]
  155. class PartitioningMeta:
  156. method = PostgresPartitioningMethod.RANGE
  157. key = ["received"]
  158. def __str__(self):
  159. return self.eventID
  160. @property
  161. def eventID(self):
  162. return self.id.hex
  163. @property
  164. def message(self):
  165. """Often the title and message are the same. If message isn't stored, assume it's the title"""
  166. return self.data.get("message", self.title)
  167. @property
  168. def metadata(self):
  169. """Return metadata if exists, else return just the title as metadata"""
  170. return self.data.get("metadata", {"title": self.title})
  171. @property
  172. def platform(self):
  173. return self.data.get("platform")