-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
312 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
target_decisioning_engine/tests/test_notification_provider.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.