Browse Source

Start work on #167 add transaction tag processing for environment and
release. Does not yet update transaction group.

David Burke 2 years ago
parent
commit
81fb9de74b

+ 18 - 20
events/serializers.py

@@ -112,6 +112,21 @@ class SentrySDKEventSerializer(BaseSerializer):
     )
     _meta = serializers.JSONField(required=False)
 
+    def get_environment(self, name: str, project):
+        environment, _ = Environment.objects.get_or_create(
+            name=name[: Environment._meta.get_field("name").max_length],
+            organization=project.organization,
+        )
+        environment.projects.add(project)
+        return environment
+
+    def get_release(self, version: str, project):
+        release, _ = Release.objects.get_or_create(
+            version=version, organization=project.organization
+        )
+        release.projects.add(project)
+        return release
+
 
 class FormattedMessageSerializer(serializers.Serializer):
     formatted = serializers.CharField(
@@ -236,21 +251,6 @@ class StoreDefaultSerializer(SentrySDKEventSerializer):
             return data["message"]
         return data.get("logentry", {}).get("message", "")
 
-    def get_environment(self, name: str, project):
-        environment, _ = Environment.objects.get_or_create(
-            name=name[: Environment._meta.get_field("name").max_length],
-            organization=project.organization,
-        )
-        environment.projects.add(project)
-        return environment
-
-    def get_release(self, version: str, project):
-        release, _ = Release.objects.get_or_create(
-            version=version, organization=project.organization
-        )
-        release.projects.add(project)
-        return release
-
     def is_url(self, filename: str) -> bool:
         return filename.startswith(("file:", "http:", "https:", "applewebdata:"))
 
@@ -287,9 +287,8 @@ class StoreDefaultSerializer(SentrySDKEventSerializer):
             for value in exception.get("values", []):
                 self.normalize_stacktrace(value.get("stacktrace"))
 
-        release = None
-        if data.get("release"):
-            release = self.get_release(data["release"], project)
+        if release := data.get("release"):
+            release = self.get_release(release, project)
 
         for Processor in EVENT_PROCESSORS:
             Processor(project, release, data).run()
@@ -323,8 +322,7 @@ class StoreDefaultSerializer(SentrySDKEventSerializer):
             if level:
                 defaults["level"] = level
 
-            environment = None
-            if data.get("environment"):
+            if environment := data.get("environment"):
                 environment = self.get_environment(data["environment"], project)
             tags = []
             if environment:

+ 123 - 0
events/test_data/transactions/environment_release.json

@@ -0,0 +1,123 @@
+[
+  {
+    "event_id": "037419a37a8e426ba52e82cd647f2d14",
+    "sent_at": "2022-08-01T16:59:58.064Z",
+    "sdk": { "name": "sentry.javascript.angular", "version": "7.8.1" },
+    "trace": {
+      "environment": "dev",
+      "release": "1.0",
+      "public_key": "4d886b61b58e48a685455c51c434d2a6",
+      "trace_id": "df3b21283b08480ba166b32692d85453",
+      "sample_rate": "1"
+    }
+  },
+  {
+    "type": "transaction",
+    "sample_rates": [{ "id": "client_sampler", "rate": 1 }]
+  },
+  {
+    "contexts": {
+      "angular": { "version": 14 },
+      "trace": {
+        "op": "pageload",
+        "span_id": "a468669a1489ecf7",
+        "tags": {
+          "hardwareConcurrency": "16",
+          "sentry_reportAllChanges": false
+        },
+        "trace_id": "df3b21283b08480ba166b32692d85453"
+      }
+    },
+    "spans": [
+      {
+        "description": "unloadEvent",
+        "op": "browser",
+        "parent_span_id": "a468669a1489ecf7",
+        "span_id": "a39d104d785c87c3",
+        "start_timestamp": 1659373196.921,
+        "timestamp": 1659373196.921,
+        "trace_id": "df3b21283b08480ba166b32692d85453"
+      },
+      {
+        "description": "first-contentful-paint",
+        "op": "paint",
+        "parent_span_id": "a468669a1489ecf7",
+        "span_id": "ba375d967f28e05b",
+        "start_timestamp": 1659373197.0779998,
+        "timestamp": 1659373197.0779998,
+        "trace_id": "df3b21283b08480ba166b32692d85453"
+      }
+    ],
+    "start_timestamp": 1659373196.891,
+    "tags": { "hardwareConcurrency": "16", "sentry_reportAllChanges": false },
+    "timestamp": 1659373197.135,
+    "transaction": "/tracing",
+    "type": "transaction",
+    "transaction_info": { "source": "url" },
+    "measurements": {
+      "fcp": { "value": 186.99979782104492, "unit": "millisecond" },
+      "mark.fcp": { "value": 1659373197.0779998, "unit": "second" },
+      "ttfb": { "value": 26.999950408935547, "unit": "millisecond" },
+      "ttfb.requestTime": { "value": 0.9999275207519531, "unit": "millisecond" }
+    },
+    "platform": "javascript",
+    "event_id": "037419a37a8e426ba52e82cd647f2d14",
+    "environment": "dev",
+    "release": "1.0",
+    "sdk": {
+      "integrations": [
+        "InboundFilters",
+        "FunctionToString",
+        "TryCatch",
+        "Breadcrumbs",
+        "GlobalHandlers",
+        "LinkedErrors",
+        "Dedupe",
+        "HttpContext",
+        "BrowserTracing"
+      ],
+      "name": "sentry.javascript.angular",
+      "version": "7.8.1",
+      "packages": [{ "name": "npm:@sentry/angular", "version": "7.8.1" }]
+    },
+    "breadcrumbs": [
+      {
+        "timestamp": 1659373197.052,
+        "category": "console",
+        "data": { "arguments": ["what what"], "logger": "console" },
+        "level": "log",
+        "message": "what what"
+      },
+      {
+        "timestamp": 1659373197.052,
+        "category": "console",
+        "data": { "arguments": ["DOIT"], "logger": "console" },
+        "level": "log",
+        "message": "DOIT"
+      },
+      {
+        "timestamp": 1659373197.074,
+        "category": "console",
+        "data": {
+          "arguments": [
+            "Angular is running in development mode. Call enableProdMode() to enable production mode."
+          ],
+          "logger": "console"
+        },
+        "level": "log",
+        "message": "Angular is running in development mode. Call enableProdMode() to enable production mode."
+      },
+      {
+        "timestamp": 1659373197.08,
+        "category": "navigation",
+        "data": { "from": "/tracing", "to": "/tracing" }
+      }
+    ],
+    "request": {
+      "url": "http://localhost:4201/tracing",
+      "headers": {
+        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0"
+      }
+    }
+  }
+]

+ 45 - 2
events/tests/test_envelope_api.py

@@ -1,11 +1,12 @@
 import json
+import uuid
 
 from django.shortcuts import reverse
 from model_bakery import baker
 from rest_framework.test import APITestCase
 
 from glitchtip import test_utils  # pylint: disable=unused-import
-from performance.models import TransactionEvent
+from performance.models import TransactionEvent, TransactionGroup
 
 from ..models import Event
 
@@ -17,10 +18,17 @@ class EnvelopeStoreTestCase(APITestCase):
         self.params = f"?sentry_key={self.projectkey.public_key}"
         self.url = reverse("envelope_store", args=[self.project.id]) + self.params
 
-    def get_payload(self, path):
+    def get_payload(self, path, replace_id=False, set_release=None):
         """Convert JSON file into envelope format string"""
         with open(path) as json_file:
             json_data = json.load(json_file)
+            if replace_id:
+                new_id = uuid.uuid4().hex
+                json_data[0]["event_id"] = new_id
+                json_data[2]["event_id"] = new_id
+            if set_release:
+                json_data[0]["trace"]["release"] = set_release
+                json_data[2]["release"] = set_release
             data = "\n".join([json.dumps(line) for line in json_data])
         return data
 
@@ -48,3 +56,38 @@ class EnvelopeStoreTestCase(APITestCase):
         data = self.get_payload("events/test_data/transactions/js_angular.json")
         res = self.client.generic("POST", self.url, data)
         self.assertEqual(res.status_code, 200)
+
+    def test_environment_release(self):
+        data = self.get_payload(
+            "events/test_data/transactions/environment_release.json"
+        )
+        res = self.client.generic("POST", self.url, data)
+        event_id = res.data["id"]
+        self.assertEqual(res.status_code, 200)
+        self.assertTrue(
+            TransactionEvent.objects.filter(
+                pk=event_id, tags__release="1.0", tags__environment="dev"
+            ).exists()
+        )
+        self.assertTrue(
+            TransactionGroup.objects.filter(
+                transactionevent__pk=event_id,
+                tags__release__contains="1.0",
+                tags__environment__contains="dev",
+            ).exists()
+        )
+
+        data = self.get_payload(
+            "events/test_data/transactions/environment_release.json",
+            replace_id=True,
+            set_release="1.1",
+        )
+        res = self.client.generic("POST", self.url, data)
+        # TODO, update groups
+        # self.assertTrue(
+        #     TransactionGroup.objects.filter(
+        #         transactionevent__pk=event_id,
+        #         tags__release__contains="1.1",
+        #         tags__environment__contains="dev",
+        #     ).exists()
+        # )

+ 9 - 7
events/views.py

@@ -100,8 +100,8 @@ class BaseEventAPIView(APIView):
                 .only("id", "first_event", "organization__is_accepting_events")
                 .first()
             )
