Skip to content

Commit 98e27ed

Browse files
sakshamg1304rohitesh-wingify
authored andcommitted
feat: send health check event
1 parent 5fcb1e7 commit 98e27ed

File tree

12 files changed

+186
-48
lines changed

12 files changed

+186
-48
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [1.11.0] - 2025-07-21
8+
9+
### Added
10+
11+
- Added support for sending a one-time initialization event to the server to verify correct SDK setup.
12+
713
## [1.10.1] - 2025-07-24
814

915
### Added
1016

1117
- Send the SDK name and version in the settings call to VWO as query parameters.
1218

13-
1419
## [1.10.0] - 2025-07-02
1520

1621
### Added

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def run(self):
121121

122122
setup(
123123
name="vwo-fme-python-sdk",
124-
version="1.10.1",
124+
version="1.11.0",
125125
description="VWO Feature Management and Experimentation SDK for Python",
126126
long_description=long_description,
127127
long_description_content_type="text/markdown",

vwo/constants/Constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Constants:
1717
# TODO: read from setup.py
1818
sdk_meta = {
1919
"name": "vwo-fme-python-sdk",
20-
"version": "1.10.1"
20+
"version": "1.11.0"
2121
}
2222

2323
# Constants

vwo/enums/event_enum.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@
1919
class EventEnum(Enum):
2020
VWO_VARIATION_SHOWN = "vwo_variationShown"
2121
VWO_SYNC_VISITOR_PROP = "vwo_syncVisitorProp"
22-
VWO_LOG_EVENT = "vwo_log"
22+
VWO_LOG_EVENT = "vwo_log"
23+
VWO_SDK_INIT_EVENT = "vwo_fmeSdkInit"

vwo/packages/network_layer/client/network_client.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,7 @@ def post(self, request_model: RequestModel) -> ResponseModel:
119119

120120
if response.status_code < 200 or response.status_code >= 300:
121121
raise requests.HTTPError(f"HTTP {response.status_code} error")
122-
123-
LogManager.get_instance().info(
124-
info_messages.get("NETWORK_CALL_SUCCESS").format(
125-
event=request_model.get_query().get("en"),
126-
endPoint=options["url"].split("?")[0],
127-
accountId=request_model.get_query().get("a"),
128-
userId=request_model.get_user_id(),
129-
uuid=request_model.get_body().get("d").get("visId"),
130-
)
131-
)
122+
132123
return response_model
133124

134125
except (

vwo/resources/error-messages.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,7 @@
2828
"BATCH_FLUSH_ERROR": "Error occurred while sending batch events. Error:{error}",
2929

3030
"NETWORK_CALL_RETRY_ATTEMPT": "Request failed for {endPoint}, Error: {err}. Retrying in {delay} seconds, attempt {attempt} of {maxRetries}",
31-
"NETWORK_CALL_RETRY_FAILED": "Max retries reached. Request failed for {endPoint}, Error: {err}"
31+
"NETWORK_CALL_RETRY_FAILED": "Max retries reached. Request failed for {endPoint}, Error: {err}",
32+
33+
"SDK_INIT_EVENT_FAILED": "Error occurred while sending SDK init event. Error:{err}"
3234
}

vwo/services/settings_manager.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
# limitations under the License.
1414

1515

16+
from typing import Any, Dict
17+
import random
1618
from ..packages.network_layer.manager.network_manager import NetworkManager
17-
from ..utils.network_util import get_settings_path
1819
from ..packages.network_layer.models.request_model import RequestModel
1920
from ..constants.Constants import Constants
2021
from ..packages.logger.core.log_manager import LogManager
@@ -23,6 +24,7 @@
2324
import json
2425
import requests
2526
import jsonschema
27+
import time
2628

2729

2830
class SettingsManager:
@@ -34,6 +36,8 @@ def __init__(self, options):
3436
self.expiry = Constants.SETTINGS_EXPIRY
3537
self.network_timeout = Constants.SETTINGS_TIMEOUT
3638
self.is_gateway_service_provided = False
39+
self.settings_fetch_time = None # time taken to fetch the settings
40+
self.is_settings_valid_on_init = False
3741

3842
if "gateway_service" in options and "url" in options["gateway_service"]:
3943
self.is_gateway_service_provided = True
@@ -94,14 +98,23 @@ def fetch_settings_and_cache_in_storage(self, update=False):
9498
)
9599
return None
96100

101+
102+
def get_settings_path(self) -> Dict[str, Any]:
103+
path = {
104+
"i": self.sdk_key, # Inject API key
105+
"r": random.random(), # Random number for cache busting
106+
"a": self.account_id, # Account ID
107+
}
108+
return path
109+
97110
def fetch_settings(self, is_via_webhook=False):
98111
if not self.sdk_key or not self.account_id:
99112
raise ValueError(
100113
"sdk_key is required for fetching account settings. Aborting!"
101114
)
102115

