Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions growthbook/common_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ class Options:
tracking_plugins: Optional[List[Any]] = None
http_connect_timeout: Optional[int] = None
http_read_timeout: Optional[int] = None
event_logger: Optional[Callable[..., Any]] = None


@dataclass
Expand Down
36 changes: 36 additions & 0 deletions growthbook/growthbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,7 @@ def __init__(
self._assigned: Dict[str, Any] = {}
self._subscriptions: Set[Any] = set()
self._is_updating_features = False
self._event_logger: Optional[Any] = None

# support plugins
self._plugins: List[Any] = plugins if plugins is not None else []
Expand Down Expand Up @@ -973,6 +974,7 @@ def destroy(self, timeout=10) -> None:
self._assigned.clear()
self._trackingCallback = None
self._featureUsageCallback = None
self._event_logger = None
self._forcedVariations.clear()
self._overrides.clear()
self._groups.clear()
Expand All @@ -982,6 +984,40 @@ def destroy(self, timeout=10) -> None:
except Exception as e:
logger.warning(f"Error clearing internal state: {e}")

def set_event_logger(self, fn) -> None:
"""Register a callable that will be invoked by log_event.

The callable receives (event_name: str, properties: dict, user_context: UserContext).
Typically set by GrowthBookTrackingPlugin rather than called directly.
"""
self._event_logger = fn

def log_event(self, event_name: str, properties: Optional[Dict[str, Any]] = None) -> None:
"""Log a custom event to the GrowthBook ingestor.

Requires GrowthBookTrackingPlugin to be configured; without it a warning
is emitted and the call is a no-op.

Args:
event_name: Name of the event (e.g. ``"button_clicked"``).
properties: Optional dict of event-specific properties.
"""
if self._event_logger is None:
logger.warning(
"log_event called but no event logger is configured. "
"Add GrowthBookTrackingPlugin to enable event logging."
)
return
# set_attributes() replaces self._attributes but does not write back to
# _user_ctx; _get_eval_context() does that lazily before every eval.
# Mirror the same sync here so log_event always sees current attributes.
self._user_ctx.attributes = self._attributes
self._user_ctx.url = self._url
try:
self._event_logger(event_name, properties or {}, self._user_ctx)
except Exception as e:
logger.exception("Error in event logger: %s", e)

def isOn(self, key: str) -> bool:
warnings.warn("isOn is deprecated, use is_on instead", DeprecationWarning)
return self.is_on(key)
Expand Down
38 changes: 38 additions & 0 deletions growthbook/growthbook_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,44 @@ def _fire_subscriptions(self, experiment: Experiment, result: Result) -> None:
logger.exception("Error in subscription callback")


def set_event_logger(self, fn) -> None:
"""Register a callable that will be invoked by log_event.

The callable receives (event_name: str, properties: dict, user_context: UserContext).
Typically set by GrowthBookTrackingPlugin rather than called directly.
"""
self.options.event_logger = fn

async def log_event(
self,
event_name: str,
properties: Optional[Dict[str, Any]] = None,
user_context: Optional[UserContext] = None,
) -> None:
"""Log a custom event to the GrowthBook ingestor.

Requires GrowthBookTrackingPlugin to be configured; without it a warning
is emitted and the call is a no-op.

Args:
event_name: Name of the event (e.g. ``"button_clicked"``).
properties: Optional dict of event-specific properties.
user_context: User context for the event; uses an empty context if omitted.
"""
if not self.options.event_logger:
logger.warning(
"log_event called but no event logger is configured. "
"Add GrowthBookTrackingPlugin to enable event logging."
)
return
ctx = user_context or UserContext()
try:
result = self.options.event_logger(event_name, properties or {}, ctx)
if asyncio.iscoroutine(result):
await result
except Exception as e:
logger.exception("Error in event logger: %s", e)

async def set_features(self, features: dict) -> None:
await self._feature_update_callback({"features": features})

Expand Down
Loading
Loading