class TagValue(Model):
    """
    Stores references to available filters.
    """
    __core__ = False

    project_id = BoundedPositiveIntegerField(db_index=True, null=True)
    key = models.CharField(max_length=MAX_TAG_KEY_LENGTH)
    value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH)
    data = GzippedDictField(blank=True, null=True)
    times_seen = BoundedPositiveIntegerField(default=0)
    last_seen = models.DateTimeField(default=timezone.now,
                                     db_index=True,
                                     null=True)
    first_seen = models.DateTimeField(default=timezone.now,
                                      db_index=True,
                                      null=True)

    objects = BaseManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_filtervalue'
        unique_together = (('project_id', 'key', 'value'), )
        index_together = (('project_id', 'key', 'last_seen'), )

    __repr__ = sane_repr('project_id', 'key', 'value')

    def get_label(self):
        from sentry import tagstore

        return tagstore.get_tag_value_label(self.key, self.value)
Пример #2
0
class GroupTombstone(Model):
    __core__ = False

    previous_group_id = BoundedPositiveIntegerField(unique=True)
    project = FlexibleForeignKey("sentry.Project")
    level = BoundedPositiveIntegerField(
        choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True
    )
    message = models.TextField()
    culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH, blank=True, null=True)
    data = GzippedDictField(blank=True, null=True)
    actor_id = BoundedPositiveIntegerField(null=True)

    class Meta:
        app_label = "sentry"
        db_table = "sentry_grouptombstone"

    def get_event_type(self):
        """
        Return the type of this issue.

        See ``sentry.eventtypes``.
        """
        return self.data.get("type", "default")

    def get_event_metadata(self):
        """
        Return the metadata of this issue.

        See ``sentry.eventtypes``.
        """
        return self.data["metadata"]
Пример #3
0
class TagValue(Model):
    """
    Stores references to available filters.
    """
    __core__ = False

    project = FlexibleForeignKey('sentry.Project', null=True)
    key = models.CharField(max_length=MAX_TAG_KEY_LENGTH)
    value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH)
    data = GzippedDictField(blank=True, null=True)
    times_seen = BoundedPositiveIntegerField(default=0)
    last_seen = models.DateTimeField(default=timezone.now,
                                     db_index=True,
                                     null=True)
    first_seen = models.DateTimeField(default=timezone.now,
                                      db_index=True,
                                      null=True)

    objects = BaseManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_filtervalue'
        unique_together = (('project', 'key', 'value'), )

    __repr__ = sane_repr('project_id', 'key', 'value')

    def get_label(self):
        # HACK(dcramer): quick and dirty way to hack in better display states
        if self.key == 'sentry:user':
            return self.data.get('email') or self.value
        elif self.key == 'sentry:function':
            return '%s in %s' % (self.data['function'], self.data['filename'])
        elif self.key == 'sentry:filename':
            return self.data['filename']
        return self.value

    def get_absolute_url(self):
        # HACK(dcramer): quick and dirty way to support code/users
        if self.key == 'sentry:user':
            url_name = 'sentry-user-details'
        elif self.key == 'sentry:filename':
            url_name = 'sentry-explore-code-details'
        elif self.key == 'sentry:function':
            url_name = 'sentry-explore-code-details-by-function'
        else:
            url_name = 'sentry-explore-tag-value'
            return absolute_uri(
                reverse(url_name,
                        args=[
                            self.project.organization.slug, self.project.slug,
                            self.key, self.id
                        ]))

        return absolute_uri(
            reverse(url_name,
                    args=[
                        self.project.organization.slug, self.project.slug,
                        self.id
                    ]))
Пример #4
0
class TagValue(Model):
    """
    Stores references to available filters.
    """
    __core__ = False

    project_id = BoundedPositiveIntegerField(db_index=True, null=True)
    key = models.CharField(max_length=MAX_TAG_KEY_LENGTH)
    value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH)
    data = GzippedDictField(blank=True, null=True)
    times_seen = BoundedPositiveIntegerField(default=0)
    last_seen = models.DateTimeField(default=timezone.now,
                                     db_index=True,
                                     null=True)
    first_seen = models.DateTimeField(default=timezone.now,
                                      db_index=True,
                                      null=True)

    objects = BaseManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_filtervalue'
        unique_together = (('project_id', 'key', 'value'), )

    __repr__ = sane_repr('project_id', 'key', 'value')

    def get_label(self):
        # HACK(dcramer): quick and dirty way to hack in better display states
        if self.key == 'sentry:release':
            return Release.get_display_version(self.value)
        return self.value
Пример #5
0
class TagValue(Model):
    """
    Stores references to available filters.
    """
    __core__ = False

    project_id = BoundedPositiveIntegerField(db_index=True)
    environment_id = BoundedPositiveIntegerField(null=True)
    _key = FlexibleForeignKey('tagstore.TagKey', db_column='key')
    value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH)
    data = GzippedDictField(blank=True, null=True)
    times_seen = BoundedPositiveIntegerField(default=0)
    last_seen = models.DateTimeField(
        default=timezone.now, db_index=True, null=True)
    first_seen = models.DateTimeField(
        default=timezone.now, db_index=True, null=True)

    objects = BaseManager()

    class Meta:
        app_label = 'tagstore'
        unique_together = (('project_id', 'environment_id', '_key', 'value'), )
        # TODO: environment index(es)
        index_together = (('project_id', '_key', 'last_seen'), )

    __repr__ = sane_repr('project_id', 'environment_id', '_key', 'value')

    @property
    def key(self):
        return self._key.key

    def get_label(self):
        from sentry import tagstore

        return tagstore.get_tag_value_label(self.key, self.value)
Пример #6
0
class ProcessingIssue(Model):
    __include_in_export__ = False

    project = FlexibleForeignKey("sentry.Project", db_index=True)
    checksum = models.CharField(max_length=40, db_index=True)
    type = models.CharField(max_length=30)
    data = GzippedDictField()
    datetime = models.DateTimeField(default=timezone.now)

    objects = ProcessingIssueManager()

    class Meta:
        app_label = "sentry"
        db_table = "sentry_processingissue"
        unique_together = (("project", "checksum", "type"),)

    __repr__ = sane_repr("project_id")

    @property
    def scope(self):
        return self.data["_scope"]

    @property
    def object(self):
        return self.data["_object"]
Пример #7
0
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')
Пример #8
0
class ProcessingIssue(Model):
    __core__ = False

    project = FlexibleForeignKey('sentry.Project', db_index=True)
    checksum = models.CharField(max_length=40, db_index=True)
    type = models.CharField(max_length=30)
    data = GzippedDictField()
    datetime = models.DateTimeField(default=timezone.now)

    objects = ProcessingIssueManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_processingissue'
        unique_together = (('project', 'checksum', 'type'), )

    __repr__ = sane_repr('project_id')

    @property
    def scope(self):
        return self.data['_scope']

    @property
    def object(self):
        return self.data['_object']
