Skip to content

Tracking Internal Modules

This section exposes the implementation modules behind request recording, Redis-backed counting, ORM snapshots, and cabinet analytics widgets.

App config

codex_django.tracking.apps

Django app configuration for the reusable tracking package.

Classes

TrackingConfig

Bases: AppConfig

Register the tracking runtime and optional cabinet declarations.

Source code in src/codex_django/tracking/apps.py
 6
 7
 8
 9
10
11
12
13
14
15
16
class TrackingConfig(AppConfig):
    """Register the tracking runtime and optional cabinet declarations."""

    name = "codex_django.tracking"
    label = "codex_tracking"
    verbose_name = "Codex Tracking"

    def ready(self) -> None:
        """Import dashboard providers/declarations after Django app loading."""
        from . import cabinet as _cabinet  # noqa: F401
        from . import providers as _providers  # noqa: F401
Functions
ready()

Import dashboard providers/declarations after Django app loading.

Source code in src/codex_django/tracking/apps.py
13
14
15
16
def ready(self) -> None:
    """Import dashboard providers/declarations after Django app loading."""
    from . import cabinet as _cabinet  # noqa: F401
    from . import providers as _providers  # noqa: F401

Settings

codex_django.tracking.settings

Settings helpers for the tracking runtime.

Classes

TrackingSettings dataclass

Resolved tracking settings with conservative defaults.

Source code in src/codex_django/tracking/settings.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@dataclass(frozen=True)
class TrackingSettings:
    """Resolved tracking settings with conservative defaults."""

    enabled: bool = True
    redis_enabled: bool = False
    redis_url: str = "redis://localhost:6379/0"
    key_prefix: str = "tracking"
    ttl_seconds: int = 60 * 60 * 24 * 30
    skip_prefixes: tuple[str, ...] = (
        "/static/",
        "/media/",
        "/favicon",
        "/__debug__",
        "/admin/jsi18n",
    )
    track_anonymous: bool = False
    track_redirects: bool = True
    track_dashboard_widgets: bool = True
    analytics_url: str = "/cabinet/tracking/"
    analytics_days: int = 30

Functions

get_tracking_settings()

Return normalized CODEX_TRACKING/legacy CABINET_TRACKING settings.

Source code in src/codex_django/tracking/settings.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def get_tracking_settings() -> TrackingSettings:
    """Return normalized ``CODEX_TRACKING``/legacy ``CABINET_TRACKING`` settings."""

    raw: dict[str, Any] = {}
    legacy = getattr(settings, "CABINET_TRACKING", None)
    modern = getattr(settings, "CODEX_TRACKING", None)
    if isinstance(legacy, dict):
        raw.update(legacy)
    if isinstance(modern, dict):
        raw.update(modern)

    project_name = str(getattr(settings, "PROJECT_NAME", "") or "")
    key_prefix = str(raw.get("key_prefix") or (f"{project_name}:tracking" if project_name else "tracking"))
    redis_url = str(raw.get("redis_url") or getattr(settings, "REDIS_URL", "redis://localhost:6379/0"))
    redis_enabled = bool(raw.get("redis_enabled", getattr(settings, "CODEX_REDIS_ENABLED", False)))

    return TrackingSettings(
        enabled=bool(raw.get("enabled", True)),
        redis_enabled=redis_enabled,
        redis_url=redis_url,
        key_prefix=key_prefix,
        ttl_seconds=int(raw.get("ttl_seconds", 60 * 60 * 24 * 30)),
        skip_prefixes=tuple(str(prefix) for prefix in raw.get("skip_prefixes", TrackingSettings.skip_prefixes)),
        track_anonymous=bool(raw.get("track_anonymous", False)),
        track_redirects=bool(raw.get("track_redirects", True)),
        track_dashboard_widgets=bool(raw.get("track_dashboard_widgets", True)),
        analytics_url=str(raw.get("analytics_url", "/cabinet/tracking/")),
        analytics_days=int(raw.get("analytics_days", 30)),
    )

Middleware

codex_django.tracking.middleware

Middleware for page-view tracking.

Classes

PageTrackingMiddleware

Record eligible GET responses without affecting request handling.

