diff --git a/lms/envs/common.py b/lms/envs/common.py index 90d5517fa4bb..6c78327d07e8 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2188,8 +2188,8 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # Before anything that looks at cookies, especially the session middleware 'openedx.core.djangoapps.cookie_metadata.middleware.CookieNameChange', - # Monitoring and logging for expected and ignored errors - 'openedx.core.lib.request_utils.ExpectedErrorMiddleware', + # Monitoring and logging for ignored errors + 'openedx.core.lib.request_utils.IgnoredErrorMiddleware', 'lms.djangoapps.mobile_api.middleware.AppVersionUpgrade', 'openedx.core.djangoapps.header_control.middleware.HeaderControlMiddleware', @@ -3330,7 +3330,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', ), - 'EXCEPTION_HANDLER': 'openedx.core.lib.request_utils.expected_error_exception_handler', + 'EXCEPTION_HANDLER': 'openedx.core.lib.request_utils.ignored_error_exception_handler', 'PAGE_SIZE': 10, 'URL_FORMAT_OVERRIDE': None, 'DEFAULT_THROTTLE_RATES': { diff --git a/lms/static/js/learner_dashboard/spec/program_details_view_spec.js b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js index 795667d2dec2..2387ade00b9e 100644 --- a/lms/static/js/learner_dashboard/spec/program_details_view_spec.js +++ b/lms/static/js/learner_dashboard/spec/program_details_view_spec.js @@ -544,6 +544,28 @@ describe('Program Details View', () => { .toContainText(body); }; + const testSubscriptionSunsetting = (state, heading, body) => { + const subscriptionData = { + ...options.subscriptionData[0], + subscription_state: state, + }; + // eslint-disable-next-line no-use-before-define + view = initView({ + // eslint-disable-next-line no-undef + programData: $.extend({}, options.programData, { + subscription_eligible: false, + }), + isUserB2CSubscriptionsEnabled: true, + subscriptionData: [subscriptionData], + }); + view.render(); + expect(view.$('.upgrade-subscription')[0]).not.toBeInDOM(); + expect(view.$('.upgrade-subscription .upgrade-button')).not + .toContainText(heading); + expect(view.$('.upgrade-subscription .subscription-info-brief')).not + .toContainText(body); + }; + const initView = (updates) => { // eslint-disable-next-line no-undef const viewOptions = $.extend({}, options, updates); @@ -709,8 +731,8 @@ describe('Program Details View', () => { ); }); - it('should render the get subscription link if program is subscription eligible', () => { - testSubscriptionState( + it('should not render the get subscription link if program is not active', () => { + testSubscriptionSunsetting( 'pre', 'Start 7-day free trial', '$100/month USD subscription after trial ends. Cancel anytime.', @@ -734,8 +756,8 @@ describe('Program Details View', () => { ); }); - it('should render appropriate subscription text when subscription is inactive', () => { - testSubscriptionState( + it('should not render appropriate subscription text when subscription is inactive', () => { + testSubscriptionSunsetting( 'inactive', 'Restart my subscription', '$100/month USD subscription. Cancel anytime.', diff --git a/lms/static/js/learner_dashboard/views/program_details_view.js b/lms/static/js/learner_dashboard/views/program_details_view.js index 8d6c41814702..220840c182e4 100644 --- a/lms/static/js/learner_dashboard/views/program_details_view.js +++ b/lms/static/js/learner_dashboard/views/program_details_view.js @@ -215,9 +215,13 @@ class ProgramDetailsView extends Backbone.View { && this.remainingCourseCollection.length === 0 ); + const isSubscriptionActiveSunsetting = ( + this.subscriptionModel.get('subscriptionState') === 'active' + ) + return ( this.options.isUserB2CSubscriptionsEnabled - && this.options.programData.subscription_eligible + && isSubscriptionActiveSunsetting && !programPurchasedWithoutSubscription ); } diff --git a/openedx/core/djangoapps/enrollments/views.py b/openedx/core/djangoapps/enrollments/views.py index 80f6df853194..f413cb761e14 100644 --- a/openedx/core/djangoapps/enrollments/views.py +++ b/openedx/core/djangoapps/enrollments/views.py @@ -1066,7 +1066,9 @@ def post(self, request): **Example Request** - POST /api/enrollment/v1/enrollment_allowed + POST /api/enrollment/v1/enrollment_allowed/ + + Note: The URL for this request must finish with / Example request data: ``` @@ -1086,7 +1088,6 @@ def post(self, request): - `auto_enroll` (optional, bool: default=false, _body_) **Responses** - - 200: Success, enrollment allowed found. - 400: Bad request, missing data. - 403: Forbidden, you need to be staff. - 409: Conflict, enrollment allowed already exists. @@ -1122,7 +1123,9 @@ def delete(self, request): **Example Request** - DELETE /api/enrollment/v1/enrollment_allowed + DELETE /api/enrollment/v1/enrollment_allowed/ + + Note: The URL for this request must finish with / Example request data: ``` diff --git a/openedx/core/lib/docs/decisions/0001-logging-and-monitoring-ignored-errors.rst b/openedx/core/lib/docs/decisions/0001-logging-and-monitoring-ignored-errors.rst index 563d2d8d83f2..fc922397dc27 100644 --- a/openedx/core/lib/docs/decisions/0001-logging-and-monitoring-ignored-errors.rst +++ b/openedx/core/lib/docs/decisions/0001-logging-and-monitoring-ignored-errors.rst @@ -4,7 +4,7 @@ Logging and monitoring ignored errors Status ------ -Accepted +Partially Superseded (see 0002-logging-and-monitoring-expected-errors-removed.rst) Context ------- @@ -14,7 +14,7 @@ We had two recent Production issues that took longer than necessary to diagnose Decision ________ -Add a capability for logging and monitoring ignored errors and expected errors. The feature will be configurable, the monitoring will be available by default, and the logging will be opt-in as needed. +Add a capability for logging and monitoring ignored errors. The feature will be configurable, the monitoring will be available by default, and the logging will be opt-in as needed. Note: This feature is being added to edx-platform to start, but could be moved to edx-django-utils monitoring for use in other IDAs. @@ -23,6 +23,13 @@ Consequence The new capabilities have been built in edx-platform, although they could be moved to edx-django-utils monitoring in the future for use in other IDAs. -The new feature adds the ability to mark errors as expected, temporarily or permanently, even without "ignoring" them everywhere. For example, the errors and stacktraces would still appear, but it would be possible for alert conditions to ignore expected errors. +The new feature adds the ability to mark errors as ignored, temporarily or permanently, even without "ignoring" them everywhere. For example, the errors and stacktraces would still appear, but it would be possible for alert conditions to ignore errors. -See how_tos/logging-and-monitoring-expected-errors.rst for more information. +See how_tos/logging-and-monitoring-ignored-errors.rst for more information. + +Changelog +--------- + +* **2023-09-28** - Changed status to "Partially Superseded", because expected errors will no longer be handled. See 0002-logging-and-monitoring-expected-errors-removed.rst for details. + +* **2021-03-17** - Original "Accepted" ADR diff --git a/openedx/core/lib/docs/decisions/0002-logging-and-monitoring-expected-errors-removed.rst b/openedx/core/lib/docs/decisions/0002-logging-and-monitoring-expected-errors-removed.rst new file mode 100644 index 000000000000..3eb9b89b50e8 --- /dev/null +++ b/openedx/core/lib/docs/decisions/0002-logging-and-monitoring-expected-errors-removed.rst @@ -0,0 +1,51 @@ +Logging and monitoring of expected errors removed +================================================= + +Status +------ + +Accepted + +Context +------- + +The setting ``EXPECTED_ERRORS`` was used for both expected and ignored errors in edx-platform. + +The functionality supporting expected errors was added: + +* Because New Relic didn't yet support this for Python, and +* Because it was thought that people may want more flexibility for marking errors as expected in addition to just the class name, and +* The setting provides a place to document why the change is being made. + +Updates regarding these three original reasons: + +* New Relic now supports expected errors, so the custom functionality is redundant and complicated, and +* This functionality has yet to be used, so it is also unnecessary. + +Note that the need for custom functionality for ignored errors differs from expected errors in the following ways: + +* New Relic still has problems from time to time where it stops ignoring errors that it is supposed to be ignoring, and this redundant functionality is used to catch that issue, and +* Since New Relic does not capture details of ignored errors, the custom functionality to log these errors can still come in handy if more details are needed. + +This context can also be found in `[DEPR] Expected error part of EXPECTED_ERRORS`_. + +.. _[DEPR] Expected error part of EXPECTED_ERRORS: https://github.com/openedx/edx-platform/issues/32405 + +Decision +________ + +The custom ignored error functionality proposed in 0001-logging-and-monitoring-ignored-errors.rst will remain in place, but the custom expected error functionality will be removed. + +Consequence +----------- + +* A number of code changes are required to remove the expected functionality. + + * In many placed in the code, "expected" was used to mean "ignored and expected", and all such instances will be renamed to "ignored". + * The setting ``EXPECTED_ERRORS`` will be renamed to ``IGNORED_ERRORS``, which better matches how it was being used in the first place. + * The setting ``EXPECTED_ERRORS[REASON_EXPECTED]`` will be renamed to ``IGNORED_ERRORS[REASON_IGNORED]``. + * The setting toggle ``EXPECTED_ERRORS[IS_IGNORED]`` will be removed, because it will now always be True. + * The how-to will be renamed to how_tos/logging-and-monitoring-ignored-errors.rst. + * For more details, see https://github.com/openedx/edx-platform/pull/33184 where this work is underway. + +* If anyone ever uses New Relic's expected error functionality, the reason for marking an error as expected would need to be captured elsewhere. diff --git a/openedx/core/lib/docs/how_tos/logging-and-monitoring-expected-errors.rst b/openedx/core/lib/docs/how_tos/logging-and-monitoring-expected-errors.rst deleted file mode 100644 index 0def7a08e6c4..000000000000 --- a/openedx/core/lib/docs/how_tos/logging-and-monitoring-expected-errors.rst +++ /dev/null @@ -1,41 +0,0 @@ -How to log and monitor expected errors -====================================== - -What are expected and ignored errors? -------------------------------------- - -Errors might be "expected" for various reasons. Some examples: - -* Errors like ``PermissionDenied``, or ``Ratelimited``, where users or bots are prevented from doing something that is not allowed. -* Errors that we have already diagnosed, but may either not be worth fixing, or we are unable to fix for some time. - -In some cases, a bug could introduce a large number errors where an "expected" error becomes an "unexpected" error. Or, there might be certain request paths for which the same error may be expected or unexpected. - -A subset of "expected" errors are "ignored" errors. edX uses New Relic, which can be configured to ignore errors. These errors get suppressed and leave no (stack) trace, and won't trigger error alerts. - -Monitoring expected errors --------------------------- - -At a minimum, it is recommended that you add monitoring for any expected errors, including ignored errors. You do this by using the ``EXPECTED_ERRORS`` setting. For details on configuring, see the documentation for `EXPECTED_ERRORS settings and toggles on Readthedocs`_. - -This will provide an ``error_expected`` custom attribute for every expected error. This custom attribute can be used in the following ways. - -* Use ``WHERE error_expected IS NULL`` to filter out expected errors from most alert conditions. -* Set up special alerts for an overabundance of expected errors using ``WHERE error_expected IS NOT NULL``. - -Additionally, a subset of expected errors that are configured as ignored will also get ``error_ignored_class`` and ``error_ignored_message`` custom attributes. - -* Using New Relic terminology, this extra error class and message data will live on the Transaction and not the TransactionError, because ignored errors won't have a TransactionError. -* Use these additional custom attributes to help diagnose unexpected issues with ignored errors. - -.. _EXPECTED_ERRORS settings and toggles on Readthedocs: https://edx.readthedocs.io/projects/edx-platform-technical/en/latest/search.html?q=EXPECTED_ERRORS&check_keywords=yes&area=default - -Logging expected errors ------------------------ - -Following the same documentation as for monitoring, you can also enable logging with or without a stack trace. This additional information may be useful for tracking down the source of a mysterious appearance of an otherwise expected error. - -More targeted scoping of errors -------------------------------- - -The initial implementation only enables this functionality by error ``module.Class``. If you need to scope the monitoring/logging for a more limited subset, like for certain requests, the expectation would be to enhance and document these new capabilities. diff --git a/openedx/core/lib/docs/how_tos/logging-and-monitoring-ignored-errors.rst b/openedx/core/lib/docs/how_tos/logging-and-monitoring-ignored-errors.rst new file mode 100644 index 000000000000..8305af4617ef --- /dev/null +++ b/openedx/core/lib/docs/how_tos/logging-and-monitoring-ignored-errors.rst @@ -0,0 +1,33 @@ +How to log and monitor ignored errors +====================================== + +What are ignored errors? +------------------------------------- + +Operators of Open edX might use New Relic for monitoring, which can be configured to ignore errors. These errors get suppressed and leave no (stack) trace, and won't trigger error alerts. + +Monitoring ignored errors +-------------------------- + +At a minimum, it is recommended that you add monitoring for any ignored errors. You do this by using the ``IGNORED_ERRORS`` setting. For details on configuring, see the documentation for `IGNORED_ERRORS settings and toggles on Readthedocs`_. + +This will provide an ``error_ignored`` custom attribute for every ignored error. This custom attribute can be used in the following ways. + +* Use ``SELECT * FROM TransactionError WHERE error_ignored IS True`` to find errors that should have been ignored, but are not. This may be due to a bug or misconfiguration in New Relic. + +Additionally, a subset of ignored errors that are configured as ignored will also get ``error_ignored_class`` and ``error_ignored_message`` custom attributes. + +* Using New Relic terminology, this extra error class and message data will live on the Transaction and not the TransactionError, because ignored errors won't have a TransactionError. +* Use these additional custom attributes to help diagnose unexpected issues with ignored errors. + +.. _IGNORED_ERRORS settings and toggles on Readthedocs: https://edx.readthedocs.io/projects/edx-platform-technical/en/latest/search.html?q=IGNORED_ERRORS&check_keywords=yes&area=default + +Logging ignored errors +----------------------- + +Following the same documentation as for monitoring, you can also enable logging with or without a stack trace. This additional information may be useful for tracking down the source of a mysterious appearance of an otherwise ignored error. + +More targeted scoping of errors +------------------------------- + +The initial implementation only enables this functionality by error ``module.Class``. If you need to scope the monitoring/logging for a more limited subset, like for certain requests, the expectation would be to enhance and document these new capabilities. diff --git a/openedx/core/lib/request_utils.py b/openedx/core/lib/request_utils.py index e113c550c421..36d2b12ec3d5 100644 --- a/openedx/core/lib/request_utils.py +++ b/openedx/core/lib/request_utils.py @@ -89,11 +89,11 @@ def course_id_from_url(url): return None -def expected_error_exception_handler(exc, context): +def ignored_error_exception_handler(exc, context): """ - Replacement for DRF's default exception handler to enable observing expected errors. + Replacement for DRF's default exception handler to enable observing ignored errors. - In addition to the default behaviour, add logging and monitoring for expected errors. + In addition to the default behaviour, add logging and monitoring for ignored errors. """ # Call REST framework's default exception handler first to get the standard error response. response = exception_handler(exc, context) @@ -103,13 +103,13 @@ def expected_error_exception_handler(exc, context): except TypeError: # when context is not iterable request = None - _log_and_monitor_expected_errors(request, exc, 'drf') + _log_and_monitor_ignored_errors(request, exc, 'drf') return response -class ExpectedErrorMiddleware: +class IgnoredErrorMiddleware: """ - Middleware to add logging and monitoring for expected errors. + Middleware to add logging and monitoring for ignored errors. """ def __init__(self, get_response): self.get_response = get_response @@ -120,57 +120,46 @@ def __call__(self, request): def process_exception(self, request, exception): """ - Add logging and monitoring of expected errors. + Add logging and monitoring of ignored errors. """ - _log_and_monitor_expected_errors(request, exception, 'middleware') + _log_and_monitor_ignored_errors(request, exception, 'middleware') -# .. setting_name: EXPECTED_ERRORS +# .. setting_name: IGNORED_ERRORS # .. setting_default: None -# .. setting_description: Used to configure logging and monitoring for expected errors. +# .. setting_description: Used to configure logging and monitoring for ignored errors. # This setting is configured of a list of dicts. See setting and toggle annotations for -# ``EXPECTED_ERRORS[N]['XXX']`` for details of each item in the dict. -# If this setting is a non-empty list, all uncaught errors processed will get a ``checked_error_expected_from`` -# attribute, whether they are expected or not. Those errors that are processed and match a 'MODULE_AND_CLASS' -# (documented elsewhere), will get an ``error_expected`` custom attribute. Unexpected errors would be errors with -# ``error_expected IS NULL``. For additional diagnostic information for ignored errors, see the -# EXPECTED_ERRORS[N]['IS_IGNORED'] annotation. +# ``IGNORED_ERRORS[N]['XXX']`` for details of each item in the dict. +# If this setting is a non-empty list, all uncaught errors processed will get a ``checked_error_ignored_from`` +# attribute, whether they are ignored or not. See IGNORED_ERRORS[N]['MODULE_AND_CLASS'] annotation +# for details of monitoring added if the error is to be ignored. # .. setting_warning: We use Django Middleware and a DRF custom error handler to find uncaught errors. Some errors may -# slip through the cracks, like ValidationError. Any error where ``checked_error_expected_from IS NULL`` is +# slip through the cracks, like ValidationError. Any error where ``checked_error_ignored_from IS NULL`` is # an error that was not processed. -# .. setting_name: EXPECTED_ERRORS[N]['MODULE_AND_CLASS'] +# .. setting_name: IGNORED_ERRORS[N]['MODULE_AND_CLASS'] # .. setting_default: None -# .. setting_description: Required error module and class name that is expected. For example, -# ``rest_framework.exceptions.PermissionDenied``. - -# .. toggle_name: EXPECTED_ERRORS[N]['IS_IGNORED'] -# .. toggle_implementation: DjangoSetting -# .. toggle_default: True -# .. toggle_description: Set this to False if the errors are not ignored by monitoring, but only expected, like -# for temporary problems that may take some time to fix. If True, adds the custom attributes -# ``error_ignored_class`` and ``error_ignored_message`` to help diagnose issues with ignored errors, since -# this data is not otherwise available. For example of ignoring errors in New Relic, see: +# .. setting_description: Required error module and class name that is ignored. For example, +# ``rest_framework.exceptions.PermissionDenied``. If the current error matches the module and class +# defined here, the middleware will add the custom attributes ``error_ignored_class`` and ``error_ignored_message`` +#. to help diagnose issues with ignored errors, since this data is not otherwise available. +# For an example of ignoring errors in New Relic, see: # https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-data/manage-errors-apm-collect-ignore-or-mark-expected/#ignore pylint: disable=line-too-long,useless-suppression # To query for ignored errors, you would use ``error_ignored_class IS NOT NULL``. -# Note: This is defaulted to True because it will be easier for us to detect if True is not the correct value, by -# seeing that these errors aren't actually ignored. -# .. toggle_warning: At this time, this toggle does not actually configure the error to be ignored. It is meant to match -# the ignored error configuration found elsewhere. When monitoring, no errors should ever have the attribute +# .. setting_warning: At this time, an error that matches won't actually be ignored. These settings should be set to match +# the ignored error configuration found elsewhere, like in New Relic. When monitoring, no errors should ever have the attribute # ``error_ignored_class``. Only Transactions should have this custom attribute. If found for an error, it means we -# are stating an error should be ignored when it is not actually configured as such, or the configuration is not +# are stating an error should be ignored when it is not actually configured as such, or the (New Relic) configuration is not # working. -# .. toggle_use_cases: opt_out -# .. toggle_creation_date: 2021-03-11 -# .. toggle_name: EXPECTED_ERRORS[N]['LOG_ERROR'] +# .. toggle_name: IGNORED_ERRORS[N]['LOG_ERROR'] # .. toggle_implementation: DjangoSetting # .. toggle_default: False -# .. toggle_description: If True, the error will be logged with a message like: "Expected error ...". +# .. toggle_description: If True, the error will be logged with a message like: "Ignored error ...". # .. toggle_use_cases: opt_in # .. toggle_creation_date: 2021-03-11 -# .. toggle_name: EXPECTED_ERRORS[N]['LOG_STACK_TRACE'] +# .. toggle_name: IGNORED_ERRORS[N]['LOG_STACK_TRACE'] # .. toggle_implementation: DjangoSetting # .. toggle_default: False # .. toggle_description: If True, the stacktrace will be included with the logging message. @@ -178,123 +167,121 @@ def process_exception(self, request, exception): # .. toggle_use_cases: opt_in # .. toggle_creation_date: 2021-03-11 -# .. setting_name: EXPECTED_ERRORS[N]['REASON_EXPECTED'] +# .. setting_name: IGNORED_ERRORS[N]['REASON_IGNORED'] # .. setting_default: None -# .. setting_description: Required string explaining why the error is expected and/or ignored for documentation +# .. setting_description: Required string explaining why the error is ignored for documentation # purposes. -# Warning: do not access this directly, but instead use _get_expected_error_settings_dict. -# EXPECTED ERRORS Django setting is processed and stored as a dict keyed by ERROR_MODULE_AND_CLASS. -_EXPECTED_ERROR_SETTINGS_DICT = None +# Warning: do not access this directly, but instead use _get_ignored_error_settings_dict. +# IGNORED_ERRORS Django setting is processed and stored as a dict keyed by ERROR_MODULE_AND_CLASS. +_IGNORED_ERROR_SETTINGS_DICT = None -def _get_expected_error_settings_dict(): +def _get_ignored_error_settings_dict(): """ - Returns a dict of dicts of expected error settings used for logging and monitoring. + Returns a dict of dicts of ignored error settings used for logging and monitoring. - The contents of the EXPECTED_ERRORS Django Setting list is processed for efficient lookup by module.Class. + The contents of the IGNORED_ERRORS Django Setting list is processed for efficient lookup by module.Class. Returns: - (dict): dict of dicts, mapping module-and-class name to settings for proper handling of expected errors. - Keys of the inner dicts use the lowercase version of the related Django Setting (e.g. 'REASON_EXPECTED' => - 'reason_expected'). + (dict): dict of dicts, mapping module-and-class name to settings for proper handling of ignored errors. + Keys of the inner dicts use the lowercase version of the related Django Setting (e.g. 'REASON_IGNORED' => + 'reason_ignored'). Example return value:: { 'rest_framework.exceptions:PermissionDenied': { - 'is_ignored': True, 'log_error': True, 'log_stack_trace': True, - 'reason_expected': 'In most cases, signifies a user was trying to do something they cannot do. ' + 'reason_ignored': 'In most cases, signifies a user was trying to do something they cannot do. ' 'However, an overabundance could indicate a bug, which could be monitored for.' } ... } """ - global _EXPECTED_ERROR_SETTINGS_DICT + global _IGNORED_ERROR_SETTINGS_DICT # Return cached processed mappings if already processed - if _EXPECTED_ERROR_SETTINGS_DICT is not None: - return _EXPECTED_ERROR_SETTINGS_DICT + if _IGNORED_ERROR_SETTINGS_DICT is not None: + return _IGNORED_ERROR_SETTINGS_DICT - expected_errors = getattr(settings, 'EXPECTED_ERRORS', None) - if expected_errors is None: - _EXPECTED_ERROR_SETTINGS_DICT = {} - return _EXPECTED_ERROR_SETTINGS_DICT + ignored_errors = getattr(settings, 'IGNORED_ERRORS', None) + if ignored_errors is None: + _IGNORED_ERROR_SETTINGS_DICT = {} + return _IGNORED_ERROR_SETTINGS_DICT # Use temporary variable to build mappings to avoid multi-threading issue with a partially # processed map. Worst case, it is processed more than once at start-up. - expected_error_settings_dict = {} + ignored_error_settings_dict = {} try: - for index, expected_error in enumerate(expected_errors): - module_and_class = expected_error.get('MODULE_AND_CLASS') - processed_expected_error = { - 'is_ignored': expected_error.get('IS_IGNORED', True), - 'log_error': expected_error.get('LOG_ERROR', False), - 'log_stack_trace': expected_error.get('LOG_STACK_TRACE', False), - 'reason_expected': expected_error.get('REASON_EXPECTED'), + for index, ignored_error in enumerate(ignored_errors): + module_and_class = ignored_error.get('MODULE_AND_CLASS') + processed_ignored_error = { + 'log_error': ignored_error.get('LOG_ERROR', False), + 'log_stack_trace': ignored_error.get('LOG_STACK_TRACE', False), + 'reason_ignored': ignored_error.get('REASON_IGNORED'), } # validate configuration if not isinstance(module_and_class, str): log.error( - "Skipping EXPECTED_ERRORS[%d] setting. 'MODULE_AND_CLASS' set to [%s] and should be module.Class, " + "Skipping IGNORED_ERRORS[%d] setting. 'MODULE_AND_CLASS' set to [%s] and should be module.Class, " "like 'rest_framework.exceptions.PermissionDenied'.", index, module_and_class ) continue if ':' in module_and_class: log.warning( - "Replacing ':' with '.' in EXPECTED_ERRORS[%d]['MODULE_AND_CLASS'], which was set to %s. Note that " + "Replacing ':' with '.' in IGNORED_ERRORS[%d]['MODULE_AND_CLASS'], which was set to %s. Note that " "monitoring and logging will not include the ':'.", index, module_and_class ) module_and_class = module_and_class.replace(":", ".") - if module_and_class in expected_error_settings_dict: + if module_and_class in ignored_error_settings_dict: log.warning( - "EXPECTED_ERRORS[%d] setting is overriding an earlier setting. 'MODULE_AND_CLASS' [%s] is defined " + "IGNORED_ERRORS[%d] setting is overriding an earlier setting. 'MODULE_AND_CLASS' [%s] is defined " "multiple times.", index, module_and_class ) - if not processed_expected_error['reason_expected']: + if not processed_ignored_error['reason_ignored']: log.error( - "Skipping EXPECTED_ERRORS[%d] setting. 'REASON_EXPECTED' is required to document why %s is an " - "expected error.", + "Skipping IGNORED_ERRORS[%d] setting. 'REASON_IGNORED' is required to document why %s is an " + "ignored error.", index, module_and_class ) continue - expected_error_settings_dict[module_and_class] = processed_expected_error + ignored_error_settings_dict[module_and_class] = processed_ignored_error except Exception as e: # pylint: disable=broad-except - set_custom_attribute('expected_errors_setting_misconfigured', repr(e)) - log.exception(f'Error processing setting EXPECTED_ERRORS. {repr(e)}') + set_custom_attribute('ignored_errors_setting_misconfigured', repr(e)) + log.exception(f'Error processing setting IGNORED_ERRORS. {repr(e)}') - _EXPECTED_ERROR_SETTINGS_DICT = expected_error_settings_dict - return _EXPECTED_ERROR_SETTINGS_DICT + _IGNORED_ERROR_SETTINGS_DICT = ignored_error_settings_dict + return _IGNORED_ERROR_SETTINGS_DICT -def clear_cached_expected_error_settings(): +def clear_cached_ignored_error_settings(): """ - Clears the cached expected error settings. Useful for testing. + Clears the cached ignored error settings. Useful for testing. """ - global _EXPECTED_ERROR_SETTINGS_DICT - _EXPECTED_ERROR_SETTINGS_DICT = None + global _IGNORED_ERROR_SETTINGS_DICT + _IGNORED_ERROR_SETTINGS_DICT = None -def _log_and_monitor_expected_errors(request, exception, caller): +def _log_and_monitor_ignored_errors(request, exception, caller): """ - Adds logging and monitoring for expected errors as needed. + Adds logging and monitoring for ignored errors as needed. Arguments: request: The request exception: The exception caller: Either 'middleware' or 'drf` """ - expected_error_settings_dict = _get_expected_error_settings_dict() - if not expected_error_settings_dict: + ignored_error_settings_dict = _get_ignored_error_settings_dict() + if not ignored_error_settings_dict: return # 'module.Class', for example, 'django.core.exceptions.PermissionDenied' @@ -303,8 +290,8 @@ def _log_and_monitor_expected_errors(request, exception, caller): separator = '.' if exception_module else '' module_and_class = f'{exception_module}{separator}{exception.__class__.__name__}' - # Set checked_error_expected_from custom attribute to potentially help find issues where errors are never processed. - set_custom_attribute('checked_error_expected_from', caller) + # Set checked_error_ignored_from custom attribute to potentially help find issues where errors are never processed. + set_custom_attribute('checked_error_ignored_from', caller) # check if we already added logging/monitoring from a different caller request_cache = RequestCache('openedx.core.lib.request_utils') @@ -313,39 +300,37 @@ def _log_and_monitor_expected_errors(request, exception, caller): cached_module_and_class = cached_handled_exception.value # exception was already processed by a different caller if cached_handled_exception.value == module_and_class: - set_custom_attribute('checked_error_expected_from', 'multiple') + set_custom_attribute('checked_error_ignored_from', 'multiple') return # We have confirmed using monitoring that it is very rare that middleware and drf handle different uncaught exceptions. - # We will leave this attribute in place, but it is not worth investing in a workaround, especially given that - # New Relic now offers its own expected error functionality, and this functionality may be simplified or removed. + # We will leave this attribute in place, but it is not worth investing in a workaround. set_custom_attribute('unexpected_multiple_exceptions', cached_module_and_class) log.warning( - "Unexpected scenario where different exceptions are handled by _log_and_monitor_expected_errors. " + "Unexpected scenario where different exceptions are handled by _log_and_monitor_ignored_errors. " "See 'unexpected_multiple_exceptions' custom attribute. Skipping exception for %s.", module_and_class, ) return request_cache.set('handled_exception', module_and_class) - if module_and_class not in expected_error_settings_dict: + if module_and_class not in ignored_error_settings_dict: return exception_message = str(exception) - set_custom_attribute('error_expected', True) - expected_error_settings = expected_error_settings_dict[module_and_class] - if expected_error_settings['is_ignored']: - # Additional error details are needed for ignored errors, because they are otherwise - # not available by our monitoring system, because they have been ignored. - set_custom_attribute('error_ignored_class', module_and_class) - set_custom_attribute('error_ignored_message', exception_message) + # Additional error details are needed for ignored errors, because they are otherwise + # not available by our monitoring system, because they have been ignored. + set_custom_attribute('error_ignored_class', module_and_class) + set_custom_attribute('error_ignored_message', exception_message) + + ignored_error_settings = ignored_error_settings_dict[module_and_class] - if expected_error_settings['log_error']: - exc_info = exception if expected_error_settings['log_stack_trace'] else None + if ignored_error_settings['log_error']: + exc_info = exception if ignored_error_settings['log_stack_trace'] else None request_path = getattr(request, 'path', 'request-path-unknown') log.info( - 'Expected error %s: %s: seen for path %s', + 'Ignored error %s: %s: seen for path %s', module_and_class, exception_message, request_path, diff --git a/openedx/core/lib/tests/test_request_utils.py b/openedx/core/lib/tests/test_request_utils.py index 390d5ae3c849..3eedea756bde 100644 --- a/openedx/core/lib/tests/test_request_utils.py +++ b/openedx/core/lib/tests/test_request_utils.py @@ -12,11 +12,11 @@ from edx_django_utils.cache import RequestCache from openedx.core.lib.request_utils import ( - ExpectedErrorMiddleware, - _get_expected_error_settings_dict, - clear_cached_expected_error_settings, + IgnoredErrorMiddleware, + _get_ignored_error_settings_dict, + clear_cached_ignored_error_settings, course_id_from_url, - expected_error_exception_handler, + ignored_error_exception_handler, get_request_or_stub, safe_get_host, ) @@ -102,9 +102,9 @@ def assertCourseIdFieldsMatch(self, course_id, org, course, run): assert course_id.run == run -class TestGetExpectedErrorSettingsDict(unittest.TestCase): +class TestGetIgnoredErrorSettingsDict(unittest.TestCase): """ - Tests for processing issues in _get_expected_error_settings_dict() + Tests for processing issues in _get_ignored_error_settings_dict() Note: Although this is a private method, we have broken out the testing of special cases so they don't have to be tested with the middleware and drf @@ -112,100 +112,99 @@ class TestGetExpectedErrorSettingsDict(unittest.TestCase): """ def setUp(self): super().setUp() - clear_cached_expected_error_settings() + clear_cached_ignored_error_settings() def test_get_with_no_setting(self): - expected_error_settings_dict = _get_expected_error_settings_dict() - assert expected_error_settings_dict == {} + ignored_error_settings_dict = _get_ignored_error_settings_dict() + assert ignored_error_settings_dict == {} - @override_settings(EXPECTED_ERRORS=[]) + @override_settings(IGNORED_ERRORS=[]) def test_get_with_empty_list_setting(self): - expected_error_settings_dict = _get_expected_error_settings_dict() - assert expected_error_settings_dict == {} + ignored_error_settings_dict = _get_ignored_error_settings_dict() + assert ignored_error_settings_dict == {} @patch('openedx.core.lib.request_utils.log') - @override_settings(EXPECTED_ERRORS=[{}]) + @override_settings(IGNORED_ERRORS=[{}]) def test_get_with_missing_module_and_class(self, mock_logger): - expected_error_settings_dict = _get_expected_error_settings_dict() + ignored_error_settings_dict = _get_ignored_error_settings_dict() mock_logger.error.assert_called_once_with( - "Skipping EXPECTED_ERRORS[%d] setting. 'MODULE_AND_CLASS' set to [%s] and should be module.Class, like " + "Skipping IGNORED_ERRORS[%d] setting. 'MODULE_AND_CLASS' set to [%s] and should be module.Class, like " "'rest_framework.exceptions.PermissionDenied'.", 0, None, ) - assert expected_error_settings_dict == {} + assert ignored_error_settings_dict == {} @patch('openedx.core.lib.request_utils.log') - @override_settings(EXPECTED_ERRORS=[ + @override_settings(IGNORED_ERRORS=[ { 'MODULE_AND_CLASS': 'colon.separator.warning:Class', - 'REASON_EXPECTED': 'Because', + 'REASON_IGNORED': 'Because', } ]) def test_get_with_colon_in_class_and_module(self, mock_logger): - expected_error_settings_dict = _get_expected_error_settings_dict() + ignored_error_settings_dict = _get_ignored_error_settings_dict() mock_logger.warning.assert_called_once_with( - "Replacing ':' with '.' in EXPECTED_ERRORS[%d]['MODULE_AND_CLASS'], which was set to %s. Note that " + "Replacing ':' with '.' in IGNORED_ERRORS[%d]['MODULE_AND_CLASS'], which was set to %s. Note that " "monitoring and logging will not include the ':'.", 0, 'colon.separator.warning:Class', ) - assert 'colon.separator.warning.Class' in expected_error_settings_dict + assert 'colon.separator.warning.Class' in ignored_error_settings_dict @patch('openedx.core.lib.request_utils.log') - @override_settings(EXPECTED_ERRORS=[ + @override_settings(IGNORED_ERRORS=[ { 'MODULE_AND_CLASS': 'valid.module.DuplicateClass', - 'REASON_EXPECTED': 'Because' + 'REASON_IGNORED': 'Because' }, { 'MODULE_AND_CLASS': 'valid.module.DuplicateClass', - 'REASON_EXPECTED': 'Because overridden' + 'REASON_IGNORED': 'Because overridden' }, ]) def test_get_with_duplicate_class_and_module(self, mock_logger): - expected_error_settings_dict = _get_expected_error_settings_dict() + ignored_error_settings_dict = _get_ignored_error_settings_dict() mock_logger.warning.assert_called_once_with( - "EXPECTED_ERRORS[%d] setting is overriding an earlier setting. 'MODULE_AND_CLASS' [%s] is defined " + "IGNORED_ERRORS[%d] setting is overriding an earlier setting. 'MODULE_AND_CLASS' [%s] is defined " "multiple times.", 1, 'valid.module.DuplicateClass', ) - assert 'valid.module.DuplicateClass' in expected_error_settings_dict - assert expected_error_settings_dict['valid.module.DuplicateClass']['reason_expected'] == 'Because overridden' + assert 'valid.module.DuplicateClass' in ignored_error_settings_dict + assert ignored_error_settings_dict['valid.module.DuplicateClass']['reason_ignored'] == 'Because overridden' @patch('openedx.core.lib.request_utils.log') - @override_settings(EXPECTED_ERRORS=[{'MODULE_AND_CLASS': 'valid.module.and.class.ButMissingReason'}]) + @override_settings(IGNORED_ERRORS=[{'MODULE_AND_CLASS': 'valid.module.and.class.ButMissingReason'}]) def test_get_with_missing_reason(self, mock_logger): - expected_error_settings_dict = _get_expected_error_settings_dict() + ignored_error_settings_dict = _get_ignored_error_settings_dict() mock_logger.error.assert_called_once_with( - "Skipping EXPECTED_ERRORS[%d] setting. 'REASON_EXPECTED' is required to document why %s is an expected " + "Skipping IGNORED_ERRORS[%d] setting. 'REASON_IGNORED' is required to document why %s is an ignored " "error.", 0, 'valid.module.and.class.ButMissingReason' ) - assert expected_error_settings_dict == {} + assert ignored_error_settings_dict == {} @patch('openedx.core.lib.request_utils.log') - @override_settings(EXPECTED_ERRORS=['not-a-dict']) + @override_settings(IGNORED_ERRORS=['not-a-dict']) def test_get_with_invalid_dict(self, mock_logger): - expected_error_settings_dict = _get_expected_error_settings_dict() + ignored_error_settings_dict = _get_ignored_error_settings_dict() mock_logger.exception.assert_called_once_with( - 'Error processing setting EXPECTED_ERRORS. AttributeError("\'str\' object has no attribute \'get\'")' + 'Error processing setting IGNORED_ERRORS. AttributeError("\'str\' object has no attribute \'get\'")' ) - assert expected_error_settings_dict == {} + assert ignored_error_settings_dict == {} - @override_settings(EXPECTED_ERRORS=[{ + @override_settings(IGNORED_ERRORS=[{ 'MODULE_AND_CLASS': 'test.module.TestClass', - 'REASON_EXPECTED': 'Because' + 'REASON_IGNORED': 'Because' }]) def test_get_with_defaults(self): - expected_error_settings_dict = _get_expected_error_settings_dict() - assert expected_error_settings_dict == { + ignored_error_settings_dict = _get_ignored_error_settings_dict() + assert ignored_error_settings_dict == { 'test.module.TestClass': { - 'is_ignored': True, 'log_error': False, 'log_stack_trace': False, - 'reason_expected': 'Because' + 'reason_ignored': 'Because' } } @@ -219,29 +218,29 @@ class CustomError2(Exception): @ddt.ddt -class TestExpectedErrorMiddleware(unittest.TestCase): +class TestIgnoredErrorMiddleware(unittest.TestCase): """ - Tests for ExpectedErrorMiddleware + Tests for IgnoredErrorMiddleware """ def setUp(self): super().setUp() RequestCache.clear_all_namespaces() - clear_cached_expected_error_settings() + clear_cached_ignored_error_settings() self.mock_request = RequestFactory().get('/test') self.mock_exception = CustomError1('Test failure') def test_get_response(self): expected_response = Mock() - middleware = ExpectedErrorMiddleware(lambda _: expected_response) + middleware = IgnoredErrorMiddleware(lambda _: expected_response) response = middleware(self.mock_request) assert response == expected_response @patch('openedx.core.lib.request_utils.set_custom_attribute') @patch('openedx.core.lib.request_utils.log') - def test_process_exception_no_expected_errors(self, mock_logger, mock_set_custom_attribute): - ExpectedErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) + def test_process_exception_no_ignored_errors(self, mock_logger, mock_set_custom_attribute): + IgnoredErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) mock_logger.info.assert_not_called() mock_set_custom_attribute.assert_not_called() @@ -249,61 +248,60 @@ def test_process_exception_no_expected_errors(self, mock_logger, mock_set_custom @patch('openedx.core.lib.request_utils.set_custom_attribute') @patch('openedx.core.lib.request_utils.log') @ddt.data(None, []) - def test_process_exception_with_empty_expected_errors( - self, expected_errors_setting, mock_logger, mock_set_custom_attribute, + def test_process_exception_with_empty_ignored_errors( + self, ignored_errors_setting, mock_logger, mock_set_custom_attribute, ): - with override_settings(EXPECTED_ERRORS=expected_errors_setting): - ExpectedErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) + with override_settings(IGNORED_ERRORS=ignored_errors_setting): + IgnoredErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) mock_logger.info.assert_not_called() mock_set_custom_attribute.assert_not_called() - @override_settings(EXPECTED_ERRORS=[{ + @override_settings(IGNORED_ERRORS=[{ 'MODULE_AND_CLASS': 'test.module.TestException', - 'REASON_EXPECTED': 'Because', + 'REASON_IGNORED': 'Because', }]) @patch('openedx.core.lib.request_utils.set_custom_attribute') @patch('openedx.core.lib.request_utils.log') - def test_process_exception_not_matching_expected_errors(self, mock_logger, mock_set_custom_attribute): - ExpectedErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) + def test_process_exception_not_matching_ignored_errors(self, mock_logger, mock_set_custom_attribute): + IgnoredErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) mock_logger.info.assert_not_called() - mock_set_custom_attribute.assert_called_once_with('checked_error_expected_from', 'middleware') + mock_set_custom_attribute.assert_called_once_with('checked_error_ignored_from', 'middleware') - @override_settings(EXPECTED_ERRORS=[ + @override_settings(IGNORED_ERRORS=[ { 'MODULE_AND_CLASS': 'test.module.TestException', - 'REASON_EXPECTED': 'Because', + 'REASON_IGNORED': 'Because', }, { 'MODULE_AND_CLASS': 'openedx.core.lib.tests.test_request_utils.CustomError1', - 'REASON_EXPECTED': 'Because', + 'REASON_IGNORED': 'Because', } ]) @patch('openedx.core.lib.request_utils.set_custom_attribute') @patch('openedx.core.lib.request_utils.log') - def test_process_exception_expected_error_with_defaults(self, mock_logger, mock_set_custom_attribute): - ExpectedErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) + def test_process_exception_ignored_error_with_defaults(self, mock_logger, mock_set_custom_attribute): + IgnoredErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) mock_logger.info.assert_not_called() mock_set_custom_attribute.assert_has_calls( [ - call('checked_error_expected_from', 'middleware'), - call('error_expected', True), + call('checked_error_ignored_from', 'middleware'), call('error_ignored_class', 'openedx.core.lib.tests.test_request_utils.CustomError1'), call('error_ignored_message', 'Test failure'), ], any_order=True ) - @override_settings(EXPECTED_ERRORS=[ + @override_settings(IGNORED_ERRORS=[ { 'MODULE_AND_CLASS': 'openedx.core.lib.tests.test_request_utils.CustomError1', - 'REASON_EXPECTED': 'Because', + 'REASON_IGNORED': 'Because', }, { 'MODULE_AND_CLASS': 'openedx.core.lib.tests.test_request_utils.CustomError2', - 'REASON_EXPECTED': 'Because', + 'REASON_IGNORED': 'Because', } ]) @patch('openedx.core.lib.request_utils.set_custom_attribute') @@ -312,18 +310,17 @@ def test_process_exception_called_multiple_times(self, use_same_exception, mock_ mock_first_exception = self.mock_exception mock_second_exception = mock_first_exception if use_same_exception else CustomError2("Oops") - ExpectedErrorMiddleware('mock-response').process_exception(self.mock_request, mock_first_exception) - ExpectedErrorMiddleware('mock-response').process_exception(self.mock_request, mock_second_exception) + IgnoredErrorMiddleware('mock-response').process_exception(self.mock_request, mock_first_exception) + IgnoredErrorMiddleware('mock-response').process_exception(self.mock_request, mock_second_exception) expected_calls = [ - call('checked_error_expected_from', 'middleware'), - call('error_expected', True), + call('checked_error_ignored_from', 'middleware'), call('error_ignored_class', 'openedx.core.lib.tests.test_request_utils.CustomError1'), call('error_ignored_message', 'Test failure'), - call('checked_error_expected_from', 'middleware'), + call('checked_error_ignored_from', 'middleware'), ] if use_same_exception: - expected_calls += [call('checked_error_expected_from', 'multiple')] + expected_calls += [call('checked_error_ignored_from', 'multiple')] else: expected_calls += [ call('unexpected_multiple_exceptions', 'openedx.core.lib.tests.test_request_utils.CustomError1'), @@ -331,17 +328,16 @@ def test_process_exception_called_multiple_times(self, use_same_exception, mock_ mock_set_custom_attribute.assert_has_calls(expected_calls) assert mock_set_custom_attribute.call_count == len(expected_calls) - @override_settings(EXPECTED_ERRORS=[{ + @override_settings(IGNORED_ERRORS=[{ 'MODULE_AND_CLASS': 'Exception', - 'REASON_EXPECTED': 'Because', + 'REASON_IGNORED': 'Because', }]) @patch('openedx.core.lib.request_utils.set_custom_attribute') def test_process_exception_with_plain_exception(self, mock_set_custom_attribute): mock_exception = Exception("Oops") - ExpectedErrorMiddleware('mock-response').process_exception(self.mock_request, mock_exception) + IgnoredErrorMiddleware('mock-response').process_exception(self.mock_request, mock_exception) mock_set_custom_attribute.assert_has_calls([ - call('error_expected', True), call('error_ignored_class', 'Exception'), call('error_ignored_message', 'Oops'), ]) @@ -354,60 +350,58 @@ def test_process_exception_with_plain_exception(self, mock_set_custom_attribute) (True, True), # log with stacktrace ) @ddt.unpack - def test_process_exception_expected_error_with_overrides( + def test_process_exception_ignored_error_with_overrides( self, log_error, log_stack_trace, mock_logger, mock_set_custom_attribute, ): expected_class = 'openedx.core.lib.tests.test_request_utils.CustomError1' expected_message = 'Test failure' - with override_settings(EXPECTED_ERRORS=[{ + with override_settings(IGNORED_ERRORS=[{ 'MODULE_AND_CLASS': expected_class, - 'IS_IGNORED': False, 'LOG_ERROR': log_error, 'LOG_STACK_TRACE': log_stack_trace, - 'REASON_EXPECTED': 'Because', + 'REASON_IGNORED': 'Because', }]): - ExpectedErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) + IgnoredErrorMiddleware('mock-response').process_exception(self.mock_request, self.mock_exception) if log_error: exc_info = self.mock_exception if log_stack_trace else None mock_logger.info.assert_called_once_with( - 'Expected error %s: %s: seen for path %s', expected_class, expected_message, '/test', exc_info=exc_info + 'Ignored error %s: %s: seen for path %s', expected_class, expected_message, '/test', exc_info=exc_info ) else: mock_logger.info.assert_not_called() mock_set_custom_attribute.assert_has_calls( [ - call('checked_error_expected_from', 'middleware'), - call('error_expected', True), + call('checked_error_ignored_from', 'middleware'), ], any_order=True ) @ddt.ddt -class TestExpectedErrorExceptionHandler(unittest.TestCase): +class TestIgnoredErrorExceptionHandler(unittest.TestCase): """ - Tests for expected_error_exception_handler. + Tests for ignored_error_exception_handler. - Note: Only smoke tests the handler to not duplicate all testing in TestExpectedErrorMiddleware. + Note: Only smoke tests the handler to not duplicate all testing in TestIgnoredErrorMiddleware. """ def setUp(self): super().setUp() RequestCache.clear_all_namespaces() - clear_cached_expected_error_settings() + clear_cached_ignored_error_settings() self.mock_request = RequestFactory().get('/test') self.mock_exception = CustomError1('Test failure') - @override_settings(EXPECTED_ERRORS=[{ + @override_settings(IGNORED_ERRORS=[{ 'MODULE_AND_CLASS': 'openedx.core.lib.tests.test_request_utils.CustomError1', 'LOG_ERROR': True, - 'REASON_EXPECTED': 'Because', + 'REASON_IGNORED': 'Because', }]) @patch('openedx.core.lib.request_utils.set_custom_attribute') @patch('openedx.core.lib.request_utils.log') @ddt.data(True, False) - def test_handler_with_expected_error( + def test_handler_with_ignored_error( self, use_valid_context, mock_logger, mock_set_custom_attribute ): if use_valid_context: @@ -416,12 +410,12 @@ def test_handler_with_expected_error( else: mock_context = None expected_request_path = 'request-path-unknown' - expected_error_exception_handler(self.mock_exception, mock_context) + ignored_error_exception_handler(self.mock_exception, mock_context) expected_class = 'openedx.core.lib.tests.test_request_utils.CustomError1' expected_message = 'Test failure' mock_logger.info.assert_called_once_with( - 'Expected error %s: %s: seen for path %s', + 'Ignored error %s: %s: seen for path %s', expected_class, expected_message, expected_request_path, @@ -429,8 +423,7 @@ def test_handler_with_expected_error( ) mock_set_custom_attribute.assert_has_calls( [ - call('checked_error_expected_from', 'drf'), - call('error_expected', True), + call('checked_error_ignored_from', 'drf'), call('error_ignored_class', expected_class), call('error_ignored_message', expected_message), ],