diff --git a/.env.default b/.env.default index 2991382..f28208d 100644 --- a/.env.default +++ b/.env.default @@ -1,3 +1,6 @@ +APP_LIFECYCLE="dev" +SENTRY_ENABLED="False" +SENTRY_DSN="" ADMIN_EMAIL="" ADMIN_FIRST_NAME="" BOT_NAME="" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d54b68..13ea1f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - name: Bump version and push tag id: tag_version - uses: mathieudutour/github-tag-action@v6.1 + uses: mathieudutour/github-tag-action@v6.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} default_bump: minor diff --git a/app/commands/submit_task.py b/app/commands/submit_task.py index 6a19ff0..70ef360 100644 --- a/app/commands/submit_task.py +++ b/app/commands/submit_task.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import logging +import sentry_sdk from webex_bot.models.command import Command from webex_bot.models.response import Response, response_from_adaptive_card @@ -113,7 +114,8 @@ class SubmitTaskCommand(Command): Submit(title="Cancel", data={"command_keyword": "exit"}), ], ) - return response_from_adaptive_card(card) + with sentry_sdk.start_transaction(name="submit_task_command"): + return response_from_adaptive_card(card) class SubmitTaskCallback(Command): @@ -147,7 +149,8 @@ class SubmitTaskCallback(Command): ) def execute(self, message, attachment_actions, activity) -> str: - return self.msg + with sentry_sdk.start_transaction(name="submit_task_callback"): + return self.msg class MyTasksCallback(Command): @@ -158,11 +161,13 @@ class MyTasksCallback(Command): self.msg: str = "" def pre_execute(self, message, attachment_actions, activity) -> str: - return "Getting your tasks..." + with sentry_sdk.start_transaction(name="my_tasks_preexec"): + return "Getting your tasks..." def execute(self, message, attachment_actions, activity) -> str | None: sender: str = attachment_actions.inputs.get("sender") result: bool = get_tasks(requestor=sender) - if not result: - return "Failed to get tasks. Please try again." - return + with sentry_sdk.start_transaction(name="my_tasks_exec"): + if not result: + return "Failed to get tasks. Please try again." + return diff --git a/app/main.py b/app/main.py index b84f46a..4affc14 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 +import sentry_sdk +from sentry_sdk.integrations.stdlib import StdlibIntegration + from webex_bot.webex_bot import WebexBot from app.commands.exit import ExitCommand @@ -7,8 +10,18 @@ from app.commands.submit_task import SubmitTaskCommand from app.utils.config import config +if config.sentry_enabled: + apm = sentry_sdk.init( + dsn=config.sentry_dsn, + enable_tracing=True, + environment=config.environment, + integrations=[StdlibIntegration()], + spotlight=True + ) + + def create_bot() -> WebexBot: - # Create a Bot Object + """Create and return a Webex Bot object.""" webex_bot: WebexBot = WebexBot( bot_name=config.bot_name, teams_bot_token=config.webex_token, diff --git a/app/utils/config.py b/app/utils/config.py index 27f6e32..f12eb7a 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -1,34 +1,65 @@ -#!/usr/bin/env python3 +"""Configuration module.""" import os class Config: + """Configuration module.""" def __init__(self) -> None: + """Configuration module.""" + self.__environment: str = os.environ.get("APP_LIFECYCLE", "DEV").upper() self.__bot_name: str = os.environ["BOT_NAME"] self.__webex_token: str = os.environ["WEBEX_API_KEY"] self.__admin_first_name: str = os.environ["ADMIN_FIRST_NAME"] self.__admin_emails: list = os.environ["ADMIN_EMAIL"].split(",") self.__n8n_webhook_url: str = os.environ["N8N_WEBHOOK_URL"] + self.__sentry_dsn: str = os.environ.get("SENTRY_DSN", "") + self.__sentry_enabled: bool = bool( + os.environ.get("SENTRY_ENABLED", "False").upper() == "TRUE" + and self.__sentry_dsn != "" + ) + + @property + def environment(self) -> str: + """Returns the current app lifecycle.""" + return self.__environment + + @property + def sentry_enabled(self) -> bool: + """Returns True if Sentry SDK is enabled, else False.""" + return self.__sentry_enabled + + @property + def sentry_dsn(self) -> str: + """Returns the Sentry DSN value if Sentry SDK is enabled AND + Sentry DSN is set, else blank string.""" + if not self.__sentry_enabled: + return "" + return self.__sentry_dsn @property def bot_name(self) -> str: + """Returns the bot name.""" return self.__bot_name @property def webex_token(self) -> str: + """Returns the Webex API key.""" return self.__webex_token @property def admin_first_name(self) -> str: + """Returns the first name of the bot admin.""" return self.__admin_first_name @property def admin_emails(self) -> list: + """Returns a list of admin email addresses.""" return self.__admin_emails @property def n8n_webhook_url(self) -> str: + """Returns the n8n webhook URL.""" return self.__n8n_webhook_url diff --git a/app/utils/datetime.py b/app/utils/datetime.py index 8526bb1..472371c 100644 --- a/app/utils/datetime.py +++ b/app/utils/datetime.py @@ -1,7 +1,18 @@ #!/usr/bin/env python3 +from zoneinfo import ZoneInfo + from datetime import datetime def timestamp_to_date(timestamp: int) -> str: - return datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d") + """Convert timestamp to date. + + Args: + timestamp (int): Timestamp to convert. + + Returns: + str: Date in the format YYYY-MM-DD. + """ + return datetime.fromtimestamp(timestamp=timestamp, tz=ZoneInfo("UTC")).strftime("%Y-%m-%d") + diff --git a/app/utils/n8n.py b/app/utils/n8n.py index e1d8e31..4c0f84b 100644 --- a/app/utils/n8n.py +++ b/app/utils/n8n.py @@ -1,9 +1,18 @@ import requests +import sentry_sdk from app.utils.config import config def __n8n_post(data: dict) -> bool: + """Post data to N8N webhook URL. + + Args: + data (dict): Data to post to webhook URL. + + Returns: + bool: True if successful, else False. + """ headers: dict = {"Content-Type": "application/json"} resp: requests.Response = requests.post( url=config.n8n_webhook_url, @@ -16,22 +25,45 @@ def __n8n_post(data: dict) -> bool: def submit_task(summary, description, completion_date, requestor) -> bool: - data: dict = { - "requestor": requestor, - "title": summary, - "description": description, - "completiondate": completion_date, - } - return __n8n_post(data=data) + """Submit task to N8N webhook URL. + + Args: + summary (str): Summary of task. + description (str): Description of task. + completion_date (str): Completion date of task. + requestor (str): Requestor of task. + + Returns: + bool: True if successful, else False. + """ + with sentry_sdk.start_transaction(name="submit_task"): + data: dict = { + "requestor": requestor, + "title": summary, + "description": description, + "completiondate": completion_date, + } + _data = __n8n_post(data=data) + return _data def get_tasks(requestor) -> bool: - headers: dict = {"Content-Type": "application/json"} - resp: requests.Response = requests.get( - url=config.n8n_webhook_url, - headers=headers, - timeout=10, - verify=False, - params={"requestor": requestor}, - ) - return bool(resp.status_code == 200) + """Get tasks from N8N webhook URL. + + Args: + requestor (str): Requestor of tasks. + + Returns: + bool: True if successful, else False. + """ + with sentry_sdk.start_transaction(name="get_tasks"): + headers: dict = {"Content-Type": "application/json"} + resp: requests.Response = requests.get( + url=config.n8n_webhook_url, + headers=headers, + timeout=10, + verify=False, + params={"requestor": requestor}, + ) + _data = bool(resp.status_code == 200) + return _data diff --git a/requirements.txt b/requirements.txt index 0e04502..f1aee33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ appdirs==1.4.4 astroid==3.1.0 attrs==23.2.0 -autopep8==2.0.4 +autopep8==2.1.0 backoff==2.2.1 certifi==2024.2.2 cfgv==3.4.0 @@ -10,35 +10,36 @@ click==8.1.7 coloredlogs==15.0.1 dill==0.3.8 distlib==0.3.8 -filelock==3.13.1 +filelock==3.13.4 future==1.0.0 humanfriendly==10.0 identify==2.5.35 -idna==3.6 +idna==3.7 iniconfig==2.0.0 lazy-object-proxy==1.10.0 mccabe==0.7.0 mypy-extensions==1.0.0 nodeenv==1.8.0 -packaging==23.2 +packaging==24.0 pathspec==0.12.1 platformdirs==4.2.0 pluggy==1.4.0 py==1.11.0 pycodestyle==2.11.1 PyJWT==2.8.0 -pyparsing==3.1.1 +pyparsing==3.1.2 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 PyYAML==6.0.1 requests==2.31.0 requests-toolbelt==1.0.0 +sentry-sdk==1.45.0 six==1.16.0 toml==0.10.2 tomli==2.0.1 tomlkit==0.12.4 urllib3==2.2.1 -virtualenv==20.25.1 +virtualenv==20.25.3 webex-bot==0.4.1 webexteamssdk==1.6.1 websockets==10.2 diff --git a/test.sh b/test.sh index b0f95fc..1fcaf08 100755 --- a/test.sh +++ b/test.sh @@ -1,3 +1,3 @@ export $(cat .env | xargs) python -B -m app.main -unset BOT_NAME WEBEX_API_KEY ADMIN_FIRST_NAME ADMIN_EMAIL N8N_WEBHOOK_URL \ No newline at end of file +unset APP_LIFECYCLE SENTRY_ENABLED SENTRY_DSN BOT_NAME WEBEX_API_KEY ADMIN_FIRST_NAME ADMIN_EMAIL N8N_WEBHOOK_URL diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_config.py b/tests/test_config.py index ab41d77..ebebd1c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,6 +11,8 @@ vars: dict = { "ADMIN_FIRST_NAME": "Test", "ADMIN_EMAIL": "test@test.com", "N8N_WEBHOOK_URL": "https://n8n.test.com/webhook/abcdefg", + "SENTRY_ENABLED": "false", + "SENTRY_DSN": "http://localhost" }