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 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 Activity(Model): COMMENT = 0 SET_RESOLVED = 1 SET_UNRESOLVED = 2 SET_MUTED = 3 SET_PUBLIC = 4 SET_PRIVATE = 5 SET_REGRESSION = 6 CREATE_ISSUE = 7 TYPE = ( # (TYPE, verb-slug) (COMMENT, 'comment'), (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'), ) 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.COMMENT: self.group.update(num_comments=F('num_comments') + 1) if self.event: self.event.update(num_comments=F('num_comments') + 1)
class TrackedUser(Model): project = models.ForeignKey(Project) ident = models.CharField(max_length=200) email = models.EmailField(null=True) data = GzippedDictField(blank=True, null=True) last_seen = models.DateTimeField(default=timezone.now, db_index=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True) num_events = models.PositiveIntegerField(default=0) groups = models.ManyToManyField(Group, through='sentry.AffectedUserByGroup') objects = BaseManager() class Meta: unique_together = (('project', 'ident'),) __repr__ = sane_repr('project_id', 'ident', 'email')
class MessageBase(Model): """ Abstract base class for both Event and Group. """ project = models.ForeignKey(Project, null=True) logger = models.CharField(max_length=64, blank=True, default='root', db_index=True) level = models.PositiveIntegerField(choices=settings.LOG_LEVELS, default=logging.ERROR, blank=True, db_index=True) message = models.TextField() culprit = models.CharField(max_length=200, blank=True, null=True, db_column='view') checksum = models.CharField(max_length=32, db_index=True) data = GzippedDictField(blank=True, null=True) class Meta: abstract = True def save(self, *args, **kwargs): if len(self.logger) > 64: self.logger = self.logger[0:61] + u"..." super(MessageBase, self).save(*args, **kwargs) def error(self): if self.message: message = smart_unicode(self.message) if len(message) > 100: message = message[:97] + '...' else: message = '<unlabeled message>' return message error.short_description = _('error') def has_two_part_message(self): return '\n' in self.message.strip('\n') or len(self.message) > 100 def message_top(self): if self.culprit: return self.culprit return truncatechars(self.message.splitlines()[0], 100)
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 EventBase(Model): """ Abstract base class for both Event and Group. """ project = models.ForeignKey(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) data = GzippedDictField(blank=True, null=True) num_comments = BoundedPositiveIntegerField(default=0, null=True) platform = models.CharField(max_length=64, null=True) class Meta: abstract = True def save(self, *args, **kwargs): if len(self.logger) > 64: self.logger = self.logger[0:61] + u"..." super(EventBase, self).save(*args, **kwargs) def error(self): message = strip(self.message) if message: message = truncatechars(message, 100) else: message = '<unlabeled message>' return message error.short_description = _('error') def has_two_part_message(self): message = strip(self.message) return '\n' in message or len(message) > 100 def message_top(self): culprit = strip(self.culprit) if culprit: return culprit message = strip(self.message) if not strip(message): return '<unlabeled message>' return truncatechars(message.splitlines()[0], 100) @property def team(self): return self.project.team @property def user_ident(self): """ The identifier from a user is considered from several interfaces. In order: - User.id - User.email - User.username - Http.env.REMOTE_ADDR """ user_data = self.data.get('sentry.interfaces.User') if user_data: ident = user_data.get('id') if ident: return 'id:%s' % (ident, ) ident = user_data.get('email') if ident: return 'email:%s' % (ident, ) ident = user_data.get('username') if ident: return 'username:%s' % (ident, ) http_data = self.data.get('sentry.interfaces.Http') if http_data: if 'env' in http_data: ident = http_data['env'].get('REMOTE_ADDR') if ident: return 'ip:%s' % (ident, ) return None
class Alert(Model): project = models.ForeignKey(Project) group = models.ForeignKey(Group, null=True) datetime = models.DateTimeField(default=timezone.now) message = models.TextField() data = GzippedDictField(null=True) related_groups = models.ManyToManyField(Group, through='sentry.AlertRelatedGroup', related_name='related_alerts') status = BoundedPositiveIntegerField(default=0, choices=( (STATUS_UNRESOLVED, _('Unresolved')), (STATUS_RESOLVED, _('Resolved')), ), db_index=True) __repr__ = sane_repr('project_id', 'group_id', 'datetime') # TODO: move classmethods to manager @classmethod def get_recent_for_project(cls, project_id): return cls.objects.filter( project=project_id, group_id__isnull=True, datetime__gte=timezone.now() - timedelta(minutes=60), status=STATUS_UNRESOLVED, ).order_by('-datetime') @classmethod def maybe_alert(cls, project_id, message, group_id=None): now = timezone.now() manager = cls.objects # We only create an alert based on: # - an alert for the project hasn't been created in the last 30 minutes # - an alert for the event hasn't been created in the last 60 minutes # TODO: there is a race condition if we're calling this function for the same project if manager.filter(project=project_id, datetime__gte=now - timedelta(minutes=60)).exists(): return if manager.filter(project=project_id, group=group_id, datetime__gte=now - timedelta(minutes=60)).exists(): return alert = manager.create( project_id=project_id, group_id=group_id, datetime=now, message=message, ) if not group_id and has_trending(): # Capture the top 5 trending events at the time of this error related_groups = Group.objects.get_accelerated( [project_id], minutes=MINUTE_NORMALIZATION)[:5] for group in related_groups: AlertRelatedGroup.objects.create( group=group, alert=alert, ) return alert @property def team(self): return self.project.team @property def is_resolved(self): return (self.status == STATUS_RESOLVED or self.datetime < timezone.now() - timedelta(minutes=60)) def get_absolute_url(self): return absolute_uri( reverse('sentry-alert-details', args=[self.team.slug, self.project.slug, self.id]))