Skip to content

Component Analyticsยค

Use the Component.on_render_after() hook to track component analytics, such as capturing errors for a service like Sentry or other monitoring.

Analytics example

Error tracking componentsยค

You can create a wrapper component that uses the Component.on_render_after() hook to inspect the error object. If an error occurred during the rendering of its children, you can capture and send it to your monitoring service.

{% component "sentry_error_tracker" %}
    {% component "api_widget" simulate_error=True / %}
{% endcomponent %}

The same hook can be used to track both successes and failures, allowing you to monitor the reliability of a component.

{% component "success_rate_tracker" %}
    {% component "api_widget" simulate_error=False / %}
{% endcomponent %}

Error tracking extensionยค

Capturing analytics through components is simple, but limiting:

  • You can't access metadata nor state of the component that errored
  • Component will capture at most one error
  • You must remember to call the component that captures the analytics

Instead, you can define the analytics logic as an extension. This will allow us to capture all errors, without polluting the UI.

To do that, we can use the on_component_rendered() hook to capture all errors.

from django_components.extension import ComponentExtension, OnComponentRenderedContext

class ErrorTrackingExtension(ComponentExtension):
    name = "sentry_error_tracker"

    def on_component_rendered(self, ctx: OnComponentRenderedContext):
        if ctx.error:
            print(f"SENTRY: Captured error in component {ctx.component.name}: {ctx.error}")

Don't forget to register the extension:

COMPONENTS = {
    "extensions": [
        ErrorTrackingExtension,
    ],
}

Definitionยค

from typing import Dict, List, NamedTuple

from django_components import Component, register, types

DESCRIPTION = "Track component errors or success rates to send them to Sentry or other services."

# A mock analytics service
analytics_events: List[Dict] = []
error_rate = {
    "error": 0,
    "success": 0,
}


@register("api_widget")
class ApiWidget(Component):
    class Kwargs(NamedTuple):
        simulate_error: bool = False

    def get_template_data(self, args, kwargs: Kwargs, slots, context):
        if kwargs.simulate_error:
            raise ConnectionError("API call failed")
        return {"data": "Mock API response data"}

    template: types.django_html = """
        <div class="p-4 border rounded-lg bg-gray-50">
            <h4 class="font-bold text-gray-800">API Widget</h4>
            <p class="text-gray-600">Data: {{ data }}</p>
        </div>
    """


@register("sentry_error_tracker")
class SentryErrorTracker(Component):
    def on_render_after(self, context, template, result, error):
        if error:
            event = {
                "type": "error",
                "component": self.registered_name,
                "error": error,
            }
            analytics_events.append(event)
            print(f"SENTRY: Captured error in component {self.registered_name}: {error}")

    template: types.django_html = """
        {% load component_tags %}
        {% slot "default" / %}
    """


@register("success_rate_tracker")
class SuccessRateTracker(Component):
    def on_render_after(self, context, template, result, error):
        # Track error
        if error:
            error_rate["error"] += 1
        # Track success
        else:
            error_rate["success"] += 1

    template: types.django_html = """
        {% load component_tags %}
        {% slot "default" / %}
    """

Exampleยค

To see the component in action, you can set up a view and a URL pattern as shown below.

views.pyยค

from django.http import HttpRequest, HttpResponse

from django_components import Component, register, types

from .component import analytics_events, error_rate


class AnalyticsPage(Component):
    class Media:
        js = ("https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries",)

    template: types.django_html = """
        {% load component_tags %}
        <html>
            <head>
                <title>Analytics Example</title>
            </head>
            <body class="bg-gray-100 p-8">
                <div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
                    <h1 class="text-2xl font-bold mb-4">
                        Component Analytics
                    </h1>
                    <p class="text-gray-600 mb-6">
                        Track component errors or success rates to send them
                        to Sentry or other services.
                    </p>

                    {# NOTE: Intentionally hidden so we focus on the events tracking #}
                    <div style="display: none;">
                        {% component "template_with_errors" / %}
                    </div>

                    {% component "captured_events" / %}
                </div>
            </body>
        </html>
    """

    class View:
        def get(self, request: HttpRequest) -> HttpResponse:
            # Clear events on each page load
            analytics_events.clear()
            error_rate["error"] = 0
            error_rate["success"] = 0

            return AnalyticsPage.render_to_response(request=request)


@register("template_with_errors")
class TemplateWithErrors(Component):
    template: types.django_html = """
        <div class="mb-8">
            <h2 class="text-xl font-semibold mb-2">
                Sentry Error Tracking
            </h2>
            <p class="text-sm text-gray-500 mb-2">
                This component only logs events when an error occurs.
            </p>
            {% component "error_fallback" %}
                {% component "sentry_error_tracker" %}
                    {% component "api_widget" simulate_error=True / %}
                {% endcomponent %}
            {% endcomponent %}
            {% component "sentry_error_tracker" %}
                {% component "api_widget" simulate_error=False / %}
            {% endcomponent %}
        </div>

        <div>
            <h2 class="text-xl font-semibold mb-2">
                Success Rate Analytics
            </h2>
            <p class="text-sm text-gray-500 mb-2">
                This component logs both successful and failed renders.
            </p>
            {% component "error_fallback" %}
                {% component "success_rate_tracker" %}
                    {% component "api_widget" simulate_error=True / %}
                {% endcomponent %}
            {% endcomponent %}
            {% component "success_rate_tracker" %}
                {% component "api_widget" simulate_error=False / %}
            {% endcomponent %}
        </div>
    """


# NOTE: Since this runs after `template_with_errors`,
#       the `analytics_events` will be populated.
@register("captured_events")
class CapturedEvents(Component):
    def get_template_data(self, args, kwargs, slots, context):
        return {"events": analytics_events, "error_rate": error_rate}

    template: types.django_html = """
        <div class="mt-8 p-4 border rounded-lg bg-gray-50">
            <h3 class="text-lg font-semibold mb-2">
                Captured Analytics Events
            </h3>
            <pre class="text-sm text-gray-700 whitespace-pre-wrap">
                {% for event in events %}
                    {{ event }}
                {% endfor %}
            </pre>
        </div>
        <div class="mt-8 p-4 border rounded-lg bg-gray-50">
            <h3 class="text-lg font-semibold mb-2">
                Error Rate
            </h3>
            <pre class="text-sm text-gray-700 whitespace-pre-wrap">
                {{ error_rate }}
            </pre>
            <p class="text-sm text-gray-500">
                {{ error_rate.error }} errors out of {{ error_rate.success }} calls.
            </p>
        </div>
    """

urls.pyยค

from django.urls import path

from examples.pages.analytics import AnalyticsPage

urlpatterns = [
    path("examples/analytics", AnalyticsPage.as_view(), name="analytics"),
]