Пример #9
0
class Rule(Model):
    __core__ = True

    DEFAULT_ACTION_MATCH = 'all'  # any, all
    DEFAULT_FREQUENCY = 30  # minutes

    project = FlexibleForeignKey('sentry.Project')
    environment_id = BoundedPositiveIntegerField(null=True)
    label = models.CharField(max_length=64)
    data = GzippedDictField()
    status = BoundedPositiveIntegerField(default=RuleStatus.ACTIVE,
                                         choices=(
                                             (RuleStatus.ACTIVE, 'Active'),
                                             (RuleStatus.INACTIVE, 'Inactive'),
                                         ),
                                         db_index=True)

    date_added = models.DateTimeField(default=timezone.now)

    objects = BaseManager(cache_fields=('pk', ))

    class Meta:
        db_table = 'sentry_rule'
        app_label = 'sentry'

    __repr__ = sane_repr('project_id', 'label')

    @classmethod
    def get_for_project(cls, project_id):
        cache_key = u'project:{}:rules'.format(project_id)
        rules_list = cache.get(cache_key)
        if rules_list is None:
            rules_list = list(
                cls.objects.filter(
                    project=project_id,
                    status=RuleStatus.ACTIVE,
                ))
            cache.set(cache_key, rules_list, 60)
        return rules_list

    def delete(self, *args, **kwargs):
        rv = super(Rule, self).delete(*args, **kwargs)
        cache_key = u'project:{}:rules'.format(self.project_id)
        cache.delete(cache_key)
        return rv

    def save(self, *args, **kwargs):
        rv = super(Rule, self).save(*args, **kwargs)
        cache_key = u'project:{}:rules'.format(self.project_id)
        cache.delete(cache_key)
        return rv

    def get_audit_log_data(self):
        return {
            'label': self.label,
            'data': self.data,
            'status': self.status,
        }
Пример #10
0
class AlertRelatedGroup(Model):
    group = models.ForeignKey(Group)
    alert = models.ForeignKey(Alert)
    data = GzippedDictField(null=True)

    class Meta:
        unique_together = (('group', 'alert'),)

    __repr__ = sane_repr('group_id', 'alert_id')
Пример #11
0
class Node(BaseModel):
    id = models.CharField(max_length=40, primary_key=True)
    data = GzippedDictField()
    timestamp = models.DateTimeField(default=timezone.now, db_index=True)

    __repr__ = sane_repr('timestamp')

    class Meta:
        app_label = 'nodestore'
Пример #12
0
class TagValue(Model):
    """
    Stores references to available filters.
    """
    __core__ = False

    project_id = BoundedBigIntegerField(db_index=True)
    _key = FlexibleForeignKey('tagstore.TagKey', db_column='key_id')
    value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH)
    data = GzippedDictField(blank=True, null=True)
    times_seen = BoundedPositiveIntegerField(default=0)
    last_seen = models.DateTimeField(
        default=timezone.now, db_index=True, null=True)
    first_seen = models.DateTimeField(
        default=timezone.now, db_index=True, null=True)

    objects = BaseManager()

    class Meta:
        app_label = 'tagstore'
        unique_together = (('project_id', '_key', 'value'), )
        index_together = (('project_id', '_key', 'last_seen'), )

    __repr__ = sane_repr('project_id', '_key', 'value')

    @property
    def key(self):
        if hasattr(self, '_set_key'):
            return self._set_key

        if hasattr(self, '__key_cache'):
            return self._key.key

        # fallback
        from sentry.tagstore.v2.models import TagKey

        tk = TagKey.objects.filter(
            project_id=self.project_id,
            id=self._key_id,
        ).values_list('key', flat=True).get()

        # cache for future calls
        self.key = tk

        return tk

    @key.setter
    def key(self, key):
        self._set_key = key

    def get_label(self):
        from sentry import tagstore

        return tagstore.get_tag_value_label(self.key, self.value)
