import collections from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.indexes import GinIndex from django.conf import settings from django.db import models from django.db.models import Max, Count from django.db.utils import InternalError from events.models import LogLevel from glitchtip.model_utils import FromStringIntegerChoices from glitchtip.base_models import CreatedModel from .utils import base32_encode class EventType(models.IntegerChoices): DEFAULT = 0, "default" ERROR = 1, "error" CSP = 2, "csp" TRANSACTION = 3, "transaction" class EventStatus(FromStringIntegerChoices): UNRESOLVED = 0, "unresolved" RESOLVED = 1, "resolved" IGNORED = 2, "ignored" class Issue(CreatedModel): """ Sentry called this a "group". A issue is a collection of events with meta data such as resolved status. """ # annotations Not implemented # assigned_to Not implemented culprit = models.CharField(max_length=1024, blank=True, null=True) has_seen = models.BooleanField(default=False) # is_bookmarked Not implement - is per user is_public = models.BooleanField(default=False) level = models.PositiveSmallIntegerField( choices=LogLevel.choices, default=LogLevel.ERROR ) metadata = models.JSONField() tags = models.JSONField(default=dict) project = models.ForeignKey("projects.Project", on_delete=models.CASCADE) title = models.CharField(max_length=255) type = models.PositiveSmallIntegerField( choices=EventType.choices, default=EventType.DEFAULT ) status = models.PositiveSmallIntegerField( choices=EventStatus.choices, default=EventStatus.UNRESOLVED ) # See migration 0004 for trigger that sets search_vector, count, last_seen short_id = models.PositiveIntegerField(null=True) search_vector = SearchVectorField(null=True, editable=False) count = models.PositiveIntegerField(default=1, editable=False) last_seen = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: unique_together = ( ("title", "culprit", "project", "type"), ("project", "short_id"), ) indexes = [GinIndex(fields=["search_vector"], name="search_vector_idx")] def event(self): return self.event_set.first() def __str__(self): return self.title def check_for_status_update(self): """ Determine if issue should regress back to unresolved Typically run when processing a new event related to the issue """ if self.status == EventStatus.RESOLVED: self.status = EventStatus.UNRESOLVED self.save() # Delete notifications so that new alerts are sent for regressions self.notification_set.all().delete() def get_hex_color(self): if self.level == LogLevel.INFO: return "#4b60b4" elif self.level is LogLevel.WARNING: return "#e9b949" elif self.level in [LogLevel.ERROR, LogLevel.FATAL]: return "#e52b50" @property def short_id_display(self): """ Short IDs are per project issue counters. They show as PROJECT_SLUG-ID_BASE32 The intention is to be human readable identifiers that can reference an issue. """ if self.short_id is not None: return f"{self.project.slug.upper()}-{base32_encode(self.short_id)}" return "" def get_detail_url(self): return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/issues/{self.pk}" @classmethod def update_index(cls, issue_id: int, skip_tags=False): """ Update search index/tag aggregations """ 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""" issue = ( cls.objects.extra( select={"new_vector": vector_query}, select_params=(issue_id,) ) .annotate( new_count=Count("event"), new_last_seen=Max("event__created"), new_level=Max("event__level"), ) .defer("search_vector") .get(pk=issue_id) ) update_fields = ["last_seen", "count", "level"] if ( issue.new_vector ): # This check is important, because generate_issue_tsvector returns null on size limit update_fields.append("search_vector") issue.search_vector = issue.new_vector if issue.new_last_seen: issue.last_seen = issue.new_last_seen if issue.new_count: issue.count = issue.new_count if issue.new_level: issue.level = issue.new_level if not skip_tags: update_fields.append("tags") tags = ( issue.event_set.all() .order_by("tags") .values_list("tags", flat=True) .distinct() ) super_dict = collections.defaultdict(set) for tag in tags: for key, value in tag.items(): super_dict[key].add(value) issue.tags = {k: list(v) for k, v in super_dict.items()} issue.save(update_fields=update_fields)