mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Sentry integration enhancements (#38833)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
4371068f6a
commit
d3389fa22e
@ -747,7 +747,6 @@ 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
|
||||
|
@ -361,7 +361,7 @@ homeassistant/components/script/* @home-assistant/core
|
||||
homeassistant/components/search/* @home-assistant/core
|
||||
homeassistant/components/sense/* @kbickar
|
||||
homeassistant/components/sensibo/* @andrey-git
|
||||
homeassistant/components/sentry/* @dcramer
|
||||
homeassistant/components/sentry/* @dcramer @frenck
|
||||
homeassistant/components/serial/* @fabaff
|
||||
homeassistant/components/seven_segments/* @fabaff
|
||||
homeassistant/components/seventeentrack/* @bachya
|
||||
|
@ -1,58 +1,200 @@
|
||||
"""The sentry integration."""
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, Union
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
|
||||
from sentry_sdk.integrations.logging import LoggingIntegration
|
||||
import voluptuous as vol
|
||||
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import __version__
|
||||
from homeassistant.const import __version__ as current_version
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.loader import Integration, async_get_custom_components
|
||||
|
||||
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,
|
||||
from .const import (
|
||||
CONF_DSN,
|
||||
CONF_ENVIRONMENT,
|
||||
CONF_EVENT_CUSTOM_COMPONENTS,
|
||||
CONF_EVENT_HANDLED,
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES,
|
||||
CONF_LOGGING_EVENT_LEVEL,
|
||||
CONF_LOGGING_LEVEL,
|
||||
CONF_TRACING,
|
||||
CONF_TRACING_SAMPLE_RATE,
|
||||
DEFAULT_LOGGING_EVENT_LEVEL,
|
||||
DEFAULT_LOGGING_LEVEL,
|
||||
DEFAULT_TRACING_SAMPLE_RATE,
|
||||
DOMAIN,
|
||||
ENTITY_COMPONENTS,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.117")
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
|
||||
LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$")
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""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}
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Sentry from a config entry."""
|
||||
conf = entry.data
|
||||
|
||||
hass.data[DOMAIN] = conf
|
||||
# Migrate environment from config entry data to config entry options
|
||||
if (
|
||||
CONF_ENVIRONMENT not in entry.options
|
||||
and CONF_ENVIRONMENT in entry.data
|
||||
and entry.data[CONF_ENVIRONMENT]
|
||||
):
|
||||
options = {**entry.options, CONF_ENVIRONMENT: entry.data[CONF_ENVIRONMENT]}
|
||||
data = entry.data.copy()
|
||||
data.pop(CONF_ENVIRONMENT)
|
||||
hass.config_entries.async_update_entry(entry, data=data, options=options)
|
||||
|
||||
# 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
|
||||
level=entry.options.get(CONF_LOGGING_LEVEL, DEFAULT_LOGGING_LEVEL),
|
||||
event_level=entry.options.get(
|
||||
CONF_LOGGING_EVENT_LEVEL, DEFAULT_LOGGING_EVENT_LEVEL
|
||||
),
|
||||
)
|
||||
|
||||
# Additional/extra data collection
|
||||
channel = get_channel(current_version)
|
||||
huuid = await hass.helpers.instance_id.async_get()
|
||||
system_info = await hass.helpers.system_info.async_get_system_info()
|
||||
custom_components = await async_get_custom_components(hass)
|
||||
|
||||
tracing = {}
|
||||
if entry.options.get(CONF_TRACING):
|
||||
tracing = {
|
||||
"traceparent_v2": True,
|
||||
"traces_sample_rate": entry.options.get(
|
||||
CONF_TRACING_SAMPLE_RATE, DEFAULT_TRACING_SAMPLE_RATE
|
||||
),
|
||||
}
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=conf.get(CONF_DSN),
|
||||
environment=conf.get(CONF_ENVIRONMENT),
|
||||
integrations=[sentry_logging],
|
||||
release=f"homeassistant-{__version__}",
|
||||
dsn=entry.data[CONF_DSN],
|
||||
environment=entry.options.get(CONF_ENVIRONMENT),
|
||||
integrations=[sentry_logging, AioHttpIntegration(), SqlalchemyIntegration()],
|
||||
release=current_version,
|
||||
before_send=lambda event, hint: process_before_send(
|
||||
hass,
|
||||
entry.options,
|
||||
channel,
|
||||
huuid,
|
||||
system_info,
|
||||
custom_components,
|
||||
event,
|
||||
hint,
|
||||
),
|
||||
**tracing,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_channel(version: str) -> str:
|
||||
"""Find channel based on version number."""
|
||||
if "dev0" in version:
|
||||
return "dev"
|
||||
if "dev" in version:
|
||||
return "nightly"
|
||||
if "b" in version:
|
||||
return "beta"
|
||||
return "stable"
|
||||
|
||||
|
||||
def process_before_send(
|
||||
hass: HomeAssistant,
|
||||
options,
|
||||
channel: str,
|
||||
huuid: str,
|
||||
system_info: Dict[str, Union[bool, str]],
|
||||
custom_components: Dict[str, Integration],
|
||||
event,
|
||||
hint,
|
||||
):
|
||||
"""Process a Sentry event before sending it to Sentry."""
|
||||
# Filter out handled events by default
|
||||
if (
|
||||
"tags" in event
|
||||
and event.tags.get("handled", "no") == "yes"
|
||||
and not options.get(CONF_EVENT_HANDLED)
|
||||
):
|
||||
return None
|
||||
|
||||
# Additional tags to add to the event
|
||||
additional_tags = {
|
||||
"channel": channel,
|
||||
"installation_type": system_info["installation_type"],
|
||||
"uuid": huuid,
|
||||
}
|
||||
|
||||
# Find out all integrations in use, filter "auth", because it
|
||||
# triggers security rules, hiding all data.
|
||||
integrations = [
|
||||
integration
|
||||
for integration in hass.config.components
|
||||
if integration != "auth" and "." not in integration
|
||||
]
|
||||
|
||||
# Add additional tags based on what caused the event.
|
||||
platform = entity_platform.current_platform.get()
|
||||
if platform is not None:
|
||||
# This event happened in a platform
|
||||
additional_tags["custom_component"] = "no"
|
||||
additional_tags["integration"] = platform.platform_name
|
||||
additional_tags["platform"] = platform.domain
|
||||
elif "logger" in event:
|
||||
# Logger event, try to get integration information from the logger name.
|
||||
matches = LOGGER_INFO_REGEX.findall(event["logger"])
|
||||
if matches:
|
||||
group1, group2, group3, group4 = matches[0]
|
||||
# Handle the "homeassistant." package differently
|
||||
if group1 == "homeassistant" and group2 and group3:
|
||||
if group2 == "components":
|
||||
# This logger is from a component
|
||||
additional_tags["custom_component"] = "no"
|
||||
additional_tags["integration"] = group3
|
||||
if group4 and group4 in ENTITY_COMPONENTS:
|
||||
additional_tags["platform"] = group4
|
||||
else:
|
||||
# Not a component, could be helper, or something else.
|
||||
additional_tags[group2] = group3
|
||||
else:
|
||||
# Not the "homeassistant" package, this third-party
|
||||
if not options.get(CONF_EVENT_THIRD_PARTY_PACKAGES):
|
||||
return None
|
||||
additional_tags["package"] = group1
|
||||
|
||||
# If this event is caused by an integration, add a tag if this
|
||||
# integration is custom or not.
|
||||
if (
|
||||
"integration" in additional_tags
|
||||
and additional_tags["integration"] in custom_components
|
||||
):
|
||||
if not options.get(CONF_EVENT_CUSTOM_COMPONENTS):
|
||||
return None
|
||||
additional_tags["custom_component"] = "yes"
|
||||
|
||||
# Update event with the additional tags
|
||||
event.setdefault("tags", {}).update(additional_tags)
|
||||
|
||||
# Update event data with Home Assistant Context
|
||||
event.setdefault("contexts", {}).update(
|
||||
{
|
||||
"Home Assistant": {
|
||||
"channel": channel,
|
||||
"custom_components": "\n".join(sorted(custom_components)),
|
||||
"integrations": "\n".join(sorted(integrations)),
|
||||
**system_info,
|
||||
},
|
||||
}
|
||||
)
|
||||
return event
|
||||
|
@ -1,56 +1,137 @@
|
||||
"""Config flow for sentry integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sentry_sdk.utils import BadDsn, Dsn
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import CONF_DSN, DOMAIN # pylint: disable=unused-import
|
||||
from .const import ( # pylint: disable=unused-import
|
||||
CONF_DSN,
|
||||
CONF_ENVIRONMENT,
|
||||
CONF_EVENT_CUSTOM_COMPONENTS,
|
||||
CONF_EVENT_HANDLED,
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES,
|
||||
CONF_LOGGING_EVENT_LEVEL,
|
||||
CONF_LOGGING_LEVEL,
|
||||
CONF_TRACING,
|
||||
CONF_TRACING_SAMPLE_RATE,
|
||||
DEFAULT_LOGGING_EVENT_LEVEL,
|
||||
DEFAULT_LOGGING_LEVEL,
|
||||
DEFAULT_TRACING_SAMPLE_RATE,
|
||||
DOMAIN,
|
||||
LOGGING_LEVELS,
|
||||
)
|
||||
|
||||
_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"])
|
||||
|
||||
return {"title": "Sentry"}
|
||||
|
||||
|
||||
class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
class SentryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Sentry config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> SentryOptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return SentryOptionsFlow(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a user config flow."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="already_configured")
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
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)
|
||||
Dsn(user_input["dsn"])
|
||||
except BadDsn:
|
||||
errors["base"] = "bad_dsn"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(title="Sentry", data=user_input)
|
||||
|
||||
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)
|
||||
|
||||
class SentryOptionsFlow(config_entries.OptionsFlow):
|
||||
"""Handle Sentry options."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||
"""Initialize Sentry options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Manage Sentry options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_LOGGING_EVENT_LEVEL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_LOGGING_EVENT_LEVEL, DEFAULT_LOGGING_EVENT_LEVEL
|
||||
),
|
||||
): vol.In(LOGGING_LEVELS),
|
||||
vol.Optional(
|
||||
CONF_LOGGING_LEVEL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_LOGGING_LEVEL, DEFAULT_LOGGING_LEVEL
|
||||
),
|
||||
): vol.In(LOGGING_LEVELS),
|
||||
vol.Optional(
|
||||
CONF_ENVIRONMENT,
|
||||
default=self.config_entry.options.get(CONF_ENVIRONMENT),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_EVENT_HANDLED,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_EVENT_HANDLED, False
|
||||
),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_EVENT_CUSTOM_COMPONENTS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_EVENT_CUSTOM_COMPONENTS, False
|
||||
),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES, False
|
||||
),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_TRACING,
|
||||
default=self.config_entry.options.get(CONF_TRACING, False),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_TRACING_SAMPLE_RATE,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TRACING_SAMPLE_RATE, DEFAULT_TRACING_SAMPLE_RATE
|
||||
),
|
||||
): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
@ -1,6 +1,52 @@
|
||||
"""Constants for the sentry integration."""
|
||||
|
||||
import logging
|
||||
|
||||
DOMAIN = "sentry"
|
||||
|
||||
CONF_DSN = "dsn"
|
||||
CONF_ENVIRONMENT = "environment"
|
||||
CONF_EVENT_CUSTOM_COMPONENTS = "event_custom_components"
|
||||
CONF_EVENT_HANDLED = "event_handled"
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES = "event_third_party_packages"
|
||||
CONF_LOGGING_EVENT_LEVEL = "logging_event_level"
|
||||
CONF_LOGGING_LEVEL = "logging_level"
|
||||
CONF_TRACING = "tracing"
|
||||
CONF_TRACING_SAMPLE_RATE = "tracing_sample_rate"
|
||||
|
||||
DEFAULT_LOGGING_EVENT_LEVEL = logging.ERROR
|
||||
DEFAULT_LOGGING_LEVEL = logging.WARNING
|
||||
DEFAULT_TRACING_SAMPLE_RATE = 1.0
|
||||
|
||||
LOGGING_LEVELS = {
|
||||
logging.DEBUG: "debug",
|
||||
logging.INFO: "info",
|
||||
logging.WARNING: "warning",
|
||||
logging.ERROR: "error",
|
||||
logging.CRITICAL: "critical",
|
||||
}
|
||||
|
||||
ENTITY_COMPONENTS = [
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"binary_sensor",
|
||||
"calendar",
|
||||
"camera",
|
||||
"climate",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"fan",
|
||||
"geo_location",
|
||||
"group",
|
||||
"humidifier",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"remote",
|
||||
"scene",
|
||||
"sensor",
|
||||
"switch",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
"weather",
|
||||
]
|
||||
|
@ -3,6 +3,6 @@
|
||||
"name": "Sentry",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sentry",
|
||||
"requirements": ["sentry-sdk==0.13.5"],
|
||||
"codeowners": ["@dcramer"]
|
||||
"requirements": ["sentry-sdk==0.16.5"],
|
||||
"codeowners": ["@dcramer", "@frenck"]
|
||||
}
|
||||
|
@ -1,9 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": { "title": "Sentry", "description": "Enter your Sentry DSN" }
|
||||
"user": {
|
||||
"title": "Sentry",
|
||||
"description": "Enter your Sentry DSN",
|
||||
"data": { "dsn": "DSN" }
|
||||
}
|
||||
},
|
||||
"error": { "unknown": "Unexpected error", "bad_dsn": "Invalid DSN" },
|
||||
"abort": { "already_configured": "Sentry is already configured" }
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"environment": "Optional name of the environment.",
|
||||
"event_custom_components": "Send events from custom components",
|
||||
"event_handled": "Send handled events",
|
||||
"event_third_party_packages": "Send events from third-party packages",
|
||||
"logging_event_level": "The log level Sentry will register an event for",
|
||||
"logging_level": "The log level Sentry will record logs as breadcrums for",
|
||||
"tracing": "Enable performance tracing",
|
||||
"tracing_sample_rate": "Tracing sample rate; between 0.0 and 1.0 (1.0 = 100%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1961,7 +1961,7 @@ sense-hat==2.2.0
|
||||
sense_energy==0.7.2
|
||||
|
||||
# homeassistant.components.sentry
|
||||
sentry-sdk==0.13.5
|
||||
sentry-sdk==0.16.5
|
||||
|
||||
# homeassistant.components.aquostv
|
||||
sharp_aquos_rc==0.3.2
|
||||
|
@ -888,7 +888,7 @@ samsungtvws[websocket]==1.4.0
|
||||
sense_energy==0.7.2
|
||||
|
||||
# homeassistant.components.sentry
|
||||
sentry-sdk==0.13.5
|
||||
sentry-sdk==0.16.5
|
||||
|
||||
# homeassistant.components.sighthound
|
||||
simplehound==0.3
|
||||
|
@ -1,25 +1,37 @@
|
||||
"""Test the sentry config flow."""
|
||||
import logging
|
||||
|
||||
from sentry_sdk.utils import BadDsn
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.sentry.const import DOMAIN
|
||||
from homeassistant.components.sentry.const import (
|
||||
CONF_ENVIRONMENT,
|
||||
CONF_EVENT_CUSTOM_COMPONENTS,
|
||||
CONF_EVENT_HANDLED,
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES,
|
||||
CONF_LOGGING_EVENT_LEVEL,
|
||||
CONF_LOGGING_LEVEL,
|
||||
CONF_TRACING,
|
||||
CONF_TRACING_SAMPLE_RATE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_form(hass):
|
||||
async def test_full_user_flow_implementation(hass):
|
||||
"""Test we get the form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sentry.config_flow.validate_input",
|
||||
return_value={"title": "Sentry"},
|
||||
), patch(
|
||||
with patch("homeassistant.components.sentry.config_flow.Dsn"), patch(
|
||||
"homeassistant.components.sentry.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.sentry.async_setup_entry", return_value=True,
|
||||
@ -34,23 +46,94 @@ async def test_form(hass):
|
||||
"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):
|
||||
async def test_integration_already_exists(hass):
|
||||
"""Test we only allow a single config flow."""
|
||||
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
|
||||
async def test_user_flow_bad_dsn(hass):
|
||||
"""Test we handle bad dsn error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sentry.config_flow.validate_input",
|
||||
side_effect=BadDsn,
|
||||
"homeassistant.components.sentry.config_flow.Dsn", side_effect=BadDsn,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"dsn": "foo"},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["errors"] == {"base": "bad_dsn"}
|
||||
|
||||
|
||||
async def test_user_flow_unkown_exception(hass):
|
||||
"""Test we handle any unknown exception error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sentry.config_flow.Dsn", side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"dsn": "foo"},
|
||||
)
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_options_flow(hass):
|
||||
"""Test options config flow."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={"dsn": "http://public@sentry.local/1"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.sentry.async_setup_entry", return_value=True):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_ENVIRONMENT: "Test",
|
||||
CONF_EVENT_CUSTOM_COMPONENTS: True,
|
||||
CONF_EVENT_HANDLED: True,
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES: True,
|
||||
CONF_LOGGING_EVENT_LEVEL: logging.DEBUG,
|
||||
CONF_LOGGING_LEVEL: logging.DEBUG,
|
||||
CONF_TRACING: True,
|
||||
CONF_TRACING_SAMPLE_RATE: 0.5,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["data"] == {
|
||||
CONF_ENVIRONMENT: "Test",
|
||||
CONF_EVENT_CUSTOM_COMPONENTS: True,
|
||||
CONF_EVENT_HANDLED: True,
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES: True,
|
||||
CONF_LOGGING_EVENT_LEVEL: logging.DEBUG,
|
||||
CONF_LOGGING_LEVEL: logging.DEBUG,
|
||||
CONF_TRACING: True,
|
||||
CONF_TRACING_SAMPLE_RATE: 0.5,
|
||||
}
|
||||
|
330
tests/components/sentry/test_init.py
Normal file
330
tests/components/sentry/test_init.py
Normal file
@ -0,0 +1,330 @@
|
||||
"""Tests for Sentry integration."""
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sentry import get_channel, process_before_send
|
||||
from homeassistant.components.sentry.const import (
|
||||
CONF_DSN,
|
||||
CONF_ENVIRONMENT,
|
||||
CONF_EVENT_CUSTOM_COMPONENTS,
|
||||
CONF_EVENT_HANDLED,
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES,
|
||||
CONF_TRACING,
|
||||
CONF_TRACING_SAMPLE_RATE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import __version__ as current_version
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.async_mock import MagicMock, Mock, patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_entry(hass: HomeAssistant) -> None:
|
||||
"""Test integration setup from entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_DSN: "http://public@example.com/1", CONF_ENVIRONMENT: "production"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sentry.AioHttpIntegration"
|
||||
) as sentry_aiohttp_mock, patch(
|
||||
"homeassistant.components.sentry.SqlalchemyIntegration"
|
||||
) as sentry_sqlalchemy_mock, patch(
|
||||
"homeassistant.components.sentry.LoggingIntegration"
|
||||
) as sentry_logging_mock, patch(
|
||||
"homeassistant.components.sentry.sentry_sdk"
|
||||
) as sentry_mock:
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test CONF_ENVIRONMENT is migrated to entry options
|
||||
assert CONF_ENVIRONMENT not in entry.data
|
||||
assert CONF_ENVIRONMENT in entry.options
|
||||
assert entry.options[CONF_ENVIRONMENT] == "production"
|
||||
|
||||
assert sentry_logging_mock.call_count == 1
|
||||
assert sentry_logging_mock.called_once_with(
|
||||
level=logging.WARNING, event_level=logging.WARNING
|
||||
)
|
||||
|
||||
assert sentry_aiohttp_mock.call_count == 1
|
||||
assert sentry_sqlalchemy_mock.call_count == 1
|
||||
assert sentry_mock.init.call_count == 1
|
||||
|
||||
call_args = sentry_mock.init.call_args[1]
|
||||
assert set(call_args) == {
|
||||
"dsn",
|
||||
"environment",
|
||||
"integrations",
|
||||
"release",
|
||||
"before_send",
|
||||
}
|
||||
assert call_args["dsn"] == "http://public@example.com/1"
|
||||
assert call_args["environment"] == "production"
|
||||
assert call_args["integrations"] == [
|
||||
sentry_logging_mock.return_value,
|
||||
sentry_aiohttp_mock.return_value,
|
||||
sentry_sqlalchemy_mock.return_value,
|
||||
]
|
||||
assert call_args["release"] == current_version
|
||||
assert call_args["before_send"]
|
||||
|
||||
|
||||
async def test_setup_entry_with_tracing(hass: HomeAssistant) -> None:
|
||||
"""Test integration setup from entry with tracing enabled."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_DSN: "http://public@example.com/1"},
|
||||
options={CONF_TRACING: True, CONF_TRACING_SAMPLE_RATE: 0.5},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.sentry.AioHttpIntegration"), patch(
|
||||
"homeassistant.components.sentry.SqlalchemyIntegration"
|
||||
), patch("homeassistant.components.sentry.LoggingIntegration"), patch(
|
||||
"homeassistant.components.sentry.sentry_sdk"
|
||||
) as sentry_mock:
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
call_args = sentry_mock.init.call_args[1]
|
||||
assert set(call_args) == {
|
||||
"dsn",
|
||||
"environment",
|
||||
"integrations",
|
||||
"release",
|
||||
"before_send",
|
||||
"traceparent_v2",
|
||||
"traces_sample_rate",
|
||||
}
|
||||
assert call_args["traces_sample_rate"] == 0.5
|
||||
assert call_args["traceparent_v2"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"version,channel",
|
||||
[
|
||||
("0.115.0.dev20200815", "nightly"),
|
||||
("0.115.0", "stable"),
|
||||
("0.115.0b4", "beta"),
|
||||
("0.115.0dev0", "dev"),
|
||||
],
|
||||
)
|
||||
async def test_get_channel(version, channel) -> None:
|
||||
"""Test if channel detection works from Home Assistant version number."""
|
||||
assert get_channel(version) == channel
|
||||
|
||||
|
||||
async def test_process_before_send(hass: HomeAssistant):
|
||||
"""Test regular use of the Sentry process before sending function."""
|
||||
hass.config.components.add("puppies")
|
||||
hass.config.components.add("a_integration")
|
||||
|
||||
# These should not show up in the result.
|
||||
hass.config.components.add("puppies.light")
|
||||
hass.config.components.add("auth")
|
||||
|
||||
result = process_before_send(
|
||||
hass,
|
||||
options={},
|
||||
channel="test",
|
||||
huuid="12345",
|
||||
system_info={"installation_type": "pytest"},
|
||||
custom_components=["ironing_robot", "fridge_opener"],
|
||||
event={},
|
||||
hint={},
|
||||
)
|
||||
|
||||
assert result
|
||||
assert result["tags"]
|
||||
assert result["contexts"]
|
||||
assert result["contexts"]
|
||||
|
||||
ha_context = result["contexts"]["Home Assistant"]
|
||||
assert ha_context["channel"] == "test"
|
||||
assert ha_context["custom_components"] == "fridge_opener\nironing_robot"
|
||||
assert ha_context["integrations"] == "a_integration\npuppies"
|
||||
|
||||
tags = result["tags"]
|
||||
assert tags["channel"] == "test"
|
||||
assert tags["uuid"] == "12345"
|
||||
assert tags["installation_type"] == "pytest"
|
||||
|
||||
|
||||
async def test_event_with_platform_context(hass: HomeAssistant):
|
||||
"""Test extraction of platform context information during Sentry events."""
|
||||
|
||||
current_platform_mock = Mock()
|
||||
current_platform_mock.get().platform_name = "hue"
|
||||
current_platform_mock.get().domain = "light"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sentry.entity_platform.current_platform",
|
||||
new=current_platform_mock,
|
||||
):
|
||||
result = process_before_send(
|
||||
hass,
|
||||
options={},
|
||||
channel="test",
|
||||
huuid="12345",
|
||||
system_info={"installation_type": "pytest"},
|
||||
custom_components=["ironing_robot"],
|
||||
event={},
|
||||
hint={},
|
||||
)
|
||||
|
||||
assert result
|
||||
assert result["tags"]["integration"] == "hue"
|
||||
assert result["tags"]["platform"] == "light"
|
||||
assert result["tags"]["custom_component"] == "no"
|
||||
|
||||
current_platform_mock.get().platform_name = "ironing_robot"
|
||||
current_platform_mock.get().domain = "switch"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sentry.entity_platform.current_platform",
|
||||
new=current_platform_mock,
|
||||
):
|
||||
result = process_before_send(
|
||||
hass,
|
||||
options={CONF_EVENT_CUSTOM_COMPONENTS: True},
|
||||
channel="test",
|
||||
huuid="12345",
|
||||
system_info={"installation_type": "pytest"},
|
||||
custom_components=["ironing_robot"],
|
||||
event={},
|
||||
hint={},
|
||||
)
|
||||
|
||||
assert result
|
||||
assert result["tags"]["integration"] == "ironing_robot"
|
||||
assert result["tags"]["platform"] == "switch"
|
||||
assert result["tags"]["custom_component"] == "yes"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"logger,tags",
|
||||
[
|
||||
("adguard", {"package": "adguard"}),
|
||||
(
|
||||
"homeassistant.components.hue.coordinator",
|
||||
{"integration": "hue", "custom_component": "no"},
|
||||
),
|
||||
(
|
||||
"homeassistant.components.hue.light",
|
||||
{"integration": "hue", "platform": "light", "custom_component": "no"},
|
||||
),
|
||||
(
|
||||
"homeassistant.components.ironing_robot.switch",
|
||||
{
|
||||
"integration": "ironing_robot",
|
||||
"platform": "switch",
|
||||
"custom_component": "yes",
|
||||
},
|
||||
),
|
||||
(
|
||||
"homeassistant.components.ironing_robot",
|
||||
{"integration": "ironing_robot", "custom_component": "yes"},
|
||||
),
|
||||
("homeassistant.helpers.network", {"helpers": "network"}),
|
||||
("tuyapi.test", {"package": "tuyapi"}),
|
||||
],
|
||||
)
|
||||
async def test_logger_event_extraction(hass: HomeAssistant, logger, tags):
|
||||
"""Test extraction of information from Sentry logger events."""
|
||||
|
||||
result = process_before_send(
|
||||
hass,
|
||||
options={
|
||||
CONF_EVENT_CUSTOM_COMPONENTS: True,
|
||||
CONF_EVENT_THIRD_PARTY_PACKAGES: True,
|
||||
},
|
||||
channel="test",
|
||||
huuid="12345",
|
||||
system_info={"installation_type": "pytest"},
|
||||
custom_components=["ironing_robot"],
|
||||
event={"logger": logger},
|
||||
hint={},
|
||||
)
|
||||
|
||||
assert result
|
||||
assert result["tags"] == {
|
||||
"channel": "test",
|
||||
"uuid": "12345",
|
||||
"installation_type": "pytest",
|
||||
**tags,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"logger,options,event",
|
||||
[
|
||||
("adguard", {CONF_EVENT_THIRD_PARTY_PACKAGES: True}, True),
|
||||
("adguard", {CONF_EVENT_THIRD_PARTY_PACKAGES: False}, False),
|
||||
(
|
||||
"homeassistant.components.ironing_robot.switch",
|
||||
{CONF_EVENT_CUSTOM_COMPONENTS: True},
|
||||
True,
|
||||
),
|
||||
(
|
||||
"homeassistant.components.ironing_robot.switch",
|
||||
{CONF_EVENT_CUSTOM_COMPONENTS: False},
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_filter_log_events(hass: HomeAssistant, logger, options, event):
|
||||
"""Test filtering of events based on configuration options."""
|
||||
result = process_before_send(
|
||||
hass,
|
||||
options=options,
|
||||
channel="test",
|
||||
huuid="12345",
|
||||
system_info={"installation_type": "pytest"},
|
||||
custom_components=["ironing_robot"],
|
||||
event={"logger": logger},
|
||||
hint={},
|
||||
)
|
||||
|
||||
if event:
|
||||
assert result
|
||||
else:
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"handled,options,event",
|
||||
[
|
||||
("yes", {CONF_EVENT_HANDLED: True}, True),
|
||||
("yes", {CONF_EVENT_HANDLED: False}, False),
|
||||
("no", {CONF_EVENT_HANDLED: False}, True),
|
||||
("no", {CONF_EVENT_HANDLED: True}, True),
|
||||
],
|
||||
)
|
||||
async def test_filter_handled_events(hass: HomeAssistant, handled, options, event):
|
||||
"""Tests filtering of handled events based on configuration options."""
|
||||
|
||||
event_mock = MagicMock()
|
||||
event_mock.__iter__ = ["tags"]
|
||||
event_mock.__contains__ = lambda _, val: val == "tags"
|
||||
event_mock.tags = {"handled": handled}
|
||||
|
||||
result = process_before_send(
|
||||
hass,
|
||||
options=options,
|
||||
channel="test",
|
||||
huuid="12345",
|
||||
system_info={"installation_type": "pytest"},
|
||||
custom_components=[],
|
||||
event=event_mock,
|
||||
hint={},
|
||||
)
|
||||
|
||||
if event:
|
||||
assert result
|
||||
else:
|
||||
assert result is None
|
Loading…
x
Reference in New Issue
Block a user