Пример #13
0
class AlertRelatedGroup(Model):
    group = FlexibleForeignKey('sentry.Group')
    alert = FlexibleForeignKey(Alert)
    data = GzippedDictField(null=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_alertrelatedgroup'
        unique_together = (('group', 'alert'), )

    __repr__ = sane_repr('group_id', 'alert_id')
Пример #14
0
class Rule(Model):
    __core__ = True

    DEFAULT_ACTION_MATCH = "all"  # any, all
    DEFAULT_FREQUENCY = 30  # minutes

    project = FlexibleForeignKey("sentry.Project")
    environment_id = BoundedPositiveIntegerField(null=True)
    label = models.CharField(max_length=64)
    data = GzippedDictField()
    status = BoundedPositiveIntegerField(
        default=RuleStatus.ACTIVE,
        choices=((RuleStatus.ACTIVE, "Active"), (RuleStatus.INACTIVE,
                                                 "Inactive")),
        db_index=True,
    )

    date_added = models.DateTimeField(default=timezone.now)

    objects = BaseManager(cache_fields=("pk", ))

    class Meta:
        db_table = "sentry_rule"
        app_label = "sentry"

    __repr__ = sane_repr("project_id", "label")

    @classmethod
    def get_for_project(cls, project_id):
        cache_key = u"project:{}:rules".format(project_id)
        rules_list = cache.get(cache_key)
        if rules_list is None:
            rules_list = list(
                cls.objects.filter(project=project_id,
                                   status=RuleStatus.ACTIVE))
            cache.set(cache_key, rules_list, 60)
        return rules_list

    def delete(self, *args, **kwargs):
        rv = super(Rule, self).delete(*args, **kwargs)
        cache_key = u"project:{}:rules".format(self.project_id)
        cache.delete(cache_key)
        return rv

    def save(self, *args, **kwargs):
        rv = super(Rule, self).save(*args, **kwargs)
        cache_key = u"project:{}:rules".format(self.project_id)
        cache.delete(cache_key)
        return rv

    def get_audit_log_data(self):
        return {"label": self.label, "data": self.data, "status": self.status}
Пример #15
0
class Rule(Model):
    project = models.ForeignKey('sentry.Project')
    label = models.CharField(max_length=64)
    data = GzippedDictField()
    date_added = models.DateTimeField(default=timezone.now)

    objects = BaseManager(cache_fields=('pk', ))

    class Meta:
        db_table = 'sentry_rule'
        app_label = 'sentry'

    __repr__ = sane_repr('project_id', 'label')
Пример #16
0
class Node(BaseModel):
    __core__ = False

    id = models.CharField(max_length=40, primary_key=True)
    # TODO(dcramer): this being pickle and not JSON has the ability to cause
    # hard errors as it accepts other serialization than native JSON
    data = GzippedDictField()
    timestamp = models.DateTimeField(default=timezone.now, db_index=True)

    __repr__ = sane_repr("timestamp")

    class Meta:
        app_label = "nodestore"
Пример #17
0
class Activity(Model):
    SET_RESOLVED = 1
    SET_UNRESOLVED = 2
    SET_MUTED = 3
    SET_PUBLIC = 4
    SET_PRIVATE = 5
    SET_REGRESSION = 6
    CREATE_ISSUE = 7
    NOTE = 8

    TYPE = (
        # (TYPE, verb-slug)
        (SET_RESOLVED, 'set_resolved'),
        (SET_UNRESOLVED, 'set_unresolved'),
        (SET_MUTED, 'set_muted'),
        (SET_PUBLIC, 'set_public'),
        (SET_PRIVATE, 'set_private'),
        (SET_REGRESSION, 'set_regression'),
        (CREATE_ISSUE, 'create_issue'),
        (NOTE, 'note'),
    )

    project = models.ForeignKey(Project)
    group = models.ForeignKey(Group, null=True)
    event = models.ForeignKey(Event, null=True)
    # index on (type, ident)
    type = BoundedPositiveIntegerField(choices=TYPE)
    ident = models.CharField(max_length=64, null=True)
    # if the user is not set, it's assumed to be the system
    user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
    datetime = models.DateTimeField(default=timezone.now)
    data = GzippedDictField(null=True)

    __repr__ = sane_repr('project_id', 'group_id', 'event_id', 'user_id',
                         'type', 'ident')

    def save(self, *args, **kwargs):
        created = bool(not self.id)

        super(Activity, self).save(*args, **kwargs)

        if not created:
            return

        # HACK: support Group.num_comments
        if self.type == Activity.NOTE:
            self.group.update(num_comments=F('num_comments') + 1)

            if self.event:
                self.event.update(num_comments=F('num_comments') + 1)
Пример #18
0
class GroupTombstone(Model):
    __core__ = False

    previous_group_id = BoundedPositiveIntegerField(unique=True)
    project = FlexibleForeignKey('sentry.Project')
    level = BoundedPositiveIntegerField(
        choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True
    )
    message = models.TextField()
    culprit = models.CharField(
        max_length=MAX_CULPRIT_LENGTH,
        blank=True,
        null=True,
    )
    data = GzippedDictField(blank=True, null=True)
    actor_id = BoundedPositiveIntegerField(null=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_grouptombstone'

    def get_event_type(self):
        """
        Return the type of this issue.

        See ``sentry.eventtypes``.
        """
        return self.data.get('type', 'default')

    def get_event_metadata(self):
        """
        Return the metadata of this issue.

        See ``sentry.eventtypes``.
        """
        etype = self.data.get('type')
        if etype is None:
            etype = 'default'
        if 'metadata' not in self.data:
            data = self.data.copy() if self.data else {}
            data['message'] = self.message
            return eventtypes.get(etype)(data).get_metadata()
        return self.data['metadata']
Пример #19
0
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')
Пример #20
0
class TagValue(Model):
    """
    Stores references to available filters.
    """
    __core__ = False

    project_id = BoundedPositiveIntegerField(db_index=True)
    environment_id = BoundedPositiveIntegerField()
    key_id = BoundedPositiveIntegerField()
    value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH)
    # TODO: do we even use this anymore?
    data = GzippedDictField(blank=True, null=True)
    # times_seen will live in Redis
    last_seen = models.DateTimeField(default=timezone.now,
                                     db_index=True,
                                     null=True)
    first_seen = models.DateTimeField(default=timezone.now,
                                      db_index=True,
                                      null=True)

    objects = BaseManager()

    class Meta:
        app_label = 'tagstore'
        unique_together = (('project_id', 'environment_id', 'key_id',
                            'value'), )
        # TODO: environment index(es)
        index_together = (('project_id', 'key_id', 'last_seen'), )

    __repr__ = sane_repr('project_id', 'environment_id', 'key_id', 'value')

    # TODO: key property to fetch actual key string?

    def get_label(self):
        from sentry import tagstore

        return tagstore.get_tag_value_label(self.key, self.value)
Пример #21
0
class File(Model):
    name = models.CharField(max_length=256)
    storage = models.CharField(max_length=128, null=True)
    storage_options = GzippedDictField()
    path = models.TextField(null=True)
    type = models.CharField(max_length=64)
    size = BoundedPositiveIntegerField(null=True)
    timestamp = models.DateTimeField(default=timezone.now, db_index=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_file'

    def delete(self, *args, **kwargs):
        if self.path:
            self.deletefile(commit=False)
        super(File, self).delete(*args, **kwargs)

    def generate_unique_path(self):
        pieces = self.type.split('.')
        pieces.extend(
            map(str, divmod(int(self.timestamp.strftime('%s')), ONE_DAY)))
        pieces.append('%s-%s' % (uuid4().hex, self.name))
        return '/'.join(pieces)

    def get_storage(self):
        backend = self.storage
        options = self.storage_options

        storage = get_storage_class(backend)
        return storage(**options)

    def deletefile(self, commit=False):
        assert self.path

        storage = self.get_storage()
        storage.delete(self.path)

        self.path = None

        if commit:
            self.save()

    def putfile(self, fileobj, commit=True):
        """
        Upload this given File's contents.

        A file's content is idempotent and you may not re-save a given file.

        >>> my_file = File(name='app.dsym', type='release.artifact')
        >>> my_file.putfile(fileobj, commit=False)
        >>> my_file.save()
        """
        assert not self.path

        self.path = self.generate_unique_path()
        self.storage = settings.SENTRY_FILESTORE
        self.storage_options = settings.SENTRY_FILESTORE_OPTIONS

        storage = self.get_storage()
        storage.save(self.path, fileobj)

        if commit:
            self.save()

    def getfile(self):
        """
        Return a file-like object for this File's content.

        >>> fileobj = my_file.getfile()
        >>> with open('/tmp/localfile', 'wb') as fp:
        >>>     for chunk in fileobj.chunks():
        >>>         fp.write(chunk)
        """
        assert self.path

        storage = self.get_storage()
        return storage.open(self.path)
Пример #22
0
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
Пример #23
0
class AuditLogEntry(Model):
    __core__ = False

    organization = FlexibleForeignKey('sentry.Organization')
    actor_label = models.CharField(max_length=64, null=True, blank=True)
    # if the entry was created via a user
    actor = FlexibleForeignKey('sentry.User',
                               related_name='audit_actors',
                               null=True,
                               blank=True)
    # if the entry was created via an api key
    actor_key = FlexibleForeignKey('sentry.ApiKey', null=True, blank=True)
    target_object = BoundedPositiveIntegerField(null=True)
    target_user = FlexibleForeignKey('sentry.User',
                                     null=True,
                                     blank=True,
                                     related_name='audit_targets')
    # TODO(dcramer): we want to compile this mapping into JSX for the UI
    event = BoundedPositiveIntegerField(choices=(
        # We emulate github a bit with event naming
        (AuditLogEntryEvent.MEMBER_INVITE, 'member.invite'),
        (AuditLogEntryEvent.MEMBER_ADD, 'member.add'),
        (AuditLogEntryEvent.MEMBER_ACCEPT, 'member.accept-invite'),
        (AuditLogEntryEvent.MEMBER_REMOVE, 'member.remove'),
        (AuditLogEntryEvent.MEMBER_EDIT, 'member.edit'),
        (AuditLogEntryEvent.MEMBER_JOIN_TEAM, 'member.join-team'),
        (AuditLogEntryEvent.MEMBER_LEAVE_TEAM, 'member.leave-team'),
        (AuditLogEntryEvent.TEAM_ADD, 'team.create'),
        (AuditLogEntryEvent.TEAM_EDIT, 'team.edit'),
        (AuditLogEntryEvent.TEAM_REMOVE, 'team.remove'),
        (AuditLogEntryEvent.PROJECT_ADD, 'project.create'),
        (AuditLogEntryEvent.PROJECT_EDIT, 'project.edit'),
        (AuditLogEntryEvent.PROJECT_REMOVE, 'project.remove'),
        (AuditLogEntryEvent.PROJECT_SET_PUBLIC, 'project.set-public'),
        (AuditLogEntryEvent.PROJECT_SET_PRIVATE, 'project.set-private'),
        (AuditLogEntryEvent.PROJECT_REQUEST_TRANSFER,
         'project.request-transfer'),
        (AuditLogEntryEvent.PROJECT_ACCEPT_TRANSFER,
         'project.accept-transfer'),
        (AuditLogEntryEvent.ORG_ADD, 'org.create'),
        (AuditLogEntryEvent.ORG_EDIT, 'org.edit'),
        (AuditLogEntryEvent.ORG_REMOVE, 'org.remove'),
        (AuditLogEntryEvent.ORG_RESTORE, 'org.restore'),
        (AuditLogEntryEvent.TAGKEY_REMOVE, 'tagkey.remove'),
        (AuditLogEntryEvent.PROJECTKEY_ADD, 'projectkey.create'),
        (AuditLogEntryEvent.PROJECTKEY_EDIT, 'projectkey.edit'),
        (AuditLogEntryEvent.PROJECTKEY_REMOVE, 'projectkey.remove'),
        (AuditLogEntryEvent.PROJECTKEY_ENABLE, 'projectkey.enable'),
        (AuditLogEntryEvent.PROJECTKEY_DISABLE, 'projectkey.disable'),
        (AuditLogEntryEvent.SSO_ENABLE, 'sso.enable'),
        (AuditLogEntryEvent.SSO_DISABLE, 'sso.disable'),
        (AuditLogEntryEvent.SSO_EDIT, 'sso.edit'),
        (AuditLogEntryEvent.SSO_IDENTITY_LINK, 'sso-identity.link'),
        (AuditLogEntryEvent.APIKEY_ADD, 'api-key.create'),
        (AuditLogEntryEvent.APIKEY_EDIT, 'api-key.edit'),
        (AuditLogEntryEvent.APIKEY_REMOVE, 'api-key.remove'),
        (AuditLogEntryEvent.RULE_ADD, 'rule.create'),
        (AuditLogEntryEvent.RULE_EDIT, 'rule.edit'),
        (AuditLogEntryEvent.RULE_REMOVE, 'rule.remove'),
        (AuditLogEntryEvent.SERVICEHOOK_ADD, 'serivcehook.create'),
        (AuditLogEntryEvent.SERVICEHOOK_EDIT, 'serivcehook.edit'),
        (AuditLogEntryEvent.SERVICEHOOK_REMOVE, 'serivcehook.remove'),
        (AuditLogEntryEvent.SERVICEHOOK_ENABLE, 'serivcehook.enable'),
        (AuditLogEntryEvent.SERVICEHOOK_DISABLE, 'serivcehook.disable'),
        (AuditLogEntryEvent.INTEGRATION_ADD, 'integration.add'),
        (AuditLogEntryEvent.INTEGRATION_EDIT, 'integration.edit'),
        (AuditLogEntryEvent.INTEGRATION_REMOVE, 'integration.remove'),
        (AuditLogEntryEvent.SET_ONDEMAND, 'ondemand.edit'),
        (AuditLogEntryEvent.TRIAL_STARTED, 'trial.started'),
        (AuditLogEntryEvent.PLAN_CHANGED, 'plan.changed'),
    ))
    ip_address = models.GenericIPAddressField(null=True, unpack_ipv4=True)
    data = GzippedDictField()
    datetime = models.DateTimeField(default=timezone.now)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_auditlogentry'

    __repr__ = sane_repr('organization_id', 'type')

    def save(self, *args, **kwargs):
        if not self.actor_label:
            assert self.actor or self.actor_key
            if self.actor:
                self.actor_label = self.actor.username
            else:
                self.actor_label = self.actor_key.key
        super(AuditLogEntry, self).save(*args, **kwargs)

    def get_actor_name(self):
        if self.actor:
            return self.actor.get_display_name()
        elif self.actor_key:
            return self.actor_key.key + ' (api key)'
        return self.actor_label

    def get_note(self):
        if self.event == AuditLogEntryEvent.MEMBER_INVITE:
            return 'invited member %s' % (self.data['email'], )
        elif self.event == AuditLogEntryEvent.MEMBER_ADD:
            if self.target_user == self.actor:
                return 'joined the organization'
            return 'added member %s' % (self.target_user.get_display_name(), )
        elif self.event == AuditLogEntryEvent.MEMBER_ACCEPT:
            return 'accepted the membership invite'
        elif self.event == AuditLogEntryEvent.MEMBER_REMOVE:
            if self.target_user == self.actor:
                return 'left the organization'
            return 'removed member %s' % (
                self.data.get('email')
                or self.target_user.get_display_name(), )
        elif self.event == AuditLogEntryEvent.MEMBER_EDIT:
            return 'edited member %s (role: %s, teams: %s)' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data.get('role') or 'N/A',
                ', '.join(
                    six.text_type(x)
                    for x in self.data.get('team_slugs', [])) or 'N/A',
            )
        elif self.event == AuditLogEntryEvent.MEMBER_JOIN_TEAM:
            if self.target_user == self.actor:
                return 'joined team %s' % (self.data['team_slug'], )
            return 'added %s to team %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data['team_slug'],
            )
        elif self.event == AuditLogEntryEvent.MEMBER_LEAVE_TEAM:
            if self.target_user == self.actor:
                return 'left team %s' % (self.data['team_slug'], )
            return 'removed %s from team %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data['team_slug'],
            )

        elif self.event == AuditLogEntryEvent.ORG_ADD:
            return 'created the organization'
        elif self.event == AuditLogEntryEvent.ORG_EDIT:
            return 'edited the organization setting: ' + (', '.join(
                u'{} {}'.format(k, v) for k, v in self.data.items()))
        elif self.event == AuditLogEntryEvent.ORG_REMOVE:
            return 'removed the organization'
        elif self.event == AuditLogEntryEvent.ORG_RESTORE:
            return 'restored the organization'

        elif self.event == AuditLogEntryEvent.TEAM_ADD:
            return 'created team %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.TEAM_EDIT:
            return 'edited team %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.TEAM_REMOVE:
            return 'removed team %s' % (self.data['slug'], )

        elif self.event == AuditLogEntryEvent.PROJECT_ADD:
            return 'created project %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.PROJECT_EDIT:
            return 'edited project settings ' + (' '.join([
                ' in %s to %s' % (key, value)
                for (key, value) in six.iteritems(self.data)
            ]))
        elif self.event == AuditLogEntryEvent.PROJECT_REMOVE:
            return 'removed project %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.PROJECT_REQUEST_TRANSFER:
            return 'requested to transfer project %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.PROJECT_ACCEPT_TRANSFER:
            return 'accepted transfer of project %s' % (self.data['slug'], )

        elif self.event == AuditLogEntryEvent.TAGKEY_REMOVE:
            return 'removed tags matching %s = *' % (self.data['key'], )

        elif self.event == AuditLogEntryEvent.PROJECTKEY_ADD:
            return 'added project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_EDIT:
            return 'edited project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_REMOVE:
            return 'removed project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_ENABLE:
            return 'enabled project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_DISABLE:
            return 'disabled project key %s' % (self.data['public_key'], )

        elif self.event == AuditLogEntryEvent.SSO_ENABLE:
            return 'enabled sso (%s)' % (self.data['provider'], )
        elif self.event == AuditLogEntryEvent.SSO_DISABLE:
            return 'disabled sso (%s)' % (self.data['provider'], )
        elif self.event == AuditLogEntryEvent.SSO_EDIT:
            return 'edited sso settings'
        elif self.event == AuditLogEntryEvent.SSO_IDENTITY_LINK:
            return 'linked their account to a new identity'

        elif self.event == AuditLogEntryEvent.APIKEY_ADD:
            return 'added api key %s' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.APIKEY_EDIT:
            return 'edited api key %s' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.APIKEY_REMOVE:
            return 'removed api key %s' % (self.data['label'], )

        elif self.event == AuditLogEntryEvent.RULE_ADD:
            return 'added rule "%s"' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.RULE_EDIT:
            return 'edited rule "%s"' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.RULE_REMOVE:
            return 'removed rule "%s"' % (self.data['label'], )

        elif self.event == AuditLogEntryEvent.SET_ONDEMAND:
            if self.data['ondemand'] == -1:
                return 'changed on-demand spend to unlimited'
            return 'changed on-demand max spend to $%d' % (
                self.data['ondemand'] / 100, )
        elif self.event == AuditLogEntryEvent.TRIAL_STARTED:
            return 'started trial'
        elif self.event == AuditLogEntryEvent.PLAN_CHANGED:
            return 'changed plan to %s' % (self.data['plan_name'], )

        elif self.event == AuditLogEntryEvent.SERVICEHOOK_ADD:
            return 'added a service hook for "%s"' % (truncatechars(
                self.data['url'], 64), )
        elif self.event == AuditLogEntryEvent.SERVICEHOOK_EDIT:
            return 'edited the service hook for "%s"' % (truncatechars(
                self.data['url'], 64), )
        elif self.event == AuditLogEntryEvent.SERVICEHOOK_REMOVE:
            return 'removed the service hook for "%s"' % (truncatechars(
                self.data['url'], 64), )
        elif self.event == AuditLogEntryEvent.SERVICEHOOK_ENABLE:
            return 'enabled theservice hook for "%s"' % (truncatechars(
                self.data['url'], 64), )
        elif self.event == AuditLogEntryEvent.SERVICEHOOK_DISABLE:
            return 'disabled the service hook for "%s"' % (truncatechars(
                self.data['url'], 64), )

        elif self.event == AuditLogEntryEvent.INTEGRATION_ADD:
            return 'enabled integration %s for project %s' % (
                self.data['integration'], self.data['project'])
        elif self.event == AuditLogEntryEvent.INTEGRATION_EDIT:
            return 'edited integration %s for project %s' % (
                self.data['integration'], self.data['project'])
        elif self.event == AuditLogEntryEvent.INTEGRATION_REMOVE:
            return 'disabled integration %s from project %s' % (
                self.data['integration'], self.data['project'])

        return ''
