Browse Source

feat(project): Add has_transactions flag and tracking (#18289)

Megan Heskett 4 years ago
parent
commit
28b3c19501

+ 1 - 1
migrations_lockfile.txt

@@ -10,7 +10,7 @@ auth: 0008_alter_user_username_max_length
 contenttypes: 0002_remove_content_type_name
 jira_ac: 0001_initial
 nodestore: 0001_initial
-sentry: 0062_key_transactions_unique_with_owner
+sentry: 0064_project_has_transactions
 sessions: 0001_initial
 sites: 0002_alter_domain_unique
 social_auth: 0001_initial

+ 16 - 0
src/sentry/analytics/events/first_transaction_sent.py

@@ -0,0 +1,16 @@
+from __future__ import absolute_import
+
+from sentry import analytics
+
+
+class FirstTransactionSentEvent(analytics.Event):
+    type = "first_transaction.sent"
+
+    attributes = (
+        analytics.Attribute("organization_id"),
+        analytics.Attribute("project_id"),
+        analytics.Attribute("platform", required=False),
+    )
+
+
+analytics.register(FirstTransactionSentEvent)

+ 47 - 0
src/sentry/migrations/0064_project_has_transactions.py

@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.28 on 2020-04-15 22:18
+from __future__ import unicode_literals
+
+import bitfield.models
+from django.db import migrations
+import django.db.models.manager
+
+
+class Migration(migrations.Migration):
+    """
+    BEGIN;
+    --
+    -- Alter field flags on project
+    --
+    COMMIT;
+    """
+
+    # This flag is used to mark that a migration shouldn't be automatically run in
+    # production. We set this to True for operations that we think are risky and want
+    # someone from ops to run manually and monitor.
+    # General advice is that if in doubt, mark your migration as `is_dangerous`.
+    # Some things you should always mark as dangerous:
+    # - Large data migrations. Typically we want these to be run manually by ops so that
+    #   they can be monitored. Since data migrations will now hold a transaction open
+    #   this is even more important.
+    # - Adding columns to highly active tables, even ones that are NULL.
+    is_dangerous = False
+
+    # This flag is used to decide whether to run this migration in a transaction or not.
+    # By default we prefer to run in a transaction, but for migrations where you want
+    # to `CREATE INDEX CONCURRENTLY` this needs to be set to False. Typically you'll
+    # want to create an index concurrently when adding one to an existing table.
+    atomic = True
+
+
+    dependencies = [
+        ('sentry', '0063_drop_alertrule_constraint'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='project',
+            name='flags',
+            field=bitfield.models.BitField(((b'has_releases', b'This Project has sent release data'), (b'has_issue_alerts_targeting', b'This Project has issue alerts targeting'), (b'has_transactions', b'This Project has sent transactions')), default=0, null=True),
+        ),
+    ]

+ 1 - 0
src/sentry/models/project.py

@@ -106,6 +106,7 @@ class Project(Model, PendingDeletionMixin):
         flags=(
             ("has_releases", "This Project has sent release data"),
             ("has_issue_alerts_targeting", "This Project has issue alerts targeting"),
+            ("has_transactions", "This Project has sent transactions"),
         ),
         default=0,
         null=True,

+ 19 - 0
src/sentry/receivers/transactions.py

@@ -0,0 +1,19 @@
+from __future__ import absolute_import
+
+from django.db.models import F
+
+from sentry import analytics
+from sentry.models import Project
+from sentry.signals import event_processed
+
+
+@event_processed.connect(weak=False)
+def record_first_transaction(project, event, **kwargs):
+    if event.get_event_type() == "transaction" and not project.flags.has_transactions:
+        project.update(flags=F("flags").bitor(Project.flags.has_transactions))
+        analytics.record(
+            "first_transaction.sent",
+            project_id=project.id,
+            organization_id=project.organization_id,
+            platform=project.platform,
+        )

+ 57 - 0
tests/sentry/receivers/test_transactions.py

@@ -0,0 +1,57 @@
+from __future__ import absolute_import
+
+from exam import fixture
+
+from sentry.models import Project
+from sentry.signals import event_processed
+from sentry.testutils import TestCase
+from sentry.testutils.helpers.datetime import before_now, iso_format
+
+
+class RecordFirstTransactionTest(TestCase):
+    @fixture
+    def min_ago(self):
+        return iso_format(before_now(minutes=1))
+
+    def test_transaction_processed(self):
+        assert not self.project.flags.has_transactions
+        event = self.store_event(
+            data={
+                "type": "transaction",
+                "timestamp": self.min_ago,
+                "start_timestamp": self.min_ago,
+                "contexts": {"trace": {"trace_id": "b" * 32, "span_id": "c" * 16, "op": ""}},
+            },
+            project_id=self.project.id,
+        )
+
+        event_processed.send(project=self.project, event=event, sender=type(self.project))
+        project = Project.objects.get(id=self.project.id)
+        assert project.flags.has_transactions
+
+    def test_transaction_processed_no_platform(self):
+        assert not self.project.flags.has_transactions
+        event = self.store_event(
+            data={
+                "type": "transaction",
+                "platform": None,
+                "timestamp": self.min_ago,
+                "start_timestamp": self.min_ago,
+                "contexts": {"trace": {"trace_id": "b" * 32, "span_id": "c" * 16, "op": ""}},
+            },
+            project_id=self.project.id,
+        )
+
+        event_processed.send(project=self.project, event=event, sender=type(self.project))
+        project = Project.objects.get(id=self.project.id)
+        assert project.flags.has_transactions
+
+    def test_event_processed(self):
+        assert not self.project.flags.has_transactions
+        event = self.store_event(
+            data={"type": "default", "timestamp": self.min_ago}, project_id=self.project.id
+        )
+
+        event_processed.send(project=self.project, event=event, sender=type(self.project))
+        project = Project.objects.get(id=self.project.id)
+        assert not project.flags.has_transactions