Skip to content

Commit

Permalink
Merge 896c582 into 01144fb
Browse files Browse the repository at this point in the history
  • Loading branch information
dcottingham committed May 12, 2021
2 parents 01144fb + 896c582 commit 2e092bc
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 24 deletions.
16 changes: 7 additions & 9 deletions target_decisioning_engine/decision_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# pylint: disable=too-many-statements
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments

try:
from unittest.mock import MagicMock
except ImportError:
Expand All @@ -24,6 +23,7 @@
pass

from delivery_api_client import MboxResponse
from delivery_api_client import TelemetryEntry
from delivery_api_client import MboxRequest
from delivery_api_client import ExecuteResponse
from delivery_api_client import PrefetchResponse
Expand All @@ -41,6 +41,7 @@
from target_decisioning_engine.types.decision_provider_response import DecisionProviderResponse
from target_decisioning_engine.utils import has_remote_dependency
from target_decisioning_engine.utils import get_rule_key
from target_decisioning_engine.notification_provider import NotificationProvider
from target_python_sdk.utils import flatten_list
from target_tools.constants import DEFAULT_GLOBAL_MBOX
from target_tools.logger import get_logger
Expand Down Expand Up @@ -94,10 +95,8 @@ def __init__(self, config, target_options, context, artifact, trace_provider):
rule_evaluator = RuleEvaluator(self.client_id, self.visitor_id)
self.process_rule = rule_evaluator.process_rule
self.dependency = has_remote_dependency(artifact, self.request)
# GA TODO NotificationProvider
# self.notification_provider = NotificationProvider(self.request, self.visitor, self.send_notification_func,
# self.telemetry_enabled)
self.notification_provider = MagicMock()
self.notification_provider = NotificationProvider(self.request, self.visitor, self.send_notification_func,
self.telemetry_enabled)

def _get_decisions(self, mode, post_processors):
"""
Expand Down Expand Up @@ -268,7 +267,7 @@ def _prepare_notification(self, rule, mbox_response, request_type, request_detai
:param tracer: (target_decisioning_engine.trace_provider.RequestTracer) request tracer
:return: (delivery_api_client.Model.mbox_response.MboxResponse)
"""
self.notification_provider.addNotification(mbox_response, tracer.trace_notification(rule))
self.notification_provider.add_notification(mbox_response, tracer.trace_notification(rule))
return mbox_response

def _get_execute_decisions(self, post_processors=None):
Expand Down Expand Up @@ -311,9 +310,8 @@ def run(self):
prefetch=self._get_prefetch_decisions(common_post_processor)
)

self.notification_provider.add_telemetry_entry({
"execution": self.timing_tool.time_end(TIMING_GET_OFFER)
})
telemetry_entry = TelemetryEntry(execution=self.timing_tool.time_end(TIMING_GET_OFFER))
self.notification_provider.add_telemetry_entry(telemetry_entry)
self.notification_provider.send_notifications()
logger.debug("{} - REQUEST: {} /n RESPONSE: {}".format(LOG_TAG, self.request, response))
return response
119 changes: 119 additions & 0 deletions target_decisioning_engine/notification_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Copyright 2021 Adobe. All rights reserved.
# This file is licensed to you 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 REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
"""NotificationProvider class"""
# pylint: disable=too-many-instance-attributes
import threading

from delivery_api_client import Notification
from delivery_api_client import NotificationMbox
from delivery_api_client import MetricType
from delivery_api_client import TelemetryFeatures
from delivery_api_client import DecisioningMethod
from target_decisioning_engine.constants import LOG_PREFIX
from target_python_sdk.utils import get_epoch_time_milliseconds
from target_python_sdk.utils import create_uuid
from target_tools.logger import get_logger
from target_tools.utils import noop

LOG_TAG = "{}.NotificationProvider".format(LOG_PREFIX)


class NotificationProvider:
"""NotificationProvider"""

def __init__(self, request, visitor, send_notification_func=noop, telemetry_enabled=True):
"""
:param request: (delivery_api_client.Model.delivery_request.DeliveryRequest) request
:param visitor: (delivery_api_client.Model.visitor_id.VisitorId) VisitorId instance
:param send_notification_func: (callable) function used to send the notification
:param telemetry_enabled: (bool) is telemetry enabled
"""
self.visitor = visitor
self.send_notification_func = send_notification_func
self.telemetry_enabled = telemetry_enabled
self.request = request
self.request_id = request.request_id
self.prev_event_keys = set()
self.notifications = []
self.telemetry_entries = []
self.logger = get_logger()

def add_notification(self, mbox, trace_func=noop):
"""
:param mbox: (delivery_api_client.Model.mbox_response.MboxResponse) mbox
:param trace_func: (callable) trace function
"""
display_tokens = []
for option in mbox.options:
event_token = option.event_token
event_key = "{}-{}".format(mbox.name, event_token)

if event_token and event_key not in self.prev_event_keys:
display_tokens.append(event_token)
self.prev_event_keys.add(event_key)

if not display_tokens:
return

