Skip to content

Commit

Permalink
AAP-20249: Admin Dashboard: [Feature flag] -M-A-G-I-C- Organizations (#…
Browse files Browse the repository at this point in the history
…814)

* AAP-20249: Admin Dashboard: [Feature flag] -M-A-G-I-C- Organizations

* Updates following peer review.

* More updates.

---------

Co-authored-by: Michael Anstis <[email protected]>
  • Loading branch information
manstis and manstis authored Feb 9, 2024
1 parent ac3c08e commit 8071f4f
Show file tree
Hide file tree
Showing 22 changed files with 321 additions and 119 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,6 @@ ari/kb/rules/

# Generated Grafana files
grafana/*

# Local LaunchDarkly data
./flagdata.json
19 changes: 12 additions & 7 deletions ansible_wisdom/ai/api/telemetry/api_telemetry_settings_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from ai.api.serializers import TelemetrySettingsRequestSerializer
from ai.api.utils.segment import send_segment_event
from ai.api.views import InternalServerError, ServiceUnavailable
from django.conf import settings
from drf_spectacular.utils import OpenApiResponse, extend_schema
from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
from rest_framework.exceptions import ValidationError
Expand Down Expand Up @@ -48,9 +47,6 @@ class TelemetrySettingsView(RetrieveAPIView, CreateAPIView):
def get(self, request, *args, **kwargs):
logger.debug("Telemetry settings:: GET handler")

if not settings.ADMIN_PORTAL_TELEMETRY_OPT_ENABLED:
raise ServiceUnavailable()

exception = None
organization = None
start_time = time.time()
Expand All @@ -61,8 +57,14 @@ def get(self, request, *args, **kwargs):
if not organization:
return Response(status=HTTP_400_BAD_REQUEST)

if not organization.is_schema_2_telemetry_enabled:
raise ServiceUnavailable()

return Response(status=HTTP_200_OK, data={'optOut': organization.telemetry_opt_out})

except ServiceUnavailable:
raise

except Exception as e:
exception = e
logger.exception(e)
Expand Down Expand Up @@ -94,9 +96,6 @@ def get(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs):
logger.debug("Telemetry settings:: POST handler")

if not settings.ADMIN_PORTAL_TELEMETRY_OPT_ENABLED:
raise ServiceUnavailable()

exception = None
organization = None
start_time = time.time()
Expand All @@ -107,6 +106,9 @@ def post(self, request, *args, **kwargs):
if not organization:
return Response(status=HTTP_400_BAD_REQUEST)

if not organization.is_schema_2_telemetry_enabled:
raise ServiceUnavailable()

# Extract Telemetry settings from request
telemetry_settings_serializer = TelemetrySettingsRequestSerializer(data=request.data)
telemetry_settings_serializer.is_valid(raise_exception=True)
Expand All @@ -130,6 +132,9 @@ def post(self, request, *args, **kwargs):
logger.info(e, exc_info=True)
return Response(status=HTTP_400_BAD_REQUEST)

except ServiceUnavailable:
raise

except Exception as e:
exception = e
logger.exception(e)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from http import HTTPStatus
from unittest.mock import patch

import ai.feature_flags as feature_flags
from ai.api.permissions import (
IsOrganisationAdministrator,
IsOrganisationLightspeedSubscriber,
Expand Down Expand Up @@ -36,8 +37,11 @@ def test_permission_classes(self, *args):
for permission in required_permissions:
self.assertTrue(permission in view.permission_classes)

@override_settings(ADMIN_PORTAL_TELEMETRY_OPT_ENABLED=False)
def test_get_settings_when_feature_disabled(self, *args):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
def test_get_settings_when_feature_disabled(self, LDClient, *args):
LDClient.return_value.variation.return_value = False
self.user.organization = Organization.objects.get_or_create(id=123)[0]
self.client.force_authenticate(user=self.user)
r = self.client.get(reverse('telemetry_settings'))
self.assertEqual(r.status_code, HTTPStatus.SERVICE_UNAVAILABLE)
Expand All @@ -52,7 +56,10 @@ def test_get_settings_without_org_id(self, *args):
self.assert_segment_log(log, "telemetrySettingsGet", None)

@override_settings(SEGMENT_WRITE_KEY='DUMMY_KEY_VALUE')
def test_get_settings_when_undefined(self, *args):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
def test_get_settings_when_undefined(self, LDClient, *args):
LDClient.return_value.variation.return_value = True
self.user.organization = Organization.objects.get_or_create(id=123)[0]
self.client.force_authenticate(user=self.user)

Expand All @@ -63,7 +70,10 @@ def test_get_settings_when_undefined(self, *args):
self.assert_segment_log(log, "telemetrySettingsGet", None, opt_out=False)

@override_settings(SEGMENT_WRITE_KEY='DUMMY_KEY_VALUE')
def test_get_settings_when_defined(self, *args):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
def test_get_settings_when_defined(self, LDClient, *args):
LDClient.return_value.variation.return_value = True
self.user.organization = Organization.objects.get_or_create(id=123, telemetry_opt_out=True)[
0
]
Expand All @@ -80,8 +90,11 @@ def test_set_settings_authentication_error(self, *args):
r = self.client.post(reverse('telemetry_settings'))
self.assertEqual(r.status_code, HTTPStatus.UNAUTHORIZED)

@override_settings(ADMIN_PORTAL_TELEMETRY_OPT_ENABLED=False)
def test_set_settings_when_feature_disabled(self, *args):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
def test_set_settings_when_feature_disabled(self, LDClient, *args):
LDClient.return_value.variation.return_value = False
self.user.organization = Organization.objects.get_or_create(id=123)[0]
self.client.force_authenticate(user=self.user)
r = self.client.get(reverse('telemetry_settings'))
self.assertEqual(r.status_code, HTTPStatus.SERVICE_UNAVAILABLE)
Expand All @@ -96,7 +109,10 @@ def test_set_settings_without_org_id(self, *args):
self.assert_segment_log(log, "telemetrySettingsSet", None)

@override_settings(SEGMENT_WRITE_KEY='DUMMY_KEY_VALUE')
def test_set_settings_with_valid_value(self, *args):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
def test_set_settings_with_valid_value(self, LDClient, *args):
LDClient.return_value.variation.return_value = True
self.user.organization = Organization.objects.get_or_create(id=123)[0]
self.client.force_authenticate(user=self.user)

Expand Down Expand Up @@ -129,7 +145,10 @@ def test_set_settings_with_valid_value(self, *args):
self.assertTrue(r.data['optOut'])

@override_settings(SEGMENT_WRITE_KEY='DUMMY_KEY_VALUE')
def test_set_settings_throws_exception(self, *args):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
def test_set_settings_throws_exception(self, LDClient, *args):
LDClient.return_value.variation.return_value = True
self.user.organization = Organization.objects.get_or_create(id=123)[0]
self.client.force_authenticate(user=self.user)

Expand All @@ -144,7 +163,10 @@ def test_set_settings_throws_exception(self, *args):
self.assert_segment_log(log, "telemetrySettingsSet", "DatabaseError", opt_out=False)

@override_settings(SEGMENT_WRITE_KEY='DUMMY_KEY_VALUE')
def test_set_settings_throws_validation_exception(self, *args):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
def test_set_settings_throws_validation_exception(self, LDClient, *args):
LDClient.return_value.variation.return_value = True
self.user.organization = Organization.objects.get_or_create(id=123)[0]
self.client.force_authenticate(user=self.user)

Expand Down
14 changes: 9 additions & 5 deletions ansible_wisdom/ai/api/utils/segment_analytics_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from ai.api.utils.segment import base_send_segment_event, send_segment_event
from attr import asdict
from django.conf import settings
from organizations.models import Organization
from segment.analytics import Client
from users.models import User

Expand All @@ -29,15 +30,18 @@ def send_segment_analytics_event(event_enum, event_payload_supplier, user: User)
if not user.rh_user_has_seat:
logger.info("Skipping analytics telemetry event for users that has no seat.")
return
if not settings.ADMIN_PORTAL_TELEMETRY_OPT_ENABLED:
logger.info("Analytics telemetry not active.")
return
organization = user.organization

organization: Organization = user.organization
if not organization:
logger.info("Analytics telemetry not active, because of no organization assigned for user.")
return

if not organization.is_schema_2_telemetry_enabled:
logger.info(f"Analytics telemetry not active for organization '{organization.id}'.")
return

if organization.telemetry_opt_out:
logger.info("Analytics telemetry not active for organization.")
logger.info(f"Organization '{organization.id}' has opted out of Analytics telemetry.")
return

event_name = event_enum.value
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from unittest.mock import Mock, patch

import ai.feature_flags as feature_flags
from ai.api.tests.test_views import WisdomServiceAPITestCaseBase
from ai.api.utils import segment_analytics_telemetry
from ai.api.utils.analytics_telemetry_model import (
Expand Down Expand Up @@ -74,11 +75,13 @@ def _assert_segment_analytics_error_sent(self, error, send_segment_event):
error_event_payload, "analyticsTelemetryError", self.user
)

@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
@override_settings(SEGMENT_ANALYTICS_WRITE_KEY="testWriteKey")
@override_settings(ADMIN_PORTAL_TELEMETRY_OPT_ENABLED=True)
def test_send_segment_analytics_event(self, base_send_segment_event):
analytics_event_object = AnalyticsProductFeedback("3", 123)
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
def test_send_segment_analytics_event(self, base_send_segment_event, LDClient):
LDClient.return_value.variation.return_value = True
analytics_event_object = AnalyticsProductFeedback(3, 123)
payload = Mock(return_value=analytics_event_object)
send_segment_analytics_event(AnalyticsTelemetryEvents.PRODUCT_FEEDBACK, payload, self.user)
payload.assert_called()
Expand All @@ -89,10 +92,12 @@ def test_send_segment_analytics_event(self, base_send_segment_event):
get_segment_analytics_client(),
)

@patch("ai.api.utils.segment_analytics_telemetry.send_segment_event")
@override_settings(SEGMENT_ANALYTICS_WRITE_KEY="testWriteKey")
@override_settings(ADMIN_PORTAL_TELEMETRY_OPT_ENABLED=True)
def test_send_segment_analytics_event_error_validation(self, send_segment_event):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
@patch("ai.api.utils.segment_analytics_telemetry.send_segment_event")
def test_send_segment_analytics_event_error_validation(self, send_segment_event, LDClient):
LDClient.return_value.variation.return_value = True
payload = Mock(side_effect=ValueError)
send_segment_analytics_event(AnalyticsTelemetryEvents.PRODUCT_FEEDBACK, payload, self.user)
payload.assert_called()
Expand All @@ -107,42 +112,58 @@ def test_send_segment_analytics_event_error_validation(self, send_segment_event)
error_event_payload, "analyticsTelemetryError", self.user
)

@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
@override_settings(ADMIN_PORTAL_TELEMETRY_OPT_ENABLED=True)
def test_send_segment_analytics_event_error_not_write_key(self, base_send_segment_event):
def test_send_segment_analytics_event_error_not_write_key(
self, base_send_segment_event, LDClient
):
LDClient.return_value.variation.return_value = True
self._assert_event_not_sent(base_send_segment_event)

@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
@override_settings(SEGMENT_ANALYTICS_WRITE_KEY="testWriteKey")
@override_settings(ADMIN_PORTAL_TELEMETRY_OPT_ENABLED=True)
def test_send_segment_analytics_event_error_user_no_seat(self, base_send_segment_event):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
def test_send_segment_analytics_event_error_user_no_seat(
self, base_send_segment_event, LDClient
):
LDClient.return_value.variation.return_value = True
self.user.rh_user_has_seat = False
self._assert_event_not_sent(base_send_segment_event)

@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
@override_settings(SEGMENT_ANALYTICS_WRITE_KEY="testWriteKey")
@override_settings(ADMIN_PORTAL_TELEMETRY_OPT_ENABLED=False)
def test_send_segment_analytics_event_error_no_telemetry_enabled(self, base_send_segment_event):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
def test_send_segment_analytics_event_error_no_telemetry_enabled(
self, base_send_segment_event, LDClient
):
LDClient.return_value.variation.return_value = False
self._assert_event_not_sent(base_send_segment_event)

@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
@override_settings(SEGMENT_ANALYTICS_WRITE_KEY="testWriteKey")
@override_settings(ADMIN_PORTAL_TELEMETRY_OPT_ENABLED=True)
def test_send_segment_analytics_event_error_no_org(self, base_send_segment_event):
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
def test_send_segment_analytics_event_error_no_org(self, base_send_segment_event, LDClient):
LDClient.return_value.variation.return_value = True
self.user.organization = None
self._assert_event_not_sent(base_send_segment_event)

@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
@override_settings(SEGMENT_ANALYTICS_WRITE_KEY="testWriteKey")
@override_settings(ADMIN_PORTAL_TELEMETRY_OPT_ENABLED=True)
@override_settings(LAUNCHDARKLY_SDK_KEY='dummy_key')
@patch.object(feature_flags, 'LDClient')
@patch("ai.api.utils.segment_analytics_telemetry.base_send_segment_event")
def test_send_segment_analytics_event_error_no_org_telemetry_enabled(
self, base_send_segment_event
self, base_send_segment_event, LDClient
):
LDClient.return_value.variation.return_value = True
self.user.organization.telemetry_opt_out = True
self._assert_event_not_sent(base_send_segment_event)

def _assert_event_not_sent(self, base_send_segment_event):
payload = Mock(return_value=AnalyticsProductFeedback("3", 123))
payload = Mock(return_value=AnalyticsProductFeedback(3, 123))
send_segment_analytics_event(AnalyticsTelemetryEvents.PRODUCT_FEEDBACK, payload, self.user)
payload.assert_not_called()
base_send_segment_event.assert_not_called()
34 changes: 32 additions & 2 deletions ansible_wisdom/ai/feature_flags.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
import os.path
from enum import Enum
Expand All @@ -13,10 +14,23 @@


class WisdomFlags(str, Enum):
MODEL_NAME = "model_name" # model name selection
# model name selection
MODEL_NAME = "model_name"
# Schema 2 Telemetry is enabled for an Organization
SCHEMA_2_TELEMETRY_ORG_ENABLED = "schema_2_telemetry_org_enabled"


class FeatureFlags:
instance = None

# Ensure FeatureFlags is a Singleton
def __new__(cls):
if cls.instance is not None:
return cls.instance
else:
inst = cls.instance = super().__new__(cls)
return inst

def __init__(self):
self.client = None
if settings.LAUNCHDARKLY_SDK_KEY:
Expand Down Expand Up @@ -60,4 +74,20 @@ def get(self, name: str, user: User, default: str):
logger.debug(f"retrieving feature flag {name}")
return self.client.variation(name, user_context, default)
else:
raise Exception("feature flag client is not initialized")
raise Exception("FeatureFlag client is not initialized")

def check_flag(self, flag: str, query_dict: dict):
"""
Generic LaunchDarkly check
:param flag: The LaunchDarkly 'feature flag' name
:param query_dict: The LaunchDarkly Context attributes.
These must include both 'kind' and 'key'.
:return: The LaunchDarkly response or None
"""
if self.client:
logger.debug(f"Constructing context for '{json.dumps(query_dict)}'")
context = Context.from_dict(query_dict)
logger.debug(f"Retrieving feature flag '{flag}'")
return self.client.variation(flag, context, None)
else:
raise Exception("FeatureFlag client is not initialized")
Loading

1 comment on commit 8071f4f

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClamAV Virus Definition DB Files:
----
total 228116
-rw-r--r--  1 root root 170479789 Feb  9 06:07 main.cvd
-rw-r--r--  1 root root        69 Feb  9 06:07 freshclam.dat
-rw-r--r--  1 root root  62787714 Feb  9 06:07 daily.cvd
-rw-r--r--  1 root root    291965 Feb  9 06:07 bytecode.cvd
drwxr-xr-x 16 root root      4096 Feb  9 19:08 ..
drwxr-xr-x  2 root root      4096 Feb  9 19:08 .
----
File: /var/lib/clamav/bytecode.cvd
Build time: 22 Feb 2023 16:33 -0500
Version: 334
Signatures: 91
Functionality level: 90
Builder: anvilleg
MD5: 0464067a252b1e937012ad34e811065f
Digital signature: urVBCbhJcz8v6i1E6HedDwa8TxBHnJknqg7SE+6JWBtovATpw8MWwS+kvGAi//x5u0LIFwhPvUsgEBBeFiZE0QTTWazOhJ/LfKJK+nODqha6cTvaQdKl2rSbEOv6grv7UONV8eKi383Wv07wfSNYp+lPNpt0QmejKb1TMHAYTA
Verification OK.
----
File: /var/lib/clamav/daily.cvd
Build time: 08 Feb 2024 04:37 -0500
Version: 27179
Signatures: 2052546
Functionality level: 90
Builder: raynman
MD5: cbb23cef7057f140c9d39881269d45d4
Digital signature: losI6iNnjoGlgeB/DN3OLhFJ0edeoD+h6rjAqgVCfrfz//GxEn77PUqrbWeZmS1yI52JmOPm3Mk4A3peu6P5kZUHKMSUFjiUdJy6Qkc2GLGXGIKFsMC5WA7SBlr6PPJuiTTRTaXafFEdJ08DcRW3glATrW4ZRItqjr7KDJJ8qXb
Verification OK.
----
File: /var/lib/clamav/main.cvd
Build time: 16 Sep 2021 08:32 -0400
Version: 62
Signatures: 6647427
Functionality level: 90
Builder: sigmgr
MD5: 137eccce31aacb21b5a98bb8c21cefd6
Digital signature: twaJBls8V5q64R7QY10AatEtPNuPWoVoxTaNO1jpBg7s5jIMMXpitgG1000YLp6rb0TWkEKjRqxneGTxuxWaWm7XBjsgwX2BRWh/y4fhs7uyImdKRLzQ5y8e2EkSChegF/i8clqfn+1qetq9j4gbktJ3JZpOXPoHlyr2Dv9S/Bg
Verification OK.
----
Scanning Results:
ClamAV 1.0.4/27179/Thu Feb  8 09:37:23 2024
LibClamAV Warning: Max file-size was set to 4194304000 bytes. Unfortunately, scanning files greater than 2147483647 bytes (2 GiB - 1) is not supported.

----------- SCAN SUMMARY -----------
Known viruses: 8684414
Engine version: 1.0.4
Scanned directories: 30983
Scanned files: 190211
Infected files: 0
Data scanned: 6493.05 MB
Data read: 3414.78 MB (ratio 1.90:1)
Time: 2542.024 sec (42 m 22 s)
Start Date: 2024:02:09 19:11:10
End Date:   2024:02:09 19:53:32

Please sign in to comment.