-        except ValidationError as e:
-            raise exceptions.AuthenticationFailed({"error": "Invalid api key"})
+        except ValidationError as err:
+            raise exceptions.AuthenticationFailed({"error": "Invalid api key"}) from err
         if not project:
             if Project.objects.filter(id=project_id).exists():
                 raise exceptions.AuthenticationFailed({"error": "Invalid api key"})
@@ -110,8 +110,10 @@ class BaseEventAPIView(APIView):
             raise exceptions.Throttled(detail="event rejected due to rate limit")
         return project
 
-    def get_event_serializer_class(self, data=[]):
+    def get_event_serializer_class(self, data=None):
         """Determine event type and return serializer"""
+        if data is None:
+            data = []
         if "exception" in data and data["exception"]:
             return StoreErrorSerializer
         if "platform" not in data:
@@ -125,9 +127,9 @@ class BaseEventAPIView(APIView):
         )
         try:
             serializer.is_valid(raise_exception=True)
-        except exceptions.ValidationError as e:
+        except exceptions.ValidationError as err:
             set_level("warning")
-            capture_exception(e)
+            capture_exception(err)
             logger.warning("Invalid event %s", serializer.errors)
             return Response()
         event = serializer.save()
@@ -142,9 +144,9 @@ class EventStoreAPIView(BaseEventAPIView):
             print(json.dumps(request.data))
         try:
             project = self.get_project(request, kwargs.get("id"))