Source code in src/codex_django/tracking/middleware.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class PageTrackingMiddleware:
    """Record eligible GET responses without affecting request handling."""

    def __init__(self, get_response: Any) -> None:
        self.get_response = get_response

    def __call__(self, request: Any) -> Any:
        response = self.get_response(request)
        try:
            if self._should_track(request, response):
                from .recorder import TrackingRecorder

                TrackingRecorder.record(request)
        except Exception:
            logger.exception("Tracking recording failed")
        return response

    def _should_track(self, request: Any, response: Any) -> bool:
        cfg = get_tracking_settings()
        if not cfg.enabled:
            return False
        if getattr(request, "method", "") != "GET":
            return False
        status_code = int(getattr(response, "status_code", 0) or 0)
        allowed_statuses = (200, 301, 302) if cfg.track_redirects else (200,)
        if status_code not in allowed_statuses:
            return False
        path = getattr(request, "path", "") or ""
        if any(path.startswith(prefix) for prefix in cfg.skip_prefixes):
            return False
        user = getattr(request, "user", None)
        return bool(cfg.track_anonymous or getattr(user, "is_authenticated", False))

Functions

Recorder

codex_django.tracking.recorder

Request recorder used by the tracking middleware.

Classes

TrackingRecorder

Convert a Django request into a tracking counter update.

Source code in src/codex_django/tracking/recorder.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class TrackingRecorder:
    """Convert a Django request into a tracking counter update."""

    @staticmethod
    def record(request: Any) -> None:
        """Record one page view for the current request."""

        from .manager import get_tracking_manager

        today = timezone.localdate().isoformat()
        user = getattr(request, "user", None)
        user_id: str | None = None
        if user is not None and getattr(user, "is_authenticated", False):
            user_pk = getattr(user, "pk", None)
            if user_pk is not None:
                user_id = str(user_pk)
        path = getattr(request, "path", "") or "/"
        get_tracking_manager().record(path, today, user_id)
Functions
record(request) staticmethod

Record one page view for the current request.

Source code in src/codex_django/tracking/recorder.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@staticmethod
def record(request: Any) -> None:
    """Record one page view for the current request."""

    from .manager import get_tracking_manager

    today = timezone.localdate().isoformat()
    user = getattr(request, "user", None)
    user_id: str | None = None
    if user is not None and getattr(user, "is_authenticated", False):
        user_pk = getattr(user, "pk", None)
        if user_pk is not None:
            user_id = str(user_pk)
    path = getattr(request, "path", "") or "/"
    get_tracking_manager().record(path, today, user_id)

Selector

codex_django.tracking.selector

Read-only analytics selectors for tracking data.

Classes

TrackingAnalyticsContext dataclass

Dashboard-ready tracking analytics payload.

Source code in src/codex_django/tracking/selector.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@dataclass(frozen=True)
class TrackingAnalyticsContext:
    """Dashboard-ready tracking analytics payload."""

    tracking_total_views: MetricWidgetData
    tracking_unique_visitors: MetricWidgetData
    tracking_tracked_pages: MetricWidgetData
    tracking_views_chart: dict[str, Any]
    tracking_top_pages: ListWidgetData
    tracking_recent_page_views: TableWidgetData

    def as_dict(self) -> dict[str, Any]:
        """Return the context as template-friendly keys.

        Returns:
            A plain dictionary keyed the same way cabinet templates and widget
            providers expect to receive tracking analytics payloads.
        """

        return {
            "tracking_total_views": self.tracking_total_views,
            "tracking_unique_visitors": self.tracking_unique_visitors,
            "tracking_tracked_pages": self.tracking_tracked_pages,
            "tracking_views_chart": self.tracking_views_chart,
            "tracking_top_pages": self.tracking_top_pages,
            "tracking_recent_page_views": self.tracking_recent_page_views,
        }
Functions
as_dict()

Return the context as template-friendly keys.

Returns:

Type Description
dict[str, Any]

A plain dictionary keyed the same way cabinet templates and widget

dict[str, Any]

providers expect to receive tracking analytics payloads.

Source code in src/codex_django/tracking/selector.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def as_dict(self) -> dict[str, Any]:
    """Return the context as template-friendly keys.

    Returns:
        A plain dictionary keyed the same way cabinet templates and widget
        providers expect to receive tracking analytics payloads.
    """

    return {
        "tracking_total_views": self.tracking_total_views,
        "tracking_unique_visitors": self.tracking_unique_visitors,
        "tracking_tracked_pages": self.tracking_tracked_pages,
        "tracking_views_chart": self.tracking_views_chart,
        "tracking_top_pages": self.tracking_top_pages,
        "tracking_recent_page_views": self.tracking_recent_page_views,
    }