Пример #24
0
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"))
Пример #25
0
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'))
Пример #26
0
class AuditLogEntry(Model):
    __core__ = False

    organization = FlexibleForeignKey('sentry.Organization')
    actor_label = models.CharField(max_length=64, null=True, blank=True)
    # if the entry was created via a user
    actor = FlexibleForeignKey('sentry.User', related_name='audit_actors', null=True, blank=True)
    # if the entry was created via an api key
    actor_key = FlexibleForeignKey('sentry.ApiKey', null=True, blank=True)
    target_object = BoundedPositiveIntegerField(null=True)
    target_user = FlexibleForeignKey(
        'sentry.User', null=True, blank=True, related_name='audit_targets'
    )
    # TODO(dcramer): we want to compile this mapping into JSX for the UI
    event = BoundedPositiveIntegerField(
        choices=(
            # We emulate github a bit with event naming
            (AuditLogEntryEvent.MEMBER_INVITE, 'member.invite'),
            (AuditLogEntryEvent.MEMBER_ADD, 'member.add'),
            (AuditLogEntryEvent.MEMBER_ACCEPT, 'member.accept-invite'),
            (AuditLogEntryEvent.MEMBER_REMOVE, 'member.remove'),
            (AuditLogEntryEvent.MEMBER_EDIT, 'member.edit'),
            (AuditLogEntryEvent.MEMBER_JOIN_TEAM, 'member.join-team'),
            (AuditLogEntryEvent.MEMBER_LEAVE_TEAM, 'member.leave-team'),
            (AuditLogEntryEvent.TEAM_ADD, 'team.create'),
            (AuditLogEntryEvent.TEAM_EDIT, 'team.edit'),
            (AuditLogEntryEvent.TEAM_REMOVE, 'team.remove'),
            (AuditLogEntryEvent.PROJECT_ADD, 'project.create'),
            (AuditLogEntryEvent.PROJECT_EDIT, 'project.edit'),
            (AuditLogEntryEvent.PROJECT_REMOVE, 'project.remove'),
            (AuditLogEntryEvent.PROJECT_SET_PUBLIC, 'project.set-public'),
            (AuditLogEntryEvent.PROJECT_SET_PRIVATE, 'project.set-private'),
            (AuditLogEntryEvent.ORG_ADD, 'org.create'),
            (AuditLogEntryEvent.ORG_EDIT, 'org.edit'),
            (AuditLogEntryEvent.ORG_REMOVE, 'org.remove'),
            (AuditLogEntryEvent.ORG_RESTORE, 'org.restore'),
            (AuditLogEntryEvent.TAGKEY_REMOVE, 'tagkey.remove'),
            (AuditLogEntryEvent.PROJECTKEY_ADD, 'projectkey.create'),
            (AuditLogEntryEvent.PROJECTKEY_EDIT, 'projectkey.edit'),
            (AuditLogEntryEvent.PROJECTKEY_REMOVE, 'projectkey.remove'),
            (AuditLogEntryEvent.PROJECTKEY_ENABLE, 'projectkey.enable'),
            (AuditLogEntryEvent.PROJECTKEY_DISABLE, 'projectkey.disable'),
            (AuditLogEntryEvent.SSO_ENABLE, 'sso.enable'),
            (AuditLogEntryEvent.SSO_DISABLE, 'sso.disable'),
            (AuditLogEntryEvent.SSO_EDIT, 'sso.edit'),
            (AuditLogEntryEvent.SSO_IDENTITY_LINK, 'sso-identity.link'),
            (AuditLogEntryEvent.APIKEY_ADD, 'api-key.create'),
            (AuditLogEntryEvent.APIKEY_EDIT, 'api-key.edit'),
            (AuditLogEntryEvent.APIKEY_REMOVE, 'api-key.remove'),
            (AuditLogEntryEvent.RULE_ADD, 'rule.create'),
            (AuditLogEntryEvent.RULE_EDIT, 'rule.edit'),
            (AuditLogEntryEvent.RULE_REMOVE, 'rule.remove'),
        )
    )
    ip_address = models.GenericIPAddressField(null=True, unpack_ipv4=True)
    data = GzippedDictField()
    datetime = models.DateTimeField(default=timezone.now)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_auditlogentry'

    __repr__ = sane_repr('organization_id', 'type')

    def save(self, *args, **kwargs):
        if not self.actor_label:
            assert self.actor or self.actor_key
            if self.actor:
                self.actor_label = self.actor.username
            else:
                self.actor_label = self.actor_key.key
        super(AuditLogEntry, self).save(*args, **kwargs)

    def get_actor_name(self):
        if self.actor:
            return self.actor.get_display_name()
        elif self.actor_key:
            return self.actor_key.key + ' (api key)'
        return self.actor_label

    def get_note(self):
        if self.event == AuditLogEntryEvent.MEMBER_INVITE:
            return 'invited member %s' % (self.data['email'], )
        elif self.event == AuditLogEntryEvent.MEMBER_ADD:
            if self.target_user == self.actor:
                return 'joined the organization'
            return 'added member %s' % (self.target_user.get_display_name(), )
        elif self.event == AuditLogEntryEvent.MEMBER_ACCEPT:
            return 'accepted the membership invite'
        elif self.event == AuditLogEntryEvent.MEMBER_REMOVE:
            if self.target_user == self.actor:
                return 'left the organization'
            return 'removed member %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
            )
        elif self.event == AuditLogEntryEvent.MEMBER_EDIT:
            return 'edited member %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
            )
        elif self.event == AuditLogEntryEvent.MEMBER_JOIN_TEAM:
            if self.target_user == self.actor:
                return 'joined team %s' % (self.data['team_slug'], )
            return 'added %s to team %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data['team_slug'],
            )
        elif self.event == AuditLogEntryEvent.MEMBER_LEAVE_TEAM:
            if self.target_user == self.actor:
                return 'left team %s' % (self.data['team_slug'], )
            return 'removed %s from team %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data['team_slug'],
            )

        elif self.event == AuditLogEntryEvent.ORG_ADD:
            return 'created the organization'
        elif self.event == AuditLogEntryEvent.ORG_EDIT:
            return 'edited the organization'
        elif self.event == AuditLogEntryEvent.ORG_REMOVE:
            return 'removed the organization'
        elif self.event == AuditLogEntryEvent.ORG_RESTORE:
            return 'restored the organization'

        elif self.event == AuditLogEntryEvent.TEAM_ADD:
            return 'created team %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.TEAM_EDIT:
            return 'edited team %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.TEAM_REMOVE:
            return 'removed team %s' % (self.data['slug'], )

        elif self.event == AuditLogEntryEvent.PROJECT_ADD:
            return 'created project %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.PROJECT_EDIT:
            return 'edited project %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.PROJECT_REMOVE:
            return 'removed project %s' % (self.data['slug'], )

        elif self.event == AuditLogEntryEvent.TAGKEY_REMOVE:
            return 'removed tags matching %s = *' % (self.data['key'], )

        elif self.event == AuditLogEntryEvent.PROJECTKEY_ADD:
            return 'added project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_EDIT:
            return 'edited project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_REMOVE:
            return 'removed project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_ENABLE:
            return 'enabled project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_DISABLE:
            return 'disabled project key %s' % (self.data['public_key'], )

        elif self.event == AuditLogEntryEvent.SSO_ENABLE:
            return 'enabled sso (%s)' % (self.data['provider'], )
        elif self.event == AuditLogEntryEvent.SSO_DISABLE:
            return 'disabled sso (%s)' % (self.data['provider'], )
        elif self.event == AuditLogEntryEvent.SSO_EDIT:
            return 'edited sso settings'
        elif self.event == AuditLogEntryEvent.SSO_IDENTITY_LINK:
            return 'linked their account to a new identity'

        elif self.event == AuditLogEntryEvent.APIKEY_ADD:
            return 'added api key %s' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.APIKEY_EDIT:
            return 'edited api key %s' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.APIKEY_REMOVE:
            return 'removed api key %s' % (self.data['label'], )

        elif self.event == AuditLogEntryEvent.RULE_ADD:
            return 'added rule "%s"' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.RULE_EDIT:
            return 'edited rule "%s"' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.RULE_REMOVE:
            return 'removed rule "%s"' % (self.data['label'], )

        return ''
