From 3245ce5808ae241c6ce66234d3e68a80e0cf69ca Mon Sep 17 00:00:00 2001 From: Abhi591 Date: Tue, 10 Sep 2024 16:38:14 +0530 Subject: [PATCH] fix: Check for None in user_agent and ip_address --- .gitignore | 1 + CHANGELOG.md | 6 ++ MANIFEST.in | 1 + setup.py | 3 +- vwo/constants/Constants.py | 2 +- .../network_layer/manager/network_manager.py | 11 ++- .../evaluators/segment_operand_evaluator.py | 5 +- vwo/utils/network_util.py | 96 ++++++++++++------- 8 files changed, 80 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index e14293b..a10e69a 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ dist/ coverage/ htmlcov/ .venv/ +venv/ ENV* diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e49a1d..c3133bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2024-08-29 + +### Fixed + +- Fix: Check for None values in `user_agent` and `ip_address` when sending impressions to VWO. + ## [1.0.0] - 2024-06-20 ### Added - First release of VWO Feature Management and Experimentation capabilities diff --git a/MANIFEST.in b/MANIFEST.in index 943b0f2..7bcd21d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,4 @@ include requirements.txt include requirements-dev.txt include LICENSE recursive-exclude tests * +recursive-include vwo/resources *.json \ No newline at end of file diff --git a/setup.py b/setup.py index 6d80176..932f872 100644 --- a/setup.py +++ b/setup.py @@ -121,7 +121,7 @@ def run(self): setup( name="vwo-fme-python-sdk", - version="1.0.0", + version="1.1.0", description="VWO Feature Management and Experimentation SDK for Python", long_description=long_description, long_description_content_type="text/markdown", @@ -152,5 +152,6 @@ def run(self): "doc_check": DocCheckCommand, }, packages=find_packages(exclude=["tests"]), + include_package_data=True, install_requires=REQUIREMENTS, ) diff --git a/vwo/constants/Constants.py b/vwo/constants/Constants.py index 3306767..903d49e 100644 --- a/vwo/constants/Constants.py +++ b/vwo/constants/Constants.py @@ -17,7 +17,7 @@ class Constants: # Mock package_file equivalent package_file = { 'name': 'vwo-fme-python-sdk', # Replace with actual package name - 'version': '1.0.0' # Replace with actual package version + 'version': '1.1.0' # Replace with actual package version } # Constants diff --git a/vwo/packages/network_layer/manager/network_manager.py b/vwo/packages/network_layer/manager/network_manager.py index 69b15dd..696f9bf 100644 --- a/vwo/packages/network_layer/manager/network_manager.py +++ b/vwo/packages/network_layer/manager/network_manager.py @@ -75,8 +75,9 @@ async def post_async(self, request: RequestModel): ) as response: return response.status except Exception as err: - # LogManager.get_instance().error(error_messages.get('NETWORK_CALL_FAILED').format( - # method = 'POST', - # err = err.with_traceback() - # )) - return + LogManager.get_instance().error( + error_messages.get('NETWORK_CALL_FAILED').format( + method='POST', + err=err, + ) + ) diff --git a/vwo/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.py b/vwo/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.py index 57f46ad..ac6ead7 100644 --- a/vwo/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.py +++ b/vwo/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.py @@ -23,6 +23,7 @@ from ....enums.url_enum import UrlEnum from ....utils.data_type_util import is_boolean from ....models.user.context_model import ContextModel +from ....services.settings_manager import SettingsManager class SegmentOperandEvaluator: @@ -44,7 +45,7 @@ def evaluate_custom_variable_dsl( return False if "inlist" in operand: - list_id_regex = r"inlist\((\w+:\d+)\)" + list_id_regex = r"inlist\([^)]*\)" match = re.search(list_id_regex, operand) if not match or len(match.groups()) < 1: print("Invalid 'inList' operand format") @@ -56,6 +57,8 @@ def evaluate_custom_variable_dsl( query_params_obj = { "attribute": attribute_value, "listId": list_id, + "accountId": SettingsManager.get_instance().account_id, + "sdkKey": SettingsManager.get_instance().sdk_key, } try: diff --git a/vwo/utils/network_util.py b/vwo/utils/network_util.py index 038600b..6928f7b 100644 --- a/vwo/utils/network_util.py +++ b/vwo/utils/network_util.py @@ -193,18 +193,43 @@ def get_attribute_payload_data( return properties -# Function to send a POST API request + +# Global variables for event loop management +# `event_loop_initialized` tracks whether the event loop has been initialized. +# `main_event_loop` stores the reference to the main event loop that handles async tasks. +# `loop_lock` ensures that only one thread can initialize or use the event loop at a time. +event_loop_initialized = False +main_event_loop = None +loop_lock = threading.Lock() + +# Function to send a POST API request without waiting for the response def send_post_api_request(properties: Dict[str, Any], payload: Dict[str, Any]): - url = f"{Constants.HTTPS_PROTOCOL}://{UrlService.get_base_url()}{UrlEnum.EVENTS.value}" + global event_loop_initialized, main_event_loop + + # Importing the SettingsManager here to avoid circular import issues or unnecessary imports from ..services.settings_manager import SettingsManager - headers = { - HeadersEnum.USER_AGENT.value: payload['d'].get('visitor_ua', ''), - HeadersEnum.IP.value: payload['d'].get('visitor_ip', '') - } + # Initialize the headers dictionary for the request + headers = {} + + # Retrieve 'visitor_ua' and 'visitor_ip' from the payload if they exist + # Strip any whitespace and ensure they are valid strings before adding to headers + visitor_ua = payload['d'].get('visitor_ua') + visitor_ip = payload['d'].get('visitor_ip') + + # Add 'visitor_ua' to headers if it's a valid, non-empty string after stripping whitespace + if visitor_ua and isinstance(visitor_ua, str) and visitor_ua.strip(): + headers[HeadersEnum.USER_AGENT.value] = visitor_ua.strip() + + # Add 'visitor_ip' to headers if it's a valid, non-empty string after stripping whitespace + if visitor_ip and isinstance(visitor_ip, str) and visitor_ip.strip(): + headers[HeadersEnum.IP.value] = visitor_ip.strip() try: + # Get the instance of NetworkManager that handles making network requests network_instance = NetworkManager.get_instance() + + # Create a RequestModel object that holds all the necessary data for the POST request request = RequestModel( UrlService.get_base_url(), 'POST', @@ -216,40 +241,37 @@ def send_post_api_request(properties: Dict[str, Any], payload: Dict[str, Any]): SettingsManager.get_instance().port ) - # Attempt to get the running loop - try: - loop = asyncio.get_event_loop() - except RuntimeError: - loop = None - - if loop and loop.is_running(): - # If an event loop is running, schedule the task safely - asyncio.run_coroutine_threadsafe(network_instance.post_async(request), loop) - else: - # If no event loop is running, run it in a new detached thread - run_in_thread(network_instance.post_async(request)) - - return - + # Lock the event loop initialization to prevent race conditions in multi-threaded environments + with loop_lock: + # Check if the event loop is already initialized and running + if event_loop_initialized and main_event_loop.is_running(): + # If the loop is running, submit the asynchronous POST request to the loop + # This will not block the main thread + asyncio.run_coroutine_threadsafe(network_instance.post_async(request), main_event_loop) + else: + # If the event loop has not been initialized or is not running: + # 1. Mark the event loop as initialized + # 2. Create a new event loop + # 3. Start the event loop in a separate thread so it doesn't block the main thread + event_loop_initialized = True + main_event_loop = asyncio.new_event_loop() + threading.Thread(target=start_event_loop, args=(main_event_loop,), daemon=True).start() + + # Submit the asynchronous POST request to the newly started event loop + asyncio.run_coroutine_threadsafe(network_instance.post_async(request), main_event_loop) + except Exception as err: LogManager.get_instance().error( error_messages.get('NETWORK_CALL_FAILED').format( - method = 'POST', - err = err, + method='POST', + err=err, ), ) -# Function to run an asyncio event loop in a separate thread -def run_in_thread(coro): - def run(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - loop.run_until_complete(coro) - finally: - loop.run_until_complete(loop.shutdown_asyncgens()) - loop.close() - - thread = threading.Thread(target=run) - thread.daemon = True # Ensure the thread does not block program exit - thread.start() \ No newline at end of file +# Function to start the event loop in a new thread +def start_event_loop(loop): + # Set the provided loop as the current event loop for the new thread + asyncio.set_event_loop(loop) + + # Run the event loop indefinitely to handle any submitted asynchronous tasks + loop.run_forever() \ No newline at end of file