TrackingSelector

Read page tracking analytics from Redis and flushed database rows.

Source code in src/codex_django/tracking/selector.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class TrackingSelector:
    """Read page tracking analytics from Redis and flushed database rows."""

    @staticmethod
    def daily_counts(date_str: str | None = None) -> dict[str, int]:
        """Return path counters for a single day.

        Args:
            date_str: Optional ISO date string. Defaults to the current local
                day when omitted.

        Returns:
            A mapping of request path to total page views. Redis snapshots
            override ORM snapshot values when both are present.
        """
        from .models import PageView

        day = date_str or timezone.localdate().isoformat()
        db_counts = {
            row["path"]: int(row["views"])
            for row in PageView.objects.filter(date=day).values("path", "views").order_by("-views", "path")
        }
        merged = _merge_counts(db_counts, get_tracking_manager().get_daily(day))
        return _filter_skipped_paths(merged)

    @staticmethod
    def top_pages(date_str: str | None = None, limit: int = 10) -> list[dict[str, Any]]:
        """Return the highest-traffic pages for one day.

        Args:
            date_str: Optional ISO date string. Defaults to the current local
                day when omitted.
            limit: Maximum number of rows to return.

        Returns:
            A list of ``{"path": ..., "views": ...}`` dictionaries ordered by
            descending view count and then by path.
        """

        counts = TrackingSelector.daily_counts(date_str)
        items = sorted(counts.items(), key=lambda item: (-item[1], item[0]))[:limit]
        return [{"path": path, "views": views} for path, views in items]

    @staticmethod
    def unique_visitors(date_str: str | None = None) -> int:
        """Return the approximate unique visitor count for one day.

        Args:
            date_str: Optional ISO date string. Defaults to the current local
                day when omitted.

        Returns:
            The HyperLogLog-backed unique visitor estimate stored in Redis for
            the selected day.
        """

        day = date_str or timezone.localdate().isoformat()
        return get_tracking_manager().get_unique_count(day)

    @staticmethod
    def total_views(date_str: str | None = None) -> int:
        """Return total page views for one day.

        Args:
            date_str: Optional ISO date string. Defaults to the current local
                day when omitted.

        Returns:
            The sum of all page views for the selected day.
        """

        return sum(TrackingSelector.daily_counts(date_str).values())

    @staticmethod
    def multi_day_totals(days: int | None = None) -> list[dict[str, Any]]:
        """Return daily total views for a trailing analytics window.

        Args:
            days: Optional number of days to include. Falls back to
                ``TrackingSettings.analytics_days`` when omitted.

        Returns:
            A chronologically ordered list of ``{"date": ..., "views": ...}``
            dictionaries for each day in the requested window.
        """

        from .models import PageView

        resolved_days = days or get_tracking_settings().analytics_days
        dates = _date_window(resolved_days)
        totals = {day.isoformat(): 0 for day in dates}

        rows = (
            PageView.objects.filter(date__gte=dates[0], date__lte=dates[-1])
            .values("date")
            .annotate(total=Sum("views"))
            .order_by("date")
        )
        for row in rows:
            totals[row["date"].isoformat()] = int(row["total"] or 0)

        redis_snapshots = get_tracking_manager().get_multi_day([day.isoformat() for day in dates])
        for day, snapshot in zip(dates, redis_snapshots, strict=False):
            if snapshot:
                # Convert snapshot to int values and filter skipped paths before summing
                int_snapshot = {path: int(views) for path, views in snapshot.items()}
                filtered_snapshot = _filter_skipped_paths(int_snapshot)
                totals[day.isoformat()] = sum(filtered_snapshot.values())

        return [{"date": day.isoformat(), "views": totals[day.isoformat()]} for day in dates]

    @staticmethod
    def get_analytics_context(days: int | None = None) -> TrackingAnalyticsContext:
        """Build cabinet-ready widget payloads for tracking analytics.

        Args:
            days: Optional number of trailing days to use for chart
                aggregation. Falls back to the configured analytics window.

        Returns:
            A typed ``TrackingAnalyticsContext`` containing KPI, chart, list,
            and table widgets for cabinet dashboards or analytics pages.
        """

        totals = TrackingSelector.multi_day_totals(days)
        today_total = totals[-1]["views"] if totals else 0
        yesterday_total = totals[-2]["views"] if len(totals) > 1 else 0
        tracked_pages = len(TrackingSelector.daily_counts())
        unique_visitors = TrackingSelector.unique_visitors()
        top_pages = TrackingSelector.top_pages(limit=8)

        trend_value = ""
        trend_direction = "neutral"
        if yesterday_total:
            diff = ((today_total - yesterday_total) / yesterday_total) * 100
            trend_value = f"{diff:+.0f}%"
            trend_direction = "up" if diff >= 0 else "down"

        chart_labels = [item["date"][5:] for item in totals]
        chart_values = [item["views"] for item in totals]

        return TrackingAnalyticsContext(
            tracking_total_views=MetricWidgetData(
                label=str(_("Page views today")),
                value=str(today_total),
                trend_value=trend_value or None,
                trend_label=str(_("vs yesterday")) if trend_value else None,
                trend_direction=trend_direction,
                icon="bi-eye",
            ),
            tracking_unique_visitors=MetricWidgetData(
                label=str(_("Unique visitors today")),
                value=str(unique_visitors),
                trend_label=str(_("live estimate")),
                icon="bi-person-check",
            ),
            tracking_tracked_pages=MetricWidgetData(
                label=str(_("Tracked pages today")),
                value=str(tracked_pages),
                icon="bi-file-earmark-bar-graph",
            ),
            tracking_views_chart={
                "chart_id": "trackingViewsChart",
                "title": str(_("Visits")),
                "subtitle": str(_("last 30 days")),
                "icon": "bi-graph-up",
                "type": "line",
                "kpi_value": str(sum(chart_values)),
                "kpi_trend_label": str(_("total views")),
                "chart_labels": chart_labels,
                "datasets": [
                    {
                        "label": str(_("Views")),
                        "data": chart_values,
                        "borderColor": "#2563eb",
                        "backgroundColor": "rgba(37,99,235,0.08)",
                        "fill": True,
                        "tension": 0,
                        "borderWidth": 2,
                        "pointRadius": 0,
                    }
                ],
            },
            tracking_top_pages=ListWidgetData(
                title=str(_("Top pages")),
                subtitle=str(_("today")),
                icon="bi-trophy",
                items=[
                    ListItem(label=row["path"], value=str(row["views"]), sublabel=str(_("views"))) for row in top_pages
                ],
            ),
            tracking_recent_page_views=TableWidgetData(
                columns=[
                    TableColumn(key="path", label=str(_("Path")), bold=True),
                    TableColumn(key="views", label=str(_("Views")), align="right"),
                ],
                rows=top_pages,
            ),
        )
