Skip to content

infrastructure.enrichers.device_enricher

src.infrastructure.enrichers.device_enricher

Device enricher implementation using user-agents library.

Parses user agent strings to extract device, browser, and OS information. Implements DeviceEnricher protocol with fail-open behavior.

Reference
  • docs/architecture/session-management-architecture.md

Classes

UserAgentDeviceEnricher

Device enricher using the user-agents library.

Parses user agent strings using the well-maintained user-agents library which provides accurate browser, OS, and device detection.

Implements DeviceEnricher protocol (structural typing).

Behavior
  • Fail-open: Returns empty result on parse errors
  • Non-blocking: Pure string parsing (<1ms)
  • Best-effort: Unknown agents return partial data
Source code in src/infrastructure/enrichers/device_enricher.py
class UserAgentDeviceEnricher:
    """Device enricher using the user-agents library.

    Parses user agent strings using the well-maintained user-agents library
    which provides accurate browser, OS, and device detection.

    Implements DeviceEnricher protocol (structural typing).

    Behavior:
        - Fail-open: Returns empty result on parse errors
        - Non-blocking: Pure string parsing (<1ms)
        - Best-effort: Unknown agents return partial data
    """

    def __init__(self, logger: LoggerProtocol) -> None:
        """Initialize device enricher.

        Args:
            logger: Logger for error/debug messages.
        """
        self._logger = logger

    async def enrich(self, user_agent: str) -> DeviceEnrichmentResult:
        """Parse user agent string to extract device information.

        Args:
            user_agent: Raw user agent string from HTTP header.

        Returns:
            DeviceEnrichmentResult with parsed device info.
            Returns empty result (all None) on parse failure.
        """
        if not user_agent:
            return DeviceEnrichmentResult()

        try:
            ua: UserAgent = parse_user_agent(user_agent)

            # Extract browser info
            browser = ua.browser.family if ua.browser.family else None
            browser_version = (
                ua.browser.version_string if ua.browser.version_string else None
            )

            # Extract OS info
            os_name = ua.os.family if ua.os.family else None
            os_version = ua.os.version_string if ua.os.version_string else None

            # Determine device type
            device_type = self._determine_device_type(ua)

            # Check if bot
            is_bot = ua.is_bot

            # Build human-readable device info
            device_info = self._build_device_info(browser, os_name)

            return DeviceEnrichmentResult(
                device_info=device_info,
                browser=browser,
                browser_version=browser_version,
                os=os_name,
                os_version=os_version,
                device_type=device_type,
                is_bot=is_bot,
            )

        except Exception as e:
            self._logger.warning(
                "Failed to parse user agent",
                user_agent=user_agent[:100],
                error=str(e),
            )
            return DeviceEnrichmentResult()

    def _determine_device_type(self, ua: UserAgent) -> str:
        """Determine device type from parsed user agent.

        Args:
            ua: Parsed UserAgent object.

        Returns:
            Device type: "mobile", "tablet", "pc", or "other".
        """
        if ua.is_mobile:
            return "mobile"
        if ua.is_tablet:
            return "tablet"
        if ua.is_pc:
            return "desktop"
        return "other"

    def _build_device_info(
        self,
        browser: str | None,
        os_name: str | None,
    ) -> str | None:
        """Build human-readable device info string.

        Args:
            browser: Browser name.
            os_name: OS name.

        Returns:
            Human-readable string like "Chrome on Mac OS X", or None.
        """
        if browser and os_name:
            return f"{browser} on {os_name}"
        if browser:
            return browser
        if os_name:
            return f"Unknown browser on {os_name}"
        return None
Functions
__init__
__init__(logger: LoggerProtocol) -> None

Parameters:

Name Type Description Default
logger LoggerProtocol

Logger for error/debug messages.

required
Source code in src/infrastructure/enrichers/device_enricher.py
def __init__(self, logger: LoggerProtocol) -> None:
    """Initialize device enricher.

    Args:
        logger: Logger for error/debug messages.
    """
    self._logger = logger
enrich async
enrich(user_agent: str) -> DeviceEnrichmentResult

Parse user agent string to extract device information.

Parameters:

Name Type Description Default
user_agent str

Raw user agent string from HTTP header.

required

Returns:

Type Description
DeviceEnrichmentResult

DeviceEnrichmentResult with parsed device info.

DeviceEnrichmentResult

Returns empty result (all None) on parse failure.

Source code in src/infrastructure/enrichers/device_enricher.py
async def enrich(self, user_agent: str) -> DeviceEnrichmentResult:
    """Parse user agent string to extract device information.

    Args:
        user_agent: Raw user agent string from HTTP header.

    Returns:
        DeviceEnrichmentResult with parsed device info.
        Returns empty result (all None) on parse failure.
    """
    if not user_agent:
        return DeviceEnrichmentResult()

    try:
        ua: UserAgent = parse_user_agent(user_agent)

        # Extract browser info
        browser = ua.browser.family if ua.browser.family else None
        browser_version = (
            ua.browser.version_string if ua.browser.version_string else None
        )

        # Extract OS info
        os_name = ua.os.family if ua.os.family else None
        os_version = ua.os.version_string if ua.os.version_string else None

        # Determine device type
        device_type = self._determine_device_type(ua)

        # Check if bot
        is_bot = ua.is_bot

        # Build human-readable device info
        device_info = self._build_device_info(browser, os_name)

        return DeviceEnrichmentResult(
            device_info=device_info,
            browser=browser,
            browser_version=browser_version,
            os=os_name,
            os_version=os_version,
            device_type=device_type,
            is_bot=is_bot,
        )

    except Exception as e:
        self._logger.warning(
            "Failed to parse user agent",
            user_agent=user_agent[:100],
            error=str(e),
        )
        return DeviceEnrichmentResult()