Skip to content

Commit 9016c3c

Browse files
StuMasonclaude
andauthored
Feature/analytics engine (#9)
* feat: Add analytics baseline infrastructure (Sprint 1.1-1.6) - Add UserBaseline model with IQR-based anomaly detection - Add Alembic migration for user_baselines table and analytics indices - Implement BaselineService with calculations for: - HRV RMSSD (from nightly recharge) - Sleep score - Resting heart rate - Training load and ratio - Add baselines API endpoints: - GET /users/{id}/baselines - List all baselines - GET /users/{id}/baselines/{metric} - Get specific baseline - POST /users/{id}/baselines/calculate - Trigger calculation - GET /users/{id}/baselines/check/{metric}/{value} - Anomaly check - GET /users/{id}/analytics/status - Data readiness and feature unlocks Features available based on data days: - 7 days: Basic stats - 14 days: Trend analysis - 21 days: Personalized baselines - 30 days: Predictive models - 60 days: Advanced ML - 90 days: Long-term patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: Add baseline recalculation and comprehensive tests (Sprint 1.7-1.9) - Add automatic baseline recalculation after sync operations - Create test data seeding fixtures with 90 days of realistic data - Weekly HRV patterns (Monday dips, Friday peaks) - Sleep score variations - Training load periodization (4-week cycles) - Occasional anomalies (2% chance) - Add analytics fixtures for different data scenarios (90d, 21d, 7d, 3d) - Write 13 comprehensive tests covering: - Baseline status levels (ready/partial/insufficient) - IQR calculations and bounds - Anomaly detection (warning/critical thresholds) - Multi-metric baseline calculation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * style: Fix import sorting and formatting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Add missing type parameters for mypy strict mode - Add type params to ASGIConnection[Any, Any, Any, Any] - Add type params to Response[Any] - Add type params to Request[Any, Any, Any] - Import ASGIApp from litestar.types - Add type: ignore for scope comparison (litestar typing issue) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Update Claude Code Review workflow to use sticky comments - Enable use_sticky_comment for automatic PR comment posting - Remove gh pr comment from allowed tools (was causing shell escaping issues) - Add Write tool for flexibility - Update prompt to output review directly instead of posting manually The previous approach had Claude try to post via gh pr comment, but markdown with code blocks caused shell escaping failures. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Address code review findings for baselines API - Fix type safety in is_anomaly(): Remove type: ignore comments by checking all required values (q1, q3, iqr) before calculations - Add user_id format validation: Regex-based validation prevents injection attacks, limits to alphanumeric with _ and - only - Add float input validation: Reject NaN, inf, -inf values in check_anomaly endpoint to prevent edge case errors - Remove redundant quantile check: quantiles(n=4) always returns exactly [Q1, Q2, Q3], no length check needed - Document service-level API key authorization model: Clarify that user_id=None keys intentionally have admin access for SaaS backends Security improvements: - ValidationException for invalid user_id format (1-100 chars) - ValidationException for non-finite float values - Don't reveal all valid metric names in error messages Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e9b0f8e commit 9016c3c

File tree

17 files changed

+1825
-12
lines changed

17 files changed

+1825
-12
lines changed

.github/workflows/claude-code-review.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ jobs:
2626
uses: anthropics/claude-code-action@v1
2727
with:
2828
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
29+
use_sticky_comment: true
2930
claude_args: |
30-
--allowedTools "Bash(gh pr diff *),Bash(gh pr view *),Bash(gh pr comment *),Read,Glob,Grep"
31+
--allowedTools "Bash(gh pr diff *),Bash(gh pr view *),Read,Glob,Grep,Write"
3132
prompt: |
3233
Review PR #${{ github.event.pull_request.number }} thoroughly.
3334
@@ -40,4 +41,8 @@ jobs:
4041
4. **Style**: Consistency with codebase patterns?
4142
5. **Improvements**: Suggestions for better approaches?
4243
43-
Be specific. Reference line numbers. Post your review as a PR comment using `gh pr comment`.
44+
Be specific. Reference line numbers.
45+
46+
IMPORTANT: Do NOT try to post comments using `gh pr comment`. Instead, output your complete review directly as your final response. The GitHub Action will automatically post it as a PR comment.
47+
48+
Format your review with clear markdown sections and provide an overall recommendation (approve, request changes, or comment).
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""Add user_baselines table and analytics indices
2+
3+
Revision ID: d5e6f7g8h9i0
4+
Revises: c4d5e6f7g8h9
5+
Create Date: 2026-01-13
6+
7+
This migration adds:
8+
1. user_baselines table for storing computed personal baselines
9+
2. Composite indices on existing tables for efficient baseline calculations
10+
"""
11+
12+
from collections.abc import Sequence
13+
14+
import sqlalchemy as sa
15+
16+
from alembic import op
17+
18+
# revision identifiers, used by Alembic.
19+
revision: str = "d5e6f7g8h9i0"
20+
down_revision: str | Sequence[str] | None = "c4d5e6f7g8h9"
21+
branch_labels: str | Sequence[str] | None = None
22+
depends_on: str | Sequence[str] | None = None
23+
24+
25+
def upgrade() -> None:
26+
"""Upgrade schema."""
27+
# Create user_baselines table
28+
op.create_table(
29+
"user_baselines",
30+
sa.Column("id", sa.String(length=36), nullable=False),
31+
sa.Column("user_id", sa.String(length=255), nullable=False),
32+
sa.Column("metric_name", sa.String(length=50), nullable=False),
33+
# Baseline values
34+
sa.Column("baseline_value", sa.Float(), nullable=False),
35+
sa.Column("baseline_7d", sa.Float(), nullable=True),
36+
sa.Column("baseline_30d", sa.Float(), nullable=True),
37+
sa.Column("baseline_90d", sa.Float(), nullable=True),
38+
# Statistics for anomaly detection
39+
sa.Column("std_dev", sa.Float(), nullable=True),
40+
sa.Column("median_value", sa.Float(), nullable=True),
41+
sa.Column("q1", sa.Float(), nullable=True),
42+
sa.Column("q3", sa.Float(), nullable=True),
43+
sa.Column("min_value", sa.Float(), nullable=True),
44+
sa.Column("max_value", sa.Float(), nullable=True),
45+
# Data quality
46+
sa.Column("sample_count", sa.Integer(), nullable=False, server_default="0"),
47+
sa.Column(
48+
"status",
49+
sa.String(length=20),
50+
nullable=False,
51+
server_default="insufficient",
52+
),
53+
# Data range
54+
sa.Column("data_start_date", sa.Date(), nullable=True),
55+
sa.Column("data_end_date", sa.Date(), nullable=True),
56+
# Calculation metadata
57+
sa.Column("calculated_at", sa.DateTime(timezone=True), nullable=False),
58+
# Timestamps from TimestampMixin
59+
sa.Column(
60+
"created_at",
61+
sa.DateTime(timezone=True),
62+
server_default=sa.text("now()"),
63+
nullable=False,
64+
),
65+
sa.Column(
66+
"updated_at",
67+
sa.DateTime(timezone=True),
68+
server_default=sa.text("now()"),
69+
nullable=False,
70+
),
71+
# Constraints
72+
sa.PrimaryKeyConstraint("id"),
73+
sa.UniqueConstraint("user_id", "metric_name", name="uq_user_baseline"),
74+
comment="User-specific baseline calculations for analytics",
75+
)
76+
77+
# Indices for user_baselines
78+
op.create_index(op.f("ix_user_baselines_user_id"), "user_baselines", ["user_id"], unique=False)
79+
op.create_index(
80+
op.f("ix_user_baselines_metric_name"),
81+
"user_baselines",
82+
["metric_name"],
83+
unique=False,
84+
)
85+
op.create_index(op.f("ix_user_baselines_status"), "user_baselines", ["status"], unique=False)
86+
87+
# Composite indices on existing tables for efficient baseline calculations
88+
# These support queries like: WHERE user_id = ? AND date >= ? ORDER BY date
89+
op.create_index(
90+
"idx_nightly_recharge_user_date",
91+
"nightly_recharge",
92+
["user_id", "date"],
93+
unique=False,
94+
)
95+
op.create_index(
96+
"idx_sleep_user_date",
97+
"sleep",
98+
["user_id", "date"],
99+
unique=False,
100+
)
101+
op.create_index(
102+
"idx_activity_user_date",
103+
"activity",
104+
["user_id", "date"],
105+
unique=False,
106+
)
107+
op.create_index(
108+
"idx_cardio_load_user_date",
109+
"cardio_load",
110+
["user_id", "date"],
111+
unique=False,
112+
)
113+
op.create_index(
114+
"idx_continuous_hr_user_date",
115+
"continuous_heart_rate",
116+
["user_id", "date"],
117+
unique=False,
118+
)
119+
120+
121+
def downgrade() -> None:
122+
"""Downgrade schema."""
123+
# Drop composite indices from existing tables
124+
op.drop_index("idx_continuous_hr_user_date", table_name="continuous_heart_rate")
125+
op.drop_index("idx_cardio_load_user_date", table_name="cardio_load")
126+
op.drop_index("idx_activity_user_date", table_name="activity")
127+
op.drop_index("idx_sleep_user_date", table_name="sleep")
128+
op.drop_index("idx_nightly_recharge_user_date", table_name="nightly_recharge")
129+
130+
# Drop user_baselines indices
131+
op.drop_index(op.f("ix_user_baselines_status"), table_name="user_baselines")
132+
op.drop_index(op.f("ix_user_baselines_metric_name"), table_name="user_baselines")
133+
op.drop_index(op.f("ix_user_baselines_user_id"), table_name="user_baselines")
134+
135+
# Drop user_baselines table
136+
op.drop_table("user_baselines")

src/polar_flow_server/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""API routes."""
22

3+
from polar_flow_server.api.baselines import baselines_router
34
from polar_flow_server.api.data import data_router
45
from polar_flow_server.api.health import health_router
56
from polar_flow_server.api.keys import keys_router, oauth_router
@@ -12,6 +13,7 @@
1213
sleep_router,
1314
sync_router,
1415
data_router,
16+
baselines_router, # Analytics baselines
1517
oauth_router, # OAuth flow and code exchange
1618
keys_router, # Key management (regenerate, revoke, status)
1719
]

0 commit comments

Comments
 (0)