Browse Source

feat(profiling): Add support for profile outcomes (#34703)

* Add support for profile outcomes
Francesco Vigliaturo 2 years ago
parent
commit
3af5610ea7

+ 1 - 1
requirements-base.txt

@@ -55,7 +55,7 @@ rfc3339-validator==0.1.2
 rfc3986-validator==0.1.1
 # [end] jsonschema format validators
 sentry-arroyo==0.0.16
-sentry-relay==0.8.11
+sentry-relay==0.8.12
 sentry-sdk>=1.4.3,<1.6.0
 snuba-sdk==1.0.0
 simplejson==3.17.2

+ 12 - 7
src/sentry/profiles/consumer.py

@@ -1,4 +1,4 @@
-from typing import Any, Dict, MutableMapping, Optional, Sequence, cast
+from typing import Any, Dict, MutableMapping, Optional, Sequence, Tuple, cast
 
 import msgpack
 from confluent_kafka import Message
@@ -21,7 +21,9 @@ def get_profiles_consumer(
 
 
 class ProfilesConsumer(AbstractBatchWorker):  # type: ignore
-    def process_message(self, message: Message) -> Optional[MutableMapping[str, Any]]:
+    def process_message(
+        self, message: Message
+    ) -> Tuple[Optional[int], Optional[MutableMapping[str, Any]]]:
         message = msgpack.unpackb(message.value(), use_list=False)
         profile = cast(Dict[str, Any], json.loads(message["payload"]))
         profile.update(
@@ -31,11 +33,14 @@ class ProfilesConsumer(AbstractBatchWorker):  # type: ignore
                 "received": message["received"],
             }
         )
-        return profile
-
-    def flush_batch(self, profiles: Sequence[MutableMapping[str, Any]]) -> None:
-        for profile in profiles:
-            process_profile.s(profile=profile).apply_async()
+        return (message.get("key_id"), profile)
+
+    def flush_batch(
+        self, messages: Sequence[Tuple[Optional[int], MutableMapping[str, Any]]]
+    ) -> None:
+        for message in messages:
+            key_id, profile = message
+            process_profile.s(profile=profile, key_id=key_id).apply_async()
 
     def shutdown(self) -> None:
         pass

+ 26 - 8
src/sentry/profiles/task.py

@@ -1,15 +1,19 @@
+from datetime import datetime
 from time import sleep, time
-from typing import Any, MutableMapping
+from typing import Any, MutableMapping, Optional
 
 from django.conf import settings
+from pytz import UTC
 from symbolic import ProguardMapper  # type: ignore
 
+from sentry.constants import DataCategory
 from sentry.lang.native.symbolicator import Symbolicator
 from sentry.models import Project, ProjectDebugFile
 from sentry.profiles.device import classify_device
 from sentry.tasks.base import instrumented_task
 from sentry.tasks.symbolication import RetrySymbolication
 from sentry.utils import json, kafka_config
+from sentry.utils.outcomes import Outcome, track_outcome
 from sentry.utils.pubsub import KafkaPublisher
 
 processed_profiles_publisher = None
@@ -22,11 +26,15 @@ processed_profiles_publisher = None
     max_retries=5,
     acks_late=True,
 )
-def process_profile(profile: MutableMapping[str, Any], **kwargs: Any) -> None:
+def process_profile(
+    profile: MutableMapping[str, Any], key_id: Optional[int], **kwargs: Any
+) -> None:
+    project = Project.objects.get_from_cache(id=profile["project_id"])
+
     if profile["platform"] == "cocoa":
-        profile = _symbolicate(profile=profile)
+        profile = _symbolicate(profile=profile, project=project)
     elif profile["platform"] == "android":
-        profile = _deobfuscate(profile=profile)
+        profile = _deobfuscate(profile=profile, project=project)
 
     profile = _normalize(profile=profile)
 
@@ -43,6 +51,18 @@ def process_profile(profile: MutableMapping[str, Any], **kwargs: Any) -> None:
         json.dumps(profile),
     )
 