Functions
daily_counts(date_str=None) staticmethod

Return path counters for a single day.

Parameters:

Name Type Description Default
date_str str | None

Optional ISO date string. Defaults to the current local day when omitted.

None

Returns:

Type Description
dict[str, int]

A mapping of request path to total page views. Redis snapshots

dict[str, int]

override ORM snapshot values when both are present.

Source code in src/codex_django/tracking/selector.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@staticmethod
def daily_counts(date_str: str | None = None) -> dict[str, int]:
    """Return path counters for a single day.

    Args:
        date_str: Optional ISO date string. Defaults to the current local
            day when omitted.

    Returns:
        A mapping of request path to total page views. Redis snapshots
        override ORM snapshot values when both are present.
    """
    from .models import PageView

    day = date_str or timezone.localdate().isoformat()
    db_counts = {
        row["path"]: int(row["views"])
        for row in PageView.objects.filter(date=day).values("path", "views").order_by("-views", "path")
    }
    merged = _merge_counts(db_counts, get_tracking_manager().get_daily(day))
    return _filter_skipped_paths(merged)
top_pages(date_str=None, limit=10) staticmethod

Return the highest-traffic pages for one day.

Parameters:

Name Type Description Default
date_str str | None

Optional ISO date string. Defaults to the current local day when omitted.

None
limit int

Maximum number of rows to return.

10

Returns:

Type Description
list[dict[str, Any]]

