models.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import collections
  2. from django.contrib.postgres.search import SearchVectorField
  3. from django.contrib.postgres.indexes import GinIndex
  4. from django.conf import settings
  5. from django.db import models
  6. from django.db.models import Max, Count
  7. from events.models import LogLevel
  8. from glitchtip.model_utils import FromStringIntegerChoices
  9. from glitchtip.base_models import CreatedModel
  10. from .utils import base32_encode
  11. class EventType(models.IntegerChoices):
  12. DEFAULT = 0, "default"
  13. ERROR = 1, "error"
  14. CSP = 2, "csp"
  15. TRANSACTION = 3, "transaction"
  16. class EventStatus(FromStringIntegerChoices):
  17. UNRESOLVED = 0, "unresolved"
  18. RESOLVED = 1, "resolved"
  19. IGNORED = 2, "ignored"
  20. class Issue(CreatedModel):
  21. """
  22. Sentry called this a "group". A issue is a collection of events with meta data
  23. such as resolved status.
  24. """
  25. # annotations Not implemented
  26. # assigned_to Not implemented
  27. culprit = models.CharField(max_length=1024, blank=True, null=True)
  28. has_seen = models.BooleanField(default=False)
  29. # is_bookmarked Not implement - is per user
  30. is_public = models.BooleanField(default=False)
  31. level = models.PositiveSmallIntegerField(
  32. choices=LogLevel.choices, default=LogLevel.ERROR
  33. )
  34. metadata = models.JSONField()
  35. tags = models.JSONField(default=dict)
  36. project = models.ForeignKey("projects.Project", on_delete=models.CASCADE)
  37. title = models.CharField(max_length=255)
  38. type = models.PositiveSmallIntegerField(
  39. choices=EventType.choices, default=EventType.DEFAULT
  40. )
  41. status = models.PositiveSmallIntegerField(
  42. choices=EventStatus.choices, default=EventStatus.UNRESOLVED
  43. )
  44. # See migration 0004 for trigger that sets search_vector, count, last_seen
  45. short_id = models.PositiveIntegerField(null=True)
  46. search_vector = SearchVectorField(null=True, editable=False)
  47. count = models.PositiveIntegerField(default=1, editable=False)
  48. last_seen = models.DateTimeField(auto_now_add=True, db_index=True)
  49. class Meta:
  50. unique_together = (
  51. ("title", "culprit", "project", "type"),
  52. ("project", "short_id"),
  53. )
  54. indexes = [GinIndex(fields=["search_vector"], name="search_vector_idx")]
  55. def event(self):
  56. return self.event_set.first()
  57. def __str__(self):
  58. return self.title
  59. def check_for_status_update(self):
  60. """
  61. Determine if issue should regress back to unresolved
  62. Typically run when processing a new event related to the issue
  63. """
  64. if self.status == EventStatus.RESOLVED:
  65. self.status = EventStatus.UNRESOLVED
  66. self.save()
  67. # Delete notifications so that new alerts are sent for regressions
  68. self.notification_set.all().delete()
  69. def get_hex_color(self):
  70. if self.level == LogLevel.INFO:
  71. return "#4b60b4"
  72. elif self.level is LogLevel.WARNING:
  73. return "#e9b949"
  74. elif self.level in [LogLevel.ERROR, LogLevel.FATAL]:
  75. return "#e52b50"
  76. @property
  77. def short_id_display(self):
  78. """
  79. Short IDs are per project issue counters. They show as PROJECT_SLUG-ID_BASE32
  80. The intention is to be human readable identifiers that can reference an issue.
  81. """
  82. if self.short_id is not None:
  83. return f"{self.project.slug.upper()}-{base32_encode(self.short_id)}"
  84. return ""
  85. def get_detail_url(self):
  86. return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/issues/{self.pk}"
  87. @classmethod
  88. def update_index(cls, issue_id: int, skip_tags=False):
  89. """
  90. Update search index/tag aggregations
  91. """
  92. vector_query = """SELECT generate_issue_tsvector(jsonb_agg(data)) from (SELECT events_event.data from events_event where issue_id = %s limit 200) as data"""
  93. issue = (
  94. cls.objects.extra(
  95. select={"new_vector": vector_query}, select_params=(issue_id,)
  96. )
  97. .annotate(
  98. new_count=Count("event"),
  99. new_last_seen=Max("event__created"),
  100. new_level=Max("event__level"),
  101. )
  102. .defer("search_vector")
  103. .get(pk=issue_id)
  104. )
  105. update_fields = ["last_seen", "count", "level"]
  106. if (
  107. issue.new_vector
  108. ): # This check is important, because generate_issue_tsvector returns null on size limit
  109. update_fields.append("search_vector")
  110. issue.search_vector = issue.new_vector
  111. if issue.new_last_seen:
  112. issue.last_seen = issue.new_last_seen
  113. if issue.new_count:
  114. issue.count = issue.new_count
  115. if issue.new_level:
  116. issue.level = issue.new_level
  117. if not skip_tags:
  118. update_fields.append("tags")
  119. tags = (
  120. issue.event_set.all()
  121. .order_by("tags")
  122. .values_list("tags", flat=True)
  123. .distinct()
  124. )
  125. super_dict = collections.defaultdict(set)
  126. for tag in tags:
  127. for key, value in tag.items():
  128. super_dict[key].add(value)
  129. issue.tags = {k: list(v) for k, v in super_dict.items()}
  130. issue.save(update_fields=update_fields)