class TagValue(Model): """ Stores references to available filters. """ __core__ = False project_id = BoundedPositiveIntegerField(db_index=True, null=True) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) data = GzippedDictField(blank=True, null=True) times_seen = BoundedPositiveIntegerField(default=0) last_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) objects = BaseManager() class Meta: app_label = 'sentry' db_table = 'sentry_filtervalue' unique_together = (('project_id', 'key', 'value'), ) index_together = (('project_id', 'key', 'last_seen'), ) __repr__ = sane_repr('project_id', 'key', 'value') def get_label(self): from sentry import tagstore return tagstore.get_tag_value_label(self.key, self.value)
class GroupTombstone(Model): __core__ = False previous_group_id = BoundedPositiveIntegerField(unique=True) project = FlexibleForeignKey("sentry.Project") level = BoundedPositiveIntegerField( choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True ) message = models.TextField() culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH, blank=True, null=True) data = GzippedDictField(blank=True, null=True) actor_id = BoundedPositiveIntegerField(null=True) class Meta: app_label = "sentry" db_table = "sentry_grouptombstone" def get_event_type(self): """ Return the type of this issue. See ``sentry.eventtypes``. """ return self.data.get("type", "default") def get_event_metadata(self): """ Return the metadata of this issue. See ``sentry.eventtypes``. """ return self.data["metadata"]
class TagValue(Model): """ Stores references to available filters. """ __core__ = False project = FlexibleForeignKey('sentry.Project', null=True) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) data = GzippedDictField(blank=True, null=True) times_seen = BoundedPositiveIntegerField(default=0) last_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) objects = BaseManager() class Meta: app_label = 'sentry' db_table = 'sentry_filtervalue' unique_together = (('project', 'key', 'value'), ) __repr__ = sane_repr('project_id', 'key', 'value') def get_label(self): # HACK(dcramer): quick and dirty way to hack in better display states if self.key == 'sentry:user': return self.data.get('email') or self.value elif self.key == 'sentry:function': return '%s in %s' % (self.data['function'], self.data['filename']) elif self.key == 'sentry:filename': return self.data['filename'] return self.value def get_absolute_url(self): # HACK(dcramer): quick and dirty way to support code/users if self.key == 'sentry:user': url_name = 'sentry-user-details' elif self.key == 'sentry:filename': url_name = 'sentry-explore-code-details' elif self.key == 'sentry:function': url_name = 'sentry-explore-code-details-by-function' else: url_name = 'sentry-explore-tag-value' return absolute_uri( reverse(url_name, args=[ self.project.organization.slug, self.project.slug, self.key, self.id ])) return absolute_uri( reverse(url_name, args=[ self.project.organization.slug, self.project.slug, self.id ]))
class TagValue(Model): """ Stores references to available filters. """ __core__ = False project_id = BoundedPositiveIntegerField(db_index=True, null=True) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) data = GzippedDictField(blank=True, null=True) times_seen = BoundedPositiveIntegerField(default=0) last_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) objects = BaseManager() class Meta: app_label = 'sentry' db_table = 'sentry_filtervalue' unique_together = (('project_id', 'key', 'value'), ) __repr__ = sane_repr('project_id', 'key', 'value') def get_label(self): # HACK(dcramer): quick and dirty way to hack in better display states if self.key == 'sentry:release': return Release.get_display_version(self.value) return self.value
class TagValue(Model): """ Stores references to available filters. """ __core__ = False project_id = BoundedPositiveIntegerField(db_index=True) environment_id = BoundedPositiveIntegerField(null=True) _key = FlexibleForeignKey('tagstore.TagKey', db_column='key') value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) data = GzippedDictField(blank=True, null=True) times_seen = BoundedPositiveIntegerField(default=0) last_seen = models.DateTimeField( default=timezone.now, db_index=True, null=True) first_seen = models.DateTimeField( default=timezone.now, db_index=True, null=True) objects = BaseManager() class Meta: app_label = 'tagstore' unique_together = (('project_id', 'environment_id', '_key', 'value'), ) # TODO: environment index(es) index_together = (('project_id', '_key', 'last_seen'), ) __repr__ = sane_repr('project_id', 'environment_id', '_key', 'value') @property def key(self): return self._key.key def get_label(self): from sentry import tagstore return tagstore.get_tag_value_label(self.key, self.value)
class ProcessingIssue(Model): __include_in_export__ = False project = FlexibleForeignKey("sentry.Project", db_index=True) checksum = models.CharField(max_length=40, db_index=True) type = models.CharField(max_length=30) data = GzippedDictField() datetime = models.DateTimeField(default=timezone.now) objects = ProcessingIssueManager() class Meta: app_label = "sentry" db_table = "sentry_processingissue" unique_together = (("project", "checksum", "type"),) __repr__ = sane_repr("project_id") @property def scope(self): return self.data["_scope"] @property def object(self): return self.data["_object"]
class AccessGroup(Model): """ An access group identifies a set of members with a defined set of permissions (and project access) for a Team. Groups may be automated through extensions (such as LDAP) so that membership is automatically maintained. If this is the case the ``managed`` attribute will be ``True``. """ team = models.ForeignKey(Team) name = models.CharField(max_length=64) type = BoundedIntegerField(choices=MEMBER_TYPES, default=MEMBER_USER) managed = models.BooleanField(default=False) data = GzippedDictField(blank=True, null=True) date_added = models.DateTimeField(default=timezone.now) projects = models.ManyToManyField('sentry.Project') members = models.ManyToManyField(settings.AUTH_USER_MODEL) objects = BaseManager() class Meta: unique_together = (('team', 'name'),) __repr__ = sane_repr('team_id', 'name', 'type', 'managed')
class ProcessingIssue(Model): __core__ = False project = FlexibleForeignKey('sentry.Project', db_index=True) checksum = models.CharField(max_length=40, db_index=True) type = models.CharField(max_length=30) data = GzippedDictField() datetime = models.DateTimeField(default=timezone.now) objects = ProcessingIssueManager() class Meta: app_label = 'sentry' db_table = 'sentry_processingissue' unique_together = (('project', 'checksum', 'type'), ) __repr__ = sane_repr('project_id') @property def scope(self): return self.data['_scope'] @property def object(self): return self.data['_object']
class Rule(Model): __core__ = True DEFAULT_ACTION_MATCH = 'all' # any, all DEFAULT_FREQUENCY = 30 # minutes project = FlexibleForeignKey('sentry.Project') environment_id = BoundedPositiveIntegerField(null=True) label = models.CharField(max_length=64) data = GzippedDictField() status = BoundedPositiveIntegerField(default=RuleStatus.ACTIVE, choices=( (RuleStatus.ACTIVE, 'Active'), (RuleStatus.INACTIVE, 'Inactive'), ), db_index=True) date_added = models.DateTimeField(default=timezone.now) objects = BaseManager(cache_fields=('pk', )) class Meta: db_table = 'sentry_rule' app_label = 'sentry' __repr__ = sane_repr('project_id', 'label') @classmethod def get_for_project(cls, project_id): cache_key = u'project:{}:rules'.format(project_id) rules_list = cache.get(cache_key) if rules_list is None: rules_list = list( cls.objects.filter( project=project_id, status=RuleStatus.ACTIVE, )) cache.set(cache_key, rules_list, 60) return rules_list def delete(self, *args, **kwargs): rv = super(Rule, self).delete(*args, **kwargs) cache_key = u'project:{}:rules'.format(self.project_id) cache.delete(cache_key) return rv def save(self, *args, **kwargs): rv = super(Rule, self).save(*args, **kwargs) cache_key = u'project:{}:rules'.format(self.project_id) cache.delete(cache_key) return rv def get_audit_log_data(self): return { 'label': self.label, 'data': self.data, 'status': self.status, }
class AlertRelatedGroup(Model): group = models.ForeignKey(Group) alert = models.ForeignKey(Alert) data = GzippedDictField(null=True) class Meta: unique_together = (('group', 'alert'),) __repr__ = sane_repr('group_id', 'alert_id')
class Node(BaseModel): id = models.CharField(max_length=40, primary_key=True) data = GzippedDictField() timestamp = models.DateTimeField(default=timezone.now, db_index=True) __repr__ = sane_repr('timestamp') class Meta: app_label = 'nodestore'
class TagValue(Model): """ Stores references to available filters. """ __core__ = False project_id = BoundedBigIntegerField(db_index=True) _key = FlexibleForeignKey('tagstore.TagKey', db_column='key_id') value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) data = GzippedDictField(blank=True, null=True) times_seen = BoundedPositiveIntegerField(default=0) last_seen = models.DateTimeField( default=timezone.now, db_index=True, null=True) first_seen = models.DateTimeField( default=timezone.now, db_index=True, null=True) objects = BaseManager() class Meta: app_label = 'tagstore' unique_together = (('project_id', '_key', 'value'), ) index_together = (('project_id', '_key', 'last_seen'), ) __repr__ = sane_repr('project_id', '_key', 'value') @property def key(self): if hasattr(self, '_set_key'): return self._set_key if hasattr(self, '__key_cache'): return self._key.key # fallback from sentry.tagstore.v2.models import TagKey tk = TagKey.objects.filter( project_id=self.project_id, id=self._key_id, ).values_list('key', flat=True).get() # cache for future calls self.key = tk return tk @key.setter def key(self, key): self._set_key = key def get_label(self): from sentry import tagstore return tagstore.get_tag_value_label(self.key, self.value)
class AlertRelatedGroup(Model): group = FlexibleForeignKey('sentry.Group') alert = FlexibleForeignKey(Alert) data = GzippedDictField(null=True) class Meta: app_label = 'sentry' db_table = 'sentry_alertrelatedgroup' unique_together = (('group', 'alert'), ) __repr__ = sane_repr('group_id', 'alert_id')
class Rule(Model): __core__ = True DEFAULT_ACTION_MATCH = "all" # any, all DEFAULT_FREQUENCY = 30 # minutes project = FlexibleForeignKey("sentry.Project") environment_id = BoundedPositiveIntegerField(null=True) label = models.CharField(max_length=64) data = GzippedDictField() status = BoundedPositiveIntegerField( default=RuleStatus.ACTIVE, choices=((RuleStatus.ACTIVE, "Active"), (RuleStatus.INACTIVE, "Inactive")), db_index=True, ) date_added = models.DateTimeField(default=timezone.now) objects = BaseManager(cache_fields=("pk", )) class Meta: db_table = "sentry_rule" app_label = "sentry" __repr__ = sane_repr("project_id", "label") @classmethod def get_for_project(cls, project_id): cache_key = u"project:{}:rules".format(project_id) rules_list = cache.get(cache_key) if rules_list is None: rules_list = list( cls.objects.filter(project=project_id, status=RuleStatus.ACTIVE)) cache.set(cache_key, rules_list, 60) return rules_list def delete(self, *args, **kwargs): rv = super(Rule, self).delete(*args, **kwargs) cache_key = u"project:{}:rules".format(self.project_id) cache.delete(cache_key) return rv def save(self, *args, **kwargs): rv = super(Rule, self).save(*args, **kwargs) cache_key = u"project:{}:rules".format(self.project_id) cache.delete(cache_key) return rv def get_audit_log_data(self): return {"label": self.label, "data": self.data, "status": self.status}
class Rule(Model): project = models.ForeignKey('sentry.Project') label = models.CharField(max_length=64) data = GzippedDictField() date_added = models.DateTimeField(default=timezone.now) objects = BaseManager(cache_fields=('pk', )) class Meta: db_table = 'sentry_rule' app_label = 'sentry' __repr__ = sane_repr('project_id', 'label')
class Node(BaseModel): __core__ = False id = models.CharField(max_length=40, primary_key=True) # TODO(dcramer): this being pickle and not JSON has the ability to cause # hard errors as it accepts other serialization than native JSON data = GzippedDictField() timestamp = models.DateTimeField(default=timezone.now, db_index=True) __repr__ = sane_repr("timestamp") class Meta: app_label = "nodestore"
class Activity(Model): SET_RESOLVED = 1 SET_UNRESOLVED = 2 SET_MUTED = 3 SET_PUBLIC = 4 SET_PRIVATE = 5 SET_REGRESSION = 6 CREATE_ISSUE = 7 NOTE = 8 TYPE = ( # (TYPE, verb-slug) (SET_RESOLVED, 'set_resolved'), (SET_UNRESOLVED, 'set_unresolved'), (SET_MUTED, 'set_muted'), (SET_PUBLIC, 'set_public'), (SET_PRIVATE, 'set_private'), (SET_REGRESSION, 'set_regression'), (CREATE_ISSUE, 'create_issue'), (NOTE, 'note'), ) project = models.ForeignKey(Project) group = models.ForeignKey(Group, null=True) event = models.ForeignKey(Event, null=True) # index on (type, ident) type = BoundedPositiveIntegerField(choices=TYPE) ident = models.CharField(max_length=64, null=True) # if the user is not set, it's assumed to be the system user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True) datetime = models.DateTimeField(default=timezone.now) data = GzippedDictField(null=True) __repr__ = sane_repr('project_id', 'group_id', 'event_id', 'user_id', 'type', 'ident') def save(self, *args, **kwargs): created = bool(not self.id) super(Activity, self).save(*args, **kwargs) if not created: return # HACK: support Group.num_comments if self.type == Activity.NOTE: self.group.update(num_comments=F('num_comments') + 1) if self.event: self.event.update(num_comments=F('num_comments') + 1)
class GroupTombstone(Model): __core__ = False previous_group_id = BoundedPositiveIntegerField(unique=True) project = FlexibleForeignKey('sentry.Project') level = BoundedPositiveIntegerField( choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True ) message = models.TextField() culprit = models.CharField( max_length=MAX_CULPRIT_LENGTH, blank=True, null=True, ) data = GzippedDictField(blank=True, null=True) actor_id = BoundedPositiveIntegerField(null=True) class Meta: app_label = 'sentry' db_table = 'sentry_grouptombstone' def get_event_type(self): """ Return the type of this issue. See ``sentry.eventtypes``. """ return self.data.get('type', 'default') def get_event_metadata(self): """ Return the metadata of this issue. See ``sentry.eventtypes``. """ etype = self.data.get('type') if etype is None: etype = 'default' if 'metadata' not in self.data: data = self.data.copy() if self.data else {} data['message'] = self.message return eventtypes.get(etype)(data).get_metadata() return self.data['metadata']
class TagValue(Model): """ Stores references to available filters. """ project = models.ForeignKey(Project, null=True) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) data = GzippedDictField(blank=True, null=True) times_seen = BoundedPositiveIntegerField(default=0) last_seen = models.DateTimeField( default=timezone.now, db_index=True, null=True) first_seen = models.DateTimeField( default=timezone.now, db_index=True, null=True) objects = BaseManager() class Meta: db_table = 'sentry_filtervalue' unique_together = (('project', 'key', 'value'),) __repr__ = sane_repr('project_id', 'key', 'value')
class TagValue(Model): """ Stores references to available filters. """ __core__ = False project_id = BoundedPositiveIntegerField(db_index=True) environment_id = BoundedPositiveIntegerField() key_id = BoundedPositiveIntegerField() value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) # TODO: do we even use this anymore? data = GzippedDictField(blank=True, null=True) # times_seen will live in Redis last_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) objects = BaseManager() class Meta: app_label = 'tagstore' unique_together = (('project_id', 'environment_id', 'key_id', 'value'), ) # TODO: environment index(es) index_together = (('project_id', 'key_id', 'last_seen'), ) __repr__ = sane_repr('project_id', 'environment_id', 'key_id', 'value') # TODO: key property to fetch actual key string? def get_label(self): from sentry import tagstore return tagstore.get_tag_value_label(self.key, self.value)
class File(Model): name = models.CharField(max_length=256) storage = models.CharField(max_length=128, null=True) storage_options = GzippedDictField() path = models.TextField(null=True) type = models.CharField(max_length=64) size = BoundedPositiveIntegerField(null=True) timestamp = models.DateTimeField(default=timezone.now, db_index=True) class Meta: app_label = 'sentry' db_table = 'sentry_file' def delete(self, *args, **kwargs): if self.path: self.deletefile(commit=False) super(File, self).delete(*args, **kwargs) def generate_unique_path(self): pieces = self.type.split('.') pieces.extend( map(str, divmod(int(self.timestamp.strftime('%s')), ONE_DAY))) pieces.append('%s-%s' % (uuid4().hex, self.name)) return '/'.join(pieces) def get_storage(self): backend = self.storage options = self.storage_options storage = get_storage_class(backend) return storage(**options) def deletefile(self, commit=False): assert self.path storage = self.get_storage() storage.delete(self.path) self.path = None if commit: self.save() def putfile(self, fileobj, commit=True): """ Upload this given File's contents. A file's content is idempotent and you may not re-save a given file. >>> my_file = File(name='app.dsym', type='release.artifact') >>> my_file.putfile(fileobj, commit=False) >>> my_file.save() """ assert not self.path self.path = self.generate_unique_path() self.storage = settings.SENTRY_FILESTORE self.storage_options = settings.SENTRY_FILESTORE_OPTIONS storage = self.get_storage() storage.save(self.path, fileobj) if commit: self.save() def getfile(self): """ Return a file-like object for this File's content. >>> fileobj = my_file.getfile() >>> with open('/tmp/localfile', 'wb') as fp: >>> for chunk in fileobj.chunks(): >>> fp.write(chunk) """ assert self.path storage = self.get_storage() return storage.open(self.path)
class Group(Model): """ Aggregated message which summarizes a set of Events. """ __include_in_export__ = False project = FlexibleForeignKey("sentry.Project") logger = models.CharField(max_length=64, blank=True, default=str(DEFAULT_LOGGER_NAME), db_index=True) level = BoundedPositiveIntegerField( choices=[(key, str(val)) for key, val in sorted(LOG_LEVELS.items())], default=logging.ERROR, blank=True, db_index=True, ) message = models.TextField() culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH, blank=True, null=True, db_column="view") num_comments = BoundedPositiveIntegerField(default=0, null=True) platform = models.CharField(max_length=64, null=True) status = BoundedPositiveIntegerField( default=0, choices=( (GroupStatus.UNRESOLVED, _("Unresolved")), (GroupStatus.RESOLVED, _("Resolved")), (GroupStatus.IGNORED, _("Ignored")), ), db_index=True, ) times_seen = BoundedPositiveIntegerField(default=1, db_index=True) last_seen = models.DateTimeField(default=timezone.now, db_index=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True) first_release = FlexibleForeignKey("sentry.Release", null=True, on_delete=models.PROTECT) resolved_at = models.DateTimeField(null=True, db_index=True) # active_at should be the same as first_seen by default active_at = models.DateTimeField(null=True, db_index=True) time_spent_total = BoundedIntegerField(default=0) time_spent_count = BoundedIntegerField(default=0) score = BoundedIntegerField(default=0) # deprecated, do not use. GroupShare has superseded is_public = models.NullBooleanField(default=False, null=True) data = GzippedDictField(blank=True, null=True) short_id = BoundedBigIntegerField(null=True) objects = GroupManager(cache_fields=("id", )) class Meta: app_label = "sentry" db_table = "sentry_groupedmessage" verbose_name_plural = _("grouped messages") verbose_name = _("grouped message") permissions = (("can_view", "Can view"), ) index_together = [ ("project", "first_release"), ("project", "id"), ("project", "status", "last_seen", "id"), ] unique_together = ( ("project", "short_id"), ("project", "id"), ) __repr__ = sane_repr("project_id") def __str__(self): return f"({self.times_seen}) {self.error()}" def save(self, *args, **kwargs): if not self.last_seen: self.last_seen = timezone.now() if not self.first_seen: self.first_seen = self.last_seen if not self.active_at: self.active_at = self.first_seen # We limit what we store for the message body self.message = strip(self.message) if self.message: self.message = truncatechars(self.message.splitlines()[0], 255) if self.times_seen is None: self.times_seen = 1 self.score = type(self).calculate_score(times_seen=self.times_seen, last_seen=self.last_seen) super().save(*args, **kwargs) def get_absolute_url( self, params: Mapping[str, str] | None = None, event_id: int | None = None, organization_slug: str | None = None, ) -> str: # Built manually in preference to django.urls.reverse, # because reverse has a measured performance impact. event_path = f"events/{event_id}/" if event_id else "" url = "organizations/{org}/issues/{id}/{event_path}{params}".format( # Pass organization_slug if this needs to be called multiple times to avoid n+1 queries org=urlquote(self.organization.slug if organization_slug is None else organization_slug), id=self.id, event_path=event_path, params="?" + urlencode(params) if params else "", ) return absolute_uri(url) @property def qualified_short_id(self): if self.short_id is not None: return f"{self.project.slug.upper()}-{base32_encode(self.short_id)}" def is_over_resolve_age(self): resolve_age = self.project.get_option("sentry:resolve_age", None) if not resolve_age: return False return self.last_seen < timezone.now() - timedelta( hours=int(resolve_age)) def is_ignored(self): return self.get_status() == GroupStatus.IGNORED def is_unresolved(self): return self.get_status() == GroupStatus.UNRESOLVED # TODO(dcramer): remove in 9.0 / after plugins no long ref is_muted = is_ignored def is_resolved(self): return self.get_status() == GroupStatus.RESOLVED def get_status(self): # XXX(dcramer): GroupSerializer reimplements this logic from sentry.models import GroupSnooze status = self.status if status == GroupStatus.IGNORED: try: snooze = GroupSnooze.objects.get_from_cache(group=self) except GroupSnooze.DoesNotExist: pass else: if not snooze.is_valid(group=self): status = GroupStatus.UNRESOLVED if status == GroupStatus.UNRESOLVED and self.is_over_resolve_age(): return GroupStatus.RESOLVED return status def get_share_id(self): from sentry.models import GroupShare try: return GroupShare.objects.filter(group_id=self.id).values_list( "uuid", flat=True)[0] except IndexError: # Otherwise it has not been shared yet. return None def get_score(self): return type(self).calculate_score(self.times_seen, self.last_seen) def get_latest_event(self) -> Event | None: if not hasattr(self, "_latest_event"): self._latest_event = self.get_latest_event_for_environments() return self._latest_event def get_latest_event_for_environments(self, environments=()): return get_oldest_or_latest_event_for_environments( EventOrdering.LATEST, environments=environments, issue_id=self.id, project_id=self.project_id, ) def get_oldest_event_for_environments(self, environments=()): return get_oldest_or_latest_event_for_environments( EventOrdering.OLDEST, environments=environments, issue_id=self.id, project_id=self.project_id, ) def _get_cache_key(self, project_id, group_id, first): return f"g-r:{group_id}-{project_id}-{first}" def __get_release(self, project_id, group_id, first=True, use_cache=True): from sentry.models import GroupRelease, Release orderby = "first_seen" if first else "-last_seen" cache_key = self._get_cache_key(project_id, group_id, first) try: release_version = cache.get(cache_key) if use_cache else None if release_version is None: release_version = Release.objects.get( id__in=GroupRelease.objects.filter(group_id=group_id). order_by(orderby).values("release_id")[:1]).version cache.set(cache_key, release_version, 3600) elif release_version is False: release_version = None return release_version except Release.DoesNotExist: cache.set(cache_key, False, 3600) return None def get_first_release(self): if self.first_release_id is None: first_release = self.__get_release(self.project_id, self.id, True) return first_release return self.first_release.version def get_last_release(self, use_cache=True): return self.__get_release(self.project_id, self.id, False, use_cache=use_cache) def get_event_type(self): """ Return the type of this issue. See ``sentry.eventtypes``. """ return self.data.get("type", "default") def get_event_metadata(self) -> Mapping[str, str]: """ Return the metadata of this issue. See ``sentry.eventtypes``. """ return self.data["metadata"] @property def title(self) -> str: et = eventtypes.get(self.get_event_type())() return et.get_title(self.get_event_metadata()) def location(self): et = eventtypes.get(self.get_event_type())() return et.get_location(self.get_event_metadata()) def error(self): warnings.warn("Group.error is deprecated, use Group.title", DeprecationWarning) return self.title error.short_description = _("error") @property def message_short(self): warnings.warn("Group.message_short is deprecated, use Group.title", DeprecationWarning) return self.title @property def organization(self): return self.project.organization @property def checksum(self): warnings.warn("Group.checksum is no longer used", DeprecationWarning) return "" def get_email_subject(self): return f"{self.qualified_short_id} - {self.title}" def count_users_seen(self): return tagstore.get_groups_user_counts([self.project_id], [self.id], environment_ids=None, start=self.first_seen)[self.id] @classmethod def calculate_score(cls, times_seen, last_seen): return math.log(float(times_seen or 1)) * 600 + float( last_seen.strftime("%s")) @staticmethod def issues_mapping(group_ids, project_ids, organization): """Create a dictionary of group_ids to their qualified_short_ids""" return { i.id: i.qualified_short_id for i in Group.objects.filter(id__in=group_ids, project_id__in=project_ids, project__organization=organization) } def get_assignee(self) -> Team | User | None: from sentry.models import GroupAssignee try: group_assignee = GroupAssignee.objects.get(group=self) except GroupAssignee.DoesNotExist: return None assigned_actor = group_assignee.assigned_actor() try: return assigned_actor.resolve() except assigned_actor.type.DoesNotExist: return None
class AuditLogEntry(Model): __core__ = False organization = FlexibleForeignKey('sentry.Organization') actor_label = models.CharField(max_length=64, null=True, blank=True) # if the entry was created via a user actor = FlexibleForeignKey('sentry.User', related_name='audit_actors', null=True, blank=True) # if the entry was created via an api key actor_key = FlexibleForeignKey('sentry.ApiKey', null=True, blank=True) target_object = BoundedPositiveIntegerField(null=True) target_user = FlexibleForeignKey('sentry.User', null=True, blank=True, related_name='audit_targets') # TODO(dcramer): we want to compile this mapping into JSX for the UI event = BoundedPositiveIntegerField(choices=( # We emulate github a bit with event naming (AuditLogEntryEvent.MEMBER_INVITE, 'member.invite'), (AuditLogEntryEvent.MEMBER_ADD, 'member.add'), (AuditLogEntryEvent.MEMBER_ACCEPT, 'member.accept-invite'), (AuditLogEntryEvent.MEMBER_REMOVE, 'member.remove'), (AuditLogEntryEvent.MEMBER_EDIT, 'member.edit'), (AuditLogEntryEvent.MEMBER_JOIN_TEAM, 'member.join-team'), (AuditLogEntryEvent.MEMBER_LEAVE_TEAM, 'member.leave-team'), (AuditLogEntryEvent.TEAM_ADD, 'team.create'), (AuditLogEntryEvent.TEAM_EDIT, 'team.edit'), (AuditLogEntryEvent.TEAM_REMOVE, 'team.remove'), (AuditLogEntryEvent.PROJECT_ADD, 'project.create'), (AuditLogEntryEvent.PROJECT_EDIT, 'project.edit'), (AuditLogEntryEvent.PROJECT_REMOVE, 'project.remove'), (AuditLogEntryEvent.PROJECT_SET_PUBLIC, 'project.set-public'), (AuditLogEntryEvent.PROJECT_SET_PRIVATE, 'project.set-private'), (AuditLogEntryEvent.PROJECT_REQUEST_TRANSFER, 'project.request-transfer'), (AuditLogEntryEvent.PROJECT_ACCEPT_TRANSFER, 'project.accept-transfer'), (AuditLogEntryEvent.ORG_ADD, 'org.create'), (AuditLogEntryEvent.ORG_EDIT, 'org.edit'), (AuditLogEntryEvent.ORG_REMOVE, 'org.remove'), (AuditLogEntryEvent.ORG_RESTORE, 'org.restore'), (AuditLogEntryEvent.TAGKEY_REMOVE, 'tagkey.remove'), (AuditLogEntryEvent.PROJECTKEY_ADD, 'projectkey.create'), (AuditLogEntryEvent.PROJECTKEY_EDIT, 'projectkey.edit'), (AuditLogEntryEvent.PROJECTKEY_REMOVE, 'projectkey.remove'), (AuditLogEntryEvent.PROJECTKEY_ENABLE, 'projectkey.enable'), (AuditLogEntryEvent.PROJECTKEY_DISABLE, 'projectkey.disable'), (AuditLogEntryEvent.SSO_ENABLE, 'sso.enable'), (AuditLogEntryEvent.SSO_DISABLE, 'sso.disable'), (AuditLogEntryEvent.SSO_EDIT, 'sso.edit'), (AuditLogEntryEvent.SSO_IDENTITY_LINK, 'sso-identity.link'), (AuditLogEntryEvent.APIKEY_ADD, 'api-key.create'), (AuditLogEntryEvent.APIKEY_EDIT, 'api-key.edit'), (AuditLogEntryEvent.APIKEY_REMOVE, 'api-key.remove'), (AuditLogEntryEvent.RULE_ADD, 'rule.create'), (AuditLogEntryEvent.RULE_EDIT, 'rule.edit'), (AuditLogEntryEvent.RULE_REMOVE, 'rule.remove'), (AuditLogEntryEvent.SERVICEHOOK_ADD, 'serivcehook.create'), (AuditLogEntryEvent.SERVICEHOOK_EDIT, 'serivcehook.edit'), (AuditLogEntryEvent.SERVICEHOOK_REMOVE, 'serivcehook.remove'), (AuditLogEntryEvent.SERVICEHOOK_ENABLE, 'serivcehook.enable'), (AuditLogEntryEvent.SERVICEHOOK_DISABLE, 'serivcehook.disable'), (AuditLogEntryEvent.INTEGRATION_ADD, 'integration.add'), (AuditLogEntryEvent.INTEGRATION_EDIT, 'integration.edit'), (AuditLogEntryEvent.INTEGRATION_REMOVE, 'integration.remove'), (AuditLogEntryEvent.SET_ONDEMAND, 'ondemand.edit'), (AuditLogEntryEvent.TRIAL_STARTED, 'trial.started'), (AuditLogEntryEvent.PLAN_CHANGED, 'plan.changed'), )) ip_address = models.GenericIPAddressField(null=True, unpack_ipv4=True) data = GzippedDictField() datetime = models.DateTimeField(default=timezone.now) class Meta: app_label = 'sentry' db_table = 'sentry_auditlogentry' __repr__ = sane_repr('organization_id', 'type') def save(self, *args, **kwargs): if not self.actor_label: assert self.actor or self.actor_key if self.actor: self.actor_label = self.actor.username else: self.actor_label = self.actor_key.key super(AuditLogEntry, self).save(*args, **kwargs) def get_actor_name(self): if self.actor: return self.actor.get_display_name() elif self.actor_key: return self.actor_key.key + ' (api key)' return self.actor_label def get_note(self): if self.event == AuditLogEntryEvent.MEMBER_INVITE: return 'invited member %s' % (self.data['email'], ) elif self.event == AuditLogEntryEvent.MEMBER_ADD: if self.target_user == self.actor: return 'joined the organization' return 'added member %s' % (self.target_user.get_display_name(), ) elif self.event == AuditLogEntryEvent.MEMBER_ACCEPT: return 'accepted the membership invite' elif self.event == AuditLogEntryEvent.MEMBER_REMOVE: if self.target_user == self.actor: return 'left the organization' return 'removed member %s' % ( self.data.get('email') or self.target_user.get_display_name(), ) elif self.event == AuditLogEntryEvent.MEMBER_EDIT: return 'edited member %s (role: %s, teams: %s)' % ( self.data.get('email') or self.target_user.get_display_name(), self.data.get('role') or 'N/A', ', '.join( six.text_type(x) for x in self.data.get('team_slugs', [])) or 'N/A', ) elif self.event == AuditLogEntryEvent.MEMBER_JOIN_TEAM: if self.target_user == self.actor: return 'joined team %s' % (self.data['team_slug'], ) return 'added %s to team %s' % ( self.data.get('email') or self.target_user.get_display_name(), self.data['team_slug'], ) elif self.event == AuditLogEntryEvent.MEMBER_LEAVE_TEAM: if self.target_user == self.actor: return 'left team %s' % (self.data['team_slug'], ) return 'removed %s from team %s' % ( self.data.get('email') or self.target_user.get_display_name(), self.data['team_slug'], ) elif self.event == AuditLogEntryEvent.ORG_ADD: return 'created the organization' elif self.event == AuditLogEntryEvent.ORG_EDIT: return 'edited the organization setting: ' + (', '.join( u'{} {}'.format(k, v) for k, v in self.data.items())) elif self.event == AuditLogEntryEvent.ORG_REMOVE: return 'removed the organization' elif self.event == AuditLogEntryEvent.ORG_RESTORE: return 'restored the organization' elif self.event == AuditLogEntryEvent.TEAM_ADD: return 'created team %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.TEAM_EDIT: return 'edited team %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.TEAM_REMOVE: return 'removed team %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.PROJECT_ADD: return 'created project %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.PROJECT_EDIT: return 'edited project settings ' + (' '.join([ ' in %s to %s' % (key, value) for (key, value) in six.iteritems(self.data) ])) elif self.event == AuditLogEntryEvent.PROJECT_REMOVE: return 'removed project %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.PROJECT_REQUEST_TRANSFER: return 'requested to transfer project %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.PROJECT_ACCEPT_TRANSFER: return 'accepted transfer of project %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.TAGKEY_REMOVE: return 'removed tags matching %s = *' % (self.data['key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_ADD: return 'added project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_EDIT: return 'edited project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_REMOVE: return 'removed project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_ENABLE: return 'enabled project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_DISABLE: return 'disabled project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.SSO_ENABLE: return 'enabled sso (%s)' % (self.data['provider'], ) elif self.event == AuditLogEntryEvent.SSO_DISABLE: return 'disabled sso (%s)' % (self.data['provider'], ) elif self.event == AuditLogEntryEvent.SSO_EDIT: return 'edited sso settings' elif self.event == AuditLogEntryEvent.SSO_IDENTITY_LINK: return 'linked their account to a new identity' elif self.event == AuditLogEntryEvent.APIKEY_ADD: return 'added api key %s' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.APIKEY_EDIT: return 'edited api key %s' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.APIKEY_REMOVE: return 'removed api key %s' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.RULE_ADD: return 'added rule "%s"' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.RULE_EDIT: return 'edited rule "%s"' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.RULE_REMOVE: return 'removed rule "%s"' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.SET_ONDEMAND: if self.data['ondemand'] == -1: return 'changed on-demand spend to unlimited' return 'changed on-demand max spend to $%d' % ( self.data['ondemand'] / 100, ) elif self.event == AuditLogEntryEvent.TRIAL_STARTED: return 'started trial' elif self.event == AuditLogEntryEvent.PLAN_CHANGED: return 'changed plan to %s' % (self.data['plan_name'], ) elif self.event == AuditLogEntryEvent.SERVICEHOOK_ADD: return 'added a service hook for "%s"' % (truncatechars( self.data['url'], 64), ) elif self.event == AuditLogEntryEvent.SERVICEHOOK_EDIT: return 'edited the service hook for "%s"' % (truncatechars( self.data['url'], 64), ) elif self.event == AuditLogEntryEvent.SERVICEHOOK_REMOVE: return 'removed the service hook for "%s"' % (truncatechars( self.data['url'], 64), ) elif self.event == AuditLogEntryEvent.SERVICEHOOK_ENABLE: return 'enabled theservice hook for "%s"' % (truncatechars( self.data['url'], 64), ) elif self.event == AuditLogEntryEvent.SERVICEHOOK_DISABLE: return 'disabled the service hook for "%s"' % (truncatechars( self.data['url'], 64), ) elif self.event == AuditLogEntryEvent.INTEGRATION_ADD: return 'enabled integration %s for project %s' % ( self.data['integration'], self.data['project']) elif self.event == AuditLogEntryEvent.INTEGRATION_EDIT: return 'edited integration %s for project %s' % ( self.data['integration'], self.data['project']) elif self.event == AuditLogEntryEvent.INTEGRATION_REMOVE: return 'disabled integration %s from project %s' % ( self.data['integration'], self.data['project']) return ''
class Group(Model): """ Aggregated message which summarizes a set of Events. """ __core__ = False project = FlexibleForeignKey("sentry.Project") logger = models.CharField(max_length=64, blank=True, default=DEFAULT_LOGGER_NAME, db_index=True) level = BoundedPositiveIntegerField(choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True, db_index=True) message = models.TextField() culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH, blank=True, null=True, db_column="view") num_comments = BoundedPositiveIntegerField(default=0, null=True) platform = models.CharField(max_length=64, null=True) status = BoundedPositiveIntegerField( default=0, choices=( (GroupStatus.UNRESOLVED, _("Unresolved")), (GroupStatus.RESOLVED, _("Resolved")), (GroupStatus.IGNORED, _("Ignored")), ), db_index=True, ) times_seen = BoundedPositiveIntegerField(default=1, db_index=True) last_seen = models.DateTimeField(default=timezone.now, db_index=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True) first_release = FlexibleForeignKey("sentry.Release", null=True, on_delete=models.PROTECT) resolved_at = models.DateTimeField(null=True, db_index=True) # active_at should be the same as first_seen by default active_at = models.DateTimeField(null=True, db_index=True) time_spent_total = BoundedIntegerField(default=0) time_spent_count = BoundedIntegerField(default=0) score = BoundedIntegerField(default=0) # deprecated, do not use. GroupShare has superseded is_public = models.NullBooleanField(default=False, null=True) data = GzippedDictField(blank=True, null=True) short_id = BoundedBigIntegerField(null=True) objects = GroupManager(cache_fields=("id", )) class Meta: app_label = "sentry" db_table = "sentry_groupedmessage" verbose_name_plural = _("grouped messages") verbose_name = _("grouped message") permissions = (("can_view", "Can view"), ) index_together = [("project", "first_release"), ("project", "id")] unique_together = (("project", "short_id"), ) __repr__ = sane_repr("project_id") def __unicode__(self): return "(%s) %s" % (self.times_seen, self.error()) def save(self, *args, **kwargs): if not self.last_seen: self.last_seen = timezone.now() if not self.first_seen: self.first_seen = self.last_seen if not self.active_at: self.active_at = self.first_seen # We limit what we store for the message body self.message = strip(self.message) if self.message: self.message = truncatechars(self.message.splitlines()[0], 255) if self.times_seen is None: self.times_seen = 1 self.score = type(self).calculate_score(times_seen=self.times_seen, last_seen=self.last_seen) super(Group, self).save(*args, **kwargs) def get_absolute_url(self, params=None): url = reverse("sentry-organization-issue", args=[self.organization.slug, self.id]) if params: url = url + "?" + urlencode(params) return absolute_uri(url) @property def qualified_short_id(self): if self.short_id is not None: return "%s-%s" % (self.project.slug.upper(), base32_encode(self.short_id)) def is_over_resolve_age(self): resolve_age = self.project.get_option("sentry:resolve_age", None) if not resolve_age: return False return self.last_seen < timezone.now() - timedelta( hours=int(resolve_age)) def is_ignored(self): return self.get_status() == GroupStatus.IGNORED def is_unresolved(self): return self.get_status() == GroupStatus.UNRESOLVED # TODO(dcramer): remove in 9.0 / after plugins no long ref is_muted = is_ignored def is_resolved(self): return self.get_status() == GroupStatus.RESOLVED def get_status(self): # XXX(dcramer): GroupSerializer reimplements this logic from sentry.models import GroupSnooze status = self.status if status == GroupStatus.IGNORED: try: snooze = GroupSnooze.objects.get_from_cache(group=self) except GroupSnooze.DoesNotExist: pass else: if not snooze.is_valid(group=self): status = GroupStatus.UNRESOLVED if status == GroupStatus.UNRESOLVED and self.is_over_resolve_age(): return GroupStatus.RESOLVED return status def get_share_id(self): from sentry.models import GroupShare try: return GroupShare.objects.filter(group_id=self.id).values_list( "uuid", flat=True)[0] except IndexError: # Otherwise it has not been shared yet. return None @classmethod def from_share_id(cls, share_id): if not share_id or len(share_id) != 32: raise cls.DoesNotExist from sentry.models import GroupShare return cls.objects.get(id=GroupShare.objects.filter( uuid=share_id).values_list("group_id")) def get_score(self): return type(self).calculate_score(self.times_seen, self.last_seen) def get_latest_event(self): if not hasattr(self, "_latest_event"): self._latest_event = self.get_latest_event_for_environments() return self._latest_event def get_latest_event_for_environments(self, environments=()): return get_oldest_or_latest_event_for_environments( EventOrdering.LATEST, environments=environments, issue_id=self.id, project_id=self.project_id, ) def get_oldest_event_for_environments(self, environments=()): return get_oldest_or_latest_event_for_environments( EventOrdering.OLDEST, environments=environments, issue_id=self.id, project_id=self.project_id, ) def get_first_release(self): if self.first_release_id is None: return tagstore.get_first_release(self.project_id, self.id) return self.first_release.version def get_last_release(self): return tagstore.get_last_release(self.project_id, self.id) def get_event_type(self): """ Return the type of this issue. See ``sentry.eventtypes``. """ return self.data.get("type", "default") def get_event_metadata(self): """ Return the metadata of this issue. See ``sentry.eventtypes``. """ return self.data["metadata"] @property def title(self): et = eventtypes.get(self.get_event_type())() return et.get_title(self.get_event_metadata()) def location(self): et = eventtypes.get(self.get_event_type())() return et.get_location(self.get_event_metadata()) def error(self): warnings.warn("Group.error is deprecated, use Group.title", DeprecationWarning) return self.title error.short_description = _("error") @property def message_short(self): warnings.warn("Group.message_short is deprecated, use Group.title", DeprecationWarning) return self.title @property def organization(self): return self.project.organization @property def checksum(self): warnings.warn("Group.checksum is no longer used", DeprecationWarning) return "" def get_email_subject(self): return "%s - %s" % (self.qualified_short_id.encode("utf-8"), self.title.encode("utf-8")) def count_users_seen(self): return tagstore.get_groups_user_counts([self.project_id], [self.id], environment_ids=None, start=self.first_seen)[self.id] @classmethod def calculate_score(cls, times_seen, last_seen): return math.log(float(times_seen or 1)) * 600 + float( last_seen.strftime("%s"))
class Group(Model): """ Aggregated message which summarizes a set of Events. """ __core__ = False project = FlexibleForeignKey('sentry.Project', null=True) logger = models.CharField(max_length=64, blank=True, default=DEFAULT_LOGGER_NAME, db_index=True) level = BoundedPositiveIntegerField(choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True, db_index=True) message = models.TextField() culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH, blank=True, null=True, db_column='view') num_comments = BoundedPositiveIntegerField(default=0, null=True) platform = models.CharField(max_length=64, null=True) status = BoundedPositiveIntegerField(default=0, choices=( (GroupStatus.UNRESOLVED, _('Unresolved')), (GroupStatus.RESOLVED, _('Resolved')), (GroupStatus.MUTED, _('Muted')), ), db_index=True) times_seen = BoundedPositiveIntegerField(default=1, db_index=True) last_seen = models.DateTimeField(default=timezone.now, db_index=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True) first_release = FlexibleForeignKey('sentry.Release', null=True, on_delete=models.PROTECT) resolved_at = models.DateTimeField(null=True, db_index=True) # active_at should be the same as first_seen by default active_at = models.DateTimeField(null=True, db_index=True) time_spent_total = BoundedIntegerField(default=0) time_spent_count = BoundedIntegerField(default=0) score = BoundedIntegerField(default=0) is_public = models.NullBooleanField(default=False, null=True) data = GzippedDictField(blank=True, null=True) short_id = BoundedBigIntegerField(null=True) objects = GroupManager() class Meta: app_label = 'sentry' db_table = 'sentry_groupedmessage' verbose_name_plural = _('grouped messages') verbose_name = _('grouped message') permissions = (("can_view", "Can view"), ) index_together = (('project', 'first_release'), ) unique_together = (('project', 'short_id'), ) __repr__ = sane_repr('project_id') def __unicode__(self): return "(%s) %s" % (self.times_seen, self.error()) def save(self, *args, **kwargs): if not self.last_seen: self.last_seen = timezone.now() if not self.first_seen: self.first_seen = self.last_seen if not self.active_at: self.active_at = self.first_seen # We limit what we store for the message body self.message = strip(self.message) if self.message: self.message = truncatechars(self.message.splitlines()[0], 255) super(Group, self).save(*args, **kwargs) def get_absolute_url(self): return absolute_uri( reverse('sentry-group', args=[self.organization.slug, self.project.slug, self.id])) @property def qualified_short_id(self): if self.short_id is not None: return '%s-%s' % ( self.project.slug.upper(), base32_encode(self.short_id), ) @property def event_set(self): from sentry.models import Event return Event.objects.filter(group_id=self.id) def is_over_resolve_age(self): resolve_age = self.project.get_option('sentry:resolve_age', None) if not resolve_age: return False return self.last_seen < timezone.now() - timedelta( hours=int(resolve_age)) def is_muted(self): return self.get_status() == GroupStatus.MUTED def is_resolved(self): return self.get_status() == GroupStatus.RESOLVED def get_status(self): # XXX(dcramer): GroupSerializer reimplements this logic from sentry.models import GroupSnooze if self.status == GroupStatus.MUTED: try: snooze = GroupSnooze.objects.get(group=self) except GroupSnooze.DoesNotExist: pass else: # XXX(dcramer): if the snooze row exists then we need # to confirm its still valid if snooze.until > timezone.now(): return GroupStatus.MUTED else: return GroupStatus.UNRESOLVED if self.status == GroupStatus.UNRESOLVED and self.is_over_resolve_age( ): return GroupStatus.RESOLVED return self.status def get_share_id(self): return b16encode( ('{}.{}'.format(self.project_id, self.id)).encode('utf-8')).lower().decode('utf-8') @classmethod def from_share_id(cls, share_id): if not share_id: raise cls.DoesNotExist try: project_id, group_id = b16decode( share_id.upper()).decode('utf-8').split('.') except (ValueError, TypeError): raise cls.DoesNotExist if not (project_id.isdigit() and group_id.isdigit()): raise cls.DoesNotExist return cls.objects.get(project=project_id, id=group_id) def get_score(self): return int( math.log(self.times_seen) * 600 + float(time.mktime(self.last_seen.timetuple()))) def get_latest_event(self): from sentry.models import Event if not hasattr(self, '_latest_event'): latest_events = sorted( Event.objects.filter( group_id=self.id, ).order_by('-datetime')[0:5], key=EVENT_ORDERING_KEY, reverse=True, ) try: self._latest_event = latest_events[0] except IndexError: self._latest_event = None return self._latest_event def get_oldest_event(self): from sentry.models import Event if not hasattr(self, '_oldest_event'): oldest_events = sorted( Event.objects.filter( group_id=self.id, ).order_by('datetime')[0:5], key=EVENT_ORDERING_KEY, ) try: self._oldest_event = oldest_events[0] except IndexError: self._oldest_event = None return self._oldest_event def get_unique_tags(self, tag, since=None, order_by='-times_seen'): # TODO(dcramer): this has zero test coverage and is a critical path from sentry.models import GroupTagValue queryset = GroupTagValue.objects.filter( group=self, key=tag, ) if since: queryset = queryset.filter(last_seen__gte=since) return queryset.values_list( 'value', 'times_seen', 'first_seen', 'last_seen', ).order_by(order_by) def get_tags(self, with_internal=True): from sentry.models import GroupTagKey, TagKey if not hasattr(self, '_tag_cache'): group_tags = GroupTagKey.objects.filter( group=self, project=self.project, ) if not with_internal: group_tags = group_tags.exclude(key__startswith='sentry:') group_tags = list(group_tags.values_list('key', flat=True)) tag_keys = dict( (t.key, t) for t in TagKey.objects.filter(project=self.project, key__in=group_tags)) results = [] for key in group_tags: try: tag_key = tag_keys[key] except KeyError: label = key.replace('_', ' ').title() else: label = tag_key.get_label() results.append({ 'key': key, 'label': label, }) self._tag_cache = sorted(results, key=lambda x: x['label']) return self._tag_cache def get_event_type(self): """ Return the type of this issue. See ``sentry.eventtypes``. """ return self.data.get('type', 'default') def get_event_metadata(self): """ Return the metadata of this issue. See ``sentry.eventtypes``. """ etype = self.data.get('type') if etype is None: etype = 'default' if 'metadata' not in self.data: data = self.data.copy() if self.data else {} data['message'] = self.message return eventtypes.get(etype)(data).get_metadata() return self.data['metadata'] @property def title(self): et = eventtypes.get(self.get_event_type())(self.data) return et.to_string(self.get_event_metadata()) def error(self): warnings.warn('Group.error is deprecated, use Group.title', DeprecationWarning) return self.title error.short_description = _('error') @property def message_short(self): warnings.warn('Group.message_short is deprecated, use Group.title', DeprecationWarning) return self.title def has_two_part_message(self): warnings.warn('Group.has_two_part_message is no longer used', DeprecationWarning) return False @property def organization(self): return self.project.organization @property def team(self): return self.project.team @property def checksum(self): warnings.warn('Group.checksum is no longer used', DeprecationWarning) return '' def get_email_subject(self): return '[%s] %s: %s' % (self.project.get_full_name().encode('utf-8'), six.text_type(self.get_level_display()).upper( ).encode('utf-8'), self.title.encode('utf-8'))
class AuditLogEntry(Model): __core__ = False organization = FlexibleForeignKey('sentry.Organization') actor_label = models.CharField(max_length=64, null=True, blank=True) # if the entry was created via a user actor = FlexibleForeignKey('sentry.User', related_name='audit_actors', null=True, blank=True) # if the entry was created via an api key actor_key = FlexibleForeignKey('sentry.ApiKey', null=True, blank=True) target_object = BoundedPositiveIntegerField(null=True) target_user = FlexibleForeignKey( 'sentry.User', null=True, blank=True, related_name='audit_targets' ) # TODO(dcramer): we want to compile this mapping into JSX for the UI event = BoundedPositiveIntegerField( choices=( # We emulate github a bit with event naming (AuditLogEntryEvent.MEMBER_INVITE, 'member.invite'), (AuditLogEntryEvent.MEMBER_ADD, 'member.add'), (AuditLogEntryEvent.MEMBER_ACCEPT, 'member.accept-invite'), (AuditLogEntryEvent.MEMBER_REMOVE, 'member.remove'), (AuditLogEntryEvent.MEMBER_EDIT, 'member.edit'), (AuditLogEntryEvent.MEMBER_JOIN_TEAM, 'member.join-team'), (AuditLogEntryEvent.MEMBER_LEAVE_TEAM, 'member.leave-team'), (AuditLogEntryEvent.TEAM_ADD, 'team.create'), (AuditLogEntryEvent.TEAM_EDIT, 'team.edit'), (AuditLogEntryEvent.TEAM_REMOVE, 'team.remove'), (AuditLogEntryEvent.PROJECT_ADD, 'project.create'), (AuditLogEntryEvent.PROJECT_EDIT, 'project.edit'), (AuditLogEntryEvent.PROJECT_REMOVE, 'project.remove'), (AuditLogEntryEvent.PROJECT_SET_PUBLIC, 'project.set-public'), (AuditLogEntryEvent.PROJECT_SET_PRIVATE, 'project.set-private'), (AuditLogEntryEvent.ORG_ADD, 'org.create'), (AuditLogEntryEvent.ORG_EDIT, 'org.edit'), (AuditLogEntryEvent.ORG_REMOVE, 'org.remove'), (AuditLogEntryEvent.ORG_RESTORE, 'org.restore'), (AuditLogEntryEvent.TAGKEY_REMOVE, 'tagkey.remove'), (AuditLogEntryEvent.PROJECTKEY_ADD, 'projectkey.create'), (AuditLogEntryEvent.PROJECTKEY_EDIT, 'projectkey.edit'), (AuditLogEntryEvent.PROJECTKEY_REMOVE, 'projectkey.remove'), (AuditLogEntryEvent.PROJECTKEY_ENABLE, 'projectkey.enable'), (AuditLogEntryEvent.PROJECTKEY_DISABLE, 'projectkey.disable'), (AuditLogEntryEvent.SSO_ENABLE, 'sso.enable'), (AuditLogEntryEvent.SSO_DISABLE, 'sso.disable'), (AuditLogEntryEvent.SSO_EDIT, 'sso.edit'), (AuditLogEntryEvent.SSO_IDENTITY_LINK, 'sso-identity.link'), (AuditLogEntryEvent.APIKEY_ADD, 'api-key.create'), (AuditLogEntryEvent.APIKEY_EDIT, 'api-key.edit'), (AuditLogEntryEvent.APIKEY_REMOVE, 'api-key.remove'), (AuditLogEntryEvent.RULE_ADD, 'rule.create'), (AuditLogEntryEvent.RULE_EDIT, 'rule.edit'), (AuditLogEntryEvent.RULE_REMOVE, 'rule.remove'), ) ) ip_address = models.GenericIPAddressField(null=True, unpack_ipv4=True) data = GzippedDictField() datetime = models.DateTimeField(default=timezone.now) class Meta: app_label = 'sentry' db_table = 'sentry_auditlogentry' __repr__ = sane_repr('organization_id', 'type') def save(self, *args, **kwargs): if not self.actor_label: assert self.actor or self.actor_key if self.actor: self.actor_label = self.actor.username else: self.actor_label = self.actor_key.key super(AuditLogEntry, self).save(*args, **kwargs) def get_actor_name(self): if self.actor: return self.actor.get_display_name() elif self.actor_key: return self.actor_key.key + ' (api key)' return self.actor_label def get_note(self): if self.event == AuditLogEntryEvent.MEMBER_INVITE: return 'invited member %s' % (self.data['email'], ) elif self.event == AuditLogEntryEvent.MEMBER_ADD: if self.target_user == self.actor: return 'joined the organization' return 'added member %s' % (self.target_user.get_display_name(), ) elif self.event == AuditLogEntryEvent.MEMBER_ACCEPT: return 'accepted the membership invite' elif self.event == AuditLogEntryEvent.MEMBER_REMOVE: if self.target_user == self.actor: return 'left the organization' return 'removed member %s' % ( self.data.get('email') or self.target_user.get_display_name(), ) elif self.event == AuditLogEntryEvent.MEMBER_EDIT: return 'edited member %s' % ( self.data.get('email') or self.target_user.get_display_name(), ) elif self.event == AuditLogEntryEvent.MEMBER_JOIN_TEAM: if self.target_user == self.actor: return 'joined team %s' % (self.data['team_slug'], ) return 'added %s to team %s' % ( self.data.get('email') or self.target_user.get_display_name(), self.data['team_slug'], ) elif self.event == AuditLogEntryEvent.MEMBER_LEAVE_TEAM: if self.target_user == self.actor: return 'left team %s' % (self.data['team_slug'], ) return 'removed %s from team %s' % ( self.data.get('email') or self.target_user.get_display_name(), self.data['team_slug'], ) elif self.event == AuditLogEntryEvent.ORG_ADD: return 'created the organization' elif self.event == AuditLogEntryEvent.ORG_EDIT: return 'edited the organization' elif self.event == AuditLogEntryEvent.ORG_REMOVE: return 'removed the organization' elif self.event == AuditLogEntryEvent.ORG_RESTORE: return 'restored the organization' elif self.event == AuditLogEntryEvent.TEAM_ADD: return 'created team %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.TEAM_EDIT: return 'edited team %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.TEAM_REMOVE: return 'removed team %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.PROJECT_ADD: return 'created project %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.PROJECT_EDIT: return 'edited project %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.PROJECT_REMOVE: return 'removed project %s' % (self.data['slug'], ) elif self.event == AuditLogEntryEvent.TAGKEY_REMOVE: return 'removed tags matching %s = *' % (self.data['key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_ADD: return 'added project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_EDIT: return 'edited project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_REMOVE: return 'removed project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_ENABLE: return 'enabled project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.PROJECTKEY_DISABLE: return 'disabled project key %s' % (self.data['public_key'], ) elif self.event == AuditLogEntryEvent.SSO_ENABLE: return 'enabled sso (%s)' % (self.data['provider'], ) elif self.event == AuditLogEntryEvent.SSO_DISABLE: return 'disabled sso (%s)' % (self.data['provider'], ) elif self.event == AuditLogEntryEvent.SSO_EDIT: return 'edited sso settings' elif self.event == AuditLogEntryEvent.SSO_IDENTITY_LINK: return 'linked their account to a new identity' elif self.event == AuditLogEntryEvent.APIKEY_ADD: return 'added api key %s' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.APIKEY_EDIT: return 'edited api key %s' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.APIKEY_REMOVE: return 'removed api key %s' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.RULE_ADD: return 'added rule "%s"' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.RULE_EDIT: return 'edited rule "%s"' % (self.data['label'], ) elif self.event == AuditLogEntryEvent.RULE_REMOVE: return 'removed rule "%s"' % (self.data['label'], ) return ''
class Group(Model): """ Aggregated message which summarizes a set of Events. """ project = models.ForeignKey('sentry.Project', null=True) logger = models.CharField(max_length=64, blank=True, default='root', db_index=True) level = BoundedPositiveIntegerField(choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True, db_index=True) message = models.TextField() culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH, blank=True, null=True, db_column='view') checksum = models.CharField(max_length=32, db_index=True) num_comments = BoundedPositiveIntegerField(default=0, null=True) platform = models.CharField(max_length=64, null=True) status = BoundedPositiveIntegerField(default=0, choices=STATUS_LEVELS, db_index=True) times_seen = BoundedPositiveIntegerField(default=1, db_index=True) last_seen = models.DateTimeField(default=timezone.now, db_index=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True) resolved_at = models.DateTimeField(null=True, db_index=True) # active_at should be the same as first_seen by default active_at = models.DateTimeField(null=True, db_index=True) time_spent_total = BoundedIntegerField(default=0) time_spent_count = BoundedIntegerField(default=0) score = BoundedIntegerField(default=0) is_public = models.NullBooleanField(default=False, null=True) data = GzippedDictField(blank=True, null=True) objects = GroupManager() class Meta: app_label = 'sentry' db_table = 'sentry_groupedmessage' unique_together = (('project', 'checksum'), ) verbose_name_plural = _('grouped messages') verbose_name = _('grouped message') permissions = (("can_view", "Can view"), ) __repr__ = sane_repr('project_id', 'checksum') def __unicode__(self): return "(%s) %s" % (self.times_seen, self.error()) def save(self, *args, **kwargs): if not self.last_seen: self.last_seen = timezone.now() if not self.first_seen: self.first_seen = self.last_seen if not self.active_at: self.active_at = self.first_seen if self.message: # We limit what we store for the message body self.message = self.message.splitlines()[0][:255] super(Group, self).save(*args, **kwargs) def get_absolute_url(self): return absolute_uri( reverse('sentry-group', args=[self.organization.slug, self.project.slug, self.id])) @property def avg_time_spent(self): if not self.time_spent_count: return return float(self.time_spent_total) / self.time_spent_count def natural_key(self): return (self.project, self.checksum) def is_over_resolve_age(self): resolve_age = self.project.get_option('sentry:resolve_age', None) if not resolve_age: return False return self.last_seen < timezone.now() - timedelta( hours=int(resolve_age)) def is_muted(self): return self.get_status() == STATUS_MUTED def is_resolved(self): return self.get_status() == STATUS_RESOLVED def get_status(self): if self.status == STATUS_UNRESOLVED and self.is_over_resolve_age(): return STATUS_RESOLVED return self.status def get_score(self): return int( math.log(self.times_seen) * 600 + float(time.mktime(self.last_seen.timetuple()))) def get_latest_event(self): from sentry.models import Event if not hasattr(self, '_latest_event'): try: self._latest_event = Event.objects.filter( group=self, ).order_by('-datetime')[0] except IndexError: self._latest_event = None return self._latest_event def get_unique_tags(self, tag, since=None, order_by='-times_seen'): # TODO(dcramer): this has zero test coverage and is a critical path from sentry.models import GroupTagValue queryset = GroupTagValue.objects.filter( group=self, key=tag, ) if since: queryset = queryset.filter(last_seen__gte=since) return queryset.values_list( 'value', 'times_seen', 'first_seen', 'last_seen', ).order_by(order_by) def get_tags(self, with_internal=True): from sentry.models import GroupTagKey if not hasattr(self, '_tag_cache'): self._tag_cache = sorted([ t for t in GroupTagKey.objects.filter( group=self, project=self.project, ).values_list('key', flat=True) if with_internal or not t.startswith('sentry:') ]) return self._tag_cache def error(self): return self.message error.short_description = _('error') def has_two_part_message(self): message = strip(self.message) return '\n' in message or len(message) > 100 @property def title(self): culprit = strip(self.culprit) if culprit: return culprit return self.message @property def message_short(self): message = strip(self.message) if not message: message = '<unlabeled message>' else: message = truncatechars(message.splitlines()[0], 100) return message @property def organization(self): return self.project.organization @property def team(self): return self.project.team def get_email_subject(self): return '[%s %s] %s: %s' % ( self.team.name.encode('utf-8'), self.project.name.encode('utf-8'), six.text_type(self.get_level_display()).upper().encode('utf-8'), self.message_short.encode('utf-8'))
class Group(Model): """ Aggregated message which summarizes a set of Events. """ __core__ = False project = FlexibleForeignKey('sentry.Project', null=True) logger = models.CharField(max_length=64, blank=True, default=DEFAULT_LOGGER_NAME, db_index=True) level = BoundedPositiveIntegerField(choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True, db_index=True) message = models.TextField() culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH, blank=True, null=True, db_column='view') num_comments = BoundedPositiveIntegerField(default=0, null=True) platform = models.CharField(max_length=64, null=True) status = BoundedPositiveIntegerField(default=0, choices=( (GroupStatus.UNRESOLVED, _('Unresolved')), (GroupStatus.RESOLVED, _('Resolved')), (GroupStatus.IGNORED, _('Ignored')), ), db_index=True) times_seen = BoundedPositiveIntegerField(default=1, db_index=True) last_seen = models.DateTimeField(default=timezone.now, db_index=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True) first_release = FlexibleForeignKey('sentry.Release', null=True, on_delete=models.PROTECT) resolved_at = models.DateTimeField(null=True, db_index=True) # active_at should be the same as first_seen by default active_at = models.DateTimeField(null=True, db_index=True) time_spent_total = BoundedIntegerField(default=0) time_spent_count = BoundedIntegerField(default=0) score = BoundedIntegerField(default=0) # deprecated, do not use. GroupShare has superseded is_public = models.NullBooleanField(default=False, null=True) data = GzippedDictField(blank=True, null=True) short_id = BoundedBigIntegerField(null=True) objects = GroupManager() class Meta: app_label = 'sentry' db_table = 'sentry_groupedmessage' verbose_name_plural = _('grouped messages') verbose_name = _('grouped message') permissions = (("can_view", "Can view"), ) index_together = (('project', 'first_release'), ) unique_together = (('project', 'short_id'), ) __repr__ = sane_repr('project_id') def __unicode__(self): return "(%s) %s" % (self.times_seen, self.error()) def save(self, *args, **kwargs): if not self.last_seen: self.last_seen = timezone.now() if not self.first_seen: self.first_seen = self.last_seen if not self.active_at: self.active_at = self.first_seen # We limit what we store for the message body self.message = strip(self.message) if self.message: self.message = truncatechars(self.message.splitlines()[0], 255) super(Group, self).save(*args, **kwargs) def get_absolute_url(self): return absolute_uri( reverse('sentry-group', args=[self.organization.slug, self.project.slug, self.id])) @property def qualified_short_id(self): if self.short_id is not None: return '%s-%s' % ( self.project.slug.upper(), base32_encode(self.short_id), ) @property def event_set(self): from sentry.models import Event return Event.objects.filter(group_id=self.id) def is_over_resolve_age(self): resolve_age = self.project.get_option('sentry:resolve_age', None) if not resolve_age: return False return self.last_seen < timezone.now() - timedelta( hours=int(resolve_age)) def is_ignored(self): return self.get_status() == GroupStatus.IGNORED # TODO(dcramer): remove in 9.0 / after plugins no long ref is_muted = is_ignored def is_resolved(self): return self.get_status() == GroupStatus.RESOLVED def get_status(self): # XXX(dcramer): GroupSerializer reimplements this logic from sentry.models import GroupSnooze status = self.status if status == GroupStatus.IGNORED: try: snooze = GroupSnooze.objects.get(group=self) except GroupSnooze.DoesNotExist: pass else: if not snooze.is_valid(group=self): status = GroupStatus.UNRESOLVED if status == GroupStatus.UNRESOLVED and self.is_over_resolve_age(): return GroupStatus.RESOLVED return status def get_share_id(self): from sentry.models import GroupShare try: return GroupShare.objects.filter(group_id=self.id, ).values_list( 'uuid', flat=True)[0] except IndexError: # Otherwise it has not been shared yet. return None @classmethod def from_share_id(cls, share_id): if not share_id or len(share_id) != 32: raise cls.DoesNotExist from sentry.models import GroupShare return cls.objects.get(id=GroupShare.objects.filter( uuid=share_id, ).values_list('group_id'), ) def get_score(self): return int( math.log(self.times_seen) * 600 + float(time.mktime(self.last_seen.timetuple()))) def get_latest_event(self): from sentry.models import Event if not hasattr(self, '_latest_event'): latest_events = sorted( Event.objects.filter( group_id=self.id, ).order_by('-datetime')[0:5], key=EVENT_ORDERING_KEY, reverse=True, ) try: self._latest_event = latest_events[0] except IndexError: self._latest_event = None return self._latest_event def get_oldest_event(self): from sentry.models import Event if not hasattr(self, '_oldest_event'): oldest_events = sorted( Event.objects.filter( group_id=self.id, ).order_by('datetime')[0:5], key=EVENT_ORDERING_KEY, ) try: self._oldest_event = oldest_events[0] except IndexError: self._oldest_event = None return self._oldest_event def get_first_release(self): if self.first_release_id is None: return tagstore.get_first_release(self.project_id, self.id) return self.first_release.version def get_last_release(self): return tagstore.get_last_release(self.project_id, self.id) def get_event_type(self): """ Return the type of this issue. See ``sentry.eventtypes``. """ return self.data.get('type', 'default') def get_event_metadata(self): """ Return the metadata of this issue. See ``sentry.eventtypes``. """ etype = self.data.get('type') if etype is None: etype = 'default' if 'metadata' not in self.data: data = self.data.copy() if self.data else {} data['message'] = self.message return eventtypes.get(etype)(data).get_metadata() return self.data['metadata'] @property def title(self): et = eventtypes.get(self.get_event_type())(self.data) return et.to_string(self.get_event_metadata()) def error(self): warnings.warn('Group.error is deprecated, use Group.title', DeprecationWarning) return self.title error.short_description = _('error') @property def message_short(self): warnings.warn('Group.message_short is deprecated, use Group.title', DeprecationWarning) return self.title @property def organization(self): return self.project.organization @property def checksum(self): warnings.warn('Group.checksum is no longer used', DeprecationWarning) return '' def get_email_subject(self): return '%s - %s' % (self.qualified_short_id.encode('utf-8'), self.title.encode('utf-8')) def count_users_seen(self): return tagstore.get_groups_user_counts(self.project_id, [self.id], environment_id=None)[self.id]
class Activity(Model): __include_in_export__ = False # TODO(mgaeta): Replace all usages with ActivityTypes. ASSIGNED = ActivityType.ASSIGNED.value CREATE_ISSUE = ActivityType.CREATE_ISSUE.value DEPLOY = ActivityType.DEPLOY.value FIRST_SEEN = ActivityType.FIRST_SEEN.value MARK_REVIEWED = ActivityType.MARK_REVIEWED.value MERGE = ActivityType.MERGE.value NEW_PROCESSING_ISSUES = ActivityType.NEW_PROCESSING_ISSUES.value NOTE = ActivityType.NOTE.value RELEASE = ActivityType.RELEASE.value REPROCESS = ActivityType.REPROCESS.value SET_IGNORED = ActivityType.SET_IGNORED.value SET_PRIVATE = ActivityType.SET_PRIVATE.value SET_PUBLIC = ActivityType.SET_PUBLIC.value SET_REGRESSION = ActivityType.SET_REGRESSION.value SET_RESOLVED = ActivityType.SET_RESOLVED.value SET_RESOLVED_BY_AGE = ActivityType.SET_RESOLVED_BY_AGE.value SET_RESOLVED_IN_COMMIT = ActivityType.SET_RESOLVED_IN_COMMIT.value SET_RESOLVED_IN_PULL_REQUEST = ActivityType.SET_RESOLVED_IN_PULL_REQUEST.value SET_RESOLVED_IN_RELEASE = ActivityType.SET_RESOLVED_IN_RELEASE.value SET_UNRESOLVED = ActivityType.SET_UNRESOLVED.value UNASSIGNED = ActivityType.UNASSIGNED.value UNMERGE_DESTINATION = ActivityType.UNMERGE_DESTINATION.value UNMERGE_SOURCE = ActivityType.UNMERGE_SOURCE.value project = FlexibleForeignKey("sentry.Project") group = FlexibleForeignKey("sentry.Group", null=True) # index on (type, ident) type = BoundedPositiveIntegerField(choices=CHOICES) ident = models.CharField(max_length=64, null=True) # if the user is not set, it's assumed to be the system user = FlexibleForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL) datetime = models.DateTimeField(default=timezone.now) data = GzippedDictField(null=True) class Meta: app_label = "sentry" db_table = "sentry_activity" index_together = (("project", "datetime"),) __repr__ = sane_repr("project_id", "group_id", "event_id", "user_id", "type", "ident") @staticmethod def get_version_ident(version): return (version or "")[:64] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from sentry.models import Release # XXX(dcramer): fix for bad data if self.type in (self.RELEASE, self.DEPLOY) and isinstance(self.data["version"], Release): self.data["version"] = self.data["version"].version if self.type == self.ASSIGNED: self.data["assignee"] = str(self.data["assignee"]) def save(self, *args, **kwargs): created = bool(not self.id) super().save(*args, **kwargs) if not created: return # HACK: support Group.num_comments if self.type == Activity.NOTE: self.group.update(num_comments=F("num_comments") + 1) def delete(self, *args, **kwargs): super().delete(*args, **kwargs) # HACK: support Group.num_comments if self.type == Activity.NOTE: self.group.update(num_comments=F("num_comments") - 1) def send_notification(self): activity.send_activity_notifications.delay(self.id)
class Activity(Model): __core__ = False SET_RESOLVED = 1 SET_UNRESOLVED = 2 SET_MUTED = 3 SET_PUBLIC = 4 SET_PRIVATE = 5 SET_REGRESSION = 6 CREATE_ISSUE = 7 NOTE = 8 FIRST_SEEN = 9 RELEASE = 10 ASSIGNED = 11 UNASSIGNED = 12 SET_RESOLVED_IN_RELEASE = 13 TYPE = ( # (TYPE, verb-slug) (SET_RESOLVED, 'set_resolved'), (SET_RESOLVED_IN_RELEASE, 'set_resolved_in_release'), (SET_UNRESOLVED, 'set_unresolved'), (SET_MUTED, 'set_muted'), (SET_PUBLIC, 'set_public'), (SET_PRIVATE, 'set_private'), (SET_REGRESSION, 'set_regression'), (CREATE_ISSUE, 'create_issue'), (NOTE, 'note'), (FIRST_SEEN, 'first_seen'), (RELEASE, 'release'), (ASSIGNED, 'assigned'), (UNASSIGNED, 'unassigned'), ) project = FlexibleForeignKey('sentry.Project') group = FlexibleForeignKey('sentry.Group', null=True) event = FlexibleForeignKey('sentry.Event', null=True) # index on (type, ident) type = BoundedPositiveIntegerField(choices=TYPE) ident = models.CharField(max_length=64, null=True) # if the user is not set, it's assumed to be the system user = FlexibleForeignKey(settings.AUTH_USER_MODEL, null=True) datetime = models.DateTimeField(default=timezone.now) data = GzippedDictField(null=True) class Meta: app_label = 'sentry' db_table = 'sentry_activity' __repr__ = sane_repr('project_id', 'group_id', 'event_id', 'user_id', 'type', 'ident') def save(self, *args, **kwargs): created = bool(not self.id) super(Activity, self).save(*args, **kwargs) if not created: return # HACK: support Group.num_comments if self.type == Activity.NOTE: self.group.update(num_comments=F('num_comments') + 1) if self.event: self.event.update(num_comments=F('num_comments') + 1) def delete(self, *args, **kwargs): super(Activity, self).delete(*args, **kwargs) # HACK: support Group.num_comments if self.type == Activity.NOTE: self.group.update(num_comments=F('num_comments') - 1) if self.event: self.event.update(num_comments=F('num_comments') - 1) def send_notification(self): activity.send_activity_notifications.delay(self.id)