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:
Franck Nijhof 2020-08-20 11:37:27 +02:00 committed by GitHub
parent 4371068f6a
commit d3389fa22e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 780 additions and 77 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)),
}
),
)

View file

@ -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",
]

View file

@ -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"]
}

View file

@ -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%)"
}
}
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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,
}

View 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