When two processes run pytest simultaneously, they no longer share test_make_post_sell_master.sqlite. Each gets its own file keyed by PID, preventing table-exists and readonly-database errors. Also clean up WAL/SHM journal files on exit.
5.9 KiB
MPS-3: Creator Analytics Dashboard
Problem
MPS-2 collects anonymous signals and derives Engagement, Attention, Learning, and Passive Consumption scores. Creators need somewhere to see this data and act on it — know which content pulls people in, which holds them, and where traffic comes from so they know where to spend time and energy.
Solution
Add a server-rendered analytics page at /s/{shop_id}/analytics. No JS graph
libraries — CSS grid tables, server-computed aggregates, progressive
enhancement. Machine learning can refine the derived scores later without
changing the page structure.
Access
- Route:
GET /s/{shop_id}/analytics - Requires
shop_editor_required(same as shop settings) - Link from shop settings page (near existing nav)
Page Sections
1. Overview Strip (shop-wide, last 7 days)
A single row of key numbers at the top:
| Metric | Query |
|---|---|
| Total views | COUNT(*) WHERE visible_ms >= 7000 AND created_timestamp > 7d ago |
| Unique products viewed | COUNT(DISTINCT product_id) WHERE visible_ms >= 7000 |
| Avg session duration | AVG(wall_clock_ms) |
| Avg ring depth | AVG(ring_position) WHERE ring_position IS NOT NULL |
| Top traffic source | MODE(referrer_class) (show label: direct/search/social/internal) |
| Device split | % per device_class shown as "42% mobile · 7% tablet · 51% desktop" |
2. Top Products by Views (last 7 / 14 / 21 days)
Ranked table, top 21 products:
| # | Title | Views (7d) | Views (14d) | Views (21d) | Trend |
|---|
"Trend" = simple arrow: views(7d) > views(14d)/2 → rising, else falling. Each title links to the product page.
Query: GROUP BY product_id, COUNT(*) WHERE visible_ms >= 7000, partitioned
by time windows using created_timestamp.
3. Ring Entry Points (top 7 front doors)
Which products do people land on first?
| # | Title | Ring Entries (21d) | % of All Entries |
|---|
Query: COUNT(*) WHERE is_ring_entry = true GROUP BY product_id ORDER BY count DESC LIMIT 7.
This tells the creator: "People find your shop through these 7 products — make sure they're polished."
4. Engagement & Attention Leaders (top 7 each)
Two side-by-side tables:
Engagement Leaders (highest lean-in):
| # | Title | Avg Engagement | Sessions |
|---|
engagement = active_ms / wall_clock_ms — computed per session, averaged per
product. Only include sessions with wall_clock_ms >= 7000 (real visits).
Attention Holders (highest focused presence):
| # | Title | Avg Attention | Sessions |
|---|
attention = visible_ms / wall_clock_ms — same filtering.
5. Study Material vs Background Favorites (top 7 each)
Two side-by-side tables:
Study Material (people rewind, slow down, re-read):
| # | Title | Learning Score | Sessions |
|---|
Learning score per session = count of true indicators:
media_seek_back_count > 0media_speed < 1.0(and not NULL)scroll_direction_changes > 3media_pause_count > 2active_ms / wall_clock_ms > 0.7
Average per product, ranked. Minimum 7 sessions to qualify.
Background Favorites (lean-back plays):
| # | Title | Passive Score | Sessions |
|---|
Passive score per session = count of true indicators:
visible_ms / wall_clock_ms > 0.7active_ms / wall_clock_ms < 0.3media_percent_played > 0.69media_play_count = 1media_pause_count = 0
Average per product, ranked. Minimum 7 sessions to qualify.
6. Traffic Sources (last 21 days)
Simple breakdown table:
| Source | Sessions | % |
|---|---|---|
| Direct | 142 | 42% |
| Search | 69 | 21% |
| Social | 47 | 14% |
| Internal | 70 | 21% |
| Unknown | 7 | 2% |
Query: COUNT(*) GROUP BY referrer_class.
7. Device Split (last 21 days)
| Device | Sessions | % |
|---|---|---|
| Mobile | 210 | 42% |
| Tablet | 42 | 8% |
| Desktop | 252 | 50% |
Query: COUNT(*) GROUP BY device_class.
Query Strategy
All queries run against mps_page_session with time filters using
created_timestamp. Since timestamps are milliseconds:
cutoff_7d = now_timestamp() - (7 * 24 * 60 * 60 * 1000)
cutoff_14d = now_timestamp() - (14 * 24 * 60 * 60 * 1000)
cutoff_21d = now_timestamp() - (21 * 24 * 60 * 60 * 1000)
For the engagement/attention/learning/passive scores, compute per-session in the SQL query using CASE expressions, then AVG per product. SQLite handles this fine for shops with < 100K sessions.
For larger shops (future), the 90-day retention + daily rollup from MPS-2 provides pre-aggregated data.
Template Layout
CSS grid, two-column on desktop, single-column on mobile. No flexbox per project rules.
[Overview Strip — full width]
[Top Products — full width]
[Ring Entry Points — full width]
[Engagement Leaders | Attention Holders — side by side]
[Study Material | Background Favorites — side by side]
[Traffic Sources | Device Split — side by side]
Tables use <table> with class="analytics-table". No zebra striping —
keep it clean. Product titles are links. Numbers right-aligned.
Privacy Note
Displayed in a small footer on the page:
All data is anonymous. No individual viewer can be identified. Counts represent aggregate sessions, not people.
Files Changed
| File | Change |
|---|---|
views/shop.py |
New analytics view with aggregate queries |
routes.py |
New route shop_analytics |
templates/analytics.j2 |
New template with 7 sections |
static/css/common.css |
.analytics-table, .analytics-overview styles |
templates/shop_settings.j2 |
Link to analytics page |
tests/test_functional.py |
Analytics page access + permission tests |
tests/test_models.py |
Score computation unit tests |
Depends On
MPS-2 (signal gathering + mps_page_session table + view_count column)