notification_mbox = NotificationMbox(name=mbox.name)
notification = Notification(id=create_uuid(),
impression_id=create_uuid(),
timestamp=get_epoch_time_milliseconds(),
type=MetricType.DISPLAY,
mbox=notification_mbox,
tokens=display_tokens)
if callable(trace_func):
trace_func(notification)

self.notifications.append(notification)

def add_telemetry_entry(self, entry):
"""
:param entry: (delivery_api_client.Model.telemetry_entry.TelemetryEntry) telemetry entry
"""
if not self.telemetry_enabled:
return

entry.request_id = self.request_id
entry.timestamp = get_epoch_time_milliseconds()
entry.features = TelemetryFeatures(decisioning_method=DecisioningMethod.ON_DEVICE)
self.telemetry_entries.append(entry)

def send_notifications(self):
"""Send notifications via the send_notification_func"""
self.logger.debug("{}.send_notifications - Notifications: {} \nTelemetry Entries: {}"
.format(LOG_TAG, self.notifications, self.telemetry_entries))

if self.notifications or self.telemetry_entries:
_id = self.request.id
context = self.request.context
experience_cloud = self.request.experience_cloud

notification = {
"request": {
"id": _id,
"context": context,
"experienceCloud": experience_cloud
},
"visitor": self.visitor
}

if self.notifications:
notification["request"]["notifications"] = self.notifications

if self.telemetry_entries:
notification["request"]["telemetry"] = {
"entries": self.telemetry_entries
}

async_send = threading.Thread(target=self.send_notification_func, args=(notification,))
async_send.start()
self.notifications = []
self.telemetry_entries = []
26 changes: 13 additions & 13 deletions target_decisioning_engine/tests/test_decisioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,9 @@
# "test": "no_value_for_template"
# }


# GA TODO - remove this once all ODD features have been implemented
EXCLUDE_SUITES = [
"TEST_SUITE_NOTIFICATIONS.json",
"TEST_SUITE_TRACE.json",
"TEST_SUITE_TELEMETRY.json"
"TEST_SUITE_TRACE.json"
]
TEST_SUITES = get_test_suites(JUST_THIS_TEST.get("suite") if JUST_THIS_TEST else None, EXCLUDE_SUITES)

Expand Down Expand Up @@ -135,8 +132,10 @@ def execute(self):
test_data = _test.get("test_data")
send_notifications_fn = Mock()

_input, output, notification_output, mock_date, mock_geo = \
[test_data.get(key) for key in ["input", "output", "notificationOutput", "mockDate", "mockGeo"]]
_input, output, mock_date, mock_geo = \
[test_data.get(key) for key in ["input", "output", "mockDate", "mockGeo"]]

notification_output = test_data.get("notificationOutput", False)

conf = test_data.get("conf") or suite_data.get("conf")
artifact = test_data.get("artifact") or suite_data.get("artifact")
Expand All @@ -153,13 +152,14 @@ def execute(self):
result_dict = to_dict(result)
expect_to_match_object(result_dict, output)

if not notification_output:
self.assertEqual(send_notifications_fn.call_count, 0)
else:
self.assertEqual(send_notifications_fn.call_count, 1)
notification_payload = send_notifications_fn.call_args[0][0]
notification_payload_dict = to_dict(notification_payload)
expect_to_match_object(notification_payload_dict, notification_output)
if notification_output is not False:
if notification_output is None:
self.assertEqual(send_notifications_fn.call_count, 0)
else:
self.assertEqual(send_notifications_fn.call_count, 1)
notification_payload = send_notifications_fn.call_args[0][0]
notification_payload_dict = to_dict(notification_payload)
expect_to_match_object(notification_payload_dict, notification_output)

return execute

Expand Down
171 changes: 171 additions & 0 deletions target_decisioning_engine/tests/test_notification_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Copyright 2021 Adobe. All rights reserved.
# This file is licensed to you 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 REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
"""Test cases for target_decisioning_engine.notification_provider module"""
try:
from unittest.mock import Mock
except ImportError:
from mock import Mock
import unittest
import time
from delivery_api_client import ChannelType
from delivery_api_client import MboxResponse
from delivery_api_client import Option
from delivery_api_client import OptionType
from delivery_api_client import VisitorId
from delivery_api_client import Metric
from delivery_api_client import MetricType
from target_python_sdk.utils import to_dict
from target_python_sdk.tests.delivery_request_setup import create_delivery_request
from target_decisioning_engine.notification_provider import NotificationProvider
from target_decisioning_engine.tests.helpers import expect_to_match_object


TARGET_REQUEST = create_delivery_request({
"context": {
"channel": ChannelType.WEB,
"address": {
"url": "http://local-target-test:8080/"
},
"userAgent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:73.0) Gecko/20100101 Firefox/73.0"
},
"prefetch": {
"mboxes": [
{
"name": "mbox-something",
"index": 1
}
]
}
})
TARGET_REQUEST.id = VisitorId()


class TestNotificationProvider(unittest.TestCase):
"""TestNotificationProvider"""