+    track_outcome(
+        org_id=project.organization_id,
+        project_id=project.id,
+        key_id=key_id,
+        outcome=Outcome.ACCEPTED,
+        reason=None,
+        timestamp=datetime.utcnow().replace(tzinfo=UTC),
+        event_id=profile["transaction_id"],
+        category=DataCategory.PROFILE,
+        quantity=1,
+    )
+
 
 def _normalize(profile: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
     classification_options = {
@@ -70,8 +90,7 @@ def _normalize(profile: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
     return profile
 
 
-def _symbolicate(profile: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
-    project = Project.objects.get_from_cache(id=profile["project_id"])
+def _symbolicate(profile: MutableMapping[str, Any], project: Project) -> MutableMapping[str, Any]:
     symbolicator = Symbolicator(project=project, event_id=profile["profile_id"])
     modules = profile["debug_meta"]["images"]
     stacktraces = [
@@ -120,12 +139,11 @@ def _symbolicate(profile: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
     return profile
 
 
-def _deobfuscate(profile: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
+def _deobfuscate(profile: MutableMapping[str, Any], project: Project) -> MutableMapping[str, Any]:
     debug_file_id = profile.get("build_id")
     if debug_file_id is None or debug_file_id == "":
         return profile
 
-    project = Project.objects.get_from_cache(id=profile["project_id"])
     dif_paths = ProjectDebugFile.difcache.fetch_difs(project, [debug_file_id], features=["mapping"])
     debug_file_path = dif_paths.get(debug_file_id)
     if debug_file_path is None:

+ 24 - 1
tests/sentry/profiles/test_consumer.py

@@ -12,6 +12,16 @@ from sentry.utils import json
 class ProfilesConsumerTest(TestCase, SnubaTestCase):
     @fixture
     def valid_message(self):
+        return {
+            "organization_id": 1,
+            "project_id": 1,
+            "key_id": 1,
+            "received": int(datetime.utcnow().timestamp()),
+            "payload": json.dumps({"platform": "android", "profile": ""}),
+        }
+
+    @fixture
+    def valid_message_no_key_id(self):
         return {
             "organization_id": 1,
             "project_id": 1,
@@ -23,9 +33,22 @@ class ProfilesConsumerTest(TestCase, SnubaTestCase):
         consumer = ProfilesConsumer()
         message = Mock()
         message.value.return_value = msgpack.packb(self.valid_message)
-        profile = consumer.process_message(message)
+        key_id, profile = consumer.process_message(message)
+
+        for k in ["organization_id", "project_id", "received"]:
+            assert k in profile
+
+        assert isinstance(profile["received"], int)
+        assert isinstance(key_id, int)
+
+    def test_process_no_key_id(self):
+        consumer = ProfilesConsumer()
+        message = Mock()
+        message.value.return_value = msgpack.packb(self.valid_message_no_key_id)
+        key_id, profile = consumer.process_message(message)
 
         for k in ["organization_id", "project_id", "received"]:
             assert k in profile
 
         assert isinstance(profile["received"], int)
+        assert key_id is None

+ 7 - 4
tests/sentry/profiles/test_task.py

@@ -8,6 +8,7 @@ from django.urls import reverse
 from exam import fixture
 
 from sentry.constants import MODULE_ROOT
+from sentry.models import Project
 from sentry.profiles.task import _deobfuscate, _normalize
 from sentry.testutils import TestCase
 from sentry.utils import json
@@ -141,8 +142,8 @@ class ProfilesProcessTaskTest(TestCase):
                 },
             }
         )
-
-        profile = _deobfuscate(profile)
+        project = Project.objects.get_from_cache(id=profile["project_id"])
+        profile = _deobfuscate(profile, project)
         frames = profile["profile"]["methods"]
 
         assert frames[0]["name"] == "getClassContext"
@@ -193,7 +194,8 @@ class ProfilesProcessTaskTest(TestCase):
             }
         )
 
-        profile = _deobfuscate(profile)
+        project = Project.objects.get_from_cache(id=profile["project_id"])
+        profile = _deobfuscate(profile, project)
         frames = profile["profile"]["methods"]
 
         assert sum(len(f.get("inline_frames", [{}])) for f in frames) == 4
@@ -255,7 +257,8 @@ class ProfilesProcessTaskTest(TestCase):
             }
         )
 
+        project = Project.objects.get_from_cache(id=profile["project_id"])
         obfuscated_frames = profile["profile"]["methods"].copy()
-        profile = _deobfuscate(profile)
+        profile = _deobfuscate(profile, project)
 
         assert profile["profile"]["methods"] == obfuscated_frames