Skip to content

Commit aaa4b4d

Browse files
chore(flags): add metrics for events with flags and flag audit log (#83315)
closes getsentry/team-replay#531 adds logging for events w/ feature flags (& number of flags being sent) and audit log posts
1 parent 9915727 commit aaa4b4d

File tree

4 files changed

+56
-12
lines changed

4 files changed

+56
-12
lines changed

src/sentry/flags/endpoints/hooks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from sentry.api.exceptions import ResourceDoesNotExist
1111
from sentry.flags.providers import DeserializationError, get_provider, write
1212
from sentry.models.organization import Organization
13+
from sentry.utils import metrics
1314

1415

1516
@region_silo_endpoint
@@ -33,6 +34,7 @@ def post(self, request: Request, organization: Organization, provider: str) -> R
3334
return Response("Not authorized.", status=401)
3435
else:
3536
write(provider_cls.handle(request.data))
37+
metrics.incr("feature_flags.audit_log_event_posted", tags={"provider": provider})
3638
return Response(status=200)
3739
except DeserializationError as exc:
3840
sentry_sdk.capture_exception()

src/sentry/tasks/post_process.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,8 +1497,12 @@ def check_if_flags_sent(job: PostProcessJob) -> None:
14971497
event = job["event"]
14981498
project = event.project
14991499
flag_context = get_path(event.data, "contexts", "flags")
1500-
if flag_context and not project.flags.has_flags:
1501-
first_flag_received.send_robust(project=project, sender=Project)
1500+
1501+
if flag_context:
1502+
metrics.incr("feature_flags.event_has_flags_context")
1503+
metrics.distribution("feature_flags.num_flags_sent", len(flag_context))
1504+
if not project.flags.has_flags:
1505+
first_flag_received.send_robust(project=project, sender=Project)
15021506

15031507

15041508
GROUP_CATEGORY_POST_PROCESS_PIPELINE = {

tests/sentry/flags/endpoints/test_hooks.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest.mock import call, patch
2+
13
from django.urls import reverse
24

35
from sentry.flags.models import (
@@ -11,6 +13,7 @@
1113
from sentry.utils import json
1214

1315

16+
@patch("sentry.utils.metrics.incr")
1417
class OrganizationFlagsHooksEndpointTestCase(APITestCase):
1518
endpoint = "sentry-api-0-organization-flag-hooks"
1619

@@ -22,7 +25,7 @@ def setUp(self):
2225
def features(self):
2326
return {"organizations:feature-flag-audit-log": True}
2427

25-
def test_generic_post_create(self):
28+
def test_generic_post_create(self, mock_incr):
2629
request_data = {
2730
"data": [
2831
{
@@ -47,9 +50,12 @@ def test_generic_post_create(self):
4750
headers={"X-Sentry-Signature": signature},
4851
)
4952
assert response.status_code == 200, response.content
53+
mock_incr.assert_any_call(
54+
"feature_flags.audit_log_event_posted", tags={"provider": "generic"}
55+
)
5056
assert FlagAuditLogModel.objects.count() == 1
5157

52-
def test_unleash_post_create(self):
58+
def test_unleash_post_create(self, mock_incr):
5359
request_data = {
5460
"id": 28,
5561
"tags": [{"type": "simple", "value": "testvalue"}],
@@ -75,9 +81,12 @@ def test_unleash_post_create(self):
7581
headers={"Authorization": signature},
7682
)
7783
assert response.status_code == 200, response.content
84+
mock_incr.assert_any_call(
85+
"feature_flags.audit_log_event_posted", tags={"provider": "unleash"}
86+
)
7887
assert FlagAuditLogModel.objects.count() == 1
7988

80-
def test_launchdarkly_post_create(self):
89+
def test_launchdarkly_post_create(self, mock_incr):
8190
request_data = LD_REQUEST
8291
signature = hmac_sha256_hex_digest(key="456", message=json.dumps(request_data).encode())
8392

@@ -95,6 +104,9 @@ def test_launchdarkly_post_create(self):
95104
)
96105

97106
assert response.status_code == 200
107+
mock_incr.assert_any_call(
108+
"feature_flags.audit_log_event_posted", tags={"provider": "launchdarkly"}
109+
)
98110
assert FlagAuditLogModel.objects.count() == 1
99111
flag = FlagAuditLogModel.objects.first()
100112
assert flag is not None
@@ -106,13 +118,14 @@ def test_launchdarkly_post_create(self):
106118
assert flag.tags is not None
107119
assert flag.tags["description"] == "flag was created"
108120

109-
def test_launchdarkly_post_create_invalid_signature(self):
121+
def test_launchdarkly_post_create_invalid_signature(self, mock_incr):
110122
with self.feature(self.features):
111123
sig = hmac_sha256_hex_digest(key="123", message=b"456")
112124
response = self.client.post(self.url, LD_REQUEST, headers={"X-LD-Signature": sig})
113125
assert response.status_code == 401
126+
assert call("feature_flags.audit_log_event_posted") not in mock_incr.call_args_list
114127

115-
def test_post_launchdarkly_deserialization_failed(self):
128+
def test_post_launchdarkly_deserialization_failed(self, mock_incr):
116129
signature = hmac_sha256_hex_digest(key="123", message=json.dumps({}).encode())
117130
FlagWebHookSigningSecretModel.objects.create(
118131
organization=self.organization, provider="launchdarkly", secret="123"
@@ -122,22 +135,26 @@ def test_post_launchdarkly_deserialization_failed(self):
122135
response = self.client.post(self.url, {}, headers={"X-LD-Signature": signature})
123136
assert response.status_code == 200
124137
assert FlagAuditLogModel.objects.count() == 0
138+
assert call("feature_flags.audit_log_event_posted") not in mock_incr.call_args_list
125139

126-
def test_post_invalid_provider(self):
140+
def test_post_invalid_provider(self, mock_incr):
127141
url = reverse(self.endpoint, args=(self.organization.slug, "test"))
128142
with self.feature(self.features):
129143
response = self.client.post(url, {})
130144
assert response.status_code == 404
145+
assert call("feature_flags.audit_log_event_posted") not in mock_incr.call_args_list
131146

132-
def test_post_disabled(self):
147+
def test_post_disabled(self, mock_incr):
133148
response = self.client.post(self.url, data={})
134149
assert response.status_code == 404
135150
assert response.content == b'"Not enabled."'
151+
assert call("feature_flags.audit_log_event_posted") not in mock_incr.call_args_list
136152

137-
def test_post_missing_signature(self):
153+
def test_post_missing_signature(self, mock_incr):
138154
with self.feature(self.features):
139155
response = self.client.post(self.url, {})
140156
assert response.status_code == 401, response.content
157+
assert call("feature_flags.audit_log_event_posted") not in mock_incr.call_args_list
141158

142159

143160
LD_REQUEST = {

tests/sentry/tasks/test_post_process.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,14 +2199,33 @@ def test_uptime_detection_no_feature(self):
21992199

22002200

22012201
@patch("sentry.analytics.record")
2202+
@patch("sentry.utils.metrics.incr")
2203+
@patch("sentry.utils.metrics.distribution")
22022204
class CheckIfFlagsSentTestMixin(BasePostProgressGroupMixin):
2203-
def test_set_has_flags(self, mock_record):
2205+
def test_set_has_flags(self, mock_dist, mock_incr, mock_record):
22042206
project = self.create_project()
22052207
event_id = "a" * 32
22062208
event = self.create_event(
2207-
data={"event_id": event_id, "contexts": {"flags": {"values": []}}},
2209+
data={
2210+
"event_id": event_id,
2211+
"contexts": {
2212+
"flags": {
2213+
"values": [
2214+
{
2215+
"flag": "test-flag-1",
2216+
"result": False,
2217+
},
2218+
{
2219+
"flag": "test-flag-2",
2220+
"result": True,
2221+
},
2222+
]
2223+
}
2224+
},
2225+
},
22082226
project_id=project.id,
22092227
)
2228+
22102229
self.call_post_process_group(
22112230
is_new=True,
22122231
is_regression=False,
@@ -2217,6 +2236,8 @@ def test_set_has_flags(self, mock_record):
22172236
project.refresh_from_db()
22182237
assert project.flags.has_flags
22192238

2239+
mock_incr.assert_any_call("feature_flags.event_has_flags_context")
2240+
mock_dist.assert_any_call("feature_flags.num_flags_sent", 2)
22202241
mock_record.assert_called_with(
22212242
"first_flag.sent",
22222243
organization_id=self.organization.id,

0 commit comments

Comments
 (0)