def setUp(self):
self.mock_notify = Mock()
self.provider = NotificationProvider(TARGET_REQUEST, None, self.mock_notify)

def test_send_notifications_display_type(self):
event_token = "B8C2FP2IuBgmeJcDfXHjGpNWHtnQtQrJfmRrQugEa2qCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q=="
options = [Option(content="<h1>it's firefox</h1>",
type=OptionType.HTML,
event_token=event_token)]
mbox = MboxResponse(options=options, metrics=[], name="browser-mbox")
self.provider.add_notification(mbox)

self.provider.send_notifications()

time.sleep(1)
self.assertEqual(self.mock_notify.call_count, 1)
self.assertEqual(len(self.mock_notify.call_args[0][0]["request"]["notifications"]), 1)
received = to_dict(self.mock_notify.call_args[0][0]["request"]["notifications"][0])
expected = {
"id": "expect.any(String)",
"impressionId": "expect.any(String)",
"timestamp": "expect.any(Number)",
"type": "display",
"mbox": {
"name": "browser-mbox"
},
"tokens": [
"B8C2FP2IuBgmeJcDfXHjGpNWHtnQtQrJfmRrQugEa2qCnQ9Y9OaLL2gsdrWQTvE54PwSz67rmXWmSnkXpSSS2Q=="
]
}
expect_to_match_object(received, expected)

def test_send_notifications_no_duplicates(self):
event_token = "yYWdmhDasVXGPWlpX1TRZDSAQdPpz2XBromX4n+pX9jf5r+rP39VCIaiiZlXOAYq"
content = [{
"type": "insertAfter",
"selector": "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)",
"cssSelector": "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)",
"content":
'<p id="action_insert_15882850825432970">Better to remain silent and be thought a fool \
than to speak out and remove all doubt.</p>'
}]
options = [Option(content=content,
type=OptionType.ACTIONS,
event_token=event_token)]
metrics = [Metric(type=MetricType.CLICK,
event_token="/GMYvcjhUsR6WVqQElppUw==",
selector="#action_insert_15882853393943012")]
mbox = MboxResponse(options=options, metrics=metrics, name="target-global-mbox")
self.provider.add_notification(mbox)
self.provider.add_notification(mbox) # duplicate

self.provider.send_notifications()

time.sleep(1)
self.assertEqual(self.mock_notify.call_count, 1)
self.assertEqual(len(self.mock_notify.call_args[0][0]["request"]["notifications"]), 1)
received = to_dict(self.mock_notify.call_args[0][0]["request"]["notifications"][0])
expected = {
"id": "expect.any(String)",
"impressionId": "expect.any(String)",
"timestamp": "expect.any(Number)",
"type": "display",
"mbox": {
"name": "target-global-mbox"
},
"tokens": [
"yYWdmhDasVXGPWlpX1TRZDSAQdPpz2XBromX4n+pX9jf5r+rP39VCIaiiZlXOAYq"
]
}
expect_to_match_object(received, expected)

def test_send_notifications_distinct_per_mbox(self):
event_token = "yYWdmhDasVXGPWlpX1TRZDSAQdPpz2XBromX4n+pX9jf5r+rP39VCIaiiZlXOAYq"
content = [{
"type": "insertAfter",
"selector": "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)",
"cssSelector": "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)",
"content":
'<p id="action_insert_15882850825432970">Better to remain silent and be thought a fool \
than to speak out and remove all doubt.</p>'
}]
options = [Option(content=content,
type=OptionType.ACTIONS,
event_token=event_token)]
metrics = [Metric(type=MetricType.CLICK,
event_token="/GMYvcjhUsR6WVqQElppUw==",
selector="#action_insert_15882853393943012")]
mbox = MboxResponse(options=options, metrics=metrics, name="my-mbox")
self.provider.add_notification(mbox)

another_mbox = MboxResponse(options=options, metrics=metrics, name="another-mbox")
self.provider.add_notification(another_mbox)

self.provider.send_notifications()

time.sleep(1)
self.assertEqual(self.mock_notify.call_count, 1)
self.assertEqual(len(self.mock_notify.call_args[0][0]["request"]["notifications"]), 2)
first_received = to_dict(self.mock_notify.call_args[0][0]["request"]["notifications"][0])
expected = {
"id": "expect.any(String)",
"impressionId": "expect.any(String)",
"timestamp": "expect.any(Number)",
"type": "display",
"mbox": {},
"tokens": [
"yYWdmhDasVXGPWlpX1TRZDSAQdPpz2XBromX4n+pX9jf5r+rP39VCIaiiZlXOAYq"
]
}
first_expected = dict(expected)
first_expected["mbox"]["name"] = "my-mbox"
expect_to_match_object(first_received, first_expected)

second_received = to_dict(self.mock_notify.call_args[0][0]["request"]["notifications"][1])
second_expected = dict(expected)
second_expected["mbox"]["name"] = "another-mbox"
expect_to_match_object(second_received, second_expected)

0 comments on commit 2e092bc

Please sign in to comment.