make_post_sell/docs/tickets/mps-3.md
russell@unturf.com fca2f4a051 Isolate parallel test runs with PID-based database filenames
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.
2026-02-09 16:24:25 -05:00

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 > 0
  • media_speed < 1.0 (and not NULL)
  • scroll_direction_changes > 3
  • media_pause_count > 2
  • active_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.7
  • active_ms / wall_clock_ms < 0.3
  • media_percent_played > 0.69
  • media_play_count = 1
  • media_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)