Пример #27
0
class Group(Model):
    """
    Aggregated message which summarizes a set of Events.
    """
    project = models.ForeignKey('sentry.Project', null=True)
    logger = models.CharField(max_length=64,
                              blank=True,
                              default='root',
                              db_index=True)
    level = BoundedPositiveIntegerField(choices=LOG_LEVELS.items(),
                                        default=logging.ERROR,
                                        blank=True,
                                        db_index=True)
    message = models.TextField()
    culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH,
                               blank=True,
                               null=True,
                               db_column='view')
    checksum = models.CharField(max_length=32, db_index=True)
    num_comments = BoundedPositiveIntegerField(default=0, null=True)
    platform = models.CharField(max_length=64, null=True)
    status = BoundedPositiveIntegerField(default=0,
                                         choices=STATUS_LEVELS,
                                         db_index=True)
    times_seen = BoundedPositiveIntegerField(default=1, db_index=True)
    last_seen = models.DateTimeField(default=timezone.now, db_index=True)
    first_seen = models.DateTimeField(default=timezone.now, db_index=True)
    resolved_at = models.DateTimeField(null=True, db_index=True)
    # active_at should be the same as first_seen by default
    active_at = models.DateTimeField(null=True, db_index=True)
    time_spent_total = BoundedIntegerField(default=0)
    time_spent_count = BoundedIntegerField(default=0)
    score = BoundedIntegerField(default=0)
    is_public = models.NullBooleanField(default=False, null=True)
    data = GzippedDictField(blank=True, null=True)

    objects = GroupManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_groupedmessage'
        unique_together = (('project', 'checksum'), )
        verbose_name_plural = _('grouped messages')
        verbose_name = _('grouped message')
        permissions = (("can_view", "Can view"), )

    __repr__ = sane_repr('project_id', 'checksum')

    def __unicode__(self):
        return "(%s) %s" % (self.times_seen, self.error())

    def save(self, *args, **kwargs):
        if not self.last_seen:
            self.last_seen = timezone.now()
        if not self.first_seen:
            self.first_seen = self.last_seen
        if not self.active_at:
            self.active_at = self.first_seen
        if self.message:
            # We limit what we store for the message body
            self.message = self.message.splitlines()[0][:255]
        super(Group, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return absolute_uri(
            reverse('sentry-group',
                    args=[self.organization.slug, self.project.slug, self.id]))

    @property
    def avg_time_spent(self):
        if not self.time_spent_count:
            return
        return float(self.time_spent_total) / self.time_spent_count

    def natural_key(self):
        return (self.project, self.checksum)

    def is_over_resolve_age(self):
        resolve_age = self.project.get_option('sentry:resolve_age', None)
        if not resolve_age:
            return False
        return self.last_seen < timezone.now() - timedelta(
            hours=int(resolve_age))

    def is_muted(self):
        return self.get_status() == STATUS_MUTED

    def is_resolved(self):
        return self.get_status() == STATUS_RESOLVED

    def get_status(self):
        if self.status == STATUS_UNRESOLVED and self.is_over_resolve_age():
            return STATUS_RESOLVED
        return self.status

    def get_score(self):
        return int(
            math.log(self.times_seen) * 600 +
            float(time.mktime(self.last_seen.timetuple())))

    def get_latest_event(self):
        from sentry.models import Event

        if not hasattr(self, '_latest_event'):
            try:
                self._latest_event = Event.objects.filter(
                    group=self, ).order_by('-datetime')[0]
            except IndexError:
                self._latest_event = None
        return self._latest_event

    def get_unique_tags(self, tag, since=None, order_by='-times_seen'):
        # TODO(dcramer): this has zero test coverage and is a critical path
        from sentry.models import GroupTagValue

        queryset = GroupTagValue.objects.filter(
            group=self,
            key=tag,
        )
        if since:
            queryset = queryset.filter(last_seen__gte=since)
        return queryset.values_list(
            'value',
            'times_seen',
            'first_seen',
            'last_seen',
        ).order_by(order_by)

    def get_tags(self, with_internal=True):
        from sentry.models import GroupTagKey

        if not hasattr(self, '_tag_cache'):
            self._tag_cache = sorted([
                t for t in GroupTagKey.objects.filter(
                    group=self,
                    project=self.project,
                ).values_list('key', flat=True)
                if with_internal or not t.startswith('sentry:')
            ])
        return self._tag_cache

    def error(self):
        return self.message

    error.short_description = _('error')

    def has_two_part_message(self):
        message = strip(self.message)
        return '\n' in message or len(message) > 100

    @property
    def title(self):
        culprit = strip(self.culprit)
        if culprit:
            return culprit
        return self.message

    @property
    def message_short(self):
        message = strip(self.message)
        if not message:
            message = '<unlabeled message>'
        else:
            message = truncatechars(message.splitlines()[0], 100)
        return message

    @property
    def organization(self):
        return self.project.organization

    @property
    def team(self):
        return self.project.team

    def get_email_subject(self):
        return '[%s %s] %s: %s' % (
            self.team.name.encode('utf-8'), self.project.name.encode('utf-8'),
            six.text_type(self.get_level_display()).upper().encode('utf-8'),
            self.message_short.encode('utf-8'))
class Group(Model):
    """
    Aggregated message which summarizes a set of Events.
    """
    __core__ = False

    project = FlexibleForeignKey('sentry.Project', null=True)
    logger = models.CharField(max_length=64,
                              blank=True,
                              default=DEFAULT_LOGGER_NAME,
                              db_index=True)
    level = BoundedPositiveIntegerField(choices=LOG_LEVELS.items(),
                                        default=logging.ERROR,
                                        blank=True,
                                        db_index=True)
    message = models.TextField()
    culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH,
                               blank=True,
                               null=True,
                               db_column='view')
    num_comments = BoundedPositiveIntegerField(default=0, null=True)
    platform = models.CharField(max_length=64, null=True)
    status = BoundedPositiveIntegerField(default=0,
                                         choices=(
                                             (GroupStatus.UNRESOLVED,
                                              _('Unresolved')),
                                             (GroupStatus.RESOLVED,
                                              _('Resolved')),
                                             (GroupStatus.IGNORED,
                                              _('Ignored')),
                                         ),
                                         db_index=True)
    times_seen = BoundedPositiveIntegerField(default=1, db_index=True)
    last_seen = models.DateTimeField(default=timezone.now, db_index=True)
    first_seen = models.DateTimeField(default=timezone.now, db_index=True)
    first_release = FlexibleForeignKey('sentry.Release',
                                       null=True,
                                       on_delete=models.PROTECT)
    resolved_at = models.DateTimeField(null=True, db_index=True)
    # active_at should be the same as first_seen by default
    active_at = models.DateTimeField(null=True, db_index=True)
    time_spent_total = BoundedIntegerField(default=0)
    time_spent_count = BoundedIntegerField(default=0)
    score = BoundedIntegerField(default=0)
    # deprecated, do not use. GroupShare has superseded
    is_public = models.NullBooleanField(default=False, null=True)
    data = GzippedDictField(blank=True, null=True)
    short_id = BoundedBigIntegerField(null=True)

    objects = GroupManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_groupedmessage'
        verbose_name_plural = _('grouped messages')
        verbose_name = _('grouped message')
        permissions = (("can_view", "Can view"), )
        index_together = (('project', 'first_release'), )
        unique_together = (('project', 'short_id'), )

    __repr__ = sane_repr('project_id')

    def __unicode__(self):
        return "(%s) %s" % (self.times_seen, self.error())

    def save(self, *args, **kwargs):
        if not self.last_seen:
            self.last_seen = timezone.now()
        if not self.first_seen:
            self.first_seen = self.last_seen
        if not self.active_at:
            self.active_at = self.first_seen
        # We limit what we store for the message body
        self.message = strip(self.message)
        if self.message:
            self.message = truncatechars(self.message.splitlines()[0], 255)
        super(Group, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return absolute_uri(
            reverse('sentry-group',
                    args=[self.organization.slug, self.project.slug, self.id]))

    @property
    def qualified_short_id(self):
        if self.short_id is not None:
            return '%s-%s' % (
                self.project.slug.upper(),
                base32_encode(self.short_id),
            )

    @property
    def event_set(self):
        from sentry.models import Event
        return Event.objects.filter(group_id=self.id)

    def is_over_resolve_age(self):
        resolve_age = self.project.get_option('sentry:resolve_age', None)
        if not resolve_age:
            return False
        return self.last_seen < timezone.now() - timedelta(
            hours=int(resolve_age))

    def is_ignored(self):
        return self.get_status() == GroupStatus.IGNORED

    # TODO(dcramer): remove in 9.0 / after plugins no long ref
    is_muted = is_ignored

    def is_resolved(self):
        return self.get_status() == GroupStatus.RESOLVED

    def get_status(self):
        # XXX(dcramer): GroupSerializer reimplements this logic
        from sentry.models import GroupSnooze

        status = self.status

        if status == GroupStatus.IGNORED:
            try:
                snooze = GroupSnooze.objects.get(group=self)
            except GroupSnooze.DoesNotExist:
                pass
            else:
                if not snooze.is_valid(group=self):
                    status = GroupStatus.UNRESOLVED

        if status == GroupStatus.UNRESOLVED and self.is_over_resolve_age():
            return GroupStatus.RESOLVED
        return status

    def get_share_id(self):
        from sentry.models import GroupShare
        try:
            return GroupShare.objects.filter(group_id=self.id, ).values_list(
                'uuid', flat=True)[0]
        except IndexError:
            # Otherwise it has not been shared yet.
            return None

    @classmethod
    def from_share_id(cls, share_id):
        if not share_id or len(share_id) != 32:
            raise cls.DoesNotExist

        from sentry.models import GroupShare
        return cls.objects.get(id=GroupShare.objects.filter(
            uuid=share_id, ).values_list('group_id'), )

    def get_score(self):
        return int(
            math.log(self.times_seen) * 600 +
            float(time.mktime(self.last_seen.timetuple())))

    def get_latest_event(self):
        from sentry.models import Event

        if not hasattr(self, '_latest_event'):
            latest_events = sorted(
                Event.objects.filter(
                    group_id=self.id, ).order_by('-datetime')[0:5],
                key=EVENT_ORDERING_KEY,
                reverse=True,
            )
            try:
                self._latest_event = latest_events[0]
            except IndexError:
                self._latest_event = None
        return self._latest_event

    def get_oldest_event(self):
        from sentry.models import Event

        if not hasattr(self, '_oldest_event'):
            oldest_events = sorted(
                Event.objects.filter(
                    group_id=self.id, ).order_by('datetime')[0:5],
                key=EVENT_ORDERING_KEY,
            )
            try:
                self._oldest_event = oldest_events[0]
            except IndexError:
                self._oldest_event = None
        return self._oldest_event

    def get_first_release(self):
        if self.first_release_id is None:
            return tagstore.get_first_release(self.project_id, self.id)

        return self.first_release.version

    def get_last_release(self):
        return tagstore.get_last_release(self.project_id, self.id)

    def get_event_type(self):
        """
        Return the type of this issue.

        See ``sentry.eventtypes``.
        """
        return self.data.get('type', 'default')

    def get_event_metadata(self):
        """
        Return the metadata of this issue.

        See ``sentry.eventtypes``.
        """
        etype = self.data.get('type')
        if etype is None:
            etype = 'default'
        if 'metadata' not in self.data:
            data = self.data.copy() if self.data else {}
            data['message'] = self.message
            return eventtypes.get(etype)(data).get_metadata()
        return self.data['metadata']

    @property
    def title(self):
        et = eventtypes.get(self.get_event_type())(self.data)
        return et.to_string(self.get_event_metadata())

    def error(self):
        warnings.warn('Group.error is deprecated, use Group.title',
                      DeprecationWarning)
        return self.title

    error.short_description = _('error')

    @property
    def message_short(self):
        warnings.warn('Group.message_short is deprecated, use Group.title',
                      DeprecationWarning)
        return self.title

    @property
    def organization(self):
        return self.project.organization

    @property
    def checksum(self):
        warnings.warn('Group.checksum is no longer used', DeprecationWarning)
        return ''

    def get_email_subject(self):
        return '%s - %s' % (self.qualified_short_id.encode('utf-8'),
                            self.title.encode('utf-8'))

    def count_users_seen(self):
        return tagstore.get_groups_user_counts(self.project_id, [self.id],
                                               environment_id=None)[self.id]
Пример #29
0
class Activity(Model):
    __include_in_export__ = False

    # TODO(mgaeta): Replace all usages with ActivityTypes.
    ASSIGNED = ActivityType.ASSIGNED.value
    CREATE_ISSUE = ActivityType.CREATE_ISSUE.value
    DEPLOY = ActivityType.DEPLOY.value
    FIRST_SEEN = ActivityType.FIRST_SEEN.value
    MARK_REVIEWED = ActivityType.MARK_REVIEWED.value
    MERGE = ActivityType.MERGE.value
    NEW_PROCESSING_ISSUES = ActivityType.NEW_PROCESSING_ISSUES.value
    NOTE = ActivityType.NOTE.value
    RELEASE = ActivityType.RELEASE.value
    REPROCESS = ActivityType.REPROCESS.value
    SET_IGNORED = ActivityType.SET_IGNORED.value
    SET_PRIVATE = ActivityType.SET_PRIVATE.value
    SET_PUBLIC = ActivityType.SET_PUBLIC.value
    SET_REGRESSION = ActivityType.SET_REGRESSION.value
    SET_RESOLVED = ActivityType.SET_RESOLVED.value
    SET_RESOLVED_BY_AGE = ActivityType.SET_RESOLVED_BY_AGE.value
    SET_RESOLVED_IN_COMMIT = ActivityType.SET_RESOLVED_IN_COMMIT.value
    SET_RESOLVED_IN_PULL_REQUEST = ActivityType.SET_RESOLVED_IN_PULL_REQUEST.value
    SET_RESOLVED_IN_RELEASE = ActivityType.SET_RESOLVED_IN_RELEASE.value
    SET_UNRESOLVED = ActivityType.SET_UNRESOLVED.value
    UNASSIGNED = ActivityType.UNASSIGNED.value
    UNMERGE_DESTINATION = ActivityType.UNMERGE_DESTINATION.value
    UNMERGE_SOURCE = ActivityType.UNMERGE_SOURCE.value

    project = FlexibleForeignKey("sentry.Project")
    group = FlexibleForeignKey("sentry.Group", null=True)
    # index on (type, ident)
    type = BoundedPositiveIntegerField(choices=CHOICES)
    ident = models.CharField(max_length=64, null=True)
    # if the user is not set, it's assumed to be the system
    user = FlexibleForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
    datetime = models.DateTimeField(default=timezone.now)
    data = GzippedDictField(null=True)

    class Meta:
        app_label = "sentry"
        db_table = "sentry_activity"
        index_together = (("project", "datetime"),)

    __repr__ = sane_repr("project_id", "group_id", "event_id", "user_id", "type", "ident")

    @staticmethod
    def get_version_ident(version):
        return (version or "")[:64]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        from sentry.models import Release

        # XXX(dcramer): fix for bad data
        if self.type in (self.RELEASE, self.DEPLOY) and isinstance(self.data["version"], Release):
            self.data["version"] = self.data["version"].version
        if self.type == self.ASSIGNED:
            self.data["assignee"] = str(self.data["assignee"])

    def save(self, *args, **kwargs):
        created = bool(not self.id)

        super().save(*args, **kwargs)

        if not created:
            return

        # HACK: support Group.num_comments
        if self.type == Activity.NOTE:
            self.group.update(num_comments=F("num_comments") + 1)

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)

        # HACK: support Group.num_comments
        if self.type == Activity.NOTE:
            self.group.update(num_comments=F("num_comments") - 1)

    def send_notification(self):
        activity.send_activity_notifications.delay(self.id)
