Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iterate on Sentry filtering #7292

Merged
merged 60 commits into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
1743eb6
ignore errors from PyMongo in Sentry
m-vdb Nov 17, 2020
f99b462
throw MarkdownException when Markdown format is invalid
m-vdb Nov 17, 2020
08ad307
ignore CancelledError in Sentry
m-vdb Nov 17, 2020
696c836
do not load endpoint config if it is None
m-vdb Nov 17, 2020
7091bda
raise InvalidEntityFormatException when entities cannot be parsed
m-vdb Nov 17, 2020
42973d3
fix import errors
m-vdb Nov 17, 2020
dedd3db
ignore MemoryError in Sentry
m-vdb Nov 18, 2020
f198340
ignore psycopg2.OperationalError in sentry
m-vdb Nov 18, 2020
1570f89
do not ignore all PyMongo errors
m-vdb Nov 18, 2020
9ebe06b
fix test_markdown
m-vdb Nov 18, 2020
9fe3ac4
ignore botocore.NoCredentialsError
m-vdb Nov 18, 2020
be2aa57
ensure slot classes are classes
m-vdb Nov 18, 2020
ff34f68
Merge branch 'master' into fix-minor-issues-filter-sentry
m-vdb Dec 7, 2020
810ae24
raise deprecation warning if class_from_module_path() doesnt return a…
m-vdb Dec 7, 2020
080e004
raise ConnectionException instead of 3rd party errors
m-vdb Dec 7, 2020
d98490f
fix import errors
m-vdb Dec 7, 2020
cc60508
fix class_from_module_path() + add docstring
m-vdb Dec 7, 2020
373756d
fix docstring issues
m-vdb Dec 7, 2020
86b0896
do not raise a TypeError in class_from_module_path() just yet
m-vdb Dec 8, 2020
595b61e
more accurate exception docstring
m-vdb Dec 8, 2020
f904639
remove unused imports
m-vdb Dec 8, 2020
bbe8e26
Merge branch 'master' into fix-minor-issues-filter-sentry
m-vdb Dec 17, 2020
e562f0c
add changelog fragment
m-vdb Dec 17, 2020
c56e1d9
add tests for ConnectionException error handling in TrackerStore and …
m-vdb Dec 17, 2020
82fa43c
add test for error handling in find_entities_in_training_example()
m-vdb Dec 17, 2020
f9d5c42
add test for empty sections in endpoint config file
m-vdb Dec 17, 2020
365c3ef
update changelog fragment
m-vdb Dec 17, 2020
d1b26ca
add tests for class_from_module_path()
m-vdb Dec 17, 2020
84cacce
fix unused var in test
m-vdb Dec 17, 2020
ce6a01d
Merge branch 'master' into fix-minor-issues-filter-sentry
m-vdb Dec 18, 2020
72060bb
Update wording in docstrings
m-vdb Jan 11, 2021
8cb99fc
Merge branch 'master' into fix-minor-issues-filter-sentry
m-vdb Jan 11, 2021
d9196f8
split test cases
m-vdb Jan 11, 2021
1f567d7
do not catch postgres errors; sqlalchemy errors are enough
m-vdb Jan 11, 2021
628ab24
keep test fixtures close to tests
m-vdb Jan 11, 2021
e82d6af
split tests for endpoints utils
m-vdb Jan 11, 2021
1a51322
add explanation for erros we ignore in Sentry
m-vdb Jan 11, 2021
4ac7d4c
stick to import convention
m-vdb Jan 11, 2021
ad0bef1
update entities_parser docstring
m-vdb Jan 11, 2021
405bc67
fix `ConnectionException` docstring
m-vdb Jan 11, 2021
bbe8509
add test to check for entity schema error
m-vdb Jan 11, 2021
307364a
Update entities parser docstring
m-vdb Jan 12, 2021
48f262b
MarkdownException inherits ValueError
m-vdb Jan 12, 2021
c53a30b
improve docstring of class_from_module_path()
m-vdb Jan 12, 2021
20adc15
be explict about ignored expected Rasa errors
m-vdb Jan 12, 2021
c107594
parameterize tracker store tests for connection errors
m-vdb Jan 12, 2021
8a2852c
ignore BotoCoreError errors in Sentry
m-vdb Jan 12, 2021
ffb283f
validate_training_data() raises custom SchemaValidationError
m-vdb Jan 12, 2021
230c773
use local imports for connection errors
m-vdb Jan 12, 2021
9d8f2a7
fix failing lint on docstrings
m-vdb Jan 12, 2021
593d8b7
Merge branch 'master' into fix-minor-issues-filter-sentry
m-vdb Jan 12, 2021
5a9b83b
remove unnecessary top-level imports
m-vdb Jan 13, 2021
0e7f141
improve format of DynamoTrackerStore.keys() docstring
m-vdb Jan 13, 2021
8bb8ab4
remove ensure_class argument, not needed
m-vdb Jan 13, 2021
ece18bc
add more details to RasaException docstring
m-vdb Jan 13, 2021
edcd70f
InvalidEntityFormatException inherits json.JSONDecodeError
m-vdb Jan 13, 2021
536c02a
add docstring to InvalidEntityFormatException.create_from()
m-vdb Jan 14, 2021
436c828
class_from_module_path() does not look up in globals/locals
m-vdb Jan 14, 2021
3628341
Merge branch 'master' into fix-minor-issues-filter-sentry
m-vdb Jan 14, 2021
7432444
fix test in python 3.6
m-vdb Jan 14, 2021
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
12 changes: 12 additions & 0 deletions changelog/7292.improvement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Improve error handling and Sentry tracking:
- Raise `MarkdownException` when training data in Markdown format cannot be read.
- Raise `InvalidEntityFormatException` error instead of `json.JSONDecodeError` when entity format is in valid
in training data.
- Gracefully handle empty sections in endpoint config files.
- Introduce `ConnectionException` error and raise it when `TrackerStore` and `EventBroker`
cannot connect to 3rd party services, instead of raising exceptions from 3rd party libraries.
- Improve `rasa.shared.utils.common.class_from_module_path` function by making sure it always returns a class.
The function currently raises a deprecation warning if it detects an anomaly.
- Ignore `MemoryError` and `asyncio.CancelledError` in Sentry.
- `rasa.shared.utils.validation.validate_training_data` now raises a `SchemaValidationError` when validation fails
(this error inherits `jsonschema.ValidationError`, ensuring backwards compatibility).
1 change: 1 addition & 0 deletions data/test_endpoints/example_endpoints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ tracker_store:
#db: rasa
#user: username
#password: password
empty:
17 changes: 16 additions & 1 deletion rasa/core/brokers/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
from asyncio import AbstractEventLoop
from typing import Any, Dict, Text, Optional, Union

