Browse Source

ref: Add TypedBitfield (#51700)

Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Markus Unterwaditzer 1 year ago
parent
commit
e58a384de7

+ 1 - 1
migrations_lockfile.txt

@@ -6,5 +6,5 @@ To resolve this, rebase against latest master and regenerate your migration. Thi
 will then be regenerated, and you should be able to merge without conflicts.
 
 nodestore: 0002_nodestore_no_dictfield
-sentry: 0497_add_comment_reactions_column
+sentry: 0498_typed_bitfield
 social_auth: 0001_initial

+ 0 - 10
pyproject.toml

@@ -294,9 +294,7 @@ module = [
     "sentry.api.endpoints.project_group_index",
     "sentry.api.endpoints.project_group_stats",
     "sentry.api.endpoints.project_index",
-    "sentry.api.endpoints.project_key_details",
     "sentry.api.endpoints.project_key_stats",
-    "sentry.api.endpoints.project_keys",
     "sentry.api.endpoints.project_ownership",
     "sentry.api.endpoints.project_release_files",
     "sentry.api.endpoints.project_release_stats",
@@ -751,7 +749,6 @@ module = [
     "sentry.models.processingissue",
     "sentry.models.project",
     "sentry.models.projectcodeowners",
-    "sentry.models.projectkey",
     "sentry.models.projectownership",
     "sentry.models.release",
     "sentry.models.releasefile",
@@ -820,7 +817,6 @@ module = [
     "sentry.quotas.redis",
     "sentry.ratelimits.utils",
     "sentry.receivers.core",
-    "sentry.receivers.onboarding",
     "sentry.receivers.outbox",
     "sentry.receivers.outbox.control",
     "sentry.receivers.releases",
@@ -905,7 +901,6 @@ module = [
     "sentry.services.hybrid_cloud.notifications.impl",
     "sentry.services.hybrid_cloud.organization.impl",
     "sentry.services.hybrid_cloud.organizationmember_mapping.impl",
-    "sentry.services.hybrid_cloud.project_key.model",
     "sentry.services.hybrid_cloud.rpc",
     "sentry.services.hybrid_cloud.user.impl",
     "sentry.services.smtp",
@@ -1125,11 +1120,6 @@ module = [
     "tests.acceptance.chartcuterie.test_chart_renderer",
     "tests.acceptance.test_accept_organization_invite",
     "tests.acceptance.test_organization_dashboards",
-    "tests.acceptance.test_performance_landing",
-    "tests.acceptance.test_performance_overview",
-    "tests.acceptance.test_performance_trends",
-    "tests.acceptance.test_performance_vital_detail",
-    "tests.acceptance.test_replay_list",
     "tests.acceptance.test_shared_issue",
     "tests.integration.test_api",
     "tests.relay_integration.lang.java.test_plugin",

+ 2 - 2
src/bitfield/__init__.py

@@ -1,6 +1,6 @@
-from bitfield.models import BitField  # NOQA
+from bitfield.models import BitField, TypedBitfield  # NOQA
 from bitfield.types import Bit, BitHandler
 
 default_app_config = "bitfield.apps.BitFieldAppConfig"
 
-__all__ = ("Bit", "BitField", "BitHandler", "default_app_config")
+__all__ = ("Bit", "BitField", "BitHandler", "default_app_config", "TypedBitfield")

+ 30 - 0
src/bitfield/models.py

@@ -148,4 +148,34 @@ class BitField(BigIntegerField):
         return name, path, args, kwargs
 
 
+class TypedBitfieldMeta(type):
+    def __new__(cls, name, bases, clsdict):
+        if name == "TypedBitfield":
+            return type.__new__(cls, name, bases, clsdict)
+
+        flags = []
+        for attr, ty in clsdict["__annotations__"].items():
+            if attr.startswith("_"):
+                continue
+
+            if attr in ("bitfield_default", "bitfield_null"):
+                continue
+
+            assert ty in ("bool", bool), f"bitfields can only hold bools, {attr} is {ty!r}"
+            flags.append(attr)
+
+        return BitField(
+            flags=flags,
+            default=clsdict.get("bitfield_default"),
+            null=clsdict.get("bitfield_null"),
+        )
+
+    def __int__(self) -> int:
+        raise NotImplementedError()
+
+
+class TypedBitfield(metaclass=TypedBitfieldMeta):
+    pass
+
+
 BitField.register_lookup(BitQueryExactLookupStub)

+ 60 - 0
src/sentry/migrations/0498_typed_bitfield.py

@@ -0,0 +1,60 @@
+# Generated by Django 2.2.28 on 2023-06-27 17:01
+
+from django.db import migrations
+
+import bitfield.models
+from sentry.new_migrations.migrations import CheckedMigration
+
+
+class Migration(CheckedMigration):
+    # This flag is used to mark that a migration shouldn't be automatically run in production. For
+    # the most part, this should only be used for operations where it's safe to run the migration
+    # after your code has deployed. So this should not be used for most operations that alter the
+    # schema of a table.
+    # Here are some things that make sense to mark as dangerous:
+    # - Large data migrations. Typically we want these to be run manually by ops so that they can
+    #   be monitored and not block the deploy for a long period of time while they run.
+    # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
+    #   have ops run this and not block the deploy. Note that while adding an index is a schema
+    #   change, it's completely safe to run the operation after the code has deployed.
+    is_dangerous = False
+
+    dependencies = [
+        ("sentry", "0497_add_comment_reactions_column"),
+    ]
+
+    operations = [
+        migrations.SeparateDatabaseAndState(
+            database_operations=[],
+            state_operations=[
+                migrations.AlterField(
+                    model_name="project",
+                    name="flags",
+                    field=bitfield.models.BitField(
+                        [
+                            "has_releases",
+                            "has_issue_alerts_targeting",
+                            "has_transactions",
+                            "has_alert_filters",
+                            "has_sessions",
+                            "has_profiles",
+                            "has_replays",
+                            "spike_protection_error_currently_active",
+                            "spike_protection_transaction_currently_active",
+                            "spike_protection_attachment_currently_active",
+                            "has_minified_stack_trace",
+                            "has_cron_monitors",
+                            "has_cron_checkins",
+                        ],
+                        default=10,
+                        null=True,
+                    ),
+                ),
+                migrations.AlterField(
+                    model_name="projectkey",
+                    name="roles",
+                    field=bitfield.models.BitField(["store", "api"], default=1, null=None),
+                ),
+            ],
+        )
+    ]

+ 40 - 26
src/sentry/models/project.py

@@ -16,7 +16,7 @@ from django.utils import timezone
 from django.utils.http import urlencode
 from django.utils.translation import ugettext_lazy as _
 
-from bitfield import BitField
+from bitfield import TypedBitfield
 from sentry import projectoptions
 from sentry.constants import RESERVED_PROJECT_SLUGS, ObjectStatus
 from sentry.db.mixin import PendingDeletionMixin, delete_pending_deletion_option
@@ -138,31 +138,45 @@ class Project(Model, PendingDeletionMixin, OptionMixin, SnowflakeIdMixin):
     # projects that were created before this field was present
     # will have their first_event field set to date_added
     first_event = models.DateTimeField(null=True)
-    flags = BitField(
-        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"),
-            ("has_alert_filters", "This Project has filters"),
-            ("has_sessions", "This Project has sessions"),
-            ("has_profiles", "This Project has sent profiles"),
-            ("has_replays", "This Project has sent replays"),
-            ("spike_protection_error_currently_active", "spike_protection_error_currently_active"),
-            (
-                "spike_protection_transaction_currently_active",
-                "spike_protection_transaction_currently_active",
-            ),
-            (
-                "spike_protection_attachment_currently_active",
-                "spike_protection_attachment_currently_active",
-            ),
-            ("has_minified_stack_trace", "This Project has event with minified stack trace"),
-            ("has_cron_monitors", "This Project has cron monitors"),
-            ("has_cron_checkins", "This Project has sent check-ins"),
-        ),
-        default=10,
-        null=True,
-    )
+
+    class flags(TypedBitfield):
+        # This Project has sent release data
+        has_releases: bool
+        # This Project has issue alerts targeting
+        has_issue_alerts_targeting: bool
+
+        # This Project has sent transactions
+        has_transactions: bool
+
+        # This Project has filters
+        has_alert_filters: bool
+
+        # This Project has sessions
+        has_sessions: bool
+
+        # This Project has sent profiles
+        has_profiles: bool
+
+        # This Project has sent replays
+        has_replays: bool
+
+        # spike_protection_error_currently_active
+        spike_protection_error_currently_active: bool
+
+        spike_protection_transaction_currently_active: bool
+        spike_protection_attachment_currently_active: bool
+
+        # This Project has event with minified stack trace
+        has_minified_stack_trace: bool
+
+        # This Project has cron monitors
+        has_cron_monitors: bool
+
+        # This Project has sent check-ins
+        has_cron_checkins: bool
+
+        bitfield_default = 10
+        bitfield_null = True
 
     objects = ProjectManager(cache_fields=["pk"])
     platform = models.CharField(max_length=64, null=True)

+ 11 - 10
src/sentry/models/projectkey.py

@@ -9,7 +9,7 @@ from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 
-from bitfield import BitField
+from bitfield import TypedBitfield
 from sentry import features, options
 from sentry.db.models import (
     BaseManager,
@@ -52,15 +52,15 @@ class ProjectKey(Model):
     label = models.CharField(max_length=64, blank=True, null=True)
     public_key = models.CharField(max_length=32, unique=True, null=True)
     secret_key = models.CharField(max_length=32, unique=True, null=True)
-    roles = BitField(
-        flags=(
-            # access to post events to the store endpoint
-            ("store", "Event API access"),
-            # read/write access to rest API
-            ("api", "Web API access"),
-        ),
-        default=["store"],
-    )
+
+    class roles(TypedBitfield):
+        # access to post events to the store endpoint
+        store: bool
+        # read/write access to rest API
+        api: bool
+
+        bitfield_default = ["store"]
+
     status = BoundedPositiveIntegerField(
         default=0,
         choices=(
@@ -159,6 +159,7 @@ class ProjectKey(Model):
         if not public:
             key = f"{self.public_key}:{self.secret_key}"
         else:
+            assert self.public_key is not None
             key = self.public_key
 
         # If we do not have a scheme or domain/hostname, dsn is never valid