Skip to content

Commit 0d8ce8d

Browse files
committed
Set up a Sentry integration via the API
1 parent 6a60b38 commit 0d8ce8d

7 files changed

Lines changed: 233 additions & 0 deletions

File tree

api/environments/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from integrations.mixpanel.views import MixpanelConfigurationViewSet
2020
from integrations.rudderstack.views import RudderstackConfigurationViewSet
2121
from integrations.segment.views import SegmentConfigurationViewSet
22+
from integrations.sentry.views import SentryChangeTrackingConfigurationViewSet
2223
from integrations.slack.views import (
2324
SlackEnvironmentViewSet,
2425
SlackGetChannelsViewSet,
@@ -91,6 +92,11 @@
9192
MixpanelConfigurationViewSet,
9293
basename="integrations-mixpanel",
9394
)
95+
environments_router.register(
96+
r"integrations/sentry",
97+
SentryChangeTrackingConfigurationViewSet,
98+
basename="integrations-sentry",
99+
)
94100
environments_router.register(
95101
r"integrations/slack", SlackEnvironmentViewSet, basename="integrations-slack"
96102
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Generated by Django 4.2.21 on 2025-06-03 22:46
2+
3+
import django.core.validators
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import django_lifecycle.mixins
7+
import uuid
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
initial = True
13+
14+
dependencies = [
15+
("environments", "0037_add_uuid_field"),
16+
]
17+
18+
operations = [
19+
migrations.CreateModel(
20+
name="SentryChangeTrackingConfiguration",
21+
fields=[
22+
(
23+
"id",
24+
models.AutoField(
25+
auto_created=True,
26+
primary_key=True,
27+
serialize=False,
28+
verbose_name="ID",
29+
),
30+
),
31+
(
32+
"deleted_at",
33+
models.DateTimeField(
34+
blank=True,
35+
db_index=True,
36+
default=None,
37+
editable=False,
38+
null=True,
39+
),
40+
),
41+
(
42+
"uuid",
43+
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
44+
),
45+
("base_url", models.URLField(null=True)),
46+
("api_key", models.CharField(max_length=100)),
47+
("webhook_url", models.URLField()),
48+
(
49+
"secret",
50+
models.CharField(
51+
max_length=60,
52+
validators=[
53+
django.core.validators.MinLengthValidator(10),
54+
],
55+
),
56+
),
57+
(
58+
"environment",
59+
models.OneToOneField(
60+
on_delete=django.db.models.deletion.CASCADE,
61+
related_name="sentry_change_tracking_configuration",
62+
to="environments.environment",
63+
),
64+
),
65+
],
66+
options={
67+
"abstract": False,
68+
},
69+
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
70+
),
71+
]

api/integrations/sentry/migrations/__init__.py

Whitespace-only changes.

api/integrations/sentry/models.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from django.core import validators
2+
from django.db import models
3+
4+
from integrations.common.models import EnvironmentIntegrationModel
5+
6+
7+
class SentryChangeTrackingConfiguration(EnvironmentIntegrationModel):
8+
"""
9+
Integration with Sentry feature flags Change Tracking
10+
11+
https://docs.sentry.io/product/issues/issue-details/feature-flags/#change-tracking
12+
"""
13+
14+
environment = models.OneToOneField(
15+
"environments.Environment",
16+
on_delete=models.CASCADE,
17+
related_name="sentry_change_tracking_configuration",
18+
)
19+
20+
webhook_url = models.URLField(
21+
max_length=200,
22+
)
23+
24+
# TODO: Persist hashed secret instead?
25+
secret = models.CharField(
26+
max_length=60,
27+
validators=[
28+
validators.MinLengthValidator(10),
29+
],
30+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from integrations.common.serializers import BaseEnvironmentIntegrationModelSerializer
2+
3+
from .models import SentryChangeTrackingConfiguration
4+
5+
6+
class SentryChangeTrackingConfigurationSerializer(BaseEnvironmentIntegrationModelSerializer):
7+
class Meta:
8+
model = SentryChangeTrackingConfiguration
9+
fields = ["pk", "environment", "webhook_url", "secret"]
10+
read_only_fields = ["pk", "environment"]

api/integrations/sentry/views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from integrations.common.views import EnvironmentIntegrationCommonViewSet
2+
3+
from .models import SentryChangeTrackingConfiguration
4+
from .serializers import SentryChangeTrackingConfigurationSerializer
5+
6+
7+
class SentryChangeTrackingConfigurationViewSet(EnvironmentIntegrationCommonViewSet):
8+
serializer_class = SentryChangeTrackingConfigurationSerializer
9+
model_class = SentryChangeTrackingConfiguration
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import pytest
2+
3+
from environments.models import Environment
4+
from integrations.sentry.models import SentryChangeTrackingConfiguration
5+
6+
from rest_framework.test import APIClient
7+
8+
9+
def test_sentry__change_tracking__setup__accepts_new_configuration(
10+
admin_client: APIClient,
11+
environment: Environment,
12+
):
13+
# Given
14+
pass
15+
16+
# When
17+
url = f"/api/v1/environments/{environment.api_key}/integrations/sentry/"
18+
payload = {
19+
"webhook_url": "https://sentry.example.com/webhook",
20+
"secret": "hush hush!",
21+
}
22+
response = admin_client.post(url, payload, format="json")
23+
24+
# Then
25+
assert response.status_code == 201
26+
assert SentryChangeTrackingConfiguration.objects.filter(
27+
environment=environment,
28+
webhook_url="https://sentry.example.com/webhook",
29+
secret="hush hush!",
30+
).count() == 1
31+
32+
@pytest.mark.parametrize(
33+
"payload, errors",
34+
[
35+
(
36+
{
37+
"secret": "hush hush!",
38+
},
39+
{
40+
"webhook_url": ["This field is required."],
41+
}
42+
),
43+
(
44+
{
45+
"webhook_url": "https://sentry.example.com/webhook",
46+
},
47+
{
48+
"secret": ["This field is required."],
49+
}
50+
),
51+
(
52+
{
53+
"webhook_url": "https://sentry.example.com/webhook",
54+
"secret": "hush!",
55+
},
56+
{
57+
"secret": ["Ensure this field has at least 10 characters."],
58+
}
59+
),
60+
(
61+
{
62+
"webhook_url": "https://sentry.example.com/webhook",
63+
"secret": "Hush, hush, hush, hush; I've already spoken, our love is broken; Baby, hush, hush",
64+
},
65+
{
66+
"secret": ["Ensure this field has no more than 60 characters."]
67+
}
68+
),
69+
(
70+
{
71+
"webhook_url": "https://sentry.example.com/webhook",
72+
"secret": "Hush, hush, hush, hush; I've already spoken, our love is broken; Baby, hush, hush",
73+
},
74+
{
75+
"secret": ["Ensure this field has no more than 60 characters."]
76+
}
77+
),
78+
(
79+
{
80+
"webhook_url": "https://sentry.example.com/webhook",
81+
"secret": "hush hush!",
82+
},
83+
['This integration already exists for this environment.']
84+
)
85+
],
86+
)
87+
def test_sentry__change_tracking__setup__rejects_invalid_configuration(
88+
admin_client: APIClient,
89+
environment: Environment,
90+
errors: dict,
91+
payload: dict,
92+
):
93+
# Given
94+
# Already existing configuration
95+
SentryChangeTrackingConfiguration.objects.create(
96+
environment=environment,
97+
webhook_url="https://sentry.example.com/webhook",
98+
secret="hush hush!",
99+
)
100+
101+
# When
102+
url = f"/api/v1/environments/{environment.api_key}/integrations/sentry/"
103+
response = admin_client.post(url, payload, format="json")
104+
105+
# Then
106+
assert response.status_code == 400
107+
assert response.json() == errors

0 commit comments

Comments
 (0)