import aio_pika.exceptions
import sqlalchemy.exc

import rasa.shared.utils.common
import rasa.shared.utils.io
from rasa.shared.exceptions import ConnectionException
from rasa.utils.endpoints import EndpointConfig

logger = logging.getLogger(__name__)
Expand All @@ -22,7 +26,18 @@ async def create(
if isinstance(obj, EventBroker):
return obj

return await _create_from_endpoint_config(obj, loop)
import aio_pika.exceptions
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
import sqlalchemy.exc

connection_errors = (
sqlalchemy.exc.OperationalError,
aio_pika.exceptions.AMQPConnectionError,
)

try:
return await _create_from_endpoint_config(obj, loop)
except connection_errors as error:
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
raise ConnectionException("Cannot connect to event broker.") from error

@classmethod
async def from_endpoint_config(
Expand Down
40 changes: 27 additions & 13 deletions rasa/core/tracker_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
)

from boto3.dynamodb.conditions import Key
from botocore.exceptions import ClientError

import rasa.core.utils as core_utils
import rasa.shared.utils.cli
Expand All @@ -42,6 +41,7 @@
DialogueStateTracker,
EventVerbosity,
)
from rasa.shared.exceptions import ConnectionException
from rasa.shared.nlu.constants import INTENT_NAME_KEY
from rasa.utils.endpoints import EndpointConfig
import sqlalchemy as sa
Expand All @@ -65,7 +65,7 @@


