models.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  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 = (
  49. ("title", "culprit", "project", "type"),
  50. ("project", "short_id"),
  51. )
  52. indexes = [GinIndex(fields=["search_vector"], name="search_vector_idx")]
  53. def event(self):
  54. return self.event_set.first()
  55. def __str__(self):
  56. return self.title
  57. def check_for_status_update(self):
  58. """
  59. Determine if issue should regress back to unresolved
  60. Typically run when processing a new event related to the issue
  61. """
  62. if self.status == EventStatus.RESOLVED:
  63. self.status = EventStatus.UNRESOLVED
  64. self.save()
  65. # Delete notifications so that new alerts are sent for regressions
  66. self.notification_set.all().delete()
  67. def get_hex_color(self):
  68. if self.level == LogLevel.INFO:
  69. return "#4b60b4"
  70. elif self.level is LogLevel.WARNING:
  71. return "#e9b949"
  72. elif self.level in [LogLevel.ERROR, LogLevel.FATAL]:
  73. return "#e52b50"
  74. @property
  75. def short_id_display(self):
  76. """
  77. Short IDs are per project issue counters. They show as PROJECT_SLUG-ID_BASE32
  78. The intention is to be human readable identifiers that can reference an issue.
  79. """
  80. if self.short_id is not None:
  81. return f"{self.project.slug.upper()}-{base32_encode(self.short_id)}"
  82. return ""
  83. def get_detail_url(self):
  84. return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/issues/{self.pk}"
  85. @classmethod
  86. def update_index(cls, issue_id: int):
  87. """
  88. Update search index/tag aggregations
  89. """
  90. with connection.cursor() as cursor:
  91. cursor.execute("CALL update_issue_index(%s)", [issue_id])