|
1 | 1 | import logging |
2 | | -import typing |
| 2 | +from typing import Any, Callable, Literal, Protocol, Type |
3 | 3 |
|
4 | 4 | from django.conf import settings |
5 | 5 | from django.db.models.signals import post_save |
6 | 6 | from django.dispatch import receiver |
7 | 7 |
|
8 | | -from audit.models import AuditLog, RelatedObjectType # type: ignore[attr-defined] |
| 8 | +from audit.models import AuditLog |
| 9 | +from audit.related_object_type import RelatedObjectType |
9 | 10 | from audit.serializers import AuditLogListSerializer |
| 11 | +from audit.services import get_audited_instance_from_audit_log_record |
| 12 | +from features.models import FeatureState |
| 13 | +from features.signals import feature_state_change_went_live |
10 | 14 | from integrations.common.models import IntegrationsModel |
11 | 15 | from integrations.datadog.datadog import DataDogWrapper |
12 | 16 | from integrations.dynatrace.dynatrace import DynatraceWrapper |
13 | 17 | from integrations.grafana.grafana import GrafanaWrapper |
14 | 18 | from integrations.new_relic.new_relic import NewRelicWrapper |
| 19 | +from integrations.sentry.change_tracking import SentryChangeTracking |
| 20 | +from integrations.sentry.models import SentryChangeTrackingConfiguration |
15 | 21 | from integrations.slack.slack import SlackWrapper |
16 | 22 | from organisations.models import OrganisationWebhook |
17 | 23 | from webhooks.tasks import call_organisation_webhooks |
|
20 | 26 | logger = logging.getLogger(__name__) |
21 | 27 |
|
22 | 28 |
|
23 | | -AuditLogIntegrationAttrName = typing.Literal[ |
| 29 | +AuditLogIntegrationAttrName = Literal[ |
24 | 30 | "data_dog_config", |
25 | 31 | "dynatrace_config", |
26 | 32 | "grafana_config", |
|
29 | 35 | ] |
30 | 36 |
|
31 | 37 |
|
| 38 | +class _AuditLogSignalHandler(Protocol): |
| 39 | + def __call__( |
| 40 | + self, |
| 41 | + sender: Type[AuditLog], |
| 42 | + instance: AuditLog, |
| 43 | + **kwargs: Any, |
| 44 | + ) -> None: ... |
| 45 | + |
| 46 | + |
| 47 | +_DecoratedSignal = Callable[[_AuditLogSignalHandler], _AuditLogSignalHandler] |
| 48 | + |
| 49 | + |
32 | 50 | def _get_integration_config( |
33 | 51 | instance: AuditLog, |
34 | 52 | integration_name: AuditLogIntegrationAttrName, |
@@ -71,25 +89,41 @@ def call_webhooks(sender, instance, **kwargs): # type: ignore[no-untyped-def] |
71 | 89 | ) |
72 | 90 |
|
73 | 91 |
|
74 | | -def track_only_feature_related_events(signal_function): # type: ignore[no-untyped-def] |
75 | | - def signal_wrapper(sender, instance, **kwargs): # type: ignore[no-untyped-def] |
76 | | - # Only handle Feature related changes |
77 | | - if instance.related_object_type not in [ |
78 | | - RelatedObjectType.FEATURE.name, |
79 | | - RelatedObjectType.FEATURE_STATE.name, |
80 | | - RelatedObjectType.SEGMENT.name, |
81 | | - RelatedObjectType.EF_VERSION.name, |
82 | | - ]: |
83 | | - return None |
84 | | - return signal_function(sender, instance, **kwargs) |
| 92 | +def track_only(types: list[RelatedObjectType]) -> _DecoratedSignal: |
| 93 | + """ |
| 94 | + Restrict an AuditLog signal to a certain list of RelatedObjectType |
| 95 | + """ |
85 | 96 |
|
86 | | - return signal_wrapper |
| 97 | + def decorator(signal_function: _AuditLogSignalHandler) -> _AuditLogSignalHandler: |
| 98 | + def signal_wrapper( |
| 99 | + sender: Type[AuditLog], |
| 100 | + instance: AuditLog, |
| 101 | + **kwargs: Any, |
| 102 | + ) -> None: |
| 103 | + type_names = (t.name for t in types) |
| 104 | + if instance.related_object_type not in type_names: |
| 105 | + return None |
| 106 | + return signal_function(sender, instance, **kwargs) |
87 | 107 |
|
| 108 | + return signal_wrapper |
88 | 109 |
|
89 | | -def _track_event_async(instance, integration_client): # type: ignore[no-untyped-def] |
90 | | - event_data = integration_client.generate_event_data(audit_log_record=instance) |
| 110 | + return decorator |
91 | 111 |
|
92 | | - integration_client.track_event_async(event=event_data) |
| 112 | + |
| 113 | +def track_only_feature_related_events(signal_function): # type: ignore[no-untyped-def] |
| 114 | + allowed_types = [ |
| 115 | + RelatedObjectType.FEATURE, |
| 116 | + RelatedObjectType.FEATURE_STATE, |
| 117 | + RelatedObjectType.SEGMENT, |
| 118 | + RelatedObjectType.EF_VERSION, |
| 119 | + ] |
| 120 | + return track_only(allowed_types)(signal_function) |
| 121 | + |
| 122 | + |
| 123 | +def _track_event_async(instance, integration_client): # type: ignore[no-untyped-def] |
| 124 | + if event_data := integration_client.generate_event_data(audit_log_record=instance): |
| 125 | + integration_client.track_event_async(event=event_data) |
| 126 | + return |
93 | 127 |
|
94 | 128 |
|
95 | 129 | @receiver(post_save, sender=AuditLog) |
@@ -166,3 +200,34 @@ def send_audit_log_event_to_slack(sender, instance, **kwargs): # type: ignore[n |
166 | 200 | api_token=slack_project_config.api_token, channel_id=env_config.channel_id |
167 | 201 | ) |
168 | 202 | _track_event_async(instance, slack) # type: ignore[no-untyped-call] |
| 203 | + |
| 204 | + |
| 205 | +@receiver(post_save, sender=AuditLog) |
| 206 | +@track_only([RelatedObjectType.FEATURE_STATE]) |
| 207 | +def send_feature_flag_went_live_signal(sender, instance, **kwargs): # type: ignore[no-untyped-def] |
| 208 | + feature_state = get_audited_instance_from_audit_log_record(instance) |
| 209 | + if not isinstance(feature_state, FeatureState): |
| 210 | + return |
| 211 | + |
| 212 | + if feature_state.is_scheduled: |
| 213 | + return # This is handled by audit.tasks.create_feature_state_went_live_audit_log |
| 214 | + |
| 215 | + feature_state_change_went_live.send(instance) |
| 216 | + |
| 217 | + |
| 218 | +@receiver(feature_state_change_went_live) |
| 219 | +def send_audit_log_event_to_sentry(sender: AuditLog, **kwargs: Any) -> None: |
| 220 | + try: |
| 221 | + sentry_configuration = SentryChangeTrackingConfiguration.objects.get( |
| 222 | + environment=sender.environment, |
| 223 | + deleted_at__isnull=True, |
| 224 | + ) |
| 225 | + except SentryChangeTrackingConfiguration.DoesNotExist: |
| 226 | + return |
| 227 | + |
| 228 | + sentry_change_tracking = SentryChangeTracking( |
| 229 | + webhook_url=sentry_configuration.webhook_url, |
| 230 | + secret=sentry_configuration.secret, |
| 231 | + ) |
| 232 | + |
| 233 | + _track_event_async(sender, sentry_change_tracking) # type: ignore[no-untyped-call] |
0 commit comments