Skip to content

Commit 9025700

Browse files
committed
feat(uptime): Backfill dual fingerprints into grouphashes table.
We started dual writing these in #91087. This backfills both fingerprints into `GroupHash` so that we can stop relying on the old ones.
1 parent db53f8b commit 9025700

File tree

3 files changed

+205
-6
lines changed

3 files changed

+205
-6
lines changed

src/sentry/testutils/cases.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3140,12 +3140,6 @@ def tearDown(self):
31403140
self.mock_resolve_rdap_provider_ctx.__exit__(None, None, None)
31413141
self.mock_requests_get_ctx.__exit__(None, None, None)
31423142

3143-
3144-
class _OptionalCheckResult(TypedDict, total=False):
3145-
region: str
3146-
3147-
3148-
class UptimeTestCase(UptimeTestCaseMixin, TestCase):
31493143
def create_uptime_result(
31503144
self,
31513145
subscription_id: str | None = None,
@@ -3178,6 +3172,14 @@ def create_uptime_result(
31783172
}
31793173

31803174

3175+
class _OptionalCheckResult(TypedDict, total=False):
3176+
region: str
3177+
3178+
3179+
class UptimeTestCase(UptimeTestCaseMixin, TestCase):
3180+
pass
3181+
3182+
31813183
class IntegratedApiTestCase(BaseTestCase):
31823184
def should_call_api_without_proxying(self) -> bool:
31833185
return not IntegrationProxyClient.determine_whether_should_proxy_to_control()
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Generated by Django 5.1.7 on 2025-05-06 22:16
2+
from hashlib import md5
3+
4+
from django.db import migrations
5+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
6+
from django.db.migrations.state import StateApps
7+
8+
from sentry.new_migrations.migrations import CheckedMigration
9+
from sentry.utils.query import RangeQuerySetWrapperWithProgressBar
10+
11+
DATA_SOURCE_UPTIME_SUBSCRIPTION = "uptime_subscription"
12+
13+
14+
def get_project_subscription(ProjectUptimeSubscription, detector):
15+
data_source = detector.data_sources.first()
16+
return ProjectUptimeSubscription.objects.get(uptime_subscription_id=int(data_source.source_id))
17+
18+
19+
def build_detector_fingerprint_component(detector) -> str:
20+
return f"uptime-detector:{detector.id}"
21+
22+
23+
def build_subscription_fingerprint_component(subscription_id) -> str:
24+
return str(subscription_id)
25+
26+
27+
def hash_fingerprint(fingerprint: str) -> str:
28+
return md5(fingerprint.encode("utf-8")).hexdigest()
29+
30+
31+
def update_auto_detected_active_interval_seconds(
32+
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
33+
) -> None:
34+
GroupHash = apps.get_model("sentry", "GroupHash")
35+
DataSource = apps.get_model("workflow_engine", "DataSource")
36+
ProjectUptimeSubscription = apps.get_model("uptime", "ProjectUptimeSubscription")
37+
38+
for data_source in RangeQuerySetWrapperWithProgressBar(
39+
DataSource.objects.filter(type=DATA_SOURCE_UPTIME_SUBSCRIPTION)
40+
):
41+
uptime_subscription_id = int(data_source.source_id)
42+
project_subscription_id = ProjectUptimeSubscription.objects.get(
43+
uptime_subscription_id=uptime_subscription_id
44+
).id
45+
detector = data_source.detectors.first()
46+
47+
detector_fingerprint_hash = hash_fingerprint(build_detector_fingerprint_component(detector))
48+
sub_fingerprint_hash = hash_fingerprint(
49+
build_subscription_fingerprint_component(project_subscription_id)
50+
)
51+
group_hashes = list(
52+
GroupHash.objects.filter(
53+
project=detector.project,
54+
hash__in=[detector_fingerprint_hash, sub_fingerprint_hash],
55+
)
56+
)
57+
# If we have 0 or 2 group hashes then we don't need to backfill anything. Either the group doesn't exist
58+
# at all for this detector, or both hashes are already associated
59+
if len(group_hashes) != 1:
60+
continue
61+
group_hash = group_hashes[0]
62+
# If the only hash that exists is for the detector fingerprint then we don't have any work to do
63+
if group_hash.hash == detector_fingerprint_hash:
64+
continue
65+
66+
GroupHash.objects.get_or_create(
67+
project=detector.project,
68+
hash=detector_fingerprint_hash,
69+
defaults={
70+
"group": group_hash.group,
71+
},
72+
)
73+
74+
75+
class Migration(CheckedMigration):
76+
# This flag is used to mark that a migration shouldn't be automatically run in production.
77+
# This should only be used for operations where it's safe to run the migration after your
78+
# code has deployed. So this should not be used for most operations that alter the schema
79+
# of a table.
80+
# Here are some things that make sense to mark as post deployment:
81+
# - Large data migrations. Typically we want these to be run manually so that they can be
82+
# monitored and not block the deploy for a long period of time while they run.
83+
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
84+
# run this outside deployments so that we don't block them. Note that while adding an index
85+
# is a schema change, it's completely safe to run the operation after the code has deployed.
86+
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
87+
88+
is_post_deployment = True
89+
90+
dependencies = [
91+
("uptime", "0039_uptime_drop_project_subscription_uptime_status_db"),
92+
]
93+
94+
operations = [
95+
migrations.RunPython(
96+
update_auto_detected_active_interval_seconds,
97+
migrations.RunPython.noop,
98+
hints={"tables": ["sentry_grouphash"]},
99+
)
100+
]
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from hashlib import md5
2+
3+
from sentry.models.group import Group
4+
from sentry.models.grouphash import GroupHash
5+
from sentry.testutils.cases import TestMigrations, UptimeTestCaseMixin
6+
from sentry.uptime.grouptype import UptimeDomainCheckFailure
7+
from sentry.uptime.issue_platform import (
8+
build_detector_fingerprint_component,
9+
create_issue_platform_occurrence,
10+
)
11+
from sentry.uptime.types import DATA_SOURCE_UPTIME_SUBSCRIPTION, ProjectUptimeSubscriptionMode
12+
from sentry.workflow_engine.models import DataSource, DataSourceDetector
13+
14+
15+
class TestUptimeBackfillDetectorGrouphash(TestMigrations, UptimeTestCaseMixin):
16+
app = "uptime"
17+
migrate_from = "0039_uptime_drop_project_subscription_uptime_status_db"
18+
migrate_to = "0040_uptime_backfill_detector_grouphash"
19+
20+
def setup_before_migration(self, apps):
21+
self.proj_sub_no_hash = self.create_project_uptime_subscription(project=self.project)
22+
self.proj_sub_one_hash = self.create_project_uptime_subscription(project=self.project)
23+
self.proj_sub_both_hash = self.create_project_uptime_subscription(project=self.project)
24+
25+
DataSource.objects.all().delete()
26+
self.data_source_no_hash = self.create_data_source(
27+
type=DATA_SOURCE_UPTIME_SUBSCRIPTION,
28+
source_id=str(self.proj_sub_no_hash.uptime_subscription_id),
29+
)
30+
self.detector_no_hash = self.create_detector(
31+
type=UptimeDomainCheckFailure.slug,
32+
config={"mode": ProjectUptimeSubscriptionMode.MANUAL, "environment": None},
33+
project=self.project,
34+
)
35+
DataSourceDetector.objects.create(
36+
data_source=self.data_source_no_hash, detector=self.detector_no_hash
37+
)
38+
39+
self.data_source_one_hash = self.create_data_source(
40+
type=DATA_SOURCE_UPTIME_SUBSCRIPTION,
41+
source_id=str(self.proj_sub_one_hash.uptime_subscription_id),
42+
)
43+
self.detector_one_hash = self.create_detector(
44+
type=UptimeDomainCheckFailure.slug,
45+
config={"mode": ProjectUptimeSubscriptionMode.MANUAL, "environment": None},
46+
project=self.project,
47+
)
48+
DataSourceDetector.objects.create(
49+
data_source=self.data_source_one_hash, detector=self.detector_one_hash
50+
)
51+
52+
self.data_source_both_hash = self.create_data_source(
53+
type=DATA_SOURCE_UPTIME_SUBSCRIPTION,
54+
source_id=str(self.proj_sub_both_hash.uptime_subscription_id),
55+
)
56+
self.detector_both_hash = self.create_detector(
57+
type=UptimeDomainCheckFailure.slug,
58+
config={"mode": ProjectUptimeSubscriptionMode.MANUAL, "environment": None},
59+
project=self.project,
60+
)
61+
DataSourceDetector.objects.create(
62+
data_source=self.data_source_both_hash, detector=self.detector_both_hash
63+
)
64+
65+
with self.tasks(), self.feature(UptimeDomainCheckFailure.build_ingest_feature_name()):
66+
create_issue_platform_occurrence(self.create_uptime_result(), self.detector_both_hash)
67+
create_issue_platform_occurrence(self.create_uptime_result(), self.detector_one_hash)
68+
69+
# Remove the hash for the detector
70+
self.group_one_hash = Group.objects.last()
71+
GroupHash.objects.filter(
72+
group=self.group_one_hash,
73+
hash=md5(
74+
build_detector_fingerprint_component(self.detector_one_hash).encode("utf-8")
75+
).hexdigest(),
76+
).delete()
77+
78+
def test(self):
79+
assert GroupHash.objects.filter(
80+
group=self.group_one_hash,
81+
hash=md5(
82+
build_detector_fingerprint_component(self.detector_one_hash).encode("utf-8")
83+
).hexdigest(),
84+
).exists()
85+
assert not GroupHash.objects.filter(
86+
project=self.detector_no_hash.project,
87+
hash=md5(
88+
build_detector_fingerprint_component(self.detector_no_hash).encode("utf-8")
89+
).hexdigest(),
90+
).exists()
91+
both_group = GroupHash.objects.get(
92+
project=self.detector_both_hash.project,
93+
hash=md5(
94+
build_detector_fingerprint_component(self.detector_both_hash).encode("utf-8")
95+
).hexdigest(),
96+
).group
97+
assert GroupHash.objects.filter(group=both_group).count() == 2

0 commit comments

Comments
 (0)