Skip to content

Commit ce48cf2

Browse files
Abhi591rohitesh-wingify
authored andcommitted
feat: track and collect feature usage statistics
1 parent c1e4de7 commit ce48cf2

File tree

8 files changed

+163
-7
lines changed

8 files changed

+163
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ 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.9.1] - 2025-05-07
8+
9+
### Added
10+
11+
- Added a feature to track and collect usage statistics related to various SDK features and configurations which can be useful for analytics, and gathering insights into how different features are being utilized by end users.
12+
713
## [1.9.0] - 2025-05-06
814

915
### 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.9.0",
124+
version="1.9.1",
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
package_file = {
1919
"name": "vwo-fme-python-sdk",
20-
"version": "1.9.0"
20+
"version": "1.9.1"
2121
}
2222

2323
# Constants

vwo/models/vwo_options_model.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def __init__(self, options: Dict):
3434
self.integrations = options.get("integrations", None)
3535
self.network = options.get("network", None)
3636
self.vwoBuilder = options.get("vwoBuilder", None)
37+
self.is_usage_stats_disabled = options.get("is_usage_stats_disabled", False)
38+
self._vwo_meta = options.get("_vwo_meta", None)
3739

3840
# Initialize BatchEventData
3941
batch_event_data = options.get('batch_event_data', None)
@@ -140,4 +142,20 @@ def get_batch_event_data(self):
140142
141143
:return: The BatchEventData instance.
142144
"""
143-
return self.batchEventData
145+
return self.batchEventData
146+
147+
def get_is_usage_stats_disabled(self) -> bool:
148+
"""
149+
Check if usage stats are disabled.
150+
151+
:return: True if usage stats are disabled, False otherwise.
152+
"""
153+
return self.is_usage_stats_disabled
154+
155+
def get_vwo_meta(self) -> Dict:
156+
"""
157+
Get the VWO meta data.
158+
159+
:return: The VWO meta data as a dictionary.
160+
"""
161+
return self._vwo_meta

vwo/utils/network_util.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from ..packages.network_layer.manager.network_manager import NetworkManager
3333
from ..packages.network_layer.models.request_model import RequestModel
3434
from ..enums.headers_enum import HeadersEnum
35-
35+
from ..utils.usage_stats_util import UsageStatsUtil
3636

3737
def get_settings_path(sdk_key: str, account_id: str) -> Dict[str, Any]:
3838
path = {
@@ -145,6 +145,10 @@ def get_track_user_payload_data(
145145
properties["d"]["event"]["props"]["variation"] = str(variation_id)
146146
properties["d"]["event"]["props"]["isFirst"] = 1
147147

148+
usage_stats_data = UsageStatsUtil().get_usage_stats()
149+
if len(usage_stats_data) > 0:
150+
properties["d"]["event"]["props"]["vwoMeta"] = usage_stats_data
151+
148152
LogManager.get_instance().debug(
149153
debug_messages.get("IMPRESSION_FOR_TRACK_USER").format(
150154
accountId=settings.get_account_id(), userId=user_id, campaignId=campaign_id
@@ -248,7 +252,10 @@ def send_post_api_request(properties: Dict[str, Any], payload: Dict[str, Any]):
248252
# Create a background thread to send the request
249253
def send_request():
250254
try:
251-
network_instance.post(request)
255+
response = network_instance.post(request)
256+
if response.status_code == 200:
257+
# clear the usage stats data
258+
UsageStatsUtil().clear_usage_stats()
252259
except Exception as e:
253260
LogManager.get_instance().error(
254261
error_messages.get("NETWORK_CALL_FAILED").format(

vwo/utils/usage_stats_util.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 Dict, Any, Union
16+
import sys
17+
from ..constants.Constants import Constants
18+
from ..packages.logger.enums.log_level_number_enum import LogLevelNumberEnum
19+
20+
21+
class UsageStatsUtil:
22+
"""
23+
Manages usage statistics for the SDK.
24+
Tracks various features and configurations being used by the client.
25+
Implements Singleton pattern to ensure a single instance.
26+
"""
27+
28+
_instance = None
29+
30+
def __new__(cls):
31+
"""
32+
Ensures only one instance of UsageStatsUtil exists (Singleton pattern).
33+
"""
34+
if cls._instance is None:
35+
cls._instance = super(UsageStatsUtil, cls).__new__(cls)
36+
cls._instance._usage_stats_data = {}
37+
return cls._instance
38+
39+
def set_usage_stats(self, options: Dict[str, Any]) -> None:
40+
"""
41+
Sets usage statistics based on provided options.
42+
Maps various SDK features and configurations to boolean flags.
43+
44+
Args:
45+
options (Dict[str, Any]): Configuration options for the SDK containing:
46+
- storage: Storage service configuration
47+
- logger: Logger configuration
48+
- batch_events: Event batching configuration
49+
- integrations: Integrations configuration
50+
- polling_interval: Polling interval configuration
51+
- sdk_name: SDK name configuration
52+
"""
53+
data: Dict[str, Union[str, int]] = {}
54+
55+
# Map configuration options to usage stats flags
56+
if options.get("integrations"):
57+
data["ig"] = 1 # Integration enabled
58+
if options.get("batch_events"):
59+
data["eb"] = 1 # Event batching enabled
60+
61+
if options.get("gateway_service"):
62+
data["gs"] = 1 # Gateway service enabled
63+
64+
# Check for custom logger
65+
logger = options.get("logger")
66+
if logger and (logger.get("transport") or logger.get("transports")):
67+
data["cl"] = 1
68+
69+
if options.get("storage"):
70+
data["ss"] = 1 # Storage service configured
71+
72+
if logger and logger.get("level"):
73+
data["ll"] = getattr(
74+
LogLevelNumberEnum, logger["level"].upper(), LogLevelNumberEnum.ERROR
75+
) # Default to -1 if level is not recognized
76+
77+
if options.get("poll_interval"):
78+
data["pi"] = 1 # Polling interval configured
79+
80+
# Check for _vwo_meta.ea
81+
vwo_meta = options.get("_vwo_meta")
82+
if vwo_meta and vwo_meta.get("ea"):
83+
data["_ea"] = 1
84+
85+
# Add Python version if available
86+
if hasattr(sys, "version"):
87+
data["lv"] = sys.version
88+
89+
# Check threading configuration
90+
threading_config = options.get("threading")
91+
if not threading_config or (
92+
threading_config and threading_config.get("enabled") is True
93+
):
94+
data["th"] = 1
95+
# Check if max_workers is passed
96+
if threading_config and threading_config.get("max_workers"):
97+
data["th_mps"] = threading_config["max_workers"]
98+
99+
self._usage_stats_data = data
100+
101+
def get_usage_stats(self) -> Dict[str, Union[bool, str, int]]:
102+
"""
103+
Retrieves the current usage statistics.
104+
105+
Returns:
106+
Dict[str, Union[bool, str, int]]: Dictionary containing boolean flags for various SDK features in use
107+
"""
108+
return self._usage_stats_data
109+
110+
def clear_usage_stats(self) -> None:
111+
"""
112+
Clears the usage statistics data.
113+
"""
114+
self._usage_stats_data = {}

vwo/vwo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def set_instance(options: Dict) -> "VWOClient":
3232
VWO.vwo_builder = options_vwo_builder or VWOBuilder(options)
3333

3434
# Configure the builder
35-
VWO.vwo_builder.set_logger().set_settings_manager().set_storage().set_network_manager().set_segmentation().init_polling()
35+
VWO.vwo_builder.set_logger().set_settings_manager().set_storage().set_network_manager().set_segmentation().init_polling().init_usage_stats()
3636

3737
# Fetch settings synchronously and build the VWO instance
3838
settings = VWO.vwo_builder.get_settings(force=False)

vwo/vwo_builder.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .utils.settings_util import set_settings_and_add_campaigns_to_rules
3131
from .packages.storage.storage import Storage
3232
from .constants.Constants import Constants
33+
from .utils.usage_stats_util import UsageStatsUtil
3334

3435

3536
class VWOBuilder:
@@ -223,6 +224,16 @@ def init_batching(self):
223224
self.is_batching_used = False
224225

225226
return self
227+
228+
def init_usage_stats(self):
229+
"""
230+
Initializes usage statistics for the SDK.
231+
"""
232+
if self.options.get("is_usage_stats_disabled"):
233+
return
234+
235+
UsageStatsUtil().set_usage_stats(self.options)
236+
return self
226237

227238
def build(self, settings):
228239
self.vwo_instance = VWOClient(settings, self.options)
@@ -282,4 +293,4 @@ def poll():
282293
).start() # Timer expects seconds, so convert milliseconds
283294

284295
# start the polling after given interval
285-
threading.Timer(self.options["poll_interval"] / 1000, poll).start()
296+
threading.Timer(self.options["poll_interval"] / 1000, poll).start()

0 commit comments

Comments
 (0)