class TrackerStore:
"""Class to hold all of the TrackerStore classes"""
"""Represents common behavior and interface for all `TrackerStore`s."""

def __init__(
self,
Expand Down Expand Up @@ -115,7 +115,20 @@ def create(
if isinstance(obj, TrackerStore):
return obj

return _create_from_endpoint_config(obj, domain, event_broker)
from botocore.exceptions import BotoCoreError
import pymongo.errors
import sqlalchemy.exc

connection_errors = (
BotoCoreError,
pymongo.errors.ConnectionFailure,
sqlalchemy.exc.OperationalError,
)

try:
return _create_from_endpoint_config(obj, domain, event_broker)
except connection_errors as error:
raise ConnectionException("Cannot connect to tracker store.") from error

def get_or_create_tracker(
self,
Expand Down Expand Up @@ -407,7 +420,7 @@ def __init__(
def get_or_create_table(
self, table_name: Text
) -> "boto3.resources.factory.dynamodb.Table":
"""Returns table or creates one if the table name is not in the table list"""
"""Returns table or creates one if the table name is not in the table list."""
import boto3

dynamo = boto3.resource("dynamodb", region_name=self.region)
Expand Down Expand Up @@ -442,7 +455,9 @@ def get_or_create_table(
return table

def save(self, tracker):
"""Saves the current conversation state"""
"""Saves the current conversation state."""
from botocore.exceptions import ClientError

if self.event_broker:
self.stream_events(tracker)
serialized = self.serialise_tracker(tracker)
Expand Down Expand Up @@ -475,7 +490,7 @@ def _retrieve_latest_session_date(self, sender_id: Text) -> Optional[int]:
return dialogues[0].get("session_date")

def serialise_tracker(self, tracker: "DialogueStateTracker") -> Dict:
"""Serializes the tracker, returns object with decimal types"""
"""Serializes the tracker, returns object with decimal types."""
d = tracker.as_dialogue().as_dict()
d.update(
{"sender_id": tracker.sender_id,}
Expand Down Expand Up @@ -505,16 +520,15 @@ def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]:
)

def keys(self) -> Iterable[Text]:
"""Returns sender_ids of the DynamoTrackerStore"""
"""Returns sender_ids of the DynamoTrackerStore."""
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
return [
i["sender_id"]
for i in self.db.scan(ProjectionExpression="sender_id")["Items"]
]


class MongoTrackerStore(TrackerStore):
"""
Stores conversation history in Mongo
"""Stores conversation history in Mongo.

Property methods:
conversations: returns the current conversation
Expand Down Expand Up @@ -552,11 +566,11 @@ def __init__(

@property
def conversations(self):
"""Returns the current conversation"""
"""Returns the current conversation."""
return self.db[self.collection]

def _ensure_indices(self):
"""Create an index on the sender_id"""
"""Create an index on the sender_id."""
self.conversations.create_index("sender_id")

@staticmethod
Expand All @@ -569,7 +583,7 @@ def _current_tracker_state_without_events(tracker: DialogueStateTracker) -> Dict
return state

def save(self, tracker, timeout=None):
"""Saves the current conversation state"""
"""Saves the current conversation state."""
if self.event_broker:
self.stream_events(tracker)

Expand Down Expand Up @@ -684,7 +698,7 @@ def retrieve_full_tracker(
)

def keys(self) -> Iterable[Text]:
"""Returns sender_ids of the Mongo Tracker Store"""
"""Returns sender_ids of the Mongo Tracker Store."""
return [c["sender_id"] for c in self.conversations.find()]


Expand Down
21 changes: 21 additions & 0 deletions rasa/shared/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Optional, Text
import jsonschema


class RasaException(Exception):
Expand All @@ -17,6 +18,10 @@ class InvalidParameterException(RasaException, ValueError):
"""Raised when an invalid parameter is used."""


class MarkdownException(RasaException, ValueError):
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
"""Raised if there is an error reading Markdown."""


class YamlException(RasaException):
"""Raised if there is an error reading yaml."""

Expand Down Expand Up @@ -77,3 +82,19 @@ class InvalidConfigException(ValueError, RasaException):

class UnsupportedFeatureException(RasaCoreException):
"""Raised if a requested feature is not supported."""


class SchemaValidationError(RasaException, jsonschema.ValidationError):
"""Raised if schema validation via `jsonschema` failed."""


class InvalidEntityFormatException(RasaException):
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
"""Raised if the format of an entity is invalid."""


class ConnectionException(RasaException):
"""Raised when a connection to a 3rd party service fails.

It's used by our broker and tracker store classes, when
they can't connect to services like postgres, dynamoDB, mongo.
"""
15 changes: 7 additions & 8 deletions rasa/shared/nlu/training_data/entities_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

import rasa.shared.nlu.training_data.util
from rasa.shared.constants import DOCS_URL_TRAINING_DATA_NLU
from rasa.shared.exceptions import InvalidEntityFormatException
from rasa.shared.nlu.constants import (
ENTITY_ATTRIBUTE_VALUE,
ENTITY_ATTRIBUTE_TYPE,
ENTITY_ATTRIBUTE_GROUP,
ENTITY_ATTRIBUTE_ROLE,
)
from rasa.shared.nlu.training_data.message import Message
import rasa.shared.utils.io


GROUP_ENTITY_VALUE = "value"
GROUP_ENTITY_TYPE = "entity"
Expand Down Expand Up @@ -127,8 +128,8 @@ def get_validated_dict(json_str: Text) -> Dict[Text, Text]:
json_str: The entity dict as string without "{}".

Raises:
ValidationError if validation of entity dict fails.
JSONDecodeError if provided entity dict is not valid json.
SchemaValidationError if validation of parsed entity fails.
InvalidEntityFormatException if provided entity is not valid json.

Returns:
Deserialized and validated `json_str`.
Expand All @@ -141,12 +142,10 @@ def get_validated_dict(json_str: Text) -> Dict[Text, Text]:
try:
data = json.loads(f"{{{json_str}}}")
except JSONDecodeError as e:
rasa.shared.utils.io.raise_warning(
raise InvalidEntityFormatException(
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
f"Incorrect training data format ('{{{json_str}}}'). Make sure your "
f"data is valid.",
docs=DOCS_URL_TRAINING_DATA_NLU,
)
raise e
f"data is valid. More info at {DOCS_URL_TRAINING_DATA_NLU}"
) from e

validation_utils.validate_training_data(data, schema.entity_dict_schema())
wochinge marked this conversation as resolved.
Show resolved Hide resolved

Expand Down
40 changes: 2 additions & 38 deletions rasa/shared/nlu/training_data/formats/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
LEGACY_DOCS_BASE_URL,
DOCS_URL_MIGRATION_GUIDE_MD_DEPRECATION,
)
from rasa.shared.exceptions import MarkdownException
from rasa.shared.nlu.constants import TEXT
from rasa.shared.nlu.training_data.formats.readerwriter import (
TrainingDataReader,
Expand Down Expand Up @@ -135,47 +136,10 @@ def _parse_item(self, line: Text) -> None:
self.current_title, item, self.lookup_tables
)

@staticmethod
def _get_validated_dict(json_str: Text) -> Dict[Text, Text]:
"""Converts the provided json_str to a valid dict containing the entity
attributes.

Users can specify entity roles, synonyms, groups for an entity in a dict, e.g.
[LA]{"entity": "city", "role": "to", "value": "Los Angeles"}

Args:
json_str: the entity dict as string without "{}"

Raises:
ValidationError if validation of entity dict fails.
JSONDecodeError if provided entity dict is not valid json.

Returns:
a proper python dict
"""
import json
import rasa.shared.utils.validation as validation_utils
import rasa.shared.nlu.training_data.schemas.data_schema as schema

# add {} as they are not part of the regex
try:
data = json.loads(f"{{{json_str}}}")
except JSONDecodeError as e:
rasa.shared.utils.io.raise_warning(
f"Incorrect training data format ('{{{json_str}}}'), make sure your "
f"data is valid. For more information about the format visit "
f"{LEGACY_DOCS_BASE_URL}/nlu/training-data-format/."
)
raise e

validation_utils.validate_training_data(data, schema.entity_dict_schema())

return data

def _set_current_section(self, section: Text, title: Text) -> None:
"""Update parsing mode."""
if section not in AVAILABLE_SECTIONS:
raise ValueError(
raise MarkdownException(
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
"Found markdown section '{}' which is not "
"in the allowed sections '{}'."
"".format(section, "', '".join(AVAILABLE_SECTIONS))
Expand Down
52 changes: 40 additions & 12 deletions rasa/shared/utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,60 @@
import logging
from typing import Text, Dict, Optional, Any, List, Callable, Collection

import rasa.shared.utils.io
from rasa.shared.constants import NEXT_MAJOR_VERSION_FOR_DEPRECATIONS


logger = logging.getLogger(__name__)


def class_from_module_path(
module_path: Text, lookup_path: Optional[Text] = None
module_path: Text, lookup_path: Optional[Text] = None, ensure_class: bool = True
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
) -> Any:
"""Given the module name and path of a class, tries to retrieve the class.

The loaded class can be used to instantiate new objects."""
# load the module, will raise ImportError if module cannot be loaded
The loaded class can be used to instantiate new objects.

Args:
module_path: either an absolute path to a Python class,
or the name of the class in the local / global scope.
lookup_path: a path where to load the class from, if it cannot
be found in the local / global scope.
ensure_class: a boolean that will ensure that the loaded Python class
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
is actually class if `True`. When acticated, a deprecation
warning is raised when the loaded object is not a class.

Returns:
a Python class

Raises:
ImportError, in case the Python class cannot be found.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically also a TypeError in case import_module fails

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I follow 🤔 doesn't it raise a ModuleNotFoundError which inherits ImportError?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import_module in 3.8.6 also raises a TypeError which inherits from Exception

def import_module(name, package=None):
    """Import a module.

    The 'package' argument is required when performing a relative import. It
    specifies the package to use as the anchor point from which to resolve the
    relative import to an absolute import.

    """
    level = 0
    if name.startswith('.'):
        if not package:
            msg = ("the 'package' argument is required to perform a relative "
                   "import for {!r}")
            raise TypeError(msg.format(name))
        for character in name:
            if character != '.':
                break
            level += 1
    return _bootstrap._gcd_import(name[level:], package, level)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 yeah ok. It sounds a bit more like a detail. Fine if I keep it like it is?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be more transparent for the caller as they'd know which errors to handle without having to dive deeper into the code, but up to you as it's a detail as you said.

"""
klass = None
if "." in module_path:
module_name, _, class_name = module_path.rpartition(".")
m = importlib.import_module(module_name)
# get the class, will raise AttributeError if class cannot be found
return getattr(m, class_name)
klass = getattr(m, class_name, None)
else:
module = globals().get(module_path, locals().get(module_path))
if module is not None:
return module
klass = globals().get(module_path, locals().get(module_path))

if lookup_path:
if klass is None and lookup_path:
# last resort: try to import the class from the lookup path
m = importlib.import_module(lookup_path)
return getattr(m, module_path)
else:
raise ImportError(f"Cannot retrieve class from path {module_path}.")
klass = getattr(m, module_path, None)

if klass is None:
raise ImportError(f"Cannot retrieve class from path {module_path}.")
m-vdb marked this conversation as resolved.
Show resolved Hide resolved

if ensure_class and not inspect.isclass(klass):
m-vdb marked this conversation as resolved.
Show resolved Hide resolved
rasa.shared.utils.io.raise_deprecation_warning(
f"`class_from_module_path()` is expected to return a class, "
f"but {module_path} is not one. "
wochinge marked this conversation as resolved.
Show resolved Hide resolved
f"This warning will be converted "
f"into an exception in {NEXT_MAJOR_VERSION_FOR_DEPRECATIONS}."
)

return klass


def all_subclasses(cls: Any) -> List[Any]:
Expand Down
Loading