Browse Source

Revert "feat: Common functionality for Snuba Events (#11071)" (#12421)

This reverts commit 39db67e1f98667bbb3092812db8eec409eb886f4.
James Cunningham 6 years ago
parent
commit
a6037adcd6

+ 3 - 2
src/sentry/api/endpoints/group_events.py

@@ -14,10 +14,11 @@ from sentry.api.base import DocSection, EnvironmentMixin
 from sentry.api.bases import GroupEndpoint
 from sentry.api.exceptions import ResourceDoesNotExist
 from sentry.api.helpers.environments import get_environments
+from sentry.api.serializers.models.event import SnubaEvent
 from sentry.api.serializers import serialize
 from sentry.api.paginator import DateTimePaginator, GenericOffsetPaginator
 from sentry.api.utils import get_date_range_from_params
-from sentry.models import Event, Group, SnubaEvent
+from sentry.models import Event, Group
 from sentry.search.utils import (
     InvalidQuery,
     parse_query,
@@ -101,7 +102,7 @@ class GroupEventsEndpoint(GroupEndpoint, EnvironmentMixin):
                 'project_id': [group.project_id],
                 'issue': [group.id]
             },
-            selected_columns=SnubaEvent.selected_columns,
+            selected_columns=SnubaEvent.selected_columns + ['tags.key', 'tags.value'],
             orderby='-timestamp',
             referrer='api.group-events',
         )

+ 3 - 4
src/sentry/api/endpoints/organization_events.py

@@ -8,9 +8,9 @@ from rest_framework.response import Response
 
 from sentry.api.bases import OrganizationEventsEndpointBase, OrganizationEventsError, NoProjects
 from sentry.api.paginator import GenericOffsetPaginator
-from sentry.api.serializers import serialize, SimpleEventSerializer
+from sentry.api.serializers import serialize
+from sentry.api.serializers.models.event import SnubaEvent
 from sentry.api.serializers.snuba import SnubaTSResultSerializer
-from sentry.models import SnubaEvent
 from sentry.utils.dates import parse_stats_period
 from sentry.utils.snuba import raw_query
 from sentry.utils.validators import is_event_id
@@ -63,11 +63,10 @@ class OrganizationEventsEndpoint(OrganizationEventsEndpointBase):
                 **snuba_args
             )
 
-        serializer = SimpleEventSerializer()
         return self.paginate(
             request=request,
             on_results=lambda results: serialize(
-                [SnubaEvent(row) for row in results], request.user, serializer),
+                [SnubaEvent(row) for row in results], request.user),
             paginator=GenericOffsetPaginator(data_fn=data_fn)
         )
 

+ 1 - 1
src/sentry/api/endpoints/project_events.py

@@ -54,7 +54,7 @@ class ProjectEventsEndpoint(ProjectEndpoint):
 
     def _get_events_snuba(self, request, project):
         from sentry.api.paginator import GenericOffsetPaginator
-        from sentry.models import SnubaEvent
+        from sentry.api.serializers.models.event import SnubaEvent
         from sentry.utils.snuba import raw_query
 
         query = request.GET.get('query')

+ 65 - 57
src/sentry/api/serializers/models/event.py

@@ -7,14 +7,7 @@ from django.utils import timezone
 from semaphore import meta_with_chunks
 
 from sentry.api.serializers import Serializer, register, serialize
-from sentry.models import (
-    Event,
-    EventError,
-    EventAttachment,
-    Release,
-    UserReport,
-    SnubaEvent
-)
+from sentry.models import Event, EventError, EventAttachment, Release, UserReport
 from sentry.search.utils import convert_user_tag_to_query
 from sentry.utils.safe import get_path
 
@@ -35,7 +28,6 @@ def get_crash_files(events):
     return rv
 
 
-@register(SnubaEvent)
 @register(Event)
 class EventSerializer(Serializer):
     _reserved_keys = frozenset(
@@ -78,7 +70,7 @@ class EventSerializer(Serializer):
         )
 
     def _get_interface_with_meta(self, event, name, is_public=False):
-        interface = event.get_interface(name)
+        interface = event.interfaces.get(name)
         if not interface:
             return (None, None)
 
