models.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. from django.conf import settings
  2. from django.contrib.postgres.indexes import GinIndex
  3. from django.contrib.postgres.search import SearchVectorField
  4. from django.db import connection, models
  5. from events.models import LogLevel
  6. from glitchtip.base_models import CreatedModel
  7. from glitchtip.model_utils import FromStringIntegerChoices
  8. from .utils import base32_encode
  9. class EventType(models.IntegerChoices):
  10. DEFAULT = 0, "default"
  11. ERROR = 1, "error"
  12. CSP = 2, "csp"
  13. TRANSACTION = 3, "transaction"
  14. class EventStatus(FromStringIntegerChoices):
  15. UNRESOLVED = 0, "unresolved"
  16. RESOLVED = 1, "resolved"
  17. IGNORED = 2, "ignored"
  18. class Issue(CreatedModel):
  19. """
  20. Sentry called this a "group". A issue is a collection of events with meta data
  21. such as resolved status.
  22. """
  23. # annotations Not implemented
  24. # assigned_to Not implemented
  25. culprit = models.CharField(max_length=1024, blank=True, null=True)
  26. has_seen = models.BooleanField(default=False)
  27. # is_bookmarked Not implement - is per user
  28. is_public = models.BooleanField(default=False)
  29. level = models.PositiveSmallIntegerField(
  30. choices=LogLevel.choices, default=LogLevel.ERROR
  31. )
  32. metadata = models.JSONField()
  33. tags = models.JSONField(default=dict)
  34. project = models.ForeignKey("projects.Project", on_delete=models.CASCADE)
  35. title = models.CharField(max_length=255)
  36. type = models.PositiveSmallIntegerField(
  37. choices=EventType.choices, default=EventType.DEFAULT
  38. )
  39. status = models.PositiveSmallIntegerField(
  40. choices=EventStatus.choices, default=EventStatus.UNRESOLVED
  41. )
  42. # See migration 0004 for trigger that sets search_vector, count, last_seen
  43. short_id = models.PositiveIntegerField(null=True)
  44. search_vector = SearchVectorField(null=True, editable=False)
  45. count = models.PositiveIntegerField(default=1, editable=False)
  46. last_seen = models.DateTimeField(auto_now_add=True, db_index=True)
  47. class Meta:
  48. unique_together = (("project", "short_id"),)
  49. indexes = [GinIndex(fields=["search_vector"], name="search_vector_idx")]
  50. def event(self):
  51. return self.event_set.first()
  52. def __str__(self):
  53. return self.title
  54. def check_for_status_update(self):
  55. """
  56. Determine if issue should regress back to unresolved
  57. Typically run when processing a new event related to the issue
  58. """
  59. if self.status == EventStatus.RESOLVED:
  60. self.status = EventStatus.UNRESOLVED
  61. self.save()
  62. # Delete notifications so that new alerts are sent for regressions
  63. self.notification_set.all().delete()
  64. def get_hex_color(self):
  65. if self.level == LogLevel.INFO:
  66. return "#4b60b4"
  67. elif self.level is LogLevel.WARNING:
  68. return "#e9b949"
  69. elif self.level in [LogLevel.ERROR, LogLevel.FATAL]:
  70. return "#e52b50"
  71. @property
  72. def short_id_display(self):
  73. """
  74. Short IDs are per project issue counters. They show as PROJECT_SLUG-ID_BASE32
  75. The intention is to be human readable identifiers that can reference an issue.
  76. """
  77. if self.short_id is not None:
  78. return f"{self.project.slug.upper()}-{base32_encode(self.short_id)}"
  79. return ""
  80. def get_detail_url(self):
  81. return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/issues/{self.pk}"
  82. @classmethod
  83. def update_index(cls, issue_id: int):
  84. """
  85. Update search index/tag aggregations
  86. """
  87. with connection.cursor() as cursor:
  88. cursor.execute("CALL update_issue_index(%s)", [issue_id])
  89. class IssueHash(models.Model):
  90. issue = models.ForeignKey("issues.Issue", on_delete=models.CASCADE)
  91. # Redundant project allows for unique constraint
  92. project = models.ForeignKey("projects.Project", on_delete=models.CASCADE)
  93. value = models.UUIDField(db_index=True)
  94. class Meta:
  95. constraints = [
  96. models.UniqueConstraint(fields=["project", "value"], name="project hash")
  97. ]
  98. class Comment(CreatedModel):
  99. issue = models.ForeignKey("issues.Issue", on_delete=models.CASCADE)
  100. user = models.ForeignKey("users.User", null=True, on_delete=models.SET_NULL)
  101. text = models.TextField(blank=True, null=True)
  102. class Meta:
  103. ordering = ("-created",)