Skip to content

html_reports

HTML Analytics Report Generation.

This module handles HTML report generation for analytics data, including performance metrics, function call summaries, and dry-run reports.

generate_empty_html_template

generate_empty_html_template(
    date_str, report_file, console_logger, error_logger
)

Generate and save an empty HTML template when no data is available.

Source code in src/metrics/html_reports.py
def generate_empty_html_template(
    date_str: str,
    report_file: str,
    console_logger: logging.Logger,
    error_logger: logging.Logger,
) -> None:
    """Generate and save an empty HTML template when no data is available."""
    html_content = f"""<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Analytics Report for {date_str}</title>
    <style>
        table {{
            border-collapse: collapse;
            width: 100%;
            font-size: 0.95em;
        }}
        th, td {{
            border: 1px solid #dddddd;
            text-align: left;
            padding: 6px;
        }}
        th {{
            background-color: #f2f2f2;
        }}
        .error {{
            background-color: #ffcccc;
        }}
    </style>
</head>
<body>
    <h2>Analytics Report for {date_str}</h2>
    <p><strong>No analytics data was collected during this run.</strong></p>
    <p>Possible reasons:</p>
    <ul>
        <li>Script executed in dry-run mode without analytics collection</li>
        <li>No decorated functions were called</li>
        <li>Decorator failed to log events</li>
    </ul>
</body>
</html>"""
    try:
        Path(report_file).parent.mkdir(parents=True, exist_ok=True)
        with Path(report_file).open("w", encoding="utf-8") as file:
            file.write(html_content)
        console_logger.info("Empty analytics HTML report saved to %s.", report_file)
    except (OSError, UnicodeError):
        error_logger.exception("Failed to save empty HTML report")

group_events_by_duration_and_success

group_events_by_duration_and_success(
    events,
    duration_thresholds,
    group_successful_short_calls,
    error_logger,
)

Group events by duration and success status.

Source code in src/metrics/html_reports.py
def group_events_by_duration_and_success(
    events: list[dict[str, Any]],
    duration_thresholds: dict[str, float],
    group_successful_short_calls: bool,
    error_logger: logging.Logger,
) -> tuple[dict[tuple[str, str], dict[str, float]], list[dict[str, Any]]]:
    """Group events by duration and success status."""
    grouped_short_success: dict[tuple[str, str], dict[str, float]] = {}
    big_or_fail_events: list[dict[str, Any]] = []
    short_max = duration_thresholds.get("short_max", 2)

    if not group_successful_short_calls:
        return grouped_short_success, events

    for event in events:
        try:
            event_duration = event[DURATION_FIELD]
            success = event["Success"]

            # Validate duration is numeric
            if not isinstance(event_duration, int | float):
                error_logger.warning(
                    "Invalid duration type in event (expected number, got %s): %s",
                    type(event_duration).__name__,
                    event,
                )
                big_or_fail_events.append(event)
                continue

            if success and event_duration <= short_max:
                key = (
                    event.get("Function", "Unknown"),
                    event.get("Event Type", "Unknown"),
                )
                if key not in grouped_short_success:
                    grouped_short_success[key] = {"count": 0, "total_duration": 0.0}
                grouped_short_success[key]["count"] += 1
                grouped_short_success[key]["total_duration"] += event_duration
            else:
                big_or_fail_events.append(event)
        except KeyError:
            error_logger.exception(
                "Missing key in event data during grouping, event: %s",
                event,
            )
            big_or_fail_events.append(event)

    return grouped_short_success, big_or_fail_events

generate_grouped_success_table

generate_grouped_success_table(
    grouped_short_success, group_successful_short_calls
)

Generate HTML table for grouped successful short calls.

Source code in src/metrics/html_reports.py
def generate_grouped_success_table(
    grouped_short_success: dict[tuple[str, str], dict[str, float]],
    group_successful_short_calls: bool,
) -> str:
    """Generate HTML table for grouped successful short calls."""
    html = """
    <h3>Grouped Short & Successful Calls</h3>
    <table>
        <tr>
            <th>Function</th>
            <th>Event Type</th>
            <th>Count</th>
            <th>Avg Duration (s)</th>
            <th>Total Duration (s)</th>
        </tr>"""

    if not (group_successful_short_calls and grouped_short_success):
        html += """
        <tr><td colspan="5">No short successful calls found or grouping disabled.</td></tr>"""
    else:
        for (function_name, event_type), values in sorted(grouped_short_success.items()):
            count = values["count"]
            total_duration = values["total_duration"]
            avg_duration = round(total_duration / count, 4) if count > 0 else 0
            html += f"""
        <tr>
            <td>{function_name}</td>
            <td>{event_type}</td>
            <td>{count}</td>
            <td>{avg_duration}</td>
            <td>{round(total_duration, 4)}</td>
        </tr>"""

    html += "</table>"
    return html

get_duration_category

get_duration_category(event_duration, duration_thresholds)

Determine the duration category based on thresholds.

Source code in src/metrics/html_reports.py
def get_duration_category(
    event_duration: float,
    duration_thresholds: dict[str, float],
) -> str:
    """Determine the duration category based on thresholds."""
    if event_duration <= duration_thresholds.get("short_max", 2):
        return "short"
    if event_duration <= duration_thresholds.get("medium_max", 5):
        return "medium"
    return "long"

determine_event_row_class

determine_event_row_class(event, duration_thresholds)

