class TeamMember(Model): """ Identifies relationships between teams and users. Users listed as team members are considered to have access to all projects and could be thought of as team owners (though their access level may not) be set to ownership. """ team = FlexibleForeignKey('sentry.Team', related_name=None) user = FlexibleForeignKey(settings.AUTH_USER_MODEL, related_name=None) type = BoundedIntegerField(choices=( (TeamMemberType.MEMBER, _('Member')), (TeamMemberType.ADMIN, _('Admin')), (TeamMemberType.BOT, _('Bot')), ), default=TeamMemberType.MEMBER) date_added = models.DateTimeField(default=timezone.now) objects = BaseManager() class Meta: app_label = 'sentry' db_table = 'sentry_teammember' unique_together = (('team', 'user'), ) __repr__ = sane_repr('team_id', 'user_id', 'type')
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 DummyModel(Model): __core__ = False # needs defined for Sentry to not yell at you foo = models.CharField(max_length=32) normint = BoundedIntegerField(null=True) bigint = BoundedBigIntegerField(null=True) posint = BoundedPositiveIntegerField(null=True)
class ProjectCountByMinute(Model): """ Stores the total number of messages seen by a project at N minute intervals. e.g. if it happened at 08:34:55 the time would be normalized to 08:30:00 """ project = models.ForeignKey(Project, null=True) date = models.DateTimeField() # normalized to HH:MM:00 times_seen = BoundedPositiveIntegerField(default=0) time_spent_total = BoundedIntegerField(default=0) time_spent_count = BoundedIntegerField(default=0) objects = BaseManager() class Meta: unique_together = (('project', 'date'),) __repr__ = sane_repr('project_id', 'date')
class PendingTeamMember(Model): """ Identifies relationships between teams and pending invites. """ team = FlexibleForeignKey('sentry.Team', related_name="pending_member_set") email = models.EmailField() type = BoundedIntegerField(choices=MEMBER_TYPES, default=MEMBER_USER) date_added = models.DateTimeField(default=timezone.now) objects = BaseManager() class Meta: app_label = 'sentry' db_table = 'sentry_pendingteammember' unique_together = (('team', 'email'), ) __repr__ = sane_repr('team_id', 'email', 'type') @property def token(self): checksum = md5() for x in (str(self.team_id), self.email, settings.SECRET_KEY): checksum.update(x) return checksum.hexdigest() def send_invite_email(self): from sentry.utils.email import MessageBuilder context = { 'email': self.email, 'team': self.team, 'url': absolute_uri( reverse('sentry-accept-invite', kwargs={ 'member_id': self.id, 'token': self.token, })), } msg = MessageBuilder( subject='Invite to join team: %s' % (self.team.name, ), template='sentry/emails/member_invite.txt', context=context, ) try: msg.send([self.email]) except Exception as e: logger = logging.getLogger('sentry.mail.errors') logger.exception(e)
class Event(EventCommon, Model): """ An event backed by data stored in postgres. """ __core__ = False group_id = BoundedBigIntegerField(blank=True, null=True) event_id = models.CharField(max_length=32, null=True, db_column="message_id") project_id = BoundedBigIntegerField(blank=True, null=True) message = models.TextField() platform = models.CharField(max_length=64, null=True) datetime = models.DateTimeField(default=timezone.now, db_index=True) time_spent = BoundedIntegerField(null=True) data = NodeField( blank=True, null=True, ref_func=lambda x: x.project_id or x.project.id, ref_version=2, wrapper=EventDict, skip_nodestore_save=options.get("store.save-event-skips-nodestore", True), ) objects = EventManager() class Meta: app_label = "sentry" db_table = "sentry_message" verbose_name = _("message") verbose_name_plural = _("messages") unique_together = (("project_id", "event_id"), ) index_together = (("group_id", "datetime"), ) __repr__ = sane_repr("project_id", "group_id") def __getstate__(self): state = Model.__getstate__(self) # do not pickle cached info. We want to fetch this on demand # again. In particular if we were to pickle interfaces we would # pickle a CanonicalKeyView which old sentry workers do not know # about state.pop("_project_cache", None) state.pop("_environment_cache", None) state.pop("_group_cache", None) state.pop("interfaces", None) return state
class PendingTeamMember(Model): """ Identifies relationships between teams and pending invites. """ team = models.ForeignKey(Team, related_name="pending_member_set") email = models.EmailField() type = BoundedIntegerField(choices=MEMBER_TYPES, default=MEMBER_USER) date_added = models.DateTimeField(default=timezone.now) objects = BaseManager() class Meta: unique_together = (('team', 'email'), ) __repr__ = sane_repr('team_id', 'email', 'type') @property def token(self): checksum = md5() for x in (str(self.team_id), self.email, settings.SECRET_KEY): checksum.update(x) return checksum.hexdigest() def send_invite_email(self): from django.core.mail import send_mail from sentry.web.helpers import render_to_string context = { 'email': self.email, 'team': self.team, 'url': absolute_uri( reverse('sentry-accept-invite', kwargs={ 'member_id': self.id, 'token': self.token, })), } body = render_to_string('sentry/emails/member_invite.txt', context) try: send_mail('%sInvite to join team: %s' % (settings.EMAIL_SUBJECT_PREFIX, self.team.name), body, settings.SERVER_EMAIL, [self.email], fail_silently=False) except Exception, e: logger = logging.getLogger('sentry.mail.errors') logger.exception(e)
class TeamMember(Model): """ Identifies relationships between teams and users. Users listed as team members are considered to have access to all projects and could be thought of as team owners (though their access level may not) be set to ownership. """ team = models.ForeignKey(Team, related_name="member_set") user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="sentry_teammember_set") type = BoundedIntegerField(choices=MEMBER_TYPES, default=MEMBER_USER) date_added = models.DateTimeField(default=timezone.now) objects = BaseManager() class Meta: unique_together = (('team', 'user'),) __repr__ = sane_repr('team_id', 'user_id', 'type')
class GroupCountByMinute(Model): """ Stores the total number of messages seen by a group at N minute intervals. e.g. if it happened at 08:34:55 the time would be normalized to 08:30:00 """ project = models.ForeignKey(Project, null=True) group = models.ForeignKey(Group) date = models.DateTimeField(db_index=True) # normalized to HH:MM:00 times_seen = BoundedPositiveIntegerField(default=0) time_spent_total = models.FloatField(default=0) time_spent_count = BoundedIntegerField(default=0) objects = BaseManager() class Meta: db_table = 'sentry_messagecountbyminute' unique_together = (('project', 'group', 'date'), ) __repr__ = sane_repr('project_id', 'group_id', 'date')
class Event(Model): """ An individual event. """ __core__ = False group_id = BoundedBigIntegerField(blank=True, null=True) event_id = models.CharField(max_length=32, null=True, db_column="message_id") project_id = BoundedBigIntegerField(blank=True, null=True) message = models.TextField() platform = models.CharField(max_length=64, null=True) datetime = models.DateTimeField(default=timezone.now, db_index=True) time_spent = BoundedIntegerField(null=True) data = NodeField( blank=True, null=True, ref_func=lambda x: x.project_id or x.project.id, ref_version=2, wrapper=CanonicalKeyDict, ) class Meta: app_label = 'sentry' db_table = 'sentry_message' verbose_name = _('message') verbose_name_plural = _('messages') unique_together = (('project_id', 'event_id'), ) index_together = (('group_id', 'datetime'), ) __repr__ = sane_repr('project_id', 'group_id') def __getstate__(self): state = Model.__getstate__(self) # do not pickle cached info. We want to fetch this on demand # again. In particular if we were to pickle interfaces we would # pickle a CanonicalKeyView which old sentry workers do not know # about state.pop('_project_cache', None) state.pop('_group_cache', None) state.pop('interfaces', None) return state # Implement a ForeignKey-like accessor for backwards compat def _set_group(self, group): self.group_id = group.id self._group_cache = group def _get_group(self): from sentry.models import Group if not hasattr(self, '_group_cache'): self._group_cache = Group.objects.get(id=self.group_id) return self._group_cache group = property(_get_group, _set_group) # Implement a ForeignKey-like accessor for backwards compat def _set_project(self, project): self.project_id = project.id self._project_cache = project def _get_project(self): from sentry.models import Project if not hasattr(self, '_project_cache'): self._project_cache = Project.objects.get(id=self.project_id) return self._project_cache project = property(_get_project, _set_project) def get_legacy_message(self): msg_interface = self.data.get('logentry', { 'message': self.message, }) return msg_interface.get('formatted', msg_interface['message']) def get_event_type(self): """ Return the type of this event. See ``sentry.eventtypes``. """ return self.data.get('type', 'default') def get_event_metadata(self): """ Return the metadata of this event. See ``sentry.eventtypes``. """ etype = self.data.get('type', 'default') if 'metadata' not in self.data: # TODO(dcramer): remove after Dec 1 2016 data = dict(self.data or {}) data['message'] = self.message data = CanonicalKeyView(data) 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('Event.error is deprecated, use Event.title', DeprecationWarning) return self.title error.short_description = _('error') @property def message_short(self): warnings.warn('Event.message_short is deprecated, use Event.title', DeprecationWarning) return self.title @property def organization(self): return self.project.organization @property def version(self): return self.data.get('version', '5') @memoize def ip_address(self): user_data = self.data.get('user', self.data.get('user')) if user_data: value = user_data.get('ip_address') if value: return value http_data = self.data.get('request', self.data.get('http')) if http_data and 'env' in http_data: value = http_data['env'].get('REMOTE_ADDR') if value: return value return None def get_interfaces(self): return CanonicalKeyView(get_interfaces(self.data)) @memoize def interfaces(self): return self.get_interfaces() def get_tags(self): try: return sorted((t, v) for t, v in self.data.get('tags') or ()) except ValueError: # at one point Sentry allowed invalid tag sets such as (foo, bar) # vs ((tag, foo), (tag, bar)) return [] tags = property(get_tags) def get_tag(self, key): for t, v in (self.data.get('tags') or ()): if t == key: return v return None @property def dist(self): return self.get_tag('sentry:dist') def as_dict(self): # We use a OrderedDict to keep elements ordered for a potential JSON serializer data = OrderedDict() data['id'] = self.event_id data['project'] = self.project_id data['release'] = self.get_tag('sentry:release') data['dist'] = self.dist data['platform'] = self.platform data['culprit'] = self.group.culprit data['message'] = self.get_legacy_message() data['datetime'] = self.datetime data['time_spent'] = self.time_spent data['tags'] = self.get_tags() for k, v in sorted(six.iteritems(self.data)): if k == 'sdk': v = { v_k: v_v for v_k, v_v in six.iteritems(v) if v_k != 'client_ip' } data[k] = v return data @property def size(self): data_len = 0 for value in six.itervalues(self.data): data_len += len(repr(value)) return data_len # XXX(dcramer): compatibility with plugins def get_level_display(self): warnings.warn( 'Event.get_level_display is deprecated. Use Event.tags instead.', DeprecationWarning) return self.group.get_level_display() @property def level(self): warnings.warn('Event.level is deprecated. Use Event.tags instead.', DeprecationWarning) return self.group.level @property def logger(self): warnings.warn('Event.logger is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('logger') @property def site(self): warnings.warn('Event.site is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('site') @property def server_name(self): warnings.warn( 'Event.server_name is deprecated. Use Event.tags instead.') return self.get_tag('server_name') @property def culprit(self): warnings.warn( 'Event.culprit is deprecated. Use Group.culprit instead.') return self.group.culprit @property def checksum(self): warnings.warn('Event.checksum is no longer used', DeprecationWarning) return '' @property def transaction(self): return self.get_tag('transaction') def get_email_subject(self): template = self.project.get_option('mail:subject_template') if template: template = EventSubjectTemplate(template) else: template = DEFAULT_SUBJECT_TEMPLATE return truncatechars( template.safe_substitute(EventSubjectTemplateData(self), ), 128, ).encode('utf-8') def get_environment(self): from sentry.models import Environment if not hasattr(self, '_environment_cache'): self._environment_cache = Environment.objects.get( organization_id=self.project.organization_id, name=Environment.get_name_or_default( self.get_tag('environment')), ) return self._environment_cache
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 Event(Model): """ An individual event. """ group = FlexibleForeignKey('sentry.Group', blank=True, null=True, related_name="event_set") event_id = models.CharField(max_length=32, null=True, db_column="message_id") project = FlexibleForeignKey('sentry.Project', null=True) message = models.TextField() checksum = models.CharField(max_length=32, db_index=True) num_comments = BoundedPositiveIntegerField(default=0, null=True) platform = models.CharField(max_length=64, null=True) datetime = models.DateTimeField(default=timezone.now, db_index=True) time_spent = BoundedIntegerField(null=True) data = NodeField(blank=True, null=True) objects = BaseManager() class Meta: app_label = 'sentry' db_table = 'sentry_message' verbose_name = _('message') verbose_name_plural = _('messages') unique_together = (('project', 'event_id'), ) index_together = (('group', 'datetime'), ) __repr__ = sane_repr('project_id', 'group_id', 'checksum') def error(self): message = strip(self.message) if not message: message = '<unlabeled message>' else: message = truncatechars(message.splitlines()[0], 100) return 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 message_short(self): message = strip(self.message) if not message: message = '<unlabeled message>' else: message = truncatechars(message.splitlines()[0], 100) return message @property def team(self): return self.project.team @property def organization(self): return self.project.organization @property def version(self): return self.data.get('version', '5') @memoize def ip_address(self): http_data = self.data.get('sentry.interfaces.Http') if http_data and 'env' in http_data: value = http_data['env'].get('REMOTE_ADDR') if value: return value user_data = self.data.get('sentry.interfaces.User') if user_data: value = user_data.get('ip_address') if value: return value return None @memoize 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', self.data.get('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, ) ident = self.ip_address if ident: return 'ip:%s' % (ident, ) return None @memoize def interfaces(self): result = [] for key, data in self.data.iteritems(): try: cls = get_interface(key) except ValueError: continue value = safe_execute(cls.to_python, data) if not value: continue result.append((key, value)) return OrderedDict((k, v) for k, v in sorted( result, key=lambda x: x[1].get_score(), reverse=True)) def get_tags(self, with_internal=True): try: return [(t, v) for t, v in self.data.get('tags') or () if with_internal or not t.startswith('sentry:')] except ValueError: # at one point Sentry allowed invalid tag sets such as (foo, bar) # vs ((tag, foo), (tag, bar)) return [] tags = property(get_tags) def get_tag(self, key): for t, v in (self.data.get('tags') or ()): if t == key: return v return None def as_dict(self): # We use a OrderedDict to keep elements ordered for a potential JSON serializer data = OrderedDict() data['id'] = self.event_id data['project'] = self.project_id data['release'] = self.get_tag('sentry:release') data['platform'] = self.platform data['culprit'] = self.group.culprit data['message'] = self.message data['checksum'] = self.checksum data['datetime'] = self.datetime data['time_spent'] = self.time_spent data['tags'] = self.get_tags() for k, v in sorted(self.data.iteritems()): data[k] = v return data @property def size(self): data_len = len(self.message) for value in self.data.itervalues(): data_len += len(repr(value)) return data_len # XXX(dcramer): compatibility with plugins def get_level_display(self): warnings.warn( 'Event.get_level_display is deprecated. Use Event.tags instead.', DeprecationWarning) return self.group.get_level_display() @property def level(self): warnings.warn('Event.level is deprecated. Use Event.tags instead.', DeprecationWarning) return self.group.level @property def logger(self): warnings.warn('Event.logger is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('logger') @property def site(self): warnings.warn('Event.site is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('site') @property def server_name(self): warnings.warn( 'Event.server_name is deprecated. Use Event.tags instead.') return self.get_tag('server_name') @property def culprit(self): warnings.warn( 'Event.culprit is deprecated. Use Group.culprit instead.') return self.group.culprit
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 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 Event(Model): """ An individual event. """ __core__ = False group_id = BoundedBigIntegerField(blank=True, null=True) event_id = models.CharField(max_length=32, null=True, db_column="message_id") project_id = BoundedBigIntegerField(blank=True, null=True) message = models.TextField() platform = models.CharField(max_length=64, null=True) datetime = models.DateTimeField(default=timezone.now, db_index=True) time_spent = BoundedIntegerField(null=True) data = NodeField( blank=True, null=True, ref_func=ref_func, ref_version=2, wrapper=CanonicalKeyDict, ) class Meta: app_label = 'sentry' db_table = 'sentry_message' verbose_name = _('message') verbose_name_plural = _('messages') unique_together = (('project_id', 'event_id'), ) index_together = (('group_id', 'datetime'), ) __repr__ = sane_repr('project_id', 'group_id') @classmethod def generate_node_id(cls, project_id, event_id): """ Returns a deterministic node_id for this event based on the project_id and event_id which together are globally unique. The event body should be saved under this key in nodestore so it can be retrieved using the same generated id when we only have project_id and event_id. """ return md5('{}:{}'.format(project_id, event_id)).hexdigest() def __getstate__(self): state = Model.__getstate__(self) # do not pickle cached info. We want to fetch this on demand # again. In particular if we were to pickle interfaces we would # pickle a CanonicalKeyView which old sentry workers do not know # about state.pop('_project_cache', None) state.pop('_group_cache', None) state.pop('interfaces', None) return state # Implement a ForeignKey-like accessor for backwards compat def _set_group(self, group): self.group_id = group.id self._group_cache = group def _get_group(self): from sentry.models import Group if not hasattr(self, '_group_cache'): self._group_cache = Group.objects.get(id=self.group_id) return self._group_cache group = property(_get_group, _set_group) # Implement a ForeignKey-like accessor for backwards compat def _set_project(self, project): self.project_id = project.id self._project_cache = project def _get_project(self): from sentry.models import Project if not hasattr(self, '_project_cache'): self._project_cache = Project.objects.get(id=self.project_id) return self._project_cache project = property(_get_project, _set_project) def get_legacy_message(self): # TODO(mitsuhiko): remove this code once it's unused. It's still # being used by plugin code and once the message rename is through # plugins should instead swithc to the actual message attribute or # this method could return what currently is real_message. return get_path(self.data, 'logentry', 'formatted') \ or get_path(self.data, 'logentry', 'message') \ or self.message def get_event_type(self): """ Return the type of this event. See ``sentry.eventtypes``. """ return self.data.get('type', 'default') def get_event_metadata(self): """ Return the metadata of this event. See ``sentry.eventtypes``. """ from sentry.event_manager import get_event_metadata_compat return get_event_metadata_compat(self.data, self.message) def get_hashes(self): """ Returns the calculated hashes for the event. """ from sentry.event_hashing import calculate_event_hashes # If we have hashes stored in the data we use them, otherwise we # fall back to generating new ones from the data hashes = self.data.get('hashes') if hashes is not None: return hashes return calculate_event_hashes(self) def get_primary_hash(self): # TODO: This *might* need to be protected from an IndexError? return self.get_hashes()[0] @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('Event.error is deprecated, use Event.title', DeprecationWarning) return self.title error.short_description = _('error') @property def real_message(self): # XXX(mitsuhiko): this is a transitional attribute that should be # removed. `message` will be renamed to `search_message` and this # will become `message`. return get_path(self.data, 'logentry', 'formatted') \ or get_path(self.data, 'logentry', 'message') \ or '' @property def message_short(self): warnings.warn('Event.message_short is deprecated, use Event.title', DeprecationWarning) return self.title @property def organization(self): return self.project.organization @property def version(self): return self.data.get('version', '5') @memoize def ip_address(self): ip_address = get_path(self.data, 'user', 'ip_address') if ip_address: return ip_address remote_addr = get_path(self.data, 'request', 'env', 'REMOTE_ADDR') if remote_addr: return remote_addr return None def get_interfaces(self): return CanonicalKeyView(get_interfaces(self.data)) @memoize def interfaces(self): return self.get_interfaces() def get_tags(self, sorted=True): try: rv = [(t, v) for t, v in get_path( self.data, 'tags', filter=True) or () if v is not None] if sorted: rv.sort() return rv except ValueError: # at one point Sentry allowed invalid tag sets such as (foo, bar) # vs ((tag, foo), (tag, bar)) return [] tags = property(get_tags) def get_tag(self, key): for t, v in self.get_tags(): if t == key: return v return None @property def release(self): return self.get_tag('sentry:release') @property def dist(self): return self.get_tag('sentry:dist') def as_dict(self): # We use a OrderedDict to keep elements ordered for a potential JSON serializer data = OrderedDict() data['event_id'] = self.event_id data['project'] = self.project_id data['release'] = self.release data['dist'] = self.dist data['platform'] = self.platform data['message'] = self.real_message data['datetime'] = self.datetime data['time_spent'] = self.time_spent data['tags'] = [(k.split('sentry:', 1)[-1], v) for (k, v) in self.get_tags()] for k, v in sorted(six.iteritems(self.data)): if k in data: continue if k == 'sdk': v = {v_k: v_v for v_k, v_v in six.iteritems(v) if v_k != 'client_ip'} data[k] = v # for a long time culprit was not persisted. In those cases put # the culprit in from the group. if data.get('culprit') is None: data['culprit'] = self.group.culprit return data @property def size(self): data_len = 0 for value in six.itervalues(self.data): data_len += len(repr(value)) return data_len # XXX(dcramer): compatibility with plugins def get_level_display(self): warnings.warn( 'Event.get_level_display is deprecated. Use Event.tags instead.', DeprecationWarning ) return self.group.get_level_display() @property def level(self): warnings.warn('Event.level is deprecated. Use Event.tags instead.', DeprecationWarning) return self.group.level @property def logger(self): warnings.warn('Event.logger is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('logger') @property def site(self): warnings.warn('Event.site is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('site') @property def server_name(self): warnings.warn('Event.server_name is deprecated. Use Event.tags instead.') return self.get_tag('server_name') @property def culprit(self): warnings.warn('Event.culprit is deprecated. Use Group.culprit instead.') return self.group.culprit @property def checksum(self): warnings.warn('Event.checksum is no longer used', DeprecationWarning) return '' @property def transaction(self): return self.get_tag('transaction') def get_email_subject(self): template = self.project.get_option('mail:subject_template') if template: template = EventSubjectTemplate(template) else: template = DEFAULT_SUBJECT_TEMPLATE return truncatechars( template.safe_substitute( EventSubjectTemplateData(self), ), 128, ).encode('utf-8') def get_environment(self): from sentry.models import Environment if not hasattr(self, '_environment_cache'): self._environment_cache = Environment.objects.get( organization_id=self.project.organization_id, name=Environment.get_name_or_default(self.get_tag('environment')), ) return self._environment_cache # Find next and previous events based on datetime and id. We cannot # simply `ORDER BY (datetime, id)` as this is too slow (no index), so # we grab the next 5 / prev 5 events by datetime, and sort locally to # get the next/prev events. Given that timestamps only have 1-second # granularity, this will be inaccurate if there are more than 5 events # in a given second. @property def next_event(self): qs = self.__class__.objects.filter( # To be 'after', an event needs either a higher datetime, # or the same datetime and a higher id. ( Q(datetime__gt=self.datetime) | (Q(datetime=self.datetime) & Q(id__gt=self.id)) ), group_id=self.group_id, ).exclude(id=self.id).order_by('datetime') try: return sorted(qs[0:5], key=EVENT_ORDERING_KEY)[0] except IndexError: return None @property def prev_event(self): qs = self.__class__.objects.filter( # To be 'before', an event needs either a lower datetime, # or the same datetime and a lower id. ( Q(datetime__lt=self.datetime) | (Q(datetime=self.datetime) & Q(id__lt=self.id)) ), group_id=self.group_id, ).exclude(id=self.id).order_by('-datetime') try: return sorted(qs[0:5], key=EVENT_ORDERING_KEY, reverse=True)[0] except IndexError: return None
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) 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' verbose_name_plural = _('grouped messages') verbose_name = _('grouped message') permissions = (("can_view", "Can view"), ) index_together = (('project', 'first_release'), ) __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 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 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): 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)).lower() @classmethod def from_share_id(cls, share_id): try: project_id, group_id = b16decode(share_id.upper()).split('.') except ValueError: 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'): 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, 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 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 @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.message_short.encode('utf-8'))
class Event(Model): """ An individual event. """ group = models.ForeignKey('sentry.Group', blank=True, null=True, related_name="event_set") event_id = models.CharField(max_length=32, null=True, db_column="message_id") 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) datetime = models.DateTimeField(default=timezone.now, db_index=True) time_spent = BoundedIntegerField(null=True) server_name = models.CharField(max_length=128, db_index=True, null=True) site = models.CharField(max_length=128, db_index=True, null=True) data = NodeField(blank=True, null=True) objects = BaseManager() class Meta: app_label = 'sentry' db_table = 'sentry_message' verbose_name = _('message') verbose_name_plural = _('messages') unique_together = ('project', 'event_id') __repr__ = sane_repr('project_id', 'group_id', 'checksum') def error(self): message = strip(self.message) if not message: message = '<unlabeled message>' else: message = truncatechars(message.splitlines()[0], 100) 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 return self.error() @property def team(self): return self.project.team @memoize def ip_address(self): http_data = self.data.get('sentry.interfaces.Http') if http_data and 'env' in http_data: value = http_data['env'].get('REMOTE_ADDR') if value: return value user_data = self.data.get('sentry.interfaces.User') if user_data: value = user_data.get('ip_address') if value: return value return None @memoize 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,) ident = self.ip_address if ident: return 'ip:%s' % (ident,) return None @memoize def interfaces(self): result = [] for key, data in self.data.iteritems(): if '.' not in key: continue try: cls = import_string(key) except ImportError: continue # suppress invalid interfaces value = safe_execute(cls, **data) if not value: continue result.append((key, value)) return SortedDict((k, v) for k, v in sorted(result, key=lambda x: x[1].get_score(), reverse=True)) def get_version(self): if not self.data: return if '__sentry__' not in self.data: return if 'version' not in self.data['__sentry__']: return module = self.data['__sentry__'].get('module', 'ver') return module, self.data['__sentry__']['version'] def get_tags(self): try: return [ (t, v) for t, v in self.data.get('tags') or () if not t.startswith('sentry:') ] except ValueError: # at one point Sentry allowed invalid tag sets such as (foo, bar) # vs ((tag, foo), (tag, bar)) return [] def as_dict(self): # We use a SortedDict to keep elements ordered for a potential JSON serializer data = SortedDict() data['id'] = self.event_id data['checksum'] = self.checksum data['project'] = self.project.slug data['logger'] = self.logger data['level'] = self.get_level_display() data['culprit'] = self.culprit data['datetime'] = self.datetime data['time_spent'] = self.time_spent for k, v in sorted(self.data.iteritems()): data[k] = v return data @property def size(self): return len(unicode(vars(self)))
class Event(EventCommon, Model): """ An event backed by data stored in postgres. """ __core__ = False group_id = BoundedBigIntegerField(blank=True, null=True) event_id = models.CharField(max_length=32, null=True, db_column="message_id") project_id = BoundedBigIntegerField(blank=True, null=True) message = models.TextField() platform = models.CharField(max_length=64, null=True) datetime = models.DateTimeField(default=timezone.now, db_index=True) time_spent = BoundedIntegerField(null=True) data = NodeField( blank=True, null=True, ref_func=lambda x: x.project_id or x.project.id, ref_version=2, wrapper=EventDict, ) objects = EventManager() class Meta: app_label = 'sentry' db_table = 'sentry_message' verbose_name = _('message') verbose_name_plural = _('messages') unique_together = (('project_id', 'event_id'), ) index_together = (('group_id', 'datetime'), ) __repr__ = sane_repr('project_id', 'group_id') def __getstate__(self): state = Model.__getstate__(self) # do not pickle cached info. We want to fetch this on demand # again. In particular if we were to pickle interfaces we would # pickle a CanonicalKeyView which old sentry workers do not know # about state.pop('_project_cache', None) state.pop('_group_cache', None) state.pop('interfaces', None) return state # Find next and previous events based on datetime and id. We cannot # simply `ORDER BY (datetime, id)` as this is too slow (no index), so # we grab the next 5 / prev 5 events by datetime, and sort locally to # get the next/prev events. Given that timestamps only have 1-second # granularity, this will be inaccurate if there are more than 5 events # in a given second. def next_event_id(self, environments=None): events = self.__class__.objects.filter( datetime__gte=self.datetime, group_id=self.group_id, ).exclude(id=self.id).order_by('datetime')[0:5] events = [e for e in events if e.datetime == self.datetime and e.id > self.id or e.datetime > self.datetime] events.sort(key=EVENT_ORDERING_KEY) return six.text_type(events[0].event_id) if events else None def prev_event_id(self, environments=None): events = self.__class__.objects.filter( datetime__lte=self.datetime, group_id=self.group_id, ).exclude(id=self.id).order_by('-datetime')[0:5] events = [e for e in events if e.datetime == self.datetime and e.id < self.id or e.datetime < self.datetime] events.sort(key=EVENT_ORDERING_KEY, reverse=True) return six.text_type(events[0].event_id) if events else None
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 Event(EventBase): """ An individual event. """ group = models.ForeignKey(Group, blank=True, null=True, related_name="event_set") event_id = models.CharField(max_length=32, null=True, db_column="message_id") datetime = models.DateTimeField(default=timezone.now, db_index=True) time_spent = BoundedIntegerField(null=True) server_name = models.CharField(max_length=128, db_index=True, null=True) site = models.CharField(max_length=128, db_index=True, null=True) objects = BaseManager() class Meta: verbose_name = _('message') verbose_name_plural = _('messages') db_table = 'sentry_message' unique_together = ('project', 'event_id') __repr__ = sane_repr('project_id', 'group_id', 'checksum') @memoize def interfaces(self): result = [] for key, data in self.data.iteritems(): if '.' not in key: continue try: cls = import_string(key) except ImportError: continue # suppress invalid interfaces value = safe_execute(cls, **data) if not value: continue result.append((key, value)) return SortedDict((k, v) for k, v in sorted(result, key=lambda x: x[1].get_score(), reverse=True)) def get_version(self): if not self.data: return if '__sentry__' not in self.data: return if 'version' not in self.data['__sentry__']: return module = self.data['__sentry__'].get('module', 'ver') return module, self.data['__sentry__']['version'] def get_tags(self): try: return [ (t, v) for t, v in self.data.get('tags') or () if not t.startswith('sentry:') ] except ValueError: # at one point Sentry allowed invalid tag sets such as (foo, bar) # vs ((tag, foo), (tag, bar)) return [] def as_dict(self): # We use a SortedDict to keep elements ordered for a potential JSON serializer data = SortedDict() data['id'] = self.event_id data['checksum'] = self.checksum data['project'] = self.project.slug data['logger'] = self.logger data['level'] = self.get_level_display() data['culprit'] = self.culprit for k, v in sorted(self.data.iteritems()): data[k] = v return data @property def size(self): return len(unicode(vars(self)))
class Group(EventBase): """ Aggregated message which summarizes a set of Events. """ 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) objects = GroupManager() class Meta: unique_together = (('project', 'checksum'),) verbose_name_plural = _('grouped messages') verbose_name = _('grouped message') permissions = ( ("can_view", "Can view"), ) db_table = 'sentry_groupedmessage' __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 delete(self): model_list = ( GroupTagKey, GroupTag, GroupCountByMinute, EventMapping, Event ) for model in model_list: logging.info('Removing %r objects where group=%s', model, self.id) has_results = True while has_results: has_results = False for obj in model.objects.filter(group=self)[:1000]: obj.delete() has_results = True super(Group, self).delete() def get_absolute_url(self): return absolute_uri(reverse('sentry-group', args=[ self.team.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.logger, self.culprit, 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): if not hasattr(self, '_latest_event'): try: self._latest_event = self.event_set.order_by('-datetime')[0] except IndexError: self._latest_event = None return self._latest_event def get_version(self): if not self.data: return if 'version' not in self.data: return module = self.data.get('module', 'ver') return module, self.data['version'] def get_unique_tags(self, tag): return self.grouptag_set.filter( project=self.project, key=tag, ).values_list( 'value', 'times_seen', 'first_seen', 'last_seen', ).order_by('-times_seen') def get_tags(self): if not hasattr(self, '_tag_cache'): self._tag_cache = sorted([ t for t in self.grouptagkey_set.filter( project=self.project, ).values_list('key', flat=True) if not t.startswith('sentry:') ]) return self._tag_cache
class Event(Model): """ An individual event. """ __core__ = False group_id = BoundedBigIntegerField(blank=True, null=True) event_id = models.CharField(max_length=32, null=True, db_column="message_id") project_id = BoundedBigIntegerField(blank=True, null=True) message = models.TextField() platform = models.CharField(max_length=64, null=True) datetime = models.DateTimeField(default=timezone.now, db_index=True) time_spent = BoundedIntegerField(null=True) data = NodeField( blank=True, null=True, ref_func=lambda x: x.project_id or x.project.id, ref_version=2, ) objects = BaseManager() class Meta: app_label = 'sentry' db_table = 'sentry_message' verbose_name = _('message') verbose_name_plural = _('messages') unique_together = (('project_id', 'event_id'), ) index_together = (('group_id', 'datetime'), ) __repr__ = sane_repr('project_id', 'group_id') # Implement a ForeignKey-like accessor for backwards compat def _set_group(self, group): self.group_id = group.id self._group_cache = group def _get_group(self): from sentry.models import Group if not hasattr(self, '_group_cache'): self._group_cache = Group.objects.get(id=self.group_id) return self._group_cache group = property(_get_group, _set_group) # Implement a ForeignKey-like accessor for backwards compat def _set_project(self, project): self.project_id = project.id self._project_cache = project def _get_project(self): from sentry.models import Project if not hasattr(self, '_project_cache'): self._project_cache = Project.objects.get(id=self.project_id) return self._project_cache project = property(_get_project, _set_project) def get_legacy_message(self): msg_interface = self.data.get('sentry.interfaces.Message', { 'message': self.message, }) return msg_interface.get('formatted', msg_interface['message']) def get_event_type(self): """ Return the type of this event. See ``sentry.eventtypes``. """ return self.data.get('type', 'default') def get_event_metadata(self): """ Return the metadata of this event. See ``sentry.eventtypes``. """ etype = self.data.get('type', 'default') if 'metadata' not in self.data: # TODO(dcramer): remove after Dec 1 2016 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('Event.error is deprecated, use Event.title', DeprecationWarning) return self.title error.short_description = _('error') @property def message_short(self): warnings.warn('Event.message_short is deprecated, use Event.title', DeprecationWarning) return self.title def has_two_part_message(self): warnings.warn('Event.has_two_part_message is no longer used', DeprecationWarning) return False @property def team(self): return self.project.team @property def organization(self): return self.project.organization @property def version(self): return self.data.get('version', '5') @memoize def ip_address(self): user_data = self.data.get('sentry.interfaces.User', self.data.get('user')) if user_data: value = user_data.get('ip_address') if value: return value http_data = self.data.get('sentry.interfaces.Http', self.data.get('http')) if http_data and 'env' in http_data: value = http_data['env'].get('REMOTE_ADDR') if value: return value return None def get_interfaces(self): result = [] for key, data in six.iteritems(self.data): try: cls = get_interface(key) except ValueError: continue value = safe_execute(cls.to_python, data, _with_transaction=False) if not value: continue result.append((key, value)) return OrderedDict((k, v) for k, v in sorted( result, key=lambda x: x[1].get_score(), reverse=True)) @memoize def interfaces(self): return self.get_interfaces() def get_tags(self, with_internal=True): try: return sorted((t, v) for t, v in self.data.get('tags') or () if with_internal or not t.startswith('sentry:')) except ValueError: # at one point Sentry allowed invalid tag sets such as (foo, bar) # vs ((tag, foo), (tag, bar)) return [] tags = property(get_tags) def get_tag(self, key): for t, v in (self.data.get('tags') or ()): if t == key: return v return None @property def dist(self): return self.get_tag('sentry:dist') def as_dict(self): # We use a OrderedDict to keep elements ordered for a potential JSON serializer data = OrderedDict() data['id'] = self.event_id data['project'] = self.project_id data['release'] = self.get_tag('sentry:release') data['dist'] = self.dist data['platform'] = self.platform data['culprit'] = self.group.culprit data['message'] = self.get_legacy_message() data['datetime'] = self.datetime data['time_spent'] = self.time_spent data['tags'] = self.get_tags() for k, v in sorted(six.iteritems(self.data)): data[k] = v return data @property def size(self): data_len = len(self.get_legacy_message()) for value in six.itervalues(self.data): data_len += len(repr(value)) return data_len # XXX(dcramer): compatibility with plugins def get_level_display(self): warnings.warn( 'Event.get_level_display is deprecated. Use Event.tags instead.', DeprecationWarning) return self.group.get_level_display() @property def level(self): warnings.warn('Event.level is deprecated. Use Event.tags instead.', DeprecationWarning) return self.group.level @property def logger(self): warnings.warn('Event.logger is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('logger') @property def site(self): warnings.warn('Event.site is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('site') @property def server_name(self): warnings.warn( 'Event.server_name is deprecated. Use Event.tags instead.') return self.get_tag('server_name') @property def culprit(self): warnings.warn( 'Event.culprit is deprecated. Use Group.culprit instead.') return self.transaction or self.group.culprit @property def checksum(self): warnings.warn('Event.checksum is no longer used', DeprecationWarning) return '' @property def transaction(self): return self.get_tag('transaction') def get_email_subject(self): template = self.project.get_option('mail:subject_template') if template: template = EventSubjectTemplate(template) else: template = DEFAULT_SUBJECT_TEMPLATE return truncatechars( template.safe_substitute(EventSubjectTemplateData(self), ), 128, ).encode('utf-8')
class Event(Model): """ An individual event. """ __core__ = False group_id = BoundedBigIntegerField(blank=True, null=True) event_id = models.CharField(max_length=32, null=True, db_column="message_id") project_id = BoundedBigIntegerField(blank=True, null=True) message = models.TextField() platform = models.CharField(max_length=64, null=True) datetime = models.DateTimeField(default=timezone.now, db_index=True) time_spent = BoundedIntegerField(null=True) data = NodeField( blank=True, null=True, ref_func=lambda x: x.project_id or x.project.id, ref_version=2, ) objects = BaseManager() class Meta: app_label = 'sentry' db_table = 'sentry_message' verbose_name = _('message') verbose_name_plural = _('messages') unique_together = (('project_id', 'event_id'), ) index_together = (('group_id', 'datetime'), ) __repr__ = sane_repr('project_id', 'group_id') # Implement a ForeignKey-like accessor for backwards compat def _set_group(self, group): self.group_id = group.id self._group_cache = group def _get_group(self): from sentry.models import Group if not hasattr(self, '_group_cache'): self._group_cache = Group.objects.get(id=self.group_id) return self._group_cache group = property(_get_group, _set_group) # Implement a ForeignKey-like accessor for backwards compat def _set_project(self, project): self.project_id = project.id self._project_cache = project def _get_project(self): from sentry.models import Project if not hasattr(self, '_project_cache'): self._project_cache = Project.objects.get(id=self.project_id) return self._project_cache project = property(_get_project, _set_project) def error(self): message = strip(self.message) if not message: message = '<unlabeled message>' else: message = truncatechars(message.splitlines()[0], 100) return 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 message_short(self): message = strip(self.message) if not message: message = '<unlabeled message>' else: message = truncatechars(message.splitlines()[0], 100) return message @property def team(self): return self.project.team @property def organization(self): return self.project.organization @property def version(self): return self.data.get('version', '5') @memoize def ip_address(self): user_data = self.data.get('sentry.interfaces.User', self.data.get('user')) if user_data: value = user_data.get('ip_address') if value: return value http_data = self.data.get('sentry.interfaces.Http', self.data.get('http')) if http_data and 'env' in http_data: value = http_data['env'].get('REMOTE_ADDR') if value: return value return None def get_interfaces(self): result = [] for key, data in self.data.iteritems(): try: cls = get_interface(key) except ValueError: continue value = safe_execute(cls.to_python, data, _with_transaction=False) if not value: continue result.append((key, value)) return OrderedDict((k, v) for k, v in sorted( result, key=lambda x: x[1].get_score(), reverse=True)) @memoize def interfaces(self): return self.get_interfaces() def get_tags(self, with_internal=True): try: return sorted((t, v) for t, v in self.data.get('tags') or () if with_internal or not t.startswith('sentry:')) except ValueError: # at one point Sentry allowed invalid tag sets such as (foo, bar) # vs ((tag, foo), (tag, bar)) return [] tags = property(get_tags) def get_tag(self, key): for t, v in (self.data.get('tags') or ()): if t == key: return v return None def as_dict(self): # We use a OrderedDict to keep elements ordered for a potential JSON serializer data = OrderedDict() data['id'] = self.event_id data['project'] = self.project_id data['release'] = self.get_tag('sentry:release') data['platform'] = self.platform data['culprit'] = self.group.culprit data['message'] = self.message data['datetime'] = self.datetime data['time_spent'] = self.time_spent data['tags'] = self.get_tags() for k, v in sorted(self.data.iteritems()): data[k] = v return data @property def size(self): data_len = len(self.message) for value in self.data.itervalues(): data_len += len(repr(value)) return data_len # XXX(dcramer): compatibility with plugins def get_level_display(self): warnings.warn( 'Event.get_level_display is deprecated. Use Event.tags instead.', DeprecationWarning) return self.group.get_level_display() @property def level(self): warnings.warn('Event.level is deprecated. Use Event.tags instead.', DeprecationWarning) return self.group.level @property def logger(self): warnings.warn('Event.logger is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('logger') @property def site(self): warnings.warn('Event.site is deprecated. Use Event.tags instead.', DeprecationWarning) return self.get_tag('site') @property def server_name(self): warnings.warn( 'Event.server_name is deprecated. Use Event.tags instead.') return self.get_tag('server_name') @property def culprit(self): warnings.warn( 'Event.culprit is deprecated. Use Group.culprit instead.') return self.group.culprit @property def checksum(self): warnings.warn('Event.checksum is no longer used', DeprecationWarning) return ''
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 DummyModel(Model): foo = models.CharField(max_length=32) normint = BoundedIntegerField(null=True) bigint = BoundedBigIntegerField(null=True) posint = BoundedPositiveIntegerField(null=True)