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

Add Sentry component #30422

Merged
merged 1 commit into from Jan 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Expand Up @@ -606,6 +606,7 @@ omit =
homeassistant/components/sensehat/light.py
homeassistant/components/sensehat/sensor.py
homeassistant/components/sensibo/climate.py
homeassistant/components/sentry/__init__.py
homeassistant/components/serial/sensor.py
homeassistant/components/serial_pm/sensor.py
homeassistant/components/sesame/lock.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -280,6 +280,7 @@ homeassistant/components/scrape/* @fabaff
homeassistant/components/script/* @home-assistant/core
homeassistant/components/sense/* @kbickar
homeassistant/components/sensibo/* @andrey-git
homeassistant/components/sentry/* @dcramer
homeassistant/components/serial/* @fabaff
homeassistant/components/seventeentrack/* @bachya
homeassistant/components/shell_command/* @home-assistant/core
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/bootstrap.py
Expand Up @@ -31,7 +31,7 @@

DEBUGGER_INTEGRATIONS = {"ptvsd"}
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
LOGGING_INTEGRATIONS = {"logger", "system_log"}
LOGGING_INTEGRATIONS = {"logger", "system_log", "sentry"}
STAGE_1_INTEGRATIONS = {
# To record data
"recorder",
Expand Down
18 changes: 18 additions & 0 deletions homeassistant/components/sentry/.translations/en.json
@@ -0,0 +1,18 @@
{
"config": {
"title": "Sentry",
"step": {
"user": {
"title": "Sentry",
"description": "Enter your Sentry DSN"
}
},
"error": {
"unknown": "Unexpected error",
"bad_dsn": "Invalid DSN"
},
"abort": {
"already_configured": "Sentry is already configured"
}
}
}
56 changes: 56 additions & 0 deletions homeassistant/components/sentry/__init__.py
@@ -0,0 +1,56 @@
"""The sentry integration."""
import logging

import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv

from .const import CONF_DSN, CONF_ENVIRONMENT, DOMAIN

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{vol.Required(CONF_DSN): cv.string, CONF_ENVIRONMENT: cv.string}
)
},
extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Sentry component."""
conf = config.get(DOMAIN)
if conf is not None:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
Copy link
Member

Choose a reason for hiding this comment

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

We're not passing the config as data. That should be done to make it available in the import step in the config flow.

)
)

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Sentry from a config entry."""
conf = entry.data

hass.data[DOMAIN] = conf

# https://docs.sentry.io/platforms/python/logging/
sentry_logging = LoggingIntegration(
level=logging.INFO, # Capture info and above as breadcrumbs
event_level=logging.ERROR, # Send errors as events
)

sentry_sdk.init(
dsn=conf.get(CONF_DSN),
Copy link
Member

Choose a reason for hiding this comment

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

Please use dict[key] for required config keys and keys with default schema values.

environment=conf.get(CONF_ENVIRONMENT),
integrations=[sentry_logging],
)

return True
56 changes: 56 additions & 0 deletions homeassistant/components/sentry/config_flow.py
@@ -0,0 +1,56 @@
"""Config flow for sentry integration."""
import logging

from sentry_sdk.utils import BadDsn, Dsn
import voluptuous as vol

from homeassistant import config_entries, core

from .const import CONF_DSN, DOMAIN # pylint: disable=unused-import

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema({vol.Required(CONF_DSN): str})


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the DSN input allows us to connect.

Data has the keys from DATA_SCHEMA with values provided by the user.
"""
# validate the dsn
Dsn(data["dsn"])
dcramer marked this conversation as resolved.
Show resolved Hide resolved

return {"title": "Sentry"}


class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
Copy link
Member

Choose a reason for hiding this comment

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

I'd rename this class to something integration specific, eg SentryConfigFlow.

"""Handle a Sentry config flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

async def async_step_user(self, user_input=None):
"""Handle a user config flow."""
if self._async_current_entries():
return self.async_abort(reason="already_configured")

dcramer marked this conversation as resolved.
Show resolved Hide resolved
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)

return self.async_create_entry(title=info["title"], data=user_input)
except BadDsn:
errors["base"] = "bad_dsn"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)

async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)
6 changes: 6 additions & 0 deletions homeassistant/components/sentry/const.py
@@ -0,0 +1,6 @@
"""Constants for the sentry integration."""

DOMAIN = "sentry"

CONF_DSN = "dsn"
CONF_ENVIRONMENT = "environment"
12 changes: 12 additions & 0 deletions homeassistant/components/sentry/manifest.json
@@ -0,0 +1,12 @@
{
"domain": "sentry",
"name": "Sentry",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sentry",
"requirements": ["sentry-sdk==0.13.5"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": ["@dcramer"]
}
18 changes: 18 additions & 0 deletions homeassistant/components/sentry/strings.json
@@ -0,0 +1,18 @@
{
"config": {
"title": "Sentry",
"step": {
"user": {
"title": "Sentry",
"description": "Enter your Sentry DSN"
}
Copy link
Member

Choose a reason for hiding this comment

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

The data dict representing the input form data schema is missing here.

},
"error": {
"unknown": "Unexpected error",
"bad_dsn": "Invalid DSN"
},
"abort": {
"already_configured": "Sentry is already configured"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Expand Up @@ -65,6 +65,7 @@
"point",
"ps4",
"rainmachine",
"sentry",
"simplisafe",
"smartthings",
"smhi",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Expand Up @@ -1799,6 +1799,9 @@ sense-hat==2.2.0
# homeassistant.components.sense
sense_energy==0.7.0

# homeassistant.components.sentry
sentry-sdk==0.13.5

# homeassistant.components.aquostv
sharp_aquos_rc==0.3.2

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Expand Up @@ -567,6 +567,9 @@ rxv==0.6.0
# homeassistant.components.samsungtv
samsungctl[websocket]==0.7.1

# homeassistant.components.sentry
sentry-sdk==0.13.5

# homeassistant.components.simplisafe
simplisafe-python==5.3.6

Expand Down
1 change: 1 addition & 0 deletions tests/components/sentry/__init__.py
@@ -0,0 +1 @@
"""Tests for the sentry integration."""
18 changes: 18 additions & 0 deletions tests/components/sentry/conftest.py
@@ -0,0 +1,18 @@
"""Configuration for Sonos tests."""
import pytest

from homeassistant.components.sentry import DOMAIN

from tests.common import MockConfigEntry


@pytest.fixture(name="config_entry")
def config_entry_fixture():
"""Create a mock config entry."""
return MockConfigEntry(domain=DOMAIN, title="Sentry")


@pytest.fixture(name="config")
def config_fixture():
"""Create hass config fixture."""
return {DOMAIN: {"dsn": "http://public@sentry.local/1"}}
59 changes: 59 additions & 0 deletions tests/components/sentry/test_config_flow.py
@@ -0,0 +1,59 @@
"""Test the sentry config flow."""
from unittest.mock import patch

from sentry_sdk.utils import BadDsn

from homeassistant import config_entries, setup
from homeassistant.components.sentry.const import DOMAIN

from tests.common import mock_coro


async def test_form(hass):
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}

with patch(
"homeassistant.components.sentry.config_flow.validate_input",
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't patch our own code under test. We should patch the library function we use in the input validation function. We want all our code in the config flow to be covered.

return_value=mock_coro({"title": "Sentry"}),
), patch(
"homeassistant.components.sentry.async_setup", return_value=mock_coro(True)
) as mock_setup, patch(
"homeassistant.components.sentry.async_setup_entry",
return_value=mock_coro(True),
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {"dsn": "http://public@sentry.local/1"},
)

assert result2["type"] == "create_entry"
assert result2["title"] == "Sentry"
assert result2["data"] == {
"dsn": "http://public@sentry.local/1",
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1


async def test_form_bad_dsn(hass):
"""Test we handle bad dsn error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)

with patch(
"homeassistant.components.sentry.config_flow.validate_input",
Copy link
Member

Choose a reason for hiding this comment

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

See above.

side_effect=BadDsn,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {"dsn": "foo"},
)

assert result2["type"] == "form"
assert result2["errors"] == {"base": "bad_dsn"}