Skip to content

Commit

Permalink
api/view: improve the collect of the Schema1 events
Browse files Browse the repository at this point in the history
Harmonize how we collect we prepare the Schema1 events and how we
do the payload validation and the exception handling.
  • Loading branch information
goneri committed Jun 25, 2024
1 parent 2c3df3b commit 4beada0
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 175 deletions.
122 changes: 122 additions & 0 deletions ansible_ai_connect/ai/api/telemetry/schema1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright Red Hat
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import platform
from enum import Enum

from attr import Factory, asdict, field, frozen
from attrs import define, fields, validators
from django.utils import timezone

from ansible_ai_connect.healthcheck.version_info import VersionInfo
from ansible_ai_connect.users.models import User

version_info = VersionInfo()


@define
class ResponsePayload:
exception: str = field(validator=validators.instance_of(str), converter=str, default="")


@define
class Schema1Event:
event_name: str = "noName"
imageTags: str = field(
validator=validators.instance_of(str), converter=str, default=version_info.image_tags
)
hostname: str = field(
validator=validators.instance_of(str), converter=str, default=platform.node()
)
groups: list[str] = Factory(list)

rh_user_has_seat: bool = False
rh_user_org_id: int | None = None
timestamp = timezone.now().isoformat()
modelName: str = field(validator=validators.instance_of(str), converter=str, default="")
exception: bool = False
response: ResponsePayload | None = ResponsePayload()
user: User | None = None

def set_user(self, user):
self.user = user
self.rh_user_has_seat = user.rh_user_has_seat
self.rh_user_org_id = user.org_id
self.groups = list(user.groups.values_list("name", flat=True))

def set_exception(self, exception):
if exception:
self.exception = True
self.response.exception = str(exception)

def set_validated_data(self, validated_data):
for field, value in validated_data.items():
if hasattr(self, field):
setattr(self, field, value)

def as_dict(self):
# NOTE: The allowed fields should be moved in the event class itslef
def my_filter(a, v):
return not a.name in ["event_name", "user"]

return asdict(self, filter=my_filter)


@define
class ExplainPlaybookSchema1Event(Schema1Event):
event_name: str = "explainPlaybook"
explanationId: str = field(validator=validators.instance_of(str), converter=str, default="")
duration: int = field(validator=validators.instance_of(int), converter=int, default=0)


@define
class CodegenPlaybookSchema1Event(Schema1Event):
event_name: str = "codegenPlaybook"
generationId: str = field(validator=validators.instance_of(str), converter=str, default="")
wizardId: str = field(validator=validators.instance_of(str), converter=str, default="")
playbook_length: int | None = None
duration: int = field(validator=validators.instance_of(int), converter=int, default=0)

def set_validated_data(self, validated_data):
super().set_validated_data(validated_data)
self.playbook_length = len(validated_data.get("playbook", ""))


@define
class ContentMatchSchema1Event(Schema1Event):
event_name: str = "codematch"
request_data: dict | None = None
metadata: list | None = None # TODO

def set_validated_data(self, validated_data):
super().set_validated_data(validated_data)
self.request_data = validated_data


@define
class InlineSuggestionFeedbackSchema1Event(Schema1Event):
event_name: str = "inlineSuggestionFeedback"
latency: float = field(validator=validators.instance_of(float), converter=float, default=0.0)
userActionTime: int = field(validator=validators.instance_of(int), converter=int, default=0)
action: int = field(validator=validators.instance_of(int), converter=int, default=0)
suggestionId: str = field(validator=validators.instance_of(str), converter=str, default="")
activityId: str = field(validator=validators.instance_of(str), converter=str, default="")

def set_validated_data(self, validated_data):
super().set_validated_data(validated_data)
self.latency = validated_data["latency"]
self.userActionTime = validated_data["userActionTime"]
self.suggestionId = validated_data["suggestionId"]
self.activityId = validated_data.get("activityId")
self.action = validated_data["action"]
57 changes: 57 additions & 0 deletions ansible_ai_connect/ai/api/telemetry/test_schema1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright Red Hat
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from unittest import TestCase, mock

from .schema1 import Schema1Event


class TestSchema1Event(TestCase):
def test_set_user(self):
m_user = mock.Mock()
m_user.rh_user_has_seat = True
m_user.org_id = 123
m_user.groups.values_list.return_value = ["mecano"]
validated_data = {}
event1 = Schema1Event()
event1.set_user(m_user)
self.assertEqual(event1.rh_user_has_seat, True)
self.assertEqual(event1.rh_user_org_id, 123)
self.assertEqual(event1.groups, ["mecano"])

def test_as_dict(self):
event1 = Schema1Event()
as_dict = event1.as_dict()

self.assertEqual(as_dict.get("event_name"), None)
self.assertFalse(as_dict.get("exception"), False)

def test_set_exception(self):
event1 = Schema1Event()
try:
1 / 0
except Exception as e:
event1.set_exception(e)
self.assertTrue(event1.exception)
self.assertEqual(event1.response.exception, "division by zero")


class TestInlineSuggestionFeedbackSchema1Event(TestCase):
def test_validated_data(self):
validated_data = {
"latency": 1.1,
"userActionTime": 1,
"action": 123,
"suggestionId": "1e0e1404-5b8a-4d06-829a-dca0d2fff0b5",
}
2 changes: 2 additions & 0 deletions ansible_ai_connect/ai/api/utils/analytics_telemetry_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# Schema2

from enum import Enum

from attr import Factory, field, frozen
Expand Down
8 changes: 8 additions & 0 deletions ansible_ai_connect/ai/api/utils/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ def base_send_segment_event(
send_segment_event(event, "segmentError", user)


def send_schema1_event(event_obj) -> None:
print(event_obj)
if not settings.SEGMENT_WRITE_KEY:
logger.info("segment write key not set, skipping event")
return
base_send_segment_event(event_obj.as_dict(), event_obj.event_name, event_obj.user, analytics)


def redact_seated_users_data(event: Dict[str, Any], allow_list: Dict[str, Any]) -> Dict[str, Any]:
"""
Copy a dictionary to another dictionary using a nested list of allowed keys.
Expand Down
Loading

0 comments on commit 4beada0

Please sign in to comment.