@@ -104,22 +96,11 @@ class EventSerializer(Serializer):
                     'value': kv[1],
                     '_meta': meta.get(kv[0]) or get_path(meta, six.text_type(i), '1') or None,
                 }
-                # TODO this should be using event.tags but there are some weird
-                # issues around that because event.tags re-sorts the tags and
-                # this function relies on them being in the original order to
-                # look up meta.
                 for i, kv in enumerate(event.data.get('tags') or ())
                 if kv is not None and kv[0] is not None and kv[1] is not None],
             key=lambda x: x['key']
         )
 
-        # Add 'query' for each tag to tell the UI what to use as query
-        # params for this tag.
-        for tag in tags:
-            query = convert_user_tag_to_query(tag['key'], tag['value'])
-            if query:
-                tag['query'] = query
-
         tags_meta = {
             six.text_type(i): {'value': e.pop('_meta')}
             for i, e in enumerate(tags) if e.get('_meta')
@@ -240,7 +221,6 @@ class EventSerializer(Serializer):
             'id': six.text_type(obj.id),
             'groupID': six.text_type(obj.group_id),
             'eventID': six.text_type(obj.event_id),
-            'projectID': six.text_type(obj.project_id),
             'size': obj.size,
             'entries': attrs['entries'],
             'dist': obj.dist,
@@ -307,50 +287,78 @@ class SharedEventSerializer(EventSerializer):
         return result
 
 
-class SimpleEventSerializer(EventSerializer):
+class SnubaEvent(object):
     """
-    Simple event serializer that renders a basic outline of an event without
-    most interfaces/breadcrumbs. This can be used for basic event list queries
-    where we don't need the full detail. The side effect is that, if the
-    serialized events are actually SnubaEvents, we can render them without
-    needing to fetch the event bodies from nodestore.
-
-    NB it would be super easy to inadvertently add a property accessor here
-    that would require a nodestore lookup for a SnubaEvent serialized using
-    this serializer. You will only really notice you've done this when the
-    organization event search API gets real slow.
+        A simple wrapper class on a row (dict) returned from snuba representing
+        an event. Provides a class name to register a serializer against, and
+        Makes keys accessible as attributes.
     """
 
-    def get_attrs(self, item_list, user):
-        return {}
+    # The list of columns that we should request from snuba to be able to fill
+    # out a proper event object.
+    selected_columns = [
+        'event_id',
+        'project_id',
+        'message',
+        'title',
+        'location',
+        'culprit',
+        'user_id',
+        'username',
+        'ip_address',
+        'email',
+        'timestamp',
+    ]
+
+    def __init__(self, kv):
+        assert len(set(self.selected_columns) - set(kv.keys())
+                   ) == 0, "SnubaEvents need all of the selected_columns"
+        self.__dict__ = kv
+
+
+@register(SnubaEvent)
+class SnubaEventSerializer(Serializer):
+    """
+        A bare-bones version of EventSerializer which uses snuba event rows as
+        the source data but attempts to produce a compatible (subset) of the
+        serialization returned by EventSerializer.
+    """
+
+    def get_tags_dict(self, obj):
+        keys = getattr(obj, 'tags.key', None)
+        values = getattr(obj, 'tags.value', None)
+        if keys and values and len(keys) == len(values):
+            results = []
+            for key, value in zip(keys, values):
+                key = key.split('sentry:', 1)[-1]
+                result = {'key': key, 'value': value}
+                query = convert_user_tag_to_query(key, value)
+                if query:
+                    result['query'] = query
+                results.append(result)
+            results.sort(key=lambda x: x['key'])
+            return results
+        return []
 
     def serialize(self, obj, attrs, user):
-        tags = [{
-            'key': key.split('sentry:', 1)[-1],
-            'value': value,
-        } for key, value in obj.tags]
-        for tag in tags:
-            query = convert_user_tag_to_query(tag['key'], tag['value'])
-            if query:
-                tag['query'] = query
-
-        user = obj.get_interface('user')
-        if user is not None:
-            user = user.get_api_context()
-
-        return {
-            'id': six.text_type(obj.id),
-            'groupID': six.text_type(obj.group_id),
+        result = {
             'eventID': six.text_type(obj.event_id),
             'projectID': six.text_type(obj.project_id),
-            # XXX for 'message' this doesn't do the proper resolution of logentry
-            # etc. that _get_legacy_message_with_meta does.
             'message': obj.message,
             'title': obj.title,
             'location': obj.location,
             'culprit': obj.culprit,
-            'user': user,
-            'tags': tags,
-            'platform': obj.platform,
-            'dateCreated': obj.datetime,
+            'dateCreated': obj.timestamp,
+            'user': {
+                'id': obj.user_id,
+                'email': obj.email,
+                'username': obj.username,
+                'ipAddress': obj.ip_address,
+            },
         }
+
+        tags = self.get_tags_dict(obj)
+        if tags:
+            result['tags'] = tags
+
+        return result

+ 7 - 6
src/sentry/db/models/fields/node.py

@@ -27,7 +27,7 @@ from sentry.utils.canonical import CANONICAL_TYPES, CanonicalKeyDict
 
 from .gzippeddict import GzippedDictField
 
-__all__ = ('NodeField', 'NodeData')
+__all__ = ('NodeField', )
 
 logger = logging.getLogger('sentry')
 
@@ -100,9 +100,10 @@ class NodeData(collections.MutableMapping):
         return '<%s: id=%s>' % (cls_name, self.id, )
 
     def get_ref(self, instance):
-        if not self.field or not self.field.ref_func:
+        ref_func = self.field.ref_func
+        if not ref_func:
             return
-        return self.field.ref_func(instance)
+        return ref_func(instance)
 
     def copy(self):
         return self.data.copy()
@@ -125,18 +126,18 @@ class NodeData(collections.MutableMapping):
             return self._node_data
 
         rv = {}
-        if self.field is not None and self.field.wrapper is not None:
+        if self.field.wrapper is not None:
             rv = self.field.wrapper(rv)
         return rv
 
     def bind_data(self, data, ref=None):
         self.ref = data.pop('_ref', ref)
         self.ref_version = data.pop('_ref_version', None)
-        if self.field is not None and self.ref_version == self.field.ref_version and ref is not None and self.ref != ref:
+        if self.ref_version == self.field.ref_version and ref is not None and self.ref != ref:
             raise NodeIntegrityFailure(
                 'Node reference for %s is invalid: %s != %s' % (self.id, ref, self.ref, )
             )
-        if self.field is not None and self.field.wrapper is not None:
+        if self.field.wrapper is not None:
             data = self.field.wrapper(data)
         self._node_data = data
 

+ 0 - 6
src/sentry/db/models/manager.py

@@ -320,12 +320,6 @@ class BaseManager(Manager):
 class EventManager(BaseManager):
 
     def bind_nodes(self, object_list, *node_names):
-        """
-        For a list of Event objects, and a property name where we might find an
-        (unfetched) NodeData on those objects, fetch all the data blobs for
-        those NodeDatas with a single multi-get command to nodestore, and bind
-        the returned blobs to the NodeDatas
-        """
         object_node_list = []
         for name in node_names:
             object_node_list.extend(

+ 111 - 284
src/sentry/models/event.py

@@ -10,11 +10,8 @@ from __future__ import absolute_import
 import six
 import string
 import warnings
-import pytz
 
 from collections import OrderedDict
-from datetime import datetime
-from dateutil.parser import parse as parse_date
 from django.db import models
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
@@ -26,18 +23,50 @@ from sentry.db.models import (
     BoundedBigIntegerField,
     BoundedIntegerField,
     Model,
-    NodeData,
     NodeField,
     sane_repr
 )
 from sentry.db.models.manager import EventManager
 from sentry.interfaces.base import get_interfaces
+from sentry.utils.cache import memoize
 from sentry.utils.canonical import CanonicalKeyDict, CanonicalKeyView
 from sentry.utils.safe import get_path
 from sentry.utils.strings import truncatechars
 
 
-class EventCommon(object):
+class Event(Model):
+    """
+    An individual event.
+    """
+    __core__ = False
+
+    group_id = BoundedBigIntegerField(blank=True, null=True)
+    event_id = models.CharField(max_length=32, null=True, db_column="message_id")
+    project_id = BoundedBigIntegerField(blank=True, null=True)
+    message = models.TextField()
+    platform = models.CharField(max_length=64, null=True)
+    datetime = models.DateTimeField(default=timezone.now, db_index=True)
+    time_spent = BoundedIntegerField(null=True)
+    data = NodeField(
+        blank=True,
+        null=True,
+        ref_func=lambda x: x.project_id or x.project.id,
+        ref_version=2,
+        wrapper=CanonicalKeyDict,
+    )
+
+    objects = EventManager()
+
+    class Meta:
+        app_label = 'sentry'
+        db_table = 'sentry_message'
+        verbose_name = _('message')
+        verbose_name_plural = _('messages')
+        unique_together = (('project_id', 'event_id'), )
+        index_together = (('group_id', 'datetime'), )
+
+    __repr__ = sane_repr('project_id', 'group_id')
+
     @classmethod
     def generate_node_id(cls, project_id, event_id):
         """
@@ -48,40 +77,44 @@ class EventCommon(object):
         """
         return md5('{}:{}'.format(project_id, event_id)).hexdigest()
 
-    # TODO (alex) We need a better way to cache these properties. functools32
-    # doesn't quite do the trick as there is a reference bug with unsaved
-    # models. But the current _group_cache thing is also clunky because these
-    # properties need to be stripped out in __getstate__.
-    @property
-    def group(self):
+    def __getstate__(self):
+        state = Model.__getstate__(self)
+
+        # do not pickle cached info.  We want to fetch this on demand
+        # again.  In particular if we were to pickle interfaces we would
+        # pickle a CanonicalKeyView which old sentry workers do not know
+        # about
+        state.pop('_project_cache', None)
+        state.pop('_group_cache', None)
+        state.pop('interfaces', None)
+
+        return state
+
+    # Implement a ForeignKey-like accessor for backwards compat
+    def _set_group(self, group):
+        self.group_id = group.id
+        self._group_cache = group
+
+    def _get_group(self):
         from sentry.models import Group
         if not hasattr(self, '_group_cache'):
             self._group_cache = Group.objects.get(id=self.group_id)
         return self._group_cache
 
-    @group.setter
-    def group(self, group):
-        self.group_id = group.id
-        self._group_cache = group
+    group = property(_get_group, _set_group)
 
-    @property
-    def project(self):
+    # Implement a ForeignKey-like accessor for backwards compat
+    def _set_project(self, project):
+        self.project_id = project.id
+        self._project_cache = project
+
+    def _get_project(self):
         from sentry.models import Project
         if not hasattr(self, '_project_cache'):
             self._project_cache = Project.objects.get(id=self.project_id)
         return self._project_cache
 
-    @project.setter
-    def project(self, project):
-        self.project_id = project.id
-        self._project_cache = project
-
-    @property
-    def interfaces(self):
-        return CanonicalKeyView(get_interfaces(self.data))
-
-    def get_interface(self, name):
-        return self.interfaces.get(name)
+    project = property(_get_project, _set_project)
 
     def get_legacy_message(self):
         # TODO(mitsuhiko): remove this code once it's unused.  It's still
@@ -162,6 +195,11 @@ class EventCommon(object):
             or get_path(self.data, 'logentry', 'message') \
             or ''
 
+    @property
+    def message_short(self):
+        warnings.warn('Event.message_short is deprecated, use Event.title', DeprecationWarning)
+        return self.title
+
     @property
     def organization(self):
         return self.project.organization
@@ -170,7 +208,7 @@ class EventCommon(object):
     def version(self):
         return self.data.get('version', '5')
 
-    @property
+    @memoize
     def ip_address(self):
         ip_address = get_path(self.data, 'user', 'ip_address')
         if ip_address:
@@ -182,8 +220,14 @@ class EventCommon(object):
 
         return None
 
-    @property
-    def tags(self):
+    def get_interfaces(self):
+        return CanonicalKeyView(get_interfaces(self.data))
+
+    @memoize
+    def interfaces(self):
+        return self.get_interfaces()
+
+    def get_tags(self):
         try:
             rv = sorted([(t, v) for t, v in get_path(
                 self.data, 'tags', filter=True) or () if t is not None and v is not None])
@@ -193,9 +237,7 @@ class EventCommon(object):
             # vs ((tag, foo), (tag, bar))
             return []
 
-    # For compatibility, still used by plugins.
-    def get_tags(self):
-        return self.tags
+    tags = property(get_tags)
 
     def get_tag(self, key):
         for t, v in self.get_tags():
@@ -215,41 +257,6 @@ class EventCommon(object):
         """Returns the internal raw event data dict."""
         return dict(self.data.items())
 
-    @property
-    def size(self):
-        data_len = 0
-        for value in six.itervalues(self.data):
-            data_len += len(repr(value))
-        return data_len
-
-    @property
-    def transaction(self):
-        return self.get_tag('transaction')
-
-    def get_email_subject(self):
-        template = self.project.get_option('mail:subject_template')
-        if template:
-            template = EventSubjectTemplate(template)
-        else:
-            template = DEFAULT_SUBJECT_TEMPLATE
-        return truncatechars(
-            template.safe_substitute(
-                EventSubjectTemplateData(self),
-            ),
-            128,
-        ).encode('utf-8')
-
-    def get_environment(self):
-        from sentry.models import Environment
-
-        if not hasattr(self, '_environment_cache'):
-            self._environment_cache = Environment.objects.get(
-                organization_id=self.project.organization_id,
-                name=Environment.get_name_or_default(self.get_tag('environment')),
-            )
-
-        return self._environment_cache
-
     def as_dict(self):
         """Returns the data in normalized form for external consumers."""
         # We use a OrderedDict to keep elements ordered for a potential JSON serializer
@@ -262,7 +269,7 @@ class EventCommon(object):
         data['message'] = self.real_message
         data['datetime'] = self.datetime
         data['time_spent'] = self.time_spent
-        data['tags'] = [(k.split('sentry:', 1)[-1], v) for (k, v) in self.tags]
+        data['tags'] = [(k.split('sentry:', 1)[-1], v) for (k, v) in self.get_tags()]
         for k, v in sorted(six.iteritems(self.data)):
             if k in data:
                 continue
@@ -281,9 +288,12 @@ class EventCommon(object):
 
         return data
 
-    # ============================================
-    # DEPRECATED
-    # ============================================
+    @property
+    def size(self):
+        data_len = 0
+        for value in six.itervalues(self.data):
+            data_len += len(repr(value))
+        return data_len
 
     @property
     def level(self):
@@ -310,9 +320,7 @@ class EventCommon(object):
 
     @property
     def server_name(self):
-        warnings.warn(
-            'Event.server_name is deprecated. Use Event.tags instead.',
-            DeprecationWarning)
+        warnings.warn('Event.server_name is deprecated. Use Event.tags instead.')
         return self.get_tag('server_name')
 
     @property
@@ -320,220 +328,39 @@ class EventCommon(object):
         warnings.warn('Event.checksum is no longer used', DeprecationWarning)
         return ''
 
-    def error(self):  # TODO why is this not a property?
+    def error(self):
         warnings.warn('Event.error is deprecated, use Event.title', DeprecationWarning)
         return self.title
 
     error.short_description = _('error')
 
     @property
-    def message_short(self):
-        warnings.warn('Event.message_short is deprecated, use Event.title', DeprecationWarning)
-        return self.title
-
-
-class SnubaEvent(EventCommon):
-    """
-        An event backed by data stored in snuba.
-
-        This is a readonly event and does not support event creation or save.
-        The basic event data is fetched from snuba, and the event body is
-        fetched from nodestore and bound to the data property in the same way
-        as a regular Event.
-    """
-
-    # The list of columns that we should request from snuba to be able to fill
-    # out the object.
-    selected_columns = [
-        'event_id',
-        'project_id',
-        'message',
-        'title',
-        'type',
-        'location',
-        'culprit',
-        'timestamp',
-        'group_id',
-        'platform',
-
-        # Required to provide snuba-only tags
-        'tags.key',
-        'tags.value',
-
-        # Required to provide snuba-only 'user' interface
-        'user_id',
-        'username',
-        'ip_address',
-        'email',
-    ]
-
-    __repr__ = sane_repr('project_id', 'group_id')
-
-    @classmethod
-    def get_event(cls, project_id, event_id):
-        from sentry.utils import snuba
-        result = snuba.raw_query(
-            start=datetime.utcfromtimestamp(0),  # will be clamped to project retention
-            end=datetime.utcnow(),  # will be clamped to project retention
-            selected_columns=cls.selected_columns,
-            filter_keys={
-                'event_id': [event_id],
-                'project_id': [project_id],
-            },
-        )
-        if 'error' not in result and len(result['data']) == 1:
-            return SnubaEvent(result['data'][0])
-        return None
-
-    def __init__(self, snuba_values):
-        assert set(snuba_values.keys()) == set(self.selected_columns)
-
-        self.__dict__ = snuba_values
-
-        # This should be lazy loaded and will only be accessed if we access any
-        # properties on self.data
-        node_id = SnubaEvent.generate_node_id(self.project_id, self.event_id)
-        self.data = NodeData(None, node_id, data=None)
-
-    # ============================================
-    # Snuba-only implementations of properties that
-    # would otherwise require nodestore data.
-    # ============================================
-    @property
-    def tags(self):
-        """
-        Override of tags property that uses tags from snuba rather than
-        the nodestore event body. This might be useful for implementing
-        tag deletions without having to rewrite nodestore blobs.
-        """
-        keys = getattr(self, 'tags.key', None)
-        values = getattr(self, 'tags.value', None)
-        if keys and values and len(keys) == len(values):
-            return sorted(zip(keys, values))
-        return []
-
-    def get_interface(self, name):
-        """
-        Override of interface getter that lets us return some interfaces
-        directly from Snuba data.
-        """
-        if name in ['user']:
-            from sentry.interfaces.user import User
-            # This is a fake version of the User interface constructed
-            # from just the data we have in Snuba.
-            snuba_user = {
-                'id': self.user_id,
-                'email': self.email,
-                'username': self.username,
-                'ip_address': self.ip_address,
-            }
-            if any(v is not None for v in snuba_user.values()):
-                return User.to_python(snuba_user)
-        return self.interfaces.get(name)
-
-    def get_event_type(self):
-        return self.__dict__.get('type', 'default')
-
-    # These should all have been normalized to the correct values on
-    # the way in to snuba, so we should be able to just use them as is.
-    @property
-    def ip_address(self):
-        return self.__dict__['ip_address']
-
-    @property
-    def title(self):
-        return self.__dict__['title']
-
-    @property
-    def culprit(self):
-        return self.__dict__['culprit']
-
-    @property
-    def location(self):
-        return self.__dict__['location']
-
-    # ============================================
-    # Snuba implementations of django Fields
-    # ============================================
-    @property
-    def datetime(self):
-        """
-        Reconstruct the datetime of this event from the snuba timestamp
-        """
-        # dateutil seems to use tzlocal() instead of UTC even though the string
-        # ends with '+00:00', so just replace the TZ with UTC because we know
-        # all timestamps from snuba are UTC.
-        return parse_date(self.timestamp).replace(tzinfo=pytz.utc)
-
-    @property
-    def time_spent(self):
-        return None
-
-    @property
-    def id(self):
-        # Because a snuba event will never have a django row id, just return
-        # the hex event_id here. We should be moving to a world where we never
-        # have to reference the row id anyway.
-        return self.event_id
-
-    @property
-    def next_event(self):
-        return None
-
-    @property
-    def prev_event(self):
-        return None
-
-    def save(self):
-        raise NotImplementedError
-
-
-class Event(EventCommon, Model):
-    """
-    An event backed by data stored in postgres.
-
-    """
-    __core__ = False
-
-    group_id = BoundedBigIntegerField(blank=True, null=True)
-    event_id = models.CharField(max_length=32, null=True, db_column="message_id")
-    project_id = BoundedBigIntegerField(blank=True, null=True)
-    message = models.TextField()
-    platform = models.CharField(max_length=64, null=True)
-    datetime = models.DateTimeField(default=timezone.now, db_index=True)
-    time_spent = BoundedIntegerField(null=True)
-    data = NodeField(
-        blank=True,
-        null=True,
-        ref_func=lambda x: x.project_id or x.project.id,
-        ref_version=2,
-        wrapper=CanonicalKeyDict,
-    )
-
-    objects = EventManager()
-
-    class Meta:
-        app_label = 'sentry'
-        db_table = 'sentry_message'
-        verbose_name = _('message')
-        verbose_name_plural = _('messages')
-        unique_together = (('project_id', 'event_id'), )
-        index_together = (('group_id', 'datetime'), )
+    def transaction(self):
+        return self.get_tag('transaction')
 
-    __repr__ = sane_repr('project_id', 'group_id')
+    def get_email_subject(self):
+        template = self.project.get_option('mail:subject_template')
+        if template:
+            template = EventSubjectTemplate(template)
+        else:
+            template = DEFAULT_SUBJECT_TEMPLATE
+        return truncatechars(
+            template.safe_substitute(
+                EventSubjectTemplateData(self),
+            ),
+            128,
+        ).encode('utf-8')
 
-    def __getstate__(self):
-        state = Model.__getstate__(self)
+    def get_environment(self):
+        from sentry.models import Environment
 
-        # do not pickle cached info.  We want to fetch this on demand
-        # again.  In particular if we were to pickle interfaces we would
-        # pickle a CanonicalKeyView which old sentry workers do not know
-        # about
-        state.pop('_project_cache', None)
-        state.pop('_group_cache', None)
-        state.pop('interfaces', None)
+        if not hasattr(self, '_environment_cache'):
+            self._environment_cache = Environment.objects.get(
+                organization_id=self.project.organization_id,
+                name=Environment.get_name_or_default(self.get_tag('environment')),
+            )
 
-        return state
+        return self._environment_cache
 
     # Find next and previous events based on datetime and id. We cannot
     # simply `ORDER BY (datetime, id)` as this is too slow (no index), so
@@ -548,8 +375,8 @@ class Event(EventCommon, Model):
             group_id=self.group_id,
         ).exclude(id=self.id).order_by('datetime')[0:5]
 
-        events = [e for e in events if e.datetime == self.datetime and e.id > self.id or
-                  e.datetime > self.datetime]
+        events = [e for e in events if e.datetime == self.datetime and e.id > self.id
+                  or e.datetime > self.datetime]
         events.sort(key=EVENT_ORDERING_KEY)
         return events[0] if events else None
 
@@ -560,8 +387,8 @@ class Event(EventCommon, Model):
             group_id=self.group_id,
         ).exclude(id=self.id).order_by('-datetime')[0:5]
 
-        events = [e for e in events if e.datetime == self.datetime and e.id < self.id or
-                  e.datetime < self.datetime]
+        events = [e for e in events if e.datetime == self.datetime and e.id < self.id
+                  or e.datetime < self.datetime]
         events.sort(key=EVENT_ORDERING_KEY, reverse=True)
         return events[0] if events else None
 

+ 1 - 1
src/sentry/plugins/base/v1.py

@@ -444,7 +444,7 @@ class IPlugin(local, PluggableViewMixin, PluginConfigMixin, PluginStatusMixin):
         >>> def is_regression(self, group, event, **kwargs):
         >>>     # regression if 'version' tag has a value we haven't seen before
         >>>     seen_versions = set(t[0] for t in group.get_unique_tags("version"))
-        >>>     event_version = dict(event.tags).get("version")
+        >>>     event_version = dict(event.get_tags()).get("version")
         >>>     return event_version not in seen_versions
         """
 

+ 1 - 1
src/sentry/plugins/sentry_mail/models.py

@@ -263,7 +263,7 @@ class MailPlugin(NotificationPlugin):
                 interface_list.append((interface.get_title(), mark_safe(body), text_body))
 
             context.update({
-                'tags': event.tags,
+                'tags': event.get_tags(),
                 'interfaces': interface_list,
             })
 

+ 1 - 1
src/sentry/plugins/sentry_webhooks/plugin.py

@@ -90,7 +90,7 @@ class WebHooksPlugin(notify.NotificationPlugin):
             'triggering_rules': triggering_rules,
         }
         data['event'] = dict(event.data or {})
-        data['event']['tags'] = event.tags
+        data['event']['tags'] = event.get_tags()
         data['event']['event_id'] = event.event_id
         if features.has('organizations:legacy-event-id', group.project.organization):
             try:

Some files were not shown because too many files changed in this diff