A list of {"path": ..., "views": ...} dictionaries ordered by

list[dict[str, Any]]

descending view count and then by path.

Source code in src/codex_django/tracking/selector.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
@staticmethod
def top_pages(date_str: str | None = None, limit: int = 10) -> list[dict[str, Any]]:
    """Return the highest-traffic pages for one day.

    Args:
        date_str: Optional ISO date string. Defaults to the current local
            day when omitted.
        limit: Maximum number of rows to return.

    Returns:
        A list of ``{"path": ..., "views": ...}`` dictionaries ordered by
        descending view count and then by path.
    """

    counts = TrackingSelector.daily_counts(date_str)
    items = sorted(counts.items(), key=lambda item: (-item[1], item[0]))[:limit]
    return [{"path": path, "views": views} for path, views in items]
unique_visitors(date_str=None) staticmethod

Return the approximate unique visitor count for one day.

Parameters:

Name Type Description Default
date_str str | None

Optional ISO date string. Defaults to the current local day when omitted.

None

Returns:

Type Description
int

The HyperLogLog-backed unique visitor estimate stored in Redis for

int

the selected day.

Source code in src/codex_django/tracking/selector.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
@staticmethod
def unique_visitors(date_str: str | None = None) -> int:
    """Return the approximate unique visitor count for one day.

    Args:
        date_str: Optional ISO date string. Defaults to the current local
            day when omitted.

    Returns:
        The HyperLogLog-backed unique visitor estimate stored in Redis for
        the selected day.
    """

    day = date_str or timezone.localdate().isoformat()
    return get_tracking_manager().get_unique_count(day)
total_views(date_str=None) staticmethod

Return total page views for one day.

Parameters:

Name Type Description Default
date_str str | None

Optional ISO date string. Defaults to the current local day when omitted.

None

Returns:

Type Description
int

The sum of all page views for the selected day.

Source code in src/codex_django/tracking/selector.py
125
126
127
128
129
130
131
132
133
134
135
136
137
@staticmethod
def total_views(date_str: str | None = None) -> int:
    """Return total page views for one day.

    Args:
        date_str: Optional ISO date string. Defaults to the current local
            day when omitted.

    Returns:
        The sum of all page views for the selected day.
    """

    return sum(TrackingSelector.daily_counts(date_str).values())
multi_day_totals(days=None) staticmethod

Return daily total views for a trailing analytics window.

Parameters:

Name Type Description Default
days int | None

Optional number of days to include. Falls back to TrackingSettings.analytics_days when omitted.

None

Returns:

Type Description
list[dict[str, Any]]

A chronologically ordered list of {"date": ..., "views": ...}

list[dict[str, Any]]

dictionaries for each day in the requested window.

Source code in src/codex_django/tracking/selector.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
@staticmethod
def multi_day_totals(days: int | None = None) -> list[dict[str, Any]]:
    """Return daily total views for a trailing analytics window.

    Args:
        days: Optional number of days to include. Falls back to
            ``TrackingSettings.analytics_days`` when omitted.

    Returns:
        A chronologically ordered list of ``{"date": ..., "views": ...}``
        dictionaries for each day in the requested window.
    """

    from .models import PageView

    resolved_days = days or get_tracking_settings().analytics_days
    dates = _date_window(resolved_days)
    totals = {day.isoformat(): 0 for day in dates}

    rows = (
        PageView.objects.filter(date__gte=dates[0], date__lte=dates[-1])
        .values("date")
        .annotate(total=Sum("views"))
        .order_by("date")
    )
    for row in rows:
        totals[row["date"].isoformat()] = int(row["total"] or 0)

    redis_snapshots = get_tracking_manager().get_multi_day([day.isoformat() for day in dates])
    for day, snapshot in zip(dates, redis_snapshots, strict=False):
        if snapshot:
            # Convert snapshot to int values and filter skipped paths before summing
            int_snapshot = {path: int(views) for path, views in snapshot.items()}
            filtered_snapshot = _filter_skipped_paths(int_snapshot)
            totals[day.isoformat()] = sum(filtered_snapshot.values())

    return [{"date": day.isoformat(), "views": totals[day.isoformat()]} for day in dates]
get_analytics_context(days=None) staticmethod

Build cabinet-ready widget payloads for tracking analytics.

Parameters:

