Skip to content

Producer guide

Any app using ecodev_core can expose its usage statistics over HTTP by wiring get_stats_router() into its FastAPI instance. One endpoint is always registered (/stats/activities). A second endpoint (/stats/projects) is registered only when the app passes a ProjectStatsAdapter.


1. Add the stats_api config section

In every environment config (config/local.yaml, config/preprod.yaml, config/prod.yaml):

stats_api:
   api_key: "replace-with-a-secret"

Warning

The key is mandatory. An absent or null value means every inbound request is rejected with HTTP 401. There is no JWT fallback.

Generating a key

Run once per environment:

python -c "import secrets; print(secrets.token_hex(32))"

This produces a 64-character hex string. Never commit real keys to version control — inject them via environment variable or a secrets manager.


2. Wire the router — activities only

For apps that own no project table:

# app/app.py
from ecodev_core import get_stats_router

app.include_router(get_stats_router())

/stats/activities is now live. GET /stats/projects returns 404 because no adapter was supplied.


3. Wire the router — with projects

For apps that own a Project model, create a dedicated router module:

# app/routers/app_stats.py
from datetime import datetime
from typing import Iterable

from sqlmodel import col, select, Session

from ecodev_core import get_stats_router, ProjectExport, ProjectStatsAdapter

from app.db_model import Project


def _list_projects(
        session: Session,
        from_date: datetime | None,
        to_date: datetime | None,
) -> Iterable[ProjectExport]:
    stmt = select(Project)
    if from_date:
        stmt = stmt.where(col(Project.modified_at) >= from_date)
    if to_date:
        stmt = stmt.where(col(Project.modified_at) < to_date)

    for project in session.exec(stmt).all():
        yield ProjectExport(
            project_id=str(project.id),
            name=project.name,
            creator=project.user,
            created_at=project.created_at,
            modified_at=project.modified_at,
            description=getattr(project, 'description', None),
            client=getattr(project, 'client', None),
            project_type=getattr(project, 'project_type', None),
        )


stats_router = get_stats_router(
    adapter=ProjectStatsAdapter(list_projects=_list_projects)
)

Tip

Filter on modified_at, not created_at. The consumer sends the last-ingest timestamp as from_date; filtering on modified_at ensures projects updated since the last ingest are included, not just projects created since then.

Then include the router in app/app.py:

# app/app.py
from app.routers.app_stats import stats_router

app.include_router(stats_router)

4. Endpoint shapes and query surface

/stats/activitiesPagedResponse[ActivityExport]

Rows are aggregated server-side by (application, period_start, method) with count(*) as activity_count and count(distinct user) as unique_users. No raw user identity crosses the wire.

Query parameter Type Default Description
from_date ISO-8601 datetime Inclusive bucket start filter
to_date ISO-8601 datetime Exclusive bucket end filter
method string Filter to a single method name
page_size int (1–5000) 500 Max rows per page
granularity hour or month hour Bucket size for date_trunc

The response envelope is:

{
  "items": [...],
  "next_from_date": "2026-01-15T09:00:00"
}

Pass next_from_date back as from_date to fetch the next page. When null, the final page has been reached. next_from_date is a keyset cursor on the bucket timestamp, so pages remain stable while new activity arrives.

/stats/projectslist[ProjectExport]

Returns a bare JSON array (no pagination envelope). All projects matching the date filters are returned in a single response.

When no adapter is supplied, the route is not registered and returns 404 — a valid opt-out, not an error.

Query parameter Type Default Description
from_date ISO-8601 datetime Filter by modified_at >= from_date
to_date ISO-8601 datetime Filter by modified_at < to_date

5. Custom dependency

get_stats_router accepts a dependency kwarg if you need to swap the auth check:

stats_router = get_stats_router(dependency=my_custom_auth)

The default is api_key_auth from ecodev_core.app_stats.api_key.


6. Remove the legacy /get-activities endpoint

The new /stats/activities supersedes the old endpoint. Delete or comment out any existing @app.get('/get-activities') decorator in app/app.py.