databricks-tellr-app 0.3.11.dev8__tar.gz → 0.3.11.dev9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/PKG-INFO +1 -1
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/pyproject.toml +1 -1
- databricks_tellr_app-0.3.11.dev9/src/api/routes/admin_usage.py +145 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/services/usage_service.py +208 -86
- databricks_tellr_app-0.3.11.dev8/src/api/routes/admin_usage.py +0 -69
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/README.md +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/databricks_tellr_app/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/databricks_tellr_app/run.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/databricks_tellr_app.egg-info/SOURCES.txt +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/databricks_tellr_app.egg-info/requires.txt +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/databricks_tellr_app.egg-info/top_level.txt +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/setup.cfg +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/setup.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/_otel_bootstrap.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/fixtures/tour_demo_deck.json +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/main.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/mcp_auth.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/mcp_server.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/middleware/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/middleware/request_logging.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/admin.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/agent_config.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/chat.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/deck_contributors.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/export.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/feedback.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/google_slides.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/images.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/local_version.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/profiles.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/sessions.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/settings/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/settings/contributors.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/settings/deck_prompts.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/settings/identities.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/settings/slide_styles.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/setup.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/slides.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/tools.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/tour.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/verification.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/routes/version.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/schemas/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/schemas/agent_config.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/schemas/feedback.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/schemas/requests.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/schemas/responses.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/schemas/settings/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/schemas/settings/requests.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/schemas/settings/responses.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/schemas/streaming.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/services/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/services/chat_service.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/services/export_job_queue.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/services/feedback_service.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/services/job_queue.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/services/session_manager.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/services/session_naming.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/services/usage_events.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/utils/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/api/utils/validation.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/config_loader.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/context_utils.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/database.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/databricks_client.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/defaults.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/encryption.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/init_default_profile.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/lakebase.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/migrate_profiles_to_agent_config.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/mlflow_agent_spans.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/mlflow_tracing.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/otel_logging.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/permission_context.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/prompt_modules.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/settings_db.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/core/user_context.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/deck_contributor.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/feedback.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/genie_space.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/google_global_credentials.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/google_oauth_token.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/identity.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/image.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/profile.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/profile_contributor.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/prompts.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/request_log.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/session.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/slide_deck_prompt.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/slide_style_library.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/usage_event.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/database/models/user_preference.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/domain/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/domain/slide.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/domain/slide_deck.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/agent.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/agent_factory.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/config_service.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/config_validator.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/drive_uploader.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/evaluation/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/evaluation/llm_judge.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/genie_service.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/google_slides_auth.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/google_slides_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/html_to_google_slides.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/html_to_pptx.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/identity_provider.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/identity_providers/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/identity_providers/account_provider.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/identity_providers/local_provider.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/identity_providers/workspace_provider.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/image_service.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/image_tools.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/permission_service.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/pptx_from_html_huashu.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/pptx_from_records.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/pptx_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/profile_service.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/streaming_callback.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/tools/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/tools/agent_bricks_tool.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/tools/genie_tool.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/tools/mcp_tool.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/tools/model_endpoint_tool.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/tools/vector_tool.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/services/validator.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/__init__.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/css_utils.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/error_handling.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/html_safety.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/html_utils.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/image_utils.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/js_validator.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/logging_config.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/pi_filter.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/slide_hash.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/spotlight.py +0 -0
- {databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/src/utils/text_caps.py +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Admin usage-analytics endpoints (ungated, like the rest of /admin)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import date as dt_date
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
8
|
+
from sqlalchemy.orm import Session
|
|
9
|
+
|
|
10
|
+
from src.api.services.usage_service import MAX_WINDOW_DAYS, UsageService
|
|
11
|
+
from src.core.database import get_db
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
router = APIRouter(prefix="/api/admin/usage", tags=["admin-usage"])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _parse_date(value: str, name: str) -> dt_date:
|
|
18
|
+
try:
|
|
19
|
+
return datetime.strptime(value, "%Y-%m-%d").date()
|
|
20
|
+
except ValueError:
|
|
21
|
+
raise HTTPException(
|
|
22
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
23
|
+
detail=f"{name} must be a YYYY-MM-DD date",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _window_kwargs(
|
|
28
|
+
days: int | None,
|
|
29
|
+
start: str | None,
|
|
30
|
+
end: str | None,
|
|
31
|
+
all_data: bool,
|
|
32
|
+
) -> dict:
|
|
33
|
+
"""Validate window query params into UsageService kwargs.
|
|
34
|
+
|
|
35
|
+
Precedence: all > start/end range > days > default (7, applied by the
|
|
36
|
+
service). ``start`` and ``end`` must be provided together.
|
|
37
|
+
"""
|
|
38
|
+
if all_data:
|
|
39
|
+
return {"all_data": True}
|
|
40
|
+
if start is not None or end is not None:
|
|
41
|
+
if start is None or end is None:
|
|
42
|
+
raise HTTPException(
|
|
43
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
44
|
+
detail="start and end must be provided together",
|
|
45
|
+
)
|
|
46
|
+
start_date = _parse_date(start, "start")
|
|
47
|
+
end_date = _parse_date(end, "end")
|
|
48
|
+
if start_date > end_date:
|
|
49
|
+
raise HTTPException(
|
|
50
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
51
|
+
detail="start must be on or before end",
|
|
52
|
+
)
|
|
53
|
+
return {"start": start_date, "end": end_date}
|
|
54
|
+
if days is not None:
|
|
55
|
+
if not 1 <= days <= MAX_WINDOW_DAYS:
|
|
56
|
+
raise HTTPException(
|
|
57
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
58
|
+
detail=f"days must be between 1 and {MAX_WINDOW_DAYS}",
|
|
59
|
+
)
|
|
60
|
+
return {"days": days}
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _handle(callable_, *args, **kwargs):
|
|
65
|
+
try:
|
|
66
|
+
return callable_(*args, **kwargs)
|
|
67
|
+
except HTTPException:
|
|
68
|
+
raise
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"Usage analytics error: {e}", exc_info=True)
|
|
71
|
+
raise HTTPException(
|
|
72
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
73
|
+
detail="Failed to compute usage analytics",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
_DAYS = Query(default=None, description=f"Rolling window, 1..{MAX_WINDOW_DAYS}")
|
|
78
|
+
_START = Query(default=None, description="Range start (YYYY-MM-DD, inclusive)")
|
|
79
|
+
_END = Query(default=None, description="Range end (YYYY-MM-DD, inclusive)")
|
|
80
|
+
_ALL = Query(default=False, alias="all", description="Report over all data")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.get("/summary")
|
|
84
|
+
def get_summary(
|
|
85
|
+
days: int | None = _DAYS,
|
|
86
|
+
start: str | None = _START,
|
|
87
|
+
end: str | None = _END,
|
|
88
|
+
all_data: bool = _ALL,
|
|
89
|
+
db: Session = Depends(get_db),
|
|
90
|
+
):
|
|
91
|
+
kwargs = _window_kwargs(days, start, end, all_data)
|
|
92
|
+
return _handle(UsageService().get_summary, db, **kwargs)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.get("/daily")
|
|
96
|
+
def get_daily(
|
|
97
|
+
days: int | None = _DAYS,
|
|
98
|
+
start: str | None = _START,
|
|
99
|
+
end: str | None = _END,
|
|
100
|
+
all_data: bool = _ALL,
|
|
101
|
+
db: Session = Depends(get_db),
|
|
102
|
+
):
|
|
103
|
+
kwargs = _window_kwargs(days, start, end, all_data)
|
|
104
|
+
return _handle(UsageService().get_daily, db, **kwargs)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.get("/top-users")
|
|
108
|
+
def get_top_users(
|
|
109
|
+
days: int | None = _DAYS,
|
|
110
|
+
start: str | None = _START,
|
|
111
|
+
end: str | None = _END,
|
|
112
|
+
all_data: bool = _ALL,
|
|
113
|
+
db: Session = Depends(get_db),
|
|
114
|
+
):
|
|
115
|
+
kwargs = _window_kwargs(days, start, end, all_data)
|
|
116
|
+
return _handle(UsageService().get_top_users, db, **kwargs)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@router.get("/funnel")
|
|
120
|
+
def get_funnel(
|
|
121
|
+
days: int | None = _DAYS,
|
|
122
|
+
start: str | None = _START,
|
|
123
|
+
end: str | None = _END,
|
|
124
|
+
all_data: bool = _ALL,
|
|
125
|
+
db: Session = Depends(get_db),
|
|
126
|
+
):
|
|
127
|
+
kwargs = _window_kwargs(days, start, end, all_data)
|
|
128
|
+
return _handle(UsageService().get_funnel, db, **kwargs)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@router.get("/retention")
|
|
132
|
+
def get_retention(db: Session = Depends(get_db)):
|
|
133
|
+
return _handle(UsageService().get_retention, db)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@router.get("/heatmap")
|
|
137
|
+
def get_heatmap(
|
|
138
|
+
days: int | None = _DAYS,
|
|
139
|
+
start: str | None = _START,
|
|
140
|
+
end: str | None = _END,
|
|
141
|
+
all_data: bool = _ALL,
|
|
142
|
+
db: Session = Depends(get_db),
|
|
143
|
+
):
|
|
144
|
+
kwargs = _window_kwargs(days, start, end, all_data)
|
|
145
|
+
return _handle(UsageService().get_heatmap, db, **kwargs)
|
|
@@ -10,6 +10,8 @@ use `user_sessions` as a labeled proxy.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from collections import defaultdict
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import date as dt_date
|
|
13
15
|
from datetime import datetime, timedelta
|
|
14
16
|
from datetime import time as dt_time
|
|
15
17
|
|
|
@@ -28,7 +30,68 @@ from src.database.models.usage_event import (
|
|
|
28
30
|
EVENT_LOGIN,
|
|
29
31
|
)
|
|
30
32
|
|
|
33
|
+
# Preset windows offered by the UI; the API accepts any 1..MAX_WINDOW_DAYS,
|
|
34
|
+
# an explicit start/end date range, or all=true for the full history.
|
|
31
35
|
ALLOWED_WINDOWS = {7, 14, 21, 28}
|
|
36
|
+
MAX_WINDOW_DAYS = 365
|
|
37
|
+
# Hard cap on rows returned by get_daily in all-data/range mode
|
|
38
|
+
MAX_DAILY_ROWS = 730
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Window:
|
|
43
|
+
"""Resolved reporting window.
|
|
44
|
+
|
|
45
|
+
``start`` is an inclusive datetime lower bound (None = beginning of data);
|
|
46
|
+
``end`` is an exclusive upper bound. ``days`` is set only in preset/days
|
|
47
|
+
mode and is echoed back in responses for backward compatibility.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
start: datetime | None
|
|
51
|
+
end: datetime
|
|
52
|
+
days: int | None
|
|
53
|
+
all_data: bool
|
|
54
|
+
|
|
55
|
+
def to_meta(self) -> dict:
|
|
56
|
+
return {
|
|
57
|
+
"days": self.days,
|
|
58
|
+
"start": self.start.strftime("%Y-%m-%d") if self.start else None,
|
|
59
|
+
"end": (self.end - timedelta(days=1)).strftime("%Y-%m-%d"),
|
|
60
|
+
"all": self.all_data,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resolve_window(
|
|
65
|
+
days: int | None = None,
|
|
66
|
+
start: dt_date | None = None,
|
|
67
|
+
end: dt_date | None = None,
|
|
68
|
+
all_data: bool = False,
|
|
69
|
+
) -> Window:
|
|
70
|
+
"""Resolve query params into a concrete Window.
|
|
71
|
+
|
|
72
|
+
Precedence: all_data > explicit start/end range > days > default 7 days.
|
|
73
|
+
``end`` is inclusive as a date (callers pass the last day they want to
|
|
74
|
+
see); internally it becomes an exclusive midnight bound.
|
|
75
|
+
"""
|
|
76
|
+
tomorrow = datetime.combine(
|
|
77
|
+
datetime.utcnow().date() + timedelta(days=1), dt_time.min
|
|
78
|
+
)
|
|
79
|
+
if all_data:
|
|
80
|
+
return Window(start=None, end=tomorrow, days=None, all_data=True)
|
|
81
|
+
if start is not None and end is not None:
|
|
82
|
+
return Window(
|
|
83
|
+
start=datetime.combine(start, dt_time.min),
|
|
84
|
+
end=datetime.combine(end + timedelta(days=1), dt_time.min),
|
|
85
|
+
days=None,
|
|
86
|
+
all_data=False,
|
|
87
|
+
)
|
|
88
|
+
resolved_days = days if days is not None else 7
|
|
89
|
+
return Window(
|
|
90
|
+
start=_window_start(resolved_days),
|
|
91
|
+
end=tomorrow,
|
|
92
|
+
days=resolved_days,
|
|
93
|
+
all_data=False,
|
|
94
|
+
)
|
|
32
95
|
|
|
33
96
|
# Route template logged by the request-logging middleware for deck opens
|
|
34
97
|
_DECK_OPEN_ROUTE = "/api/sessions/{session_id}"
|
|
@@ -58,35 +121,50 @@ def _window_start(days: int) -> datetime:
|
|
|
58
121
|
return datetime.combine(today - timedelta(days=days - 1), dt_time.min)
|
|
59
122
|
|
|
60
123
|
|
|
61
|
-
def _day_list(days: int) -> list[str]:
|
|
62
|
-
today = datetime.utcnow().date()
|
|
63
|
-
return [
|
|
64
|
-
(today - timedelta(days=offset)).strftime("%Y-%m-%d")
|
|
65
|
-
for offset in range(days - 1, -1, -1)
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
|
|
69
124
|
class UsageService:
|
|
70
125
|
"""Aggregates usage metrics for /api/admin/usage endpoints."""
|
|
71
126
|
|
|
72
127
|
# ---------- shared helpers ----------
|
|
73
128
|
|
|
74
|
-
def _events_in_window(self, db: Session,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
129
|
+
def _events_in_window(self, db: Session, w: Window) -> list[UsageEvent]:
|
|
130
|
+
query = db.query(UsageEvent).filter(UsageEvent.ts < w.end)
|
|
131
|
+
if w.start is not None:
|
|
132
|
+
query = query.filter(UsageEvent.ts >= w.start)
|
|
133
|
+
return query.all()
|
|
134
|
+
|
|
135
|
+
def _sessions_in_window(self, db: Session, w: Window) -> list[UserSession]:
|
|
136
|
+
query = db.query(UserSession).filter(UserSession.created_at < w.end)
|
|
137
|
+
if w.start is not None:
|
|
138
|
+
query = query.filter(UserSession.created_at >= w.start)
|
|
139
|
+
return query.all()
|
|
140
|
+
|
|
141
|
+
def _decks_in_window(self, db: Session, w: Window) -> list[SessionSlideDeck]:
|
|
142
|
+
query = db.query(SessionSlideDeck).filter(SessionSlideDeck.created_at < w.end)
|
|
143
|
+
if w.start is not None:
|
|
144
|
+
query = query.filter(SessionSlideDeck.created_at >= w.start)
|
|
145
|
+
return query.all()
|
|
146
|
+
|
|
147
|
+
def _window_day_list(self, w: Window, earliest: datetime | None) -> list[str]:
|
|
148
|
+
"""Calendar days covered by the window, oldest first, capped.
|
|
149
|
+
|
|
150
|
+
In all-data mode the range starts at the earliest observed data point
|
|
151
|
+
(or today when the database is empty).
|
|
152
|
+
"""
|
|
153
|
+
last_day = (w.end - timedelta(days=1)).date()
|
|
154
|
+
if w.start is not None:
|
|
155
|
+
first_day = w.start.date()
|
|
156
|
+
elif earliest is not None:
|
|
157
|
+
first_day = earliest.date()
|
|
158
|
+
else:
|
|
159
|
+
first_day = last_day
|
|
160
|
+
total = (last_day - first_day).days + 1
|
|
161
|
+
if total > MAX_DAILY_ROWS:
|
|
162
|
+
first_day = last_day - timedelta(days=MAX_DAILY_ROWS - 1)
|
|
163
|
+
total = MAX_DAILY_ROWS
|
|
164
|
+
return [
|
|
165
|
+
(first_day + timedelta(days=offset)).strftime("%Y-%m-%d")
|
|
166
|
+
for offset in range(total)
|
|
167
|
+
]
|
|
90
168
|
|
|
91
169
|
def _history_boundary(self, db: Session) -> datetime | None:
|
|
92
170
|
first = (
|
|
@@ -119,8 +197,15 @@ class UsageService:
|
|
|
119
197
|
|
|
120
198
|
# ---------- endpoints ----------
|
|
121
199
|
|
|
122
|
-
def get_summary(
|
|
123
|
-
|
|
200
|
+
def get_summary(
|
|
201
|
+
self,
|
|
202
|
+
db: Session,
|
|
203
|
+
days: int | None = None,
|
|
204
|
+
start: dt_date | None = None,
|
|
205
|
+
end: dt_date | None = None,
|
|
206
|
+
all_data: bool = False,
|
|
207
|
+
) -> dict:
|
|
208
|
+
w = resolve_window(days=days, start=start, end=end, all_data=all_data)
|
|
124
209
|
|
|
125
210
|
identity_users = {
|
|
126
211
|
name
|
|
@@ -149,25 +234,23 @@ class UsageService:
|
|
|
149
234
|
}
|
|
150
235
|
total_decks_ever = len(live_deck_session_ids) + len(orphaned_deck_events)
|
|
151
236
|
|
|
152
|
-
events = self._events_in_window(db,
|
|
153
|
-
sessions = self._sessions_in_window(db,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
237
|
+
events = self._events_in_window(db, w)
|
|
238
|
+
sessions = self._sessions_in_window(db, w)
|
|
239
|
+
activity_query = db.query(UserSession.created_by).filter(
|
|
240
|
+
UserSession.last_activity < w.end,
|
|
241
|
+
UserSession.created_by.isnot(None),
|
|
242
|
+
)
|
|
243
|
+
if w.start is not None:
|
|
244
|
+
activity_query = activity_query.filter(
|
|
245
|
+
UserSession.last_activity >= w.start
|
|
159
246
|
)
|
|
160
|
-
}
|
|
247
|
+
active_from_last_activity = {name for (name,) in activity_query}
|
|
161
248
|
active_users = (
|
|
162
249
|
{e.username for e in events}
|
|
163
250
|
| {s.created_by for s in sessions if s.created_by}
|
|
164
251
|
| active_from_last_activity
|
|
165
252
|
)
|
|
166
|
-
decks_created = (
|
|
167
|
-
db.query(SessionSlideDeck)
|
|
168
|
-
.filter(SessionSlideDeck.created_at >= start)
|
|
169
|
-
.count()
|
|
170
|
-
)
|
|
253
|
+
decks_created = len(self._decks_in_window(db, w))
|
|
171
254
|
logins = _login_visit_count(
|
|
172
255
|
e for e in events if e.event_type == EVENT_LOGIN
|
|
173
256
|
)
|
|
@@ -177,7 +260,7 @@ class UsageService:
|
|
|
177
260
|
"total_users_ever": total_users_ever,
|
|
178
261
|
"total_decks_ever": total_decks_ever,
|
|
179
262
|
"window": {
|
|
180
|
-
|
|
263
|
+
**w.to_meta(),
|
|
181
264
|
"active_users": len(active_users),
|
|
182
265
|
"decks_created": decks_created,
|
|
183
266
|
"avg_decks_per_active_user": avg,
|
|
@@ -185,14 +268,21 @@ class UsageService:
|
|
|
185
268
|
},
|
|
186
269
|
}
|
|
187
270
|
|
|
188
|
-
def get_daily(
|
|
189
|
-
|
|
271
|
+
def get_daily(
|
|
272
|
+
self,
|
|
273
|
+
db: Session,
|
|
274
|
+
days: int | None = None,
|
|
275
|
+
start: dt_date | None = None,
|
|
276
|
+
end: dt_date | None = None,
|
|
277
|
+
all_data: bool = False,
|
|
278
|
+
) -> dict:
|
|
279
|
+
w = resolve_window(days=days, start=start, end=end, all_data=all_data)
|
|
190
280
|
boundary = self._history_boundary(db)
|
|
191
281
|
boundary_day = _day_key(boundary) if boundary else None
|
|
192
282
|
|
|
193
|
-
events = self._events_in_window(db,
|
|
194
|
-
sessions = self._sessions_in_window(db,
|
|
195
|
-
decks = self._decks_in_window(db,
|
|
283
|
+
events = self._events_in_window(db, w)
|
|
284
|
+
sessions = self._sessions_in_window(db, w)
|
|
285
|
+
decks = self._decks_in_window(db, w)
|
|
196
286
|
first_seen = self._first_seen_map(db)
|
|
197
287
|
|
|
198
288
|
login_events_by_day: dict[str, list[UsageEvent]] = defaultdict(list)
|
|
@@ -225,16 +315,31 @@ class UsageService:
|
|
|
225
315
|
|
|
226
316
|
# Anonymous deck-open volume proxy from request_logs (30-day retention)
|
|
227
317
|
proxy_retrievals_by_day: dict[str, int] = defaultdict(int)
|
|
228
|
-
|
|
229
|
-
RequestLog.timestamp
|
|
318
|
+
proxy_query = db.query(RequestLog.timestamp).filter(
|
|
319
|
+
RequestLog.timestamp < w.end,
|
|
230
320
|
RequestLog.method == "GET",
|
|
231
321
|
RequestLog.path == _DECK_OPEN_ROUTE,
|
|
232
322
|
RequestLog.status_code < 400,
|
|
233
|
-
)
|
|
323
|
+
)
|
|
324
|
+
if w.start is not None:
|
|
325
|
+
proxy_query = proxy_query.filter(RequestLog.timestamp >= w.start)
|
|
326
|
+
for (ts,) in proxy_query:
|
|
234
327
|
proxy_retrievals_by_day[_day_key(ts)] += 1
|
|
235
328
|
|
|
329
|
+
earliest = min(
|
|
330
|
+
(
|
|
331
|
+
ts
|
|
332
|
+
for ts in (
|
|
333
|
+
min((e.ts for e in events), default=None),
|
|
334
|
+
min((s.created_at for s in sessions), default=None),
|
|
335
|
+
min((d.created_at for d in decks), default=None),
|
|
336
|
+
)
|
|
337
|
+
if ts is not None
|
|
338
|
+
),
|
|
339
|
+
default=None,
|
|
340
|
+
)
|
|
236
341
|
rows = []
|
|
237
|
-
for day in
|
|
342
|
+
for day in self._window_day_list(w, earliest):
|
|
238
343
|
pre_boundary = boundary_day is None or day < boundary_day
|
|
239
344
|
if pre_boundary:
|
|
240
345
|
logins = sessions_by_day.get(day, 0)
|
|
@@ -263,36 +368,51 @@ class UsageService:
|
|
|
263
368
|
),
|
|
264
369
|
}
|
|
265
370
|
)
|
|
266
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
"history_boundary": boundary_day,
|
|
373
|
+
"window": w.to_meta(),
|
|
374
|
+
"days": rows,
|
|
375
|
+
}
|
|
267
376
|
|
|
268
|
-
def
|
|
269
|
-
|
|
377
|
+
def _deck_creator_rows(self, db: Session, w: Window) -> list:
|
|
378
|
+
query = (
|
|
379
|
+
db.query(UserSession.created_by)
|
|
380
|
+
.join(SessionSlideDeck, SessionSlideDeck.session_id == UserSession.id)
|
|
381
|
+
.filter(
|
|
382
|
+
SessionSlideDeck.created_at < w.end,
|
|
383
|
+
UserSession.created_by.isnot(None),
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
if w.start is not None:
|
|
387
|
+
query = query.filter(SessionSlideDeck.created_at >= w.start)
|
|
388
|
+
return query.all()
|
|
389
|
+
|
|
390
|
+
def get_top_users(
|
|
391
|
+
self,
|
|
392
|
+
db: Session,
|
|
393
|
+
days: int | None = None,
|
|
394
|
+
start: dt_date | None = None,
|
|
395
|
+
end: dt_date | None = None,
|
|
396
|
+
all_data: bool = False,
|
|
397
|
+
) -> list[dict]:
|
|
398
|
+
w = resolve_window(days=days, start=start, end=end, all_data=all_data)
|
|
270
399
|
stats: dict[str, dict] = defaultdict(
|
|
271
400
|
lambda: {"logins": 0, "sessions_created": 0, "decks_created": 0}
|
|
272
401
|
)
|
|
273
402
|
|
|
274
403
|
login_events_by_user: dict[str, list[UsageEvent]] = defaultdict(list)
|
|
275
|
-
for e in self._events_in_window(db,
|
|
404
|
+
for e in self._events_in_window(db, w):
|
|
276
405
|
if e.event_type == EVENT_LOGIN:
|
|
277
406
|
login_events_by_user[e.username].append(e)
|
|
278
407
|
for name, user_events in login_events_by_user.items():
|
|
279
408
|
# Collapse duplicate per-worker login rows into visits
|
|
280
409
|
stats[name]["logins"] = _login_visit_count(user_events)
|
|
281
410
|
|
|
282
|
-
for s in self._sessions_in_window(db,
|
|
411
|
+
for s in self._sessions_in_window(db, w):
|
|
283
412
|
if s.created_by:
|
|
284
413
|
stats[s.created_by]["sessions_created"] += 1
|
|
285
414
|
|
|
286
|
-
|
|
287
|
-
db.query(UserSession.created_by)
|
|
288
|
-
.join(SessionSlideDeck, SessionSlideDeck.session_id == UserSession.id)
|
|
289
|
-
.filter(
|
|
290
|
-
SessionSlideDeck.created_at >= start,
|
|
291
|
-
UserSession.created_by.isnot(None),
|
|
292
|
-
)
|
|
293
|
-
.all()
|
|
294
|
-
)
|
|
295
|
-
for (name,) in deck_rows:
|
|
415
|
+
for (name,) in self._deck_creator_rows(db, w):
|
|
296
416
|
stats[name]["decks_created"] += 1
|
|
297
417
|
|
|
298
418
|
ranked = sorted(
|
|
@@ -305,15 +425,22 @@ class UsageService:
|
|
|
305
425
|
)
|
|
306
426
|
return ranked[:20]
|
|
307
427
|
|
|
308
|
-
def get_funnel(
|
|
309
|
-
|
|
310
|
-
|
|
428
|
+
def get_funnel(
|
|
429
|
+
self,
|
|
430
|
+
db: Session,
|
|
431
|
+
days: int | None = None,
|
|
432
|
+
start: dt_date | None = None,
|
|
433
|
+
end: dt_date | None = None,
|
|
434
|
+
all_data: bool = False,
|
|
435
|
+
) -> dict:
|
|
436
|
+
w = resolve_window(days=days, start=start, end=end, all_data=all_data)
|
|
437
|
+
events = self._events_in_window(db, w)
|
|
311
438
|
|
|
312
439
|
login_events = [e for e in events if e.event_type == EVENT_LOGIN]
|
|
313
440
|
proxy = len(login_events) == 0
|
|
314
441
|
|
|
315
442
|
if proxy:
|
|
316
|
-
sessions = self._sessions_in_window(db,
|
|
443
|
+
sessions = self._sessions_in_window(db, w)
|
|
317
444
|
logins = len(sessions)
|
|
318
445
|
logged_in_users = {s.created_by for s in sessions if s.created_by}
|
|
319
446
|
else:
|
|
@@ -323,20 +450,8 @@ class UsageService:
|
|
|
323
450
|
deck_event_users = {
|
|
324
451
|
e.username for e in events if e.event_type == EVENT_DECK_CREATED
|
|
325
452
|
}
|
|
326
|
-
deck_session_users = {
|
|
327
|
-
|
|
328
|
-
for (name,) in db.query(UserSession.created_by)
|
|
329
|
-
.join(SessionSlideDeck, SessionSlideDeck.session_id == UserSession.id)
|
|
330
|
-
.filter(
|
|
331
|
-
SessionSlideDeck.created_at >= start,
|
|
332
|
-
UserSession.created_by.isnot(None),
|
|
333
|
-
)
|
|
334
|
-
}
|
|
335
|
-
decks_created = (
|
|
336
|
-
db.query(SessionSlideDeck)
|
|
337
|
-
.filter(SessionSlideDeck.created_at >= start)
|
|
338
|
-
.count()
|
|
339
|
-
)
|
|
453
|
+
deck_session_users = {name for (name,) in self._deck_creator_rows(db, w)}
|
|
454
|
+
decks_created = len(self._decks_in_window(db, w))
|
|
340
455
|
return {
|
|
341
456
|
"logins": logins,
|
|
342
457
|
"users_who_logged_in": len(logged_in_users),
|
|
@@ -388,13 +503,20 @@ class UsageService:
|
|
|
388
503
|
)
|
|
389
504
|
return rows
|
|
390
505
|
|
|
391
|
-
def get_heatmap(
|
|
392
|
-
|
|
506
|
+
def get_heatmap(
|
|
507
|
+
self,
|
|
508
|
+
db: Session,
|
|
509
|
+
days: int | None = None,
|
|
510
|
+
start: dt_date | None = None,
|
|
511
|
+
end: dt_date | None = None,
|
|
512
|
+
all_data: bool = False,
|
|
513
|
+
) -> dict:
|
|
514
|
+
w = resolve_window(days=days, start=start, end=end, all_data=all_data)
|
|
393
515
|
matrix = [[0] * 24 for _ in range(7)]
|
|
394
516
|
|
|
395
|
-
for e in self._events_in_window(db,
|
|
517
|
+
for e in self._events_in_window(db, w):
|
|
396
518
|
matrix[e.ts.weekday()][e.ts.hour] += 1
|
|
397
|
-
for s in self._sessions_in_window(db,
|
|
519
|
+
for s in self._sessions_in_window(db, w):
|
|
398
520
|
matrix[s.created_at.weekday()][s.created_at.hour] += 1
|
|
399
521
|
|
|
400
522
|
return {"matrix": matrix, "max": max(max(row) for row in matrix)}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"""Admin usage-analytics endpoints (ungated, like the rest of /admin)."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
|
-
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
6
|
-
from sqlalchemy.orm import Session
|
|
7
|
-
|
|
8
|
-
from src.api.services.usage_service import ALLOWED_WINDOWS, UsageService
|
|
9
|
-
from src.core.database import get_db
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
router = APIRouter(prefix="/api/admin/usage", tags=["admin-usage"])
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _validated_days(days: int) -> int:
|
|
16
|
-
if days not in ALLOWED_WINDOWS:
|
|
17
|
-
raise HTTPException(
|
|
18
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
19
|
-
detail=f"days must be one of {sorted(ALLOWED_WINDOWS)}",
|
|
20
|
-
)
|
|
21
|
-
return days
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _handle(callable_, *args, **kwargs):
|
|
25
|
-
try:
|
|
26
|
-
return callable_(*args, **kwargs)
|
|
27
|
-
except HTTPException:
|
|
28
|
-
raise
|
|
29
|
-
except Exception as e:
|
|
30
|
-
logger.error(f"Usage analytics error: {e}", exc_info=True)
|
|
31
|
-
raise HTTPException(
|
|
32
|
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
33
|
-
detail="Failed to compute usage analytics",
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@router.get("/summary")
|
|
38
|
-
def get_summary(days: int = Query(default=7), db: Session = Depends(get_db)):
|
|
39
|
-
days = _validated_days(days)
|
|
40
|
-
return _handle(UsageService().get_summary, db, days=days)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@router.get("/daily")
|
|
44
|
-
def get_daily(days: int = Query(default=7), db: Session = Depends(get_db)):
|
|
45
|
-
days = _validated_days(days)
|
|
46
|
-
return _handle(UsageService().get_daily, db, days=days)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@router.get("/top-users")
|
|
50
|
-
def get_top_users(days: int = Query(default=7), db: Session = Depends(get_db)):
|
|
51
|
-
days = _validated_days(days)
|
|
52
|
-
return _handle(UsageService().get_top_users, db, days=days)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@router.get("/funnel")
|
|
56
|
-
def get_funnel(days: int = Query(default=7), db: Session = Depends(get_db)):
|
|
57
|
-
days = _validated_days(days)
|
|
58
|
-
return _handle(UsageService().get_funnel, db, days=days)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@router.get("/retention")
|
|
62
|
-
def get_retention(db: Session = Depends(get_db)):
|
|
63
|
-
return _handle(UsageService().get_retention, db)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@router.get("/heatmap")
|
|
67
|
-
def get_heatmap(days: int = Query(default=7), db: Session = Depends(get_db)):
|
|
68
|
-
days = _validated_days(days)
|
|
69
|
-
return _handle(UsageService().get_heatmap, db, days=days)
|
|
File without changes
|
|
File without changes
|
{databricks_tellr_app-0.3.11.dev8 → databricks_tellr_app-0.3.11.dev9}/databricks_tellr_app/run.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|