models.py 4.4 KB

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