Name Type Description Default
days int | None

Optional number of trailing days to use for chart aggregation. Falls back to the configured analytics window.

None

Returns:

Type Description
TrackingAnalyticsContext

A typed TrackingAnalyticsContext containing KPI, chart, list,

TrackingAnalyticsContext

and table widgets for cabinet dashboards or analytics pages.

Source code in src/codex_django/tracking/selector.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
@staticmethod
def get_analytics_context(days: int | None = None) -> TrackingAnalyticsContext:
    """Build cabinet-ready widget payloads for tracking analytics.

    Args:
        days: Optional number of trailing days to use for chart
            aggregation. Falls back to the configured analytics window.

    Returns:
        A typed ``TrackingAnalyticsContext`` containing KPI, chart, list,
        and table widgets for cabinet dashboards or analytics pages.
    """

    totals = TrackingSelector.multi_day_totals(days)
    today_total = totals[-1]["views"] if totals else 0
    yesterday_total = totals[-2]["views"] if len(totals) > 1 else 0
    tracked_pages = len(TrackingSelector.daily_counts())
    unique_visitors = TrackingSelector.unique_visitors()
    top_pages = TrackingSelector.top_pages(limit=8)

    trend_value = ""
    trend_direction = "neutral"
    if yesterday_total:
        diff = ((today_total - yesterday_total) / yesterday_total) * 100
        trend_value = f"{diff:+.0f}%"
        trend_direction = "up" if diff >= 0 else "down"

    chart_labels = [item["date"][5:] for item in totals]
    chart_values = [item["views"] for item in totals]

    return TrackingAnalyticsContext(
        tracking_total_views=MetricWidgetData(
            label=str(_("Page views today")),
            value=str(today_total),
            trend_value=trend_value or None,
            trend_label=str(_("vs yesterday")) if trend_value else None,
            trend_direction=trend_direction,
            icon="bi-eye",
        ),
        tracking_unique_visitors=MetricWidgetData(
            label=str(_("Unique visitors today")),
            value=str(unique_visitors),
            trend_label=str(_("live estimate")),
            icon="bi-person-check",
        ),
        tracking_tracked_pages=MetricWidgetData(
            label=str(_("Tracked pages today")),
            value=str(tracked_pages),
            icon="bi-file-earmark-bar-graph",
        ),
        tracking_views_chart={
            "chart_id": "trackingViewsChart",
            "title": str(_("Visits")),
            "subtitle": str(_("last 30 days")),
            "icon": "bi-graph-up",
            "type": "line",
            "kpi_value": str(sum(chart_values)),
            "kpi_trend_label": str(_("total views")),
            "chart_labels": chart_labels,
            "datasets": [
                {
                    "label": str(_("Views")),
                    "data": chart_values,
                    "borderColor": "#2563eb",
                    "backgroundColor": "rgba(37,99,235,0.08)",
                    "fill": True,
                    "tension": 0,
                    "borderWidth": 2,
                    "pointRadius": 0,
                }
            ],
        },
        tracking_top_pages=ListWidgetData(
            title=str(_("Top pages")),
            subtitle=str(_("today")),
            icon="bi-trophy",
            items=[
                ListItem(label=row["path"], value=str(row["views"]), sublabel=str(_("views"))) for row in top_pages
            ],
        ),
        tracking_recent_page_views=TableWidgetData(
            columns=[
                TableColumn(key="path", label=str(_("Path")), bold=True),
                TableColumn(key="views", label=str(_("Views")), align="right"),
            ],
            rows=top_pages,
        ),
    )

Functions

Dashboard providers

codex_django.tracking.providers

DashboardSelector providers for reusable tracking widgets.

Classes

Functions

provide_tracking_analytics(request)

Expose tracking analytics widgets to the cabinet dashboard.

Source code in src/codex_django/tracking/providers.py
12
13
14
15
16
@DashboardSelector.extend(cache_key="tracking_analytics", cache_ttl=60)
def provide_tracking_analytics(request: Any) -> dict[str, Any]:
    """Expose tracking analytics widgets to the cabinet dashboard."""

    return TrackingSelector.get_analytics_context().as_dict()

Cabinet declarations

codex_django.tracking.cabinet

Cabinet declarations for the reusable tracking app.

Classes

Functions

ORM models