-        except exceptions.AuthenticationFailed as e:
+        except exceptions.AuthenticationFailed as err:
             # Replace 403 status code with 401 to match OSS Sentry
-            return Response(e.detail, status=401)
+            return Response(err.detail, status=401)
         return self.process_event(request.data, request, project)
 
 

+ 3 - 1
glitchtip/settings.py

@@ -534,7 +534,9 @@ if os.getenv("EMAIL_TIMEOUT"):
     EMAIL_TIMEOUT = env.str("EMAIL_TIMEOUT")
 if os.getenv("EMAIL_FILE_PATH"):
     EMAIL_FILE_PATH = env.str("EMAIL_FILE_PATH")
-if os.getenv("EMAIL_URL"): # Careful, this will override most EMAIL_*** settings. Set them all individually, or use EMAIL_URL to set them all at once, but don't do both.
+if os.getenv(
+    "EMAIL_URL"
+):  # Careful, this will override most EMAIL_*** settings. Set them all individually, or use EMAIL_URL to set them all at once, but don't do both.
     EMAIL_CONFIG = env.email_url("EMAIL_URL")
     vars().update(EMAIL_CONFIG)
 

+ 19 - 4
performance/serializers.py

@@ -65,14 +65,28 @@ class TransactionEventSerializer(SentrySDKEventSerializer):
     timestamp = FlexibleDateTimeField()
     transaction = serializers.CharField()
 
-    def create(self, data):
-        trace_id = data["contexts"]["trace"]["trace_id"]
+    def create(self, validated_data):
+        data = validated_data
+        contexts = data["contexts"]
+        project = self.context.get("project")
+        trace_id = contexts["trace"]["trace_id"]
+
+        tags = []
+        if environment := data.get("environment"):
+            environment = self.get_environment(data["environment"], project)
+            tags.append(("environment", environment.name))
+        if release := data.get("release"):
+            release = self.get_release(release, project)
+            tags.append(("release", release.version))
+        defaults = {}
+        defaults["tags"] = {tag[0]: [tag[1]] for tag in tags}
 
         group, _ = TransactionGroup.objects.get_or_create(
             project=self.context.get("project"),
             transaction=data["transaction"],
-            op=data["contexts"]["trace"]["op"],
+            op=contexts["trace"]["op"],
             method=data.get("request", {}).get("method"),
+            defaults=defaults,
         )
         transaction = TransactionEvent.objects.create(
             group=group,
@@ -86,10 +100,11 @@ class TransactionEventSerializer(SentrySDKEventSerializer):
             timestamp=data["timestamp"],
             start_timestamp=data["start_timestamp"],
             duration=data["timestamp"] - data["start_timestamp"],
+            tags={tag[0]: tag[1] for tag in tags},
         )
 
         first_span = SpanSerializer(
-            data=data["contexts"]["trace"]
+            data=contexts["trace"]
             | {
                 "start_timestamp": data["start_timestamp"],
                 "timestamp": data["timestamp"],