103116
network_instance = NetworkManager.get_instance()
104-
options = get_settings_path(self.sdk_key, self.account_id)
117+
options = self.get_settings_path()
105118
options["platform"] = "server"
106119
options["api-version"] = 1
107120
options["sn"] = Constants.SDK_NAME
@@ -115,6 +128,8 @@ def fetch_settings(self, is_via_webhook=False):
115128
if not is_via_webhook
116129
else Constants.WEBHOOK_SETTINTS_ENDPOINT
117130
)
131+
# Start timer for settings fetch
132+
settings_fetch_start_time = time.time() * 1000 # Convert to milliseconds
118133

119134
try:
120135
request = RequestModel(
@@ -134,6 +149,8 @@ def fetch_settings(self, is_via_webhook=False):
134149
if response.status_code != 200:
135150
raise Exception(f"Failed to fetch settings: {response_data}")
136151

152+
# Calculate settings fetch time
153+
self.settings_fetch_time = int((time.time() * 1000) - settings_fetch_start_time)
137154
return response_data
138155

139156
except Exception as err:
@@ -147,6 +164,7 @@ def get_settings(self, force_fetch=False):
147164
# For demonstration, we'll skip actual caching
148165
fetched_settings = self.fetch_settings_and_cache_in_storage()
149166
if self.is_settings_valid(fetched_settings):
167+
self.is_settings_valid_on_init = True
150168
LogManager.get_instance().info(
151169
info_messages.get("SETTINGS_FETCH_SUCCESS").format()
152170
)

vwo/utils/event_util.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright 2024-2025 Wingify Software Pvt. Ltd.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import Optional
16+
from .network_util import get_events_base_properties, get_sdk_init_event_payload, send_event
17+
from ..enums.event_enum import EventEnum
18+
from ..vwo_client import VWOClient
19+
from ..packages.logger.core.log_manager import LogManager
20+
from ..utils.log_message_util import error_messages
21+
22+
def send_sdk_init_event(settings_fetch_time: Optional[int] = None, sdk_init_time: Optional[int] = None) -> None:
23+
"""
24+
Sends an init called event to VWO.
25+
This event is triggered when the init function is called.
26+
27+
:param settings_fetch_time: Time taken to fetch settings in milliseconds
28+
:param sdk_init_time: Time taken to initialize the SDK in milliseconds
29+
"""
30+
# Create the query parameters
31+
properties = get_events_base_properties(EventEnum.VWO_SDK_INIT_EVENT.value)
32+
33+
# Create the payload with required fields
34+
payload = get_sdk_init_event_payload(EventEnum.VWO_SDK_INIT_EVENT.value, settings_fetch_time, sdk_init_time)
35+
36+
# Send the constructed payload via POST request
37+
try:
38+
vwo_instance = VWOClient.get_instance()
39+
40+
# Check if batch events are enabled
41+
if vwo_instance.batch_event_queue is not None:
42+
# Enqueue the event to the batch queue
43+
vwo_instance.batch_event_queue.enqueue(payload)
44+
else:
45+
# Send the event immediately if batch events are not enabled
46+
send_event(properties, payload, EventEnum.VWO_SDK_INIT_EVENT.value)
47+
except Exception as e:
48+
LogManager.get_instance().error(
49+
error_messages.get("SDK_INIT_EVENT_FAILED").format(
50+
err=str(e)
51+
)
52+
)
53+
pass

vwo/utils/log_message_util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def send_log_to_vwo(message: str, message_type: str) -> None:
5757
from ..utils.network_util import (
5858
get_events_base_properties,
5959
get_messaging_event_payload,
60-
send_messaging_event,
60+
send_event,
6161
)
6262

6363
if os.getenv("TEST_ENV") == "true":
@@ -80,4 +80,4 @@ def send_log_to_vwo(message: str, message_type: str) -> None:
8080
)
8181

8282
# Send the message via HTTP request
83-
send_messaging_event(properties, payload)
83+
send_event(properties, payload, EventEnum.VWO_LOG_EVENT.value)

vwo/utils/network_util.py

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,8 @@
3333
from ..packages.network_layer.models.request_model import RequestModel
3434
from ..enums.headers_enum import HeadersEnum
3535
from ..utils.usage_stats_util import UsageStatsUtil
36-
37-
38-
def get_settings_path(sdk_key: str, account_id: str) -> Dict[str, Any]:
39-
path = {
40-
"i": sdk_key, # Inject API key
41-
"r": random.random(), # Random number for cache busting
42-
"a": account_id, # Account ID
43-
}
44-
return path
45-
36+
from ..services.settings_manager import SettingsManager
37+
from ..enums.event_enum import EventEnum
4638

4739
# Function to construct tracking path for an event
4840
def get_track_event_path(event: str, account_id: str, user_id: str) -> Dict[str, Any]:
@@ -69,7 +61,6 @@ def get_event_batching_query_params(account_id: str) -> Dict[str, Any]:
6961
def get_events_base_properties(
7062
event_name: str, visitor_user_agent: str = "", ip_address: str = ""
7163
) -> Dict[str, Any]:
72-
from ..services.settings_manager import SettingsManager
7364

7465
# Get the instance of SettingsManager
7566
settings = SettingsManager.get_instance()
@@ -101,7 +92,6 @@ def _get_event_base_payload(
10192
visitor_user_agent: str = "",
10293
ip_address: str = "",
10394
) -> Dict[str, Any]:
104-
from ..services.settings_manager import SettingsManager
10595

