Skip to content

Commit

Permalink
fix: Check for None in user_agent and ip_address
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhi591 authored and rohitesh-wingify committed Sep 17, 2024
1 parent f693474 commit 3245ce5
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 45 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ dist/
coverage/
htmlcov/
.venv/
venv/
ENV*
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ include requirements.txt
include requirements-dev.txt
include LICENSE
recursive-exclude tests *
recursive-include vwo/resources *.json
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -152,5 +152,6 @@ def run(self):
"doc_check": DocCheckCommand,
},
packages=find_packages(exclude=["tests"]),
include_package_data=True,
install_requires=REQUIREMENTS,
)
2 changes: 1 addition & 1 deletion vwo/constants/Constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions vwo/packages/network_layer/manager/network_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")
Expand All @@ -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:
Expand Down
96 changes: 59 additions & 37 deletions vwo/utils/network_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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()
# 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()

0 comments on commit 3245ce5

Please sign in to comment.