codex_django.tracking.models

Database models for flushed tracking snapshots.

Classes

PageView

Bases: Model

Aggregated daily page view counts flushed from Redis.

Source code in src/codex_django/tracking/models.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class PageView(models.Model):
    """Aggregated daily page view counts flushed from Redis."""

    path = models.CharField(_("path"), max_length=500, db_index=True)
    date = models.DateField(_("date"), db_index=True)
    views = models.PositiveIntegerField(_("views"), default=0)

    class Meta:
        verbose_name = _("Page view")
        verbose_name_plural = _("Page views")
        constraints = [
            models.UniqueConstraint(fields=["path", "date"], name="codex_tracking_pageview_path_date_uniq"),
        ]
        ordering = ["-date", "-views", "path"]

    def __str__(self) -> str:
        return f"{self.date} {self.path} - {self.views}"

Admin integration

codex_django.tracking.admin

Django admin integration for tracking snapshots.

Classes

PageViewAdmin

Bases: ModelAdmin

Inspect flushed page view snapshots.

Source code in src/codex_django/tracking/admin.py
10
11
12
13
14
15
16
17
@admin.register(PageView)
class PageViewAdmin(admin.ModelAdmin):  # type: ignore[type-arg]
    """Inspect flushed page view snapshots."""

    list_display = ("date", "path", "views")
    list_filter = ("date",)
    search_fields = ("path",)
    ordering = ("-date", "-views", "path")

Flush helpers

codex_django.tracking.flush

Flush Redis tracking counters into database snapshots.

Functions

flush_page_views(date_str=None)

Upsert one day's Redis counters into PageView snapshots.

Parameters:

Name Type Description Default
date_str str | None

Optional ISO date string identifying the day to flush. Defaults to the current local day when omitted.

None

Returns:

Type Description
int

The number of path counters written to the database snapshot table.

Source code in src/codex_django/tracking/flush.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def flush_page_views(date_str: str | None = None) -> int:
    """Upsert one day's Redis counters into ``PageView`` snapshots.

    Args:
        date_str: Optional ISO date string identifying the day to flush.
            Defaults to the current local day when omitted.

    Returns:
        The number of path counters written to the database snapshot table.
    """

    from .manager import get_tracking_manager
    from .models import PageView

    day = date_str or timezone.localdate().isoformat()
    raw = get_tracking_manager().get_daily(day)
    if not raw:
        return 0

    PageView.objects.bulk_create(
        [PageView(path=path, date=day, views=int(views)) for path, views in raw.items()],
        update_conflicts=True,
        update_fields=["views"],
        unique_fields=["path", "date"],
    )
    return len(raw)

Flush management command

codex_django.tracking.management.commands.flush_page_views

Management command for flushing tracking counters.

Classes

Command

Bases: BaseCommand

Flush Redis page-view counters into the database.

Source code in src/codex_django/tracking/management/commands/flush_page_views.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Command(BaseCommand):
    """Flush Redis page-view counters into the database."""

    help = "Flush codex tracking page-view counters into PageView rows."

    def add_arguments(self, parser: CommandParser) -> None:
        parser.add_argument("--date", dest="date_str", help="Date to flush in YYYY-MM-DD format.")

    def handle(self, *args: object, **options: object) -> None:
        from codex_django.tracking.flush import flush_page_views

        date_str = options.get("date_str")
        count = flush_page_views(str(date_str) if date_str else None)
        self.stdout.write(self.style.SUCCESS(f"[codex-tracking] flushed {count} paths"))

URLs and views

codex_django.tracking.urls

URL patterns for reusable tracking cabinet views.

Functions

codex_django.tracking.views

Cabinet views for tracking analytics.

Classes

Functions

tracking_analytics_view(request)

Render the reusable cabinet analytics page for tracking data.

Source code in src/codex_django/tracking/views.py
14
15
16
17
18
19
20
21
@_staff_check
def tracking_analytics_view(request: HttpRequest) -> HttpResponse:
    """Render the reusable cabinet analytics page for tracking data."""

    request.cabinet_module = "tracking"  # type: ignore[attr-defined]
    request.cabinet_nav_group = "admin"  # type: ignore[attr-defined]
    context = TrackingSelector.get_analytics_context().as_dict()
    return render(request, "tracking/cabinet/analytics.html", context)