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 ProjectKey(Model): project = models.ForeignKey(Project, related_name='key_set') public_key = models.CharField(max_length=32, unique=True, null=True) secret_key = models.CharField(max_length=32, unique=True, null=True) user = models.ForeignKey(User, null=True) objects = BaseManager(cache_fields=( 'public_key', 'secret_key', )) def __unicode__(self): return u'project=%s, user=%s' % (self.project_id, self.user_id) @classmethod def generate_api_key(cls): return uuid.uuid4().hex def save(self, *args, **kwargs): if not self.public_key: self.public_key = ProjectKey.generate_api_key() if not self.secret_key: self.secret_key = ProjectKey.generate_api_key() super(ProjectKey, self).save(*args, **kwargs) def get_dsn(self, domain=None, secure=True): urlparts = urlparse.urlparse(settings.URL_PREFIX) return '%s://%s:%s@%s/%s' % ( urlparts.scheme, self.public_key, self.secret_key, urlparts.netloc + urlparts.path, self.project_id, )
class MessageFilterValue(Model): """ Stores the total number of messages seen by a group matching the given filter. """ project = models.ForeignKey(Project, null=True) group = models.ForeignKey(Group) times_seen = models.PositiveIntegerField(default=0) key = models.CharField(max_length=32) value = models.CharField(max_length=200) 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: unique_together = (('project', 'key', 'value', 'group'), ) def __unicode__(self): return u'group_id=%s, times_seen=%s, key=%s, value=%s' % ( self.group_id, self.times_seen, self.key, self.value) def save(self, *args, **kwargs): if not self.first_seen: self.first_seen = self.last_seen super(MessageFilterValue, self).save(*args, **kwargs)
class GroupTag(Model): """ Stores the total number of messages seen by a group matching the given filter. """ project = models.ForeignKey(Project, null=True) group = models.ForeignKey(Group) times_seen = BoundedPositiveIntegerField(default=0) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) 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_messagefiltervalue' unique_together = (('project', 'key', 'value', 'group'), ) __repr__ = sane_repr('project_id', 'group_id', 'key', 'value') def save(self, *args, **kwargs): if not self.first_seen: self.first_seen = self.last_seen super(GroupTag, self).save(*args, **kwargs)
class ProjectKey(Model): project = models.ForeignKey(Project, related_name='key_set') public_key = models.CharField(max_length=32, unique=True, null=True) secret_key = models.CharField(max_length=32, unique=True, null=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True) # For audits user_added = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, related_name='keys_added_set') date_added = models.DateTimeField(default=timezone.now, null=True) objects = BaseManager(cache_fields=( 'public_key', 'secret_key', )) __repr__ = sane_repr('project_id', 'user_id', 'public_key') def __unicode__(self): return unicode(self.public_key) @classmethod def generate_api_key(cls): return uuid.uuid4().hex def save(self, *args, **kwargs): if not self.public_key: self.public_key = ProjectKey.generate_api_key() if not self.secret_key: self.secret_key = ProjectKey.generate_api_key() super(ProjectKey, self).save(*args, **kwargs) def get_dsn(self, domain=None, secure=True, public=False): # TODO: change the DSN to use project slug once clients are compatible if not public: key = '%s:%s' % (self.public_key, self.secret_key) url = settings.SENTRY_ENDPOINT else: key = self.public_key url = settings.SENTRY_PUBLIC_ENDPOINT urlparts = urlparse.urlparse(url or settings.SENTRY_URL_PREFIX) return '%s://%s@%s/%s' % ( urlparts.scheme, key, urlparts.netloc + urlparts.path, self.project_id, ) @property def dsn_private(self): return self.get_dsn(public=False) @property def dsn_public(self): return self.get_dsn(public=True)
class SearchToken(Model): document = models.ForeignKey(SearchDocument, related_name="token_set") field = models.CharField(max_length=64, default='text') token = models.CharField(max_length=128) times_seen = models.PositiveIntegerField(default=1) objects = BaseManager() class Meta: unique_together = (('document', 'field', 'token'), )
class PendingTeamMember(Model): """ Identifies relationships between teams and pending invites. """ team = models.ForeignKey(Team, related_name="pending_member_set") email = models.EmailField() type = models.IntegerField(choices=MEMBER_TYPES, default=globals().get( settings.DEFAULT_PROJECT_ACCESS)) date_added = models.DateTimeField(default=timezone.now) objects = BaseManager() class Meta: unique_together = (('team', 'email'), ) def __unicode__(self): return u'team=%s, email=%s, type=%s' % (self.team_id, self.email, self.get_type_display()) @property def token(self): checksum = md5() for x in (str(self.team_id), self.email, settings.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': '%s%s' % (settings.URL_PREFIX, 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('%s Invite 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 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 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, e: logger = logging.getLogger('sentry.mail.errors') logger.exception(e)
class FilterValue(Model): """ Stores references to available filters. """ project = models.ForeignKey(Project, null=True) key = models.CharField(max_length=32) value = models.CharField(max_length=200) objects = BaseManager() class Meta: unique_together = (('project', 'key', 'value'), ) __repr__ = sane_repr('project_id', 'key', 'value')
class View(Model): """ A view ties directly to a view extension and simply identifies it at the db level. """ path = models.CharField(max_length=100, unique=True) verbose_name = models.CharField(max_length=200, null=True) verbose_name_plural = models.CharField(max_length=200, null=True) objects = BaseManager(cache_fields=[ 'path', ]) def __unicode__(self): return self.path
class GroupBookmark(Model): """ Identifies a bookmark relationship between a user and an aggregated event (Group). """ project = models.ForeignKey(Project, related_name="bookmark_set") # denormalized group = models.ForeignKey(Group, related_name="bookmark_set") # namespace related_name on User since we dont own the model user = models.ForeignKey(User, related_name="sentry_bookmark_set") objects = BaseManager() class Meta: # composite index includes project for efficient queries unique_together = (('project', 'user', 'group'),)
class FilterValue(Model): """ Stores references to available filters. """ project = models.ForeignKey(Project, null=True) key = models.CharField(max_length=32) value = models.CharField(max_length=200) objects = BaseManager() class Meta: unique_together = (('project', 'key', 'value'), ) def __unicode__(self): return u'key=%s, value=%s' % (self.key, self.value)
class TeamMember(Model): """ Identifies relationships between teams and users. """ team = models.ForeignKey(Team, related_name="member_set") user = models.ForeignKey(User, related_name="sentry_teammember_set") is_active = models.BooleanField(default=True) type = models.IntegerField(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 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 FilterValue(Model): """ Stores references to available filters. """ project = models.ForeignKey(Project, null=True) key = models.CharField(max_length=32) value = models.CharField(max_length=200) times_seen = models.PositiveIntegerField(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: unique_together = (('project', 'key', 'value'),) __repr__ = sane_repr('project_id', 'key', 'value')
class TeamMember(Model): """ Identifies relationships between teams and users. """ team = models.ForeignKey(Team, related_name="member_set") user = models.ForeignKey(User, related_name="sentry_teammember_set") is_active = models.BooleanField(default=True) type = models.IntegerField(choices=MEMBER_TYPES, default=globals().get(settings.DEFAULT_PROJECT_ACCESS)) date_added = models.DateTimeField(default=timezone.now) objects = BaseManager() class Meta: unique_together = (('team', 'user'),) def __unicode__(self): return u'team=%s, user=%s, type=%s' % (self.team_id, self.user_id, self.get_type_display())
class GroupTagKey(Model): """ Stores a unique tag key name for a group. An example key might be "url" or "server_name". """ project = models.ForeignKey(Project, null=True) group = models.ForeignKey(Group) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) values_seen = BoundedPositiveIntegerField(default=0) objects = BaseManager() class Meta: unique_together = (('project', 'group', 'key'), ) __repr__ = sane_repr('project_id', 'group_id', 'key')
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 = models.PositiveIntegerField(default=0) time_spent_total = models.FloatField(default=0) time_spent_count = models.IntegerField(default=0) objects = BaseManager() class Meta: unique_together = (('project', 'date'), )
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 AffectedUserByGroup(Model): """ Stores a count of how many times a ``Group`` has affected a user. """ project = models.ForeignKey(Project) tuser = models.ForeignKey(TrackedUser, null=True) group = models.ForeignKey(Group) ident = models.CharField(max_length=200, null=True) times_seen = models.PositiveIntegerField(default=0) last_seen = models.DateTimeField(default=timezone.now, db_index=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True) objects = BaseManager() class Meta: unique_together = (('project', 'tuser', 'group'), ) __repr__ = sane_repr('project_id', 'group_id', 'tuser_id')
class Team(Model): """ A team represents a group of individuals which maintain ownership of projects. """ slug = models.SlugField(unique=True) name = models.CharField(max_length=64) owner = models.ForeignKey(User) objects = BaseManager(cache_fields=( 'pk', 'slug', )) def __unicode__(self): return self.slug def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super(Team, self).save(*args, **kwargs)
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 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 MessageCountByMinute(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 = models.PositiveIntegerField(default=0) time_spent_total = models.FloatField(default=0) time_spent_count = models.IntegerField(default=0) objects = BaseManager() class Meta: unique_together = (('project', 'group', 'date'),) def __unicode__(self): return u'group_id=%s, times_seen=%s, date=%s' % (self.group_id, self.times_seen, self.date)
class ProjectDomain(Model): """ Currently unused. Planned for 'trusted domains' for JS apis. """ project = models.ForeignKey(Project, related_name="domain_set") domain = models.CharField(max_length=128) objects = BaseManager() class Meta: unique_together = (('project', 'domain'),) def __unicode__(self): return u'project=%s, domain=%s' % (self.project_id, self.domain) @classmethod def test(cls, project, url, strict=False): """ Tests whether the ``url`` is a trusted domain for the given project. """ if not url: return False url = urlparse.urlsplit(url).hostname if not url: # If we fail to parse the referral url return False if url in ('127.0.0.1', 'localhost'): return True if url.endswith('.local'): return True url = url.split('.') domains = ProjectDomain.objects.filter(project=project).values_list('domain', flat=True) for d in domains: d = d.split('.') if url[-len(d):] == d: return True return False
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 = models.FloatField(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 Event(MessageBase): """ 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=datetime.now, db_index=True) time_spent = models.FloatField(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') def __unicode__(self): return self.error() def get_absolute_url(self): if self.project_id: return reverse('sentry-group-event', kwargs={'group_id': self.group_id, 'event_id': self.pk, 'project_id': self.project_id}) return '#' @cached_property def request(self): data = self.data if 'META' in data: kwargs = { 'META': data.get('META'), 'GET': data.get('GET'), 'POST': data.get('POST'), 'FILES': data.get('FILES'), 'COOKIES': data.get('COOKIES'), 'url': data.get('url'), } elif 'sentry.interfaces.Http' in data: http = data['sentry.interfaces.Http'] kwargs = { 'META': http } else: return MockDjangoRequest() fake_request = MockDjangoRequest(**kwargs) if kwargs['url']: fake_request.path_info = '/' + kwargs['url'].split('/', 3)[-1] else: fake_request.path_info = '' fake_request.path = fake_request.path_info return fake_request @cached_property def interfaces(self): result = [] for k, v in self.data.iteritems(): if '.' not in k: continue m, c = k.rsplit('.', 1) cls = getattr(__import__(m, {}, {}, [c]), c) v = cls(**v) result.append((v.score, k, v)) return SortedDict((k, v) for _, k, v in sorted(result, key=lambda x: x[0], 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']
class Event(MessageBase): """ 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 = models.FloatField(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') def __unicode__(self): return self.error() def get_absolute_url(self): if self.project_id: return reverse('sentry-group-event', kwargs={ 'group_id': self.group_id, 'event_id': self.pk, 'project_id': self.project.slug }) return '#' @cached_property def request(self): data = self.data if 'META' in data: kwargs = { 'META': data.get('META'), 'GET': data.get('GET'), 'POST': data.get('POST'), 'FILES': data.get('FILES'), 'COOKIES': data.get('COOKIES'), 'url': data.get('url'), } elif 'sentry.interfaces.Http' in data: http = data['sentry.interfaces.Http'] kwargs = {'META': http} else: return MockDjangoRequest() fake_request = MockDjangoRequest(**kwargs) if kwargs['url']: fake_request.path_info = '/' + kwargs['url'].split('/', 3)[-1] else: fake_request.path_info = '' fake_request.path = fake_request.path_info return fake_request @cached_property def interfaces(self): result = [] for key, data in self.data.iteritems(): if '.' not in key: continue try: cls = import_string(key) except ImportError: pass # suppress invalid interfaces value = cls(**data) result.append((value.score, key, value)) return SortedDict( (k, v) for _, k, v in sorted(result, key=lambda x: x[0], 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 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