Determine the CSS class for an event table row based on success and duration.

Source code in src/metrics/html_reports.py
def determine_event_row_class(
    event: dict[str, Any],
    duration_thresholds: dict[str, float],
) -> str:
    """Determine the CSS class for an event table row based on success and duration."""
    success = event.get("Success", False)
    if not success:
        return "error"

    event_duration = event.get(DURATION_FIELD, 0)
    duration_category = get_duration_category(event_duration, duration_thresholds)
    return f"duration-{duration_category}"

format_event_table_row

format_event_table_row(event, row_class)

Format a single event as an HTML table row.

Source code in src/metrics/html_reports.py
def format_event_table_row(event: dict[str, Any], row_class: str) -> str:
    """Format a single event as an HTML table row."""
    event_duration = event.get(DURATION_FIELD, 0)
    success = event.get("Success", False)
    success_display = "Yes" if success else "No"

    return f"""
        <tr class="{row_class}">
            <td>{event.get("Function", "Unknown")}</td>
            <td>{event.get("Event Type", "Unknown")}</td>
            <td>{event.get("Start Time", "Unknown")}</td>
            <td>{event.get("End Time", "Unknown")}</td>
            <td>{event_duration}</td>
            <td>{success_display}</td>
        </tr>"""

generate_summary_table_html

generate_summary_table_html(
    call_counts, success_counts, decorator_overhead
)

Generate HTML table for function call summary.

Source code in src/metrics/html_reports.py
def generate_summary_table_html(
    call_counts: dict[str, int],
    success_counts: dict[str, int],
    decorator_overhead: dict[str, float],
) -> str:
    """Generate HTML table for function call summary."""
    html = """
    <h3>Summary</h3>
    <table>
        <tr>
            <th>Function</th>
            <th>Call Count</th>
            <th>Success Count</th>
            <th>Success Rate (%)</th>
            <th>Total Decorator Overhead (s)</th>
        </tr>"""

    if call_counts:
        for function_name, count in sorted(call_counts.items()):
            success_count = success_counts.get(function_name, 0)
            success_rate = (success_count / count * 100) if count else 0
            overhead = decorator_overhead.get(function_name, 0)

            html += f"""
        <tr>
            <td>{function_name}</td>
            <td>{count}</td>
            <td>{success_count}</td>
            <td>{success_rate:.2f}</td>
            <td>{round(overhead, 4)}</td>
        </tr>"""
    else:
        html += """
        <tr><td colspan="5">No function calls recorded.</td></tr>"""

    html += """
    </table>
</body>
</html>"""
    return html

save_html_report

save_html_report(
    events,
    call_counts,
    success_counts,
    decorator_overhead,
    config,
    console_logger=None,
    error_logger=None,
    group_successful_short_calls=False,
    force_mode=False,
)

Generate an HTML report from the provided analytics data.

Source code in src/metrics/html_reports.py
def save_html_report(
    events: list[dict[str, Any]],
    call_counts: dict[str, int],
    success_counts: dict[str, int],
    decorator_overhead: dict[str, float],
    config: AppConfig,
    console_logger: logging.Logger | None = None,
    error_logger: logging.Logger | None = None,
    group_successful_short_calls: bool = False,
    force_mode: bool = False,
) -> None:
    """Generate an HTML report from the provided analytics data."""
    if console_logger is None:
        console_logger = logging.getLogger("console_logger")
    if error_logger is None:
        error_logger = logging.getLogger("error_logger")

    # Configuration and setup
    console_logger.info(
        "Starting HTML report generation with %d events, %d function counts",
        len(events),
        len(call_counts),
    )
    date_str = datetime.now(UTC).strftime("%Y-%m-%d")

    logs_base_dir = config.logs_base_dir
    thresholds = config.analytics.duration_thresholds
    duration_thresholds = {
        "short_max": thresholds.short_max,
        "medium_max": thresholds.medium_max,
        "long_max": thresholds.long_max,
    }

    reports_dir = Path(logs_base_dir) / "analytics"
    reports_dir.mkdir(parents=True, exist_ok=True)

    report_file = get_full_log_path(
        config,
        "analytics_html_report_file",
        str(Path("analytics") / ("analytics_full.html" if force_mode else "analytics_incremental.html")),
    )

    # Check for empty data
    if not events and not call_counts:
        console_logger.warning(
            "No analytics data available for report - creating empty template",
        )
        generate_empty_html_template(date_str, report_file, console_logger, error_logger)
        return

    # Group events
    grouped_short_success, big_or_fail_events = group_events_by_duration_and_success(
        events, duration_thresholds, group_successful_short_calls, error_logger
    )

    # Generate HTML sections
    html_content = _generate_main_html_template(date_str, call_counts, success_counts, events, force_mode)
    html_content += generate_grouped_success_table(grouped_short_success, group_successful_short_calls)
    html_content += _generate_detailed_events_table_html(big_or_fail_events, duration_thresholds, error_logger)
    html_content += generate_summary_table_html(call_counts, success_counts, decorator_overhead)

    # Save the report
    try:
        Path(report_file).parent.mkdir(parents=True, exist_ok=True)
        with Path(report_file).open("w", encoding="utf-8") as file:
            file.write(html_content)
        console_logger.info("Analytics HTML report saved to %s.", report_file)
    except (OSError, UnicodeError):
        error_logger.exception("Failed to save HTML report")