diff --git a/.github/workflows/license-audit.yml b/.github/workflows/license-audit.yml deleted file mode 100644 index 951828ca..00000000 --- a/.github/workflows/license-audit.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Audit bugsnag-python dependency licenses - -on: [push, pull_request] - -jobs: - license-audit: - runs-on: 'ubuntu-latest' - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - # License Finder's Docker image uses Python 3.10 - python-version: '3.10' - - - name: Fetch decisions.yml - run: curl https://raw.githubusercontent.com/bugsnag/license-audit/master/config/decision_files/global.yml -o decisions.yml - - # License Finder doesn't use "install_requires" from setup.py, so won't check - # our dependencies if we don't put them in a requirements.txt file - - name: Set up requirements.txt for License Finder - run: | - pip3 install '.[flask]' - pip3 freeze --local --exclude bugsnag | tee requirements.txt - - - name: Run License Finder - # for some reason license finder doesn't run without a login shell (-l) - run: > - docker run -v $PWD:/scan licensefinder/license_finder /bin/bash -lc " - cd /scan && - apt-get update && - apt-get install -y python3-venv && - python3 -m venv .venv && - source .venv/bin/activate && - pip3 install -r requirements.txt && - license_finder --decisions-file decisions.yml --python-version 3 --enabled-package-managers=pip - " diff --git a/.github/workflows/maze-runner.yml b/.github/workflows/maze-runner.yml index 9d89beb0..ac3d3e7c 100644 --- a/.github/workflows/maze-runner.yml +++ b/.github/workflows/maze-runner.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 06de675a..1e1cb2a7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -9,14 +9,17 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] os: ['ubuntu-latest'] include: - - python-version: '3.5' - os: 'ubuntu-20.04' - pip-trusted-host: 'pypi.python.org pypi.org files.pythonhosted.org' - - python-version: '3.6' - os: 'ubuntu-20.04' +# Python 3.5 and 3.6 tests skipped pending PLAT-14414 +# - python-version: '3.5' +# os: 'ubuntu-22.04' +# pip-trusted-host: 'pypi.python.org pypi.org files.pythonhosted.org' +# - python-version: '3.6' +# os: 'ubuntu-22.04' + - python-version: '3.7' + os: 'ubuntu-22.04' steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 93f38ee8..9a828436 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .aws-sam maze-runner.log maze_output +Gemfile.lock *.py[co] @@ -37,3 +38,4 @@ htmlcov my_env venv .idea +*.iml diff --git a/CHANGELOG.md b/CHANGELOG.md index a856b6d3..315b7f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ Changelog ========= +## v4.8.0 (2024-07-08) + +### Enhancements + +* Remove deprecated `datetime.utcnow()` method call from utils class + [#394](https://github.com/bugsnag/bugsnag-python/pull/394). +* Set default endpoints based on API key + [#399](https://github.com/bugsnag/bugsnag-php/pull/399) + ## v4.7.1 (2024-05-22) ### Bug fixes diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 57b86365..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,143 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - appium_lib (12.0.1) - appium_lib_core (~> 5.0) - nokogiri (~> 1.8, >= 1.8.1) - tomlrb (>= 1.1, < 3.0) - appium_lib_core (5.4.0) - faye-websocket (~> 0.11.0) - selenium-webdriver (~> 4.2, < 4.6) - bugsnag (6.27.1) - concurrent-ruby (~> 1.0) - bugsnag-maze-runner (9.9.0) - appium_lib (~> 12.0.0) - appium_lib_core (~> 5.4.0) - bugsnag (~> 6.24) - cucumber (~> 7.1) - cucumber-expressions (~> 6.0.0) - curb (~> 0.9.6) - dogstatsd-ruby (~> 5.5.0) - json_schemer (~> 0.2.24) - optimist (~> 3.0.1) - os (~> 1.0.0) - rack (~> 2.2) - rake (~> 12.3.3) - rubyzip (~> 2.3.2) - selenium-webdriver (~> 4.0) - test-unit (~> 3.5.2) - webrick (~> 1.7.0) - builder (3.3.0) - childprocess (4.1.0) - concurrent-ruby (1.3.3) - cucumber (7.1.0) - builder (~> 3.2, >= 3.2.4) - cucumber-core (~> 10.1, >= 10.1.0) - cucumber-create-meta (~> 6.0, >= 6.0.1) - cucumber-cucumber-expressions (~> 14.0, >= 14.0.0) - cucumber-gherkin (~> 22.0, >= 22.0.0) - cucumber-html-formatter (~> 17.0, >= 17.0.0) - cucumber-messages (~> 17.1, >= 17.1.1) - cucumber-wire (~> 6.2, >= 6.2.0) - diff-lcs (~> 1.4, >= 1.4.4) - mime-types (~> 3.3, >= 3.3.1) - multi_test (~> 0.1, >= 0.1.2) - sys-uname (~> 1.2, >= 1.2.2) - cucumber-core (10.1.1) - cucumber-gherkin (~> 22.0, >= 22.0.0) - cucumber-messages (~> 17.1, >= 17.1.1) - cucumber-tag-expressions (~> 4.1, >= 4.1.0) - cucumber-create-meta (6.0.4) - cucumber-messages (~> 17.1, >= 17.1.1) - sys-uname (~> 1.2, >= 1.2.2) - cucumber-cucumber-expressions (14.0.0) - cucumber-expressions (6.0.1) - cucumber-gherkin (22.0.0) - cucumber-messages (~> 17.1, >= 17.1.1) - cucumber-html-formatter (17.0.0) - cucumber-messages (~> 17.1, >= 17.1.0) - cucumber-messages (17.1.1) - cucumber-tag-expressions (4.1.0) - cucumber-wire (6.2.1) - cucumber-core (~> 10.1, >= 10.1.0) - cucumber-cucumber-expressions (~> 14.0, >= 14.0.0) - curb (0.9.11) - diff-lcs (1.5.1) - dogstatsd-ruby (5.5.0) - ecma-re-validator (0.4.0) - regexp_parser (~> 2.2) - eventmachine (1.2.7) - faye-websocket (0.11.3) - eventmachine (>= 0.12.0) - websocket-driver (>= 0.5.1) - ffi (1.17.0-aarch64-linux-gnu) - ffi (1.17.0-arm-linux-gnu) - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86-linux-gnu) - ffi (1.17.0-x86_64-darwin) - ffi (1.17.0-x86_64-linux-gnu) - hana (1.3.7) - json_schemer (0.2.25) - ecma-re-validator (~> 0.3) - hana (~> 1.3) - regexp_parser (~> 2.0) - simpleidn (~> 0.2) - uri_template (~> 0.7) - mime-types (3.5.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2024.0604) - multi_test (0.1.2) - nokogiri (1.16.6-aarch64-linux) - racc (~> 1.4) - nokogiri (1.16.6-arm-linux) - racc (~> 1.4) - nokogiri (1.16.6-arm64-darwin) - racc (~> 1.4) - nokogiri (1.16.6-x86-linux) - racc (~> 1.4) - nokogiri (1.16.6-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.16.6-x86_64-linux) - racc (~> 1.4) - optimist (3.0.1) - os (1.0.1) - power_assert (2.0.3) - racc (1.8.0) - rack (2.2.9) - rake (12.3.3) - regexp_parser (2.9.2) - rexml (3.3.0) - strscan - rubyzip (2.3.2) - selenium-webdriver (4.5.0) - childprocess (>= 0.5, < 5.0) - rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) - websocket (~> 1.0) - simpleidn (0.2.3) - strscan (3.1.0) - sys-uname (1.3.0) - ffi (~> 1.1) - test-unit (3.5.9) - power_assert - tomlrb (2.0.3) - uri_template (0.7.0) - webrick (1.7.0) - websocket (1.2.10) - websocket-driver (0.7.6) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - -PLATFORMS - aarch64-linux - arm-linux - arm64-darwin - x86-linux - x86_64-darwin - x86_64-linux - -DEPENDENCIES - bugsnag-maze-runner (~> 9.6) - -BUNDLED WITH - 2.5.1 diff --git a/bugsnag/client.py b/bugsnag/client.py index 558b074a..9717ce9a 100644 --- a/bugsnag/client.py +++ b/bugsnag/client.py @@ -32,10 +32,11 @@ class Client: """ def __init__(self, configuration: Optional[Configuration] = None, - install_sys_hook=True, **kwargs): + install_sys_hook=True, configure=True, **kwargs): self.configuration = configuration or Configuration() # type: Configuration # noqa: E501 self.session_tracker = SessionTracker(self.configuration) - self.configuration.configure(**kwargs) + if configure: + self.configuration.configure(**kwargs) self._context = ContextLocalState(self) self._request_tracker = RequestTracker() diff --git a/bugsnag/configuration.py b/bugsnag/configuration.py index 114de35b..20125937 100644 --- a/bugsnag/configuration.py +++ b/bugsnag/configuration.py @@ -30,8 +30,11 @@ validate_int_setter, validate_path_setter ) -from bugsnag.delivery import (create_default_delivery, DEFAULT_ENDPOINT, - DEFAULT_SESSIONS_ENDPOINT) +from bugsnag.delivery import (create_default_delivery, + DEFAULT_ENDPOINT, + DEFAULT_SESSIONS_ENDPOINT, + HUB_ENDPOINT, + HUB_SESSIONS_ENDPOINT) from bugsnag.uwsgi import warn_if_running_uwsgi_without_threads from bugsnag.error import Error @@ -55,6 +58,11 @@ _sentinel = object() +def _is_hub_api_key(api_key: str) -> bool: + hub_prefix = "00000" + return api_key is not None and api_key.startswith(hub_prefix) + + class Configuration: """ Global app-level Bugsnag configuration settings. @@ -83,8 +91,8 @@ def __init__(self, logger=_sentinel): "django.http.Http404", "django.http.response.Http404", ] - self.endpoint = DEFAULT_ENDPOINT - self.session_endpoint = DEFAULT_SESSIONS_ENDPOINT + self.endpoint = None + self.session_endpoint = None self.auto_capture_sessions = True self.traceback_exclude_modules = [] @@ -126,8 +134,6 @@ def configure(self, api_key=None, app_type=None, app_version=None, Validate and set configuration options. Will warn if an option is of an incorrect type. """ - if api_key is not None: - self.api_key = api_key if app_type is not None: self.app_type = app_type if app_version is not None: @@ -140,8 +146,6 @@ def configure(self, api_key=None, app_type=None, app_version=None, self.auto_capture_sessions = auto_capture_sessions if delivery is not None: self.delivery = delivery - if endpoint is not None: - self.endpoint = endpoint if hostname is not None: self.hostname = hostname if ignore_classes is not None: @@ -162,8 +166,6 @@ def configure(self, api_key=None, app_type=None, app_version=None, self.send_code = send_code if send_environment is not None: self.send_environment = send_environment - if session_endpoint is not None: - self.session_endpoint = session_endpoint if traceback_exclude_modules is not None: self.traceback_exclude_modules = traceback_exclude_modules if logger is not _sentinel: @@ -175,6 +177,11 @@ def configure(self, api_key=None, app_type=None, app_version=None, if max_breadcrumbs is not None: self.max_breadcrumbs = max_breadcrumbs + # Default endpoints depend on the API key + if api_key is not None: + self.api_key = api_key + self._initialize_endpoints(endpoint, session_endpoint, self.api_key) + return self def get(self, name): @@ -584,6 +591,26 @@ def _create_null_logger(self) -> logging.Logger: return logger + def _initialize_endpoints(self, endpoint, session_endpoint, api_key): + # Default endpoints depending on the API key, if not already set + if ( + endpoint is None and + session_endpoint is None and + self.endpoint is None and + self.session_endpoint is None + ): + if _is_hub_api_key(api_key): + self.endpoint = HUB_ENDPOINT + self.session_endpoint = HUB_SESSIONS_ENDPOINT + else: + self.endpoint = DEFAULT_ENDPOINT + self.session_endpoint = DEFAULT_SESSIONS_ENDPOINT + # Do set endpoints if explicitly provided + if endpoint is not None: + self.endpoint = endpoint + if session_endpoint is not None: + self.session_endpoint = session_endpoint + class RequestConfiguration: """ diff --git a/bugsnag/delivery.py b/bugsnag/delivery.py index 6dc8cd05..1a8f03cb 100644 --- a/bugsnag/delivery.py +++ b/bugsnag/delivery.py @@ -28,6 +28,8 @@ DEFAULT_ENDPOINT = 'https://notify.bugsnag.com' DEFAULT_SESSIONS_ENDPOINT = 'https://sessions.bugsnag.com' +HUB_ENDPOINT = 'https://notify.insighthub.smartbear.com' +HUB_SESSIONS_ENDPOINT = 'https://sessions.insighthub.smartbear.com' __all__ = ('default_headers', 'Delivery') @@ -82,8 +84,10 @@ def deliver_sessions(self, config, payload: Any, options=None): """ Sends sessions to Bugsnag """ - if (config.endpoint != DEFAULT_ENDPOINT and config.session_endpoint == - DEFAULT_SESSIONS_ENDPOINT): + if ((config.endpoint != DEFAULT_ENDPOINT and + config.session_endpoint == DEFAULT_SESSIONS_ENDPOINT) or + (config.endpoint != HUB_ENDPOINT and + config.session_endpoint == HUB_SESSIONS_ENDPOINT)): if not self.sent_session_warning: warnings.warn('The session endpoint has not been configured. ' 'No sessions will be sent to Bugsnag.') diff --git a/bugsnag/legacy.py b/bugsnag/legacy.py index 018e6735..32848b52 100644 --- a/bugsnag/legacy.py +++ b/bugsnag/legacy.py @@ -7,7 +7,7 @@ from bugsnag.configuration import RequestConfiguration from bugsnag.client import Client -default_client = Client() +default_client = Client(configure=False) configuration = default_client.configuration logger = configuration.logger ExcInfoType = Tuple[Type, Exception, types.TracebackType] diff --git a/bugsnag/notifier.py b/bugsnag/notifier.py index 6f011f0e..c9b3b212 100644 --- a/bugsnag/notifier.py +++ b/bugsnag/notifier.py @@ -1,5 +1,5 @@ _NOTIFIER_INFORMATION = { 'name': 'Python Bugsnag Notifier', 'url': 'https://github.com/bugsnag/bugsnag-python', - 'version': '4.7.1' + 'version': '4.8.0' } diff --git a/bugsnag/utils.py b/bugsnag/utils.py index c2e1a857..5a18be3e 100644 --- a/bugsnag/utils.py +++ b/bugsnag/utils.py @@ -4,6 +4,7 @@ from threading import local as threadlocal from typing import AnyStr, Tuple, Optional import warnings +import sys import copy import logging from datetime import datetime, timedelta @@ -422,14 +423,13 @@ def remove_query_from_url(url: AnyStr) -> Optional[AnyStr]: # milliseconds precision # Python can do this natively from version 3.6, but we need to include a # fallback implementation for Python 3.5 -try: - # this will raise if 'timespec' isn't supported - datetime.utcnow().isoformat(timespec='milliseconds') # type: ignore - +if sys.version_info >= (3, 6): + # Python 3.6+ has a built-in method for this def to_rfc3339(dt: datetime) -> str: return dt.isoformat(timespec='milliseconds') # type: ignore -except Exception: +else: + # Python 3.5 fallback implementation def _get_timezone_offset(dt: datetime) -> str: if dt.tzinfo is None: return '' @@ -464,17 +464,16 @@ def to_rfc3339(dt: datetime) -> str: def get_package_version(package_name: str) -> Optional[str]: - try: - from importlib import metadata - - return metadata.version(package_name) # type: ignore - except ImportError: + if sys.version_info >= (3, 8): try: - import pkg_resources - except ImportError: + from importlib import metadata + return metadata.version(package_name) # type: ignore + except metadata.PackageNotFoundError: return None - + else: try: - return pkg_resources.get_distribution(package_name).version - except pkg_resources.DistributionNotFound: + import pkg_resources # type: ignore + return pkg_resources.get_distribution( + package_name).version # type: ignore + except (ImportError, pkg_resources.DistributionNotFound): return None diff --git a/features/celery.feature b/features/celery.feature index c78ce0c9..85ca139e 100644 --- a/features/celery.feature +++ b/features/celery.feature @@ -12,7 +12,7 @@ Scenario Outline: Handled exceptions are delivered in Celery And the event "severityReason.type" equals "handledException" And the event "device.runtimeVersions.celery" matches "\.\d+\.\d+" - @not-python-3.11 @not-python-3.12 + @not-python-3.11 @not-python-3.12 @not-python-3.13 Examples: | celery-version | | 4 | @@ -40,7 +40,7 @@ Scenario Outline: Unhandled exceptions are delivered in Celery And the event "metaData.extra_data.args" string is empty And the event "metaData.extra_data.kwargs" string is empty - @not-python-3.11 @not-python-3.12 + @not-python-3.11 @not-python-3.12 @not-python-3.13 Examples: | celery-version | | 4 | @@ -72,7 +72,7 @@ Scenario Outline: Task arguments are added to metadata in Celery And the event "metaData.extra_data.args.1" equals "0" And the event "metaData.extra_data.kwargs" string is empty - @not-python-3.11 @not-python-3.12 + @not-python-3.11 @not-python-3.12 @not-python-3.13 Examples: | celery-version | | 4 | @@ -116,7 +116,7 @@ Scenario Outline: Successful tasks do not report errors in Celery " Then I should receive no errors - @not-python-3.11 @not-python-3.12 + @not-python-3.11 @not-python-3.12 @not-python-3.13 Examples: | celery-version | | 4 | @@ -131,7 +131,7 @@ Scenario Outline: Successful shared tasks do not report errors in Celery " Then I should receive no errors - @not-python-3.11 @not-python-3.12 + @not-python-3.11 @not-python-3.12 @not-python-3.13 Examples: | celery-version | | 4 | diff --git a/setup.py b/setup.py index ddd999a0..7c0b5e2e 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='bugsnag', - version='4.7.1', + version='4.8.0', description='Automatic error monitoring for django, flask, etc.', long_description=__doc__, author='Simon Maynard', diff --git a/tests/integrations/test_asgi.py b/tests/integrations/test_asgi.py index 47441a5c..e998f32f 100644 --- a/tests/integrations/test_asgi.py +++ b/tests/integrations/test_asgi.py @@ -191,6 +191,7 @@ async def index(req): assert breadcrumbs[0]['metaData'] == {'to': '/'} assert breadcrumbs[0]['type'] == BreadcrumbType.NAVIGATION.value + @pytest.mark.skip(reason="Skipped pending PLAT-14413") def test_websocket_crash(self): async def app(scope, receive, send): websocket = WebSocket(scope, receive=receive, send=send) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 03266940..57dfe03e 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -83,7 +83,7 @@ def test_hostname(self): def test_session_tracking_defaults(self): c = Configuration() self.assertTrue(c.auto_capture_sessions) - self.assertEqual(c.session_endpoint, "https://sessions.bugsnag.com") + self.assertEqual(c.session_endpoint, None) def test_default_middleware_location(self): c = Configuration() @@ -114,6 +114,16 @@ def test_validate_endpoint(self): c.configure(endpoint='https://notify.example.com') assert c.endpoint == 'https://notify.example.com' + def test_validate_endpoint_bugsnag_api_key(self): + c = Configuration() + c.configure(api_key='12312312312312312312312312321312') + assert c.endpoint == 'https://notify.bugsnag.com' + + def test_validate_endpoint_hub_api_key(self): + c = Configuration() + c.configure(api_key='00000312312312312312312312321312') + assert c.endpoint == 'https://notify.insighthub.smartbear.com' + def test_validate_app_type(self): c = Configuration() assert c.app_type is None @@ -410,6 +420,17 @@ def test_validate_session_endpoint(self): c.configure(session_endpoint='https://sessions.example.com') assert c.session_endpoint == 'https://sessions.example.com' + def test_validate_session_endpoint_bugsnag_api_key(self): + c = Configuration() + c.configure(api_key='12312312312312312312312312321312') + assert c.session_endpoint == 'https://sessions.bugsnag.com' + + def test_validate_session_endpoint_hub_api_key(self): + c = Configuration() + c.configure(api_key='00000312312312312312312312321312') + assert (c.session_endpoint == + 'https://sessions.insighthub.smartbear.com') + def test_validate_traceback_exclude_modules(self): c = Configuration() with pytest.warns(RuntimeWarning) as record: diff --git a/tox.ini b/tox.ini index 325d6b3d..e5135fe7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,15 @@ [tox] envlist= - py{35,36,37,38,39,310,311,312}-{test,requests,flask,tornado,wsgi,bottle} - py{36,37,38,39,310,311,312}-asgi + py{35,36,37,38,39,310,311,312,313}-{test,requests,flask,tornado,wsgi,bottle} + py{36,37,38,39,310,311,312,313}-asgi py{35,36,37}-django{18,19,110,111} py{35,36,37,38,39}-django20 py{35,36,37,38,39,310}-django{21,22} - py{36,37,38,39,310,311,312}-django3 - py{38,39,310,311,312}-django4 - py{38,39,310,311,312}-{asynctest,threadtest} - py{37,38,39,310,311,312}-exceptiongroup - py{35,312}-{lint} + py{36,37,38,39,310,311,312,313}-django3 + py{38,39,310,311,312,313}-django4 + py{38,39,310,311,312,313}-{asynctest,threadtest} + py{37,38,39,310,311,312,313}-exceptiongroup + py{35,313}-{lint} [pytest] testpaths = tests @@ -32,6 +32,7 @@ basepython = py310: python3.10 py311: python3.11 py312: python3.12 + py313: python3.13 whitelist_externals= {toxinidir}/scripts/lint.sh deps= @@ -64,7 +65,8 @@ deps= exceptiongroup: exceptiongroup lint: flake8 lint: mypy - lint: types-pkg_resources + lint: types-pkg_resources; python_version < '3.12' + lint: types-setuptools lint: types-requests lint: types-Flask lint: types-contextvars