Пример #30
0
class Activity(Model):
    __core__ = False

    SET_RESOLVED = 1
    SET_UNRESOLVED = 2
    SET_MUTED = 3
    SET_PUBLIC = 4
    SET_PRIVATE = 5
    SET_REGRESSION = 6
    CREATE_ISSUE = 7
    NOTE = 8
    FIRST_SEEN = 9
    RELEASE = 10
    ASSIGNED = 11
    UNASSIGNED = 12
    SET_RESOLVED_IN_RELEASE = 13

    TYPE = (
        # (TYPE, verb-slug)
        (SET_RESOLVED, 'set_resolved'),
        (SET_RESOLVED_IN_RELEASE, 'set_resolved_in_release'),
        (SET_UNRESOLVED, 'set_unresolved'),
        (SET_MUTED, 'set_muted'),
        (SET_PUBLIC, 'set_public'),
        (SET_PRIVATE, 'set_private'),
        (SET_REGRESSION, 'set_regression'),
        (CREATE_ISSUE, 'create_issue'),
        (NOTE, 'note'),
        (FIRST_SEEN, 'first_seen'),
        (RELEASE, 'release'),
        (ASSIGNED, 'assigned'),
        (UNASSIGNED, 'unassigned'),
    )

    project = FlexibleForeignKey('sentry.Project')
    group = FlexibleForeignKey('sentry.Group', null=True)
    event = FlexibleForeignKey('sentry.Event', null=True)
    # index on (type, ident)
    type = BoundedPositiveIntegerField(choices=TYPE)
    ident = models.CharField(max_length=64, null=True)
    # if the user is not set, it's assumed to be the system
    user = FlexibleForeignKey(settings.AUTH_USER_MODEL, null=True)
    datetime = models.DateTimeField(default=timezone.now)
    data = GzippedDictField(null=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_activity'

    __repr__ = sane_repr('project_id', 'group_id', 'event_id', 'user_id',
                         'type', 'ident')

    def save(self, *args, **kwargs):
        created = bool(not self.id)

        super(Activity, self).save(*args, **kwargs)

        if not created:
            return

        # HACK: support Group.num_comments
        if self.type == Activity.NOTE:
            self.group.update(num_comments=F('num_comments') + 1)

            if self.event:
                self.event.update(num_comments=F('num_comments') + 1)

    def delete(self, *args, **kwargs):
        super(Activity, self).delete(*args, **kwargs)

        # HACK: support Group.num_comments
        if self.type == Activity.NOTE:
            self.group.update(num_comments=F('num_comments') - 1)

            if self.event:
                self.event.update(num_comments=F('num_comments') - 1)

    def send_notification(self):
        activity.send_activity_notifications.delay(self.id)