10696
uuid_value = get_uuid(user_id, SettingsManager.get_instance().get_account_id())
10797
sdk_key = SettingsManager.get_instance().get_sdk_key()
@@ -221,9 +211,6 @@ def get_attribute_payload_data(
221211
def send_post_api_request(
222212
properties: Dict[str, Any], payload: Dict[str, Any], user_id: str
223213
):
224-
# Importing the SettingsManager here to avoid circular import issues or unnecessary imports
225-
from ..services.settings_manager import SettingsManager
226-
227214
try:
228215
# Initialize the headers dictionary for the request
229216
headers = {}
@@ -263,6 +250,17 @@ def send_request():
263250
if response.status_code == 200:
264251
# clear the usage stats data
265252
UsageStatsUtil().clear_usage_stats()
253+
254+
request_query = request.get_query()
255+
LogManager.get_instance().info(
256+
info_messages.get("NETWORK_CALL_SUCCESS").format(
257+
event=request_query.get("en"),
258+
endPoint=request.get_url().split("?")[0],
259+
accountId=request_query.get("a"),
260+
userId=request.get_user_id(),
261+
uuid=request.get_body().get("d").get("visId"),
262+
)
263+
)
266264
except Exception as e:
267265
LogManager.get_instance().error(
268266
error_messages.get("NETWORK_CALL_FAILED").format(
@@ -289,7 +287,6 @@ def send_request():
289287
def send_post_batch_request(
290288
payload: dict, account_id: int, sdk_key: str, flush_callback=None
291289
):
292-
from ..services.settings_manager import SettingsManager
293290

294291
try:
295292
# Prepare the batch payload
@@ -313,6 +310,7 @@ def send_post_batch_request(
313310
SettingsManager.get_instance().port,
314311
)
315312

313+
request_model.set_user_id("NA")
316314
# Call PostAsync to send the request asynchronously
317315
network_manager = NetworkManager.get_instance()
318316
response = network_manager.post(request_model)
@@ -345,8 +343,6 @@ def send_post_batch_request(
345343
def get_messaging_event_payload(
346344
message_type: str, message: str, event_name: str
347345
) -> Dict[str, Any]:
348-
from ..services.settings_manager import SettingsManager
349-
350346
# Get user ID and properties
351347
settings = SettingsManager.get_instance()
352348
user_id = f"{settings.get_account_id()}_{settings.get_sdk_key()}"
@@ -373,20 +369,65 @@ def get_messaging_event_payload(
373369
return properties
374370

375371

376-
def send_messaging_event(
377-
properties: Dict[str, Any], payload: Dict[str, Any]
372+
def get_sdk_init_event_payload(
373+
event_name: str,
374+
settings_fetch_time: Optional[int] = None,
375+
sdk_init_time: Optional[int] = None,
376+
) -> Dict[str, Any]:
377+
"""
378+
Constructs the payload for sdk init called event.
379+
380+
Args:
381+
event_name: The name of the event.
382+
settings_fetch_time: Time taken to fetch settings in milliseconds.
383+
sdk_init_time: Time taken to initialize the SDK in milliseconds.
384+
385+
Returns:
386+
The constructed payload with required fields.
387+
"""
388+
# Get user ID and properties
389+
settings = SettingsManager.get_instance()
390+
user_id = f"{settings.get_account_id()}_{settings.get_sdk_key()}"
391+
properties = _get_event_base_payload(None, user_id, event_name, None, None)
392+
393+
# Set the required fields as specified
394+
properties["d"]["event"]["props"][Constants.VWO_FS_ENVIRONMENT] = settings.get_sdk_key()
395+
properties["d"]["event"]["props"]["product"] = "fme"
396+
397+
data = {
398+
"isSDKInitialized": True,
399+
"settingsFetchTime": settings_fetch_time,
400+
"sdkInitTime": sdk_init_time,
401+
}
402+
properties["d"]["event"]["props"]["data"] = data
403+
404+
return properties
405+
406+
407+
def send_event(
408+
properties: Dict[str, Any], payload: Dict[str, Any], event_name: str
378409
) -> Dict[str, Any]:
379410
try:
411+
#if event_name is not VWO_LOG_EVENT, then set base url to constants.hostname
412+
if event_name == EventEnum.VWO_LOG_EVENT.value:
413+
base_url = Constants.HOST_NAME
414+
protocol = Constants.HTTPS_PROTOCOL
415+
port = 443
416+
else:
417+
base_url = UrlService.get_base_url()
418+
protocol = SettingsManager.get_instance().protocol
419+
port = SettingsManager.get_instance().port
420+
380421
# Create the request model
381422
request = RequestModel(
382-
Constants.HOST_NAME,
423+
base_url,
383424
"POST",
384425
UrlEnum.EVENTS.value,
385426
properties,
386427
payload,
387428
None,
388-
Constants.HTTPS_PROTOCOL,
389-
443,
429+
protocol,
430+
port,
390431
)
391432

392433
# Get network instance

0 commit comments

Comments
 (0)