diff --git a/openbb_terminal/base_helpers.py b/openbb_terminal/base_helpers.py index eb8317a2c69d..e7d18a61b16d 100644 --- a/openbb_terminal/base_helpers.py +++ b/openbb_terminal/base_helpers.py @@ -4,6 +4,7 @@ from typing import Any, List, Literal, Optional from dotenv import load_dotenv +from posthog import Posthog from rich.console import Console from openbb_terminal.core.config.paths import ( @@ -16,6 +17,11 @@ menus = Literal["", "featflags", "settings"] +openbb_posthog = Posthog( + "phc_8fP8xXi0ptWTlGAXOcMQnSFsul4lmLoxnwp9EiXQstO", + host="https://app.posthog.com", +) + def handle_error(name: str, default: Any, menu: menus = ""): """Handles the error by returning the default value and printing an diff --git a/openbb_terminal/core/log/generation/formatter_with_exceptions.py b/openbb_terminal/core/log/generation/formatter_with_exceptions.py index 1b569af582f4..a6a61a0b0773 100644 --- a/openbb_terminal/core/log/generation/formatter_with_exceptions.py +++ b/openbb_terminal/core/log/generation/formatter_with_exceptions.py @@ -67,8 +67,10 @@ def mock_password(text: str) -> str: @staticmethod def mock_home_directory(text: str) -> str: - user_home_directory = str(HOME_DIRECTORY) - text_mocked = text.replace(user_home_directory, "MOCKING_USER_PATH") + user_home_directory = str(HOME_DIRECTORY.as_posix()) + text_mocked = text.replace("\\\\", "/").replace( + user_home_directory, "MOCKING_USER_PATH" + ) return text_mocked diff --git a/openbb_terminal/core/models/profile_model.py b/openbb_terminal/core/models/profile_model.py index 9cbe215ddd2b..4db43c45ded0 100644 --- a/openbb_terminal/core/models/profile_model.py +++ b/openbb_terminal/core/models/profile_model.py @@ -2,6 +2,7 @@ from pydantic.dataclasses import dataclass +from openbb_terminal.base_helpers import openbb_posthog from openbb_terminal.core.models import BaseModel @@ -34,6 +35,7 @@ def load_user_info(self, session: dict, email: str, remember: bool): self.email = email self.username = self.email[: self.email.find("@")] self.remember = remember + openbb_posthog.identify(self.uuid, {"email": self.email}) def get_uuid(self) -> str: """Get uuid. diff --git a/openbb_terminal/core/models/system_model.py b/openbb_terminal/core/models/system_model.py index 9e0cfb3010f4..51930d30edc0 100644 --- a/openbb_terminal/core/models/system_model.py +++ b/openbb_terminal/core/models/system_model.py @@ -30,6 +30,7 @@ class SystemModel(BaseModel): VERSION = "3.0.0rc2" # Logging section + LOGGING_APP_ID: str = "REPLACE_ME" LOGGING_APP_NAME: str = "gst" LOGGING_AWS_ACCESS_KEY_ID: str = "REPLACE_ME" LOGGING_AWS_SECRET_ACCESS_KEY: str = "REPLACE_ME" diff --git a/openbb_terminal/core/plots/backend.py b/openbb_terminal/core/plots/backend.py index e7d9e585f2ed..19fa685fcc9b 100644 --- a/openbb_terminal/core/plots/backend.py +++ b/openbb_terminal/core/plots/backend.py @@ -170,7 +170,12 @@ def send_figure( json_data = json.loads(fig.to_json()) - json_data.update({"theme": get_current_user().preferences.CHART_STYLE}) + json_data.update( + { + "user_id": get_current_system().LOGGING_APP_ID, + "theme": get_current_user().preferences.CHART_STYLE, + } + ) self.outgoing.append( json.dumps( diff --git a/openbb_terminal/core/plots/plotly.html b/openbb_terminal/core/plots/plotly.html index d4623858add0..8929d91267f5 100644 --- a/openbb_terminal/core/plots/plotly.html +++ b/openbb_terminal/core/plots/plotly.html @@ -18,6 +18,16 @@ crossorigin="anonymous" referrerpolicy="no-referrer" > + @@ -202,6 +212,8 @@ globals.cmd_src = ""; if (typeof OpenBBMain != "undefined" && window.plotly_figure) { clearInterval(interval); + let user_id = window.plotly_figure.user_id; + posthog.identify(user_id); const date = new Date(); const formattedDate = new Intl.DateTimeFormat("en-GB", { dateStyle: "full", @@ -245,6 +257,14 @@ }); } + if (globals.cmd_src != "") { + posthog.capture("chart", { + command: globals.cmd_src, + title: window.plotly_figure.layout.title.text, + date: formattedDate, + }); + } + document .querySelector('meta[name="color-scheme"]') .setAttribute( diff --git a/openbb_terminal/loggers.py b/openbb_terminal/loggers.py index c2cac6534a4a..ad12a4658964 100644 --- a/openbb_terminal/loggers.py +++ b/openbb_terminal/loggers.py @@ -2,12 +2,16 @@ __docformat__ = "numpy" # IMPORTATION STANDARD +import atexit +import json import logging +import re import sys import time import uuid from pathlib import Path -from typing import Optional +from platform import platform, python_version +from typing import Any, Dict, Optional # IMPORTATION THIRDPARTY try: @@ -18,6 +22,7 @@ WITH_GIT = True # IMPORTATION INTERNAL +from openbb_terminal.base_helpers import openbb_posthog from openbb_terminal.core.log.generation.directories import get_log_dir from openbb_terminal.core.log.generation.formatter_with_exceptions import ( FormatterWithExceptions, @@ -31,8 +36,14 @@ LogSettings, Settings, ) -from openbb_terminal.core.log.generation.user_logger import get_user_uuid -from openbb_terminal.core.session.current_system import get_current_system +from openbb_terminal.core.log.generation.user_logger import ( + NO_USER_PLACEHOLDER, + get_user_uuid, +) +from openbb_terminal.core.session.current_system import ( + get_current_system, + set_current_system, +) logger = logging.getLogger(__name__) current_system = get_current_system() @@ -85,6 +96,107 @@ def get_commit_hash(use_env=True) -> str: return commit_hash +class PosthogHandler(logging.Handler): + """Posthog Handler""" + + def __init__(self, settings: Settings): + super().__init__() + self.settings = settings + self.app_settings = settings.app_settings + self.logged_in = False + atexit.register(openbb_posthog.shutdown) + + def emit(self, record: logging.LogRecord): + try: + self.send(record=record) + except Exception: + self.handleError(record) + + def log_to_dict(self, log_info: str) -> dict: + """Log to dict""" + log_regex = r"(KEYS|PREFERENCES|SYSTEM|CMD|QUEUE): (.*)" + log_dict = {} + + for log in re.findall(log_regex, log_info): + log_dict[log[0]] = json.loads(log[1]) + + sdk_regex = r"({\"INPUT\":.*})" + sdk_dict = re.findall(sdk_regex, log_info) + if sdk_dict: + log_dict["SDK"] = json.loads(sdk_dict[0]) + + return log_dict + + def send(self, record: logging.LogRecord): + """Send log record to Posthog""" + + app_settings = self.app_settings + + level_name = logging.getLevelName(record.levelno) + log_line = FormatterWithExceptions.filter_log_line(text=record.getMessage()) + + log_extra = self.extract_log_extra(record=record) + log_extra.update(dict(level=level_name, message=log_line)) + event_name = f"log_{level_name.lower()}" + + if log_dict := self.log_to_dict(log_info=log_line): + event_name = f"log_{list(log_dict.keys())[0].lower()}" + + log_extra = {**log_extra, **log_dict} + log_extra.pop("message", None) + + if re.match(r"^(START|END|INPUT:)", log_line): + return + + if not self.logged_in and get_user_uuid() != NO_USER_PLACEHOLDER: + self.logged_in = True + openbb_posthog.alias(get_user_uuid(), app_settings.identifier) + + log_extra.update({"$geoip_disable": True}) + + openbb_posthog.capture( + app_settings.identifier, + event_name, + properties=log_extra, + ) + + def extract_log_extra(self, record: logging.LogRecord) -> Dict[str, Any]: + """Extract log extra from record""" + + log_extra = { + "appName": self.app_settings.name, + "appId": self.app_settings.identifier, + "sessionId": self.app_settings.session_id, + "commitHash": self.app_settings.commit_hash, + "platform": platform(), + "pythonVersion": python_version(), + "terminalVersion": current_system.VERSION, + } + + if get_user_uuid() != NO_USER_PLACEHOLDER: + log_extra["userId"] = get_user_uuid() + + if hasattr(record, "extra"): + log_extra = {**log_extra, **record.extra} + + if record.exc_info: + log_extra["exception"] = { + "type": str(record.exc_info[0]), + "value": str(record.exc_info[1]), + "traceback": self.format(record), + } + + return log_extra + + +def add_posthog_handler(settings: Settings): + app_settings = settings.app_settings + handler = PosthogHandler(settings=settings) + formatter = FormatterWithExceptions(app_settings=app_settings) + handler.setFormatter(formatter) + logging.getLogger().addHandler(handler) + + def add_stdout_handler(settings: Settings): app_settings = settings.app_settings handler = logging.StreamHandler(sys.stdout) @@ -150,6 +262,12 @@ def setup_handlers(settings: Settings): FormatterWithExceptions.LOGFORMAT.replace("|", "-"), ) + if ( + not any([current_system.TEST_MODE, current_system.LOGGING_SUPPRESS]) + and current_system.LOG_COLLECT + ): + add_posthog_handler(settings=settings) + def setup_logging( app_name: Optional[str] = None, @@ -165,6 +283,9 @@ def setup_logging( session_id = get_session_id() user_id = get_user_uuid() + current_system.LOGGING_APP_ID = identifier + set_current_system(current_system) + # AWSSettings aws_access_key_id = current_system.LOGGING_AWS_ACCESS_KEY_ID aws_secret_access_key = current_system.LOGGING_AWS_SECRET_ACCESS_KEY diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index e1079abc079e..b7373e3812a7 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -301,10 +301,9 @@ def log_queue(self) -> None: if self.queue: joined_queue = self.COMMAND_SEPARATOR.join(self.queue) if not self.contains_keys(joined_queue): + queue = {"path": self.PATH, "queue": joined_queue} logger.info( - "QUEUE: {'path': '%s', 'queue': '%s'}", - self.PATH, - joined_queue, + "QUEUE: %s ", json.dumps(queue, default=str, ensure_ascii=False) ) def log_cmd_and_queue( @@ -328,7 +327,7 @@ def log_cmd_and_queue( "other_args": other_args_str, "input": the_input, } - logger.info("CMD: %s", json.dumps(cmd)) + logger.info("CMD: %s ", json.dumps(cmd)) if the_input not in self.KEYS_MENU: self.log_queue()