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.
This commit is contained in:
parent
e1a8b2f236
commit
fca2f4a051
10 changed files with 1085 additions and 34 deletions
|
|
@ -4,48 +4,192 @@
|
|||
|
||||
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.
|
||||
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 simple analytics page accessible from the shop dashboard. No graphs
|
||||
library — clean server-rendered HTML with CSS grid tables. Machine learning
|
||||
refinements to the derived scores can enhance this page later without changing
|
||||
its structure.
|
||||
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.
|
||||
|
||||
### Content
|
||||
## Access
|
||||
|
||||
- **Top products by views** — ranked list, last 7 / 14 / 21 days
|
||||
- **Ring entry points** — which products are "front doors" (top 7 by `is_ring_entry` count)
|
||||
- **Average ring depth** — how far viewers ride the ring (mean `ring_position`)
|
||||
- **Daily view totals** — last 21 days, per product and shop-wide
|
||||
- **Engagement leaders** — products with highest average engagement score
|
||||
- **Attention holders** — products with highest average attention score
|
||||
- **Study material** — products with highest learning signal (people rewind, slow down, re-read)
|
||||
- **Background favorites** — products with highest passive consumption score (lean-back plays)
|
||||
- **Traffic sources** — breakdown by `referrer_class` (direct / search / social / internal)
|
||||
- **Device split** — mobile vs tablet vs desktop percentages
|
||||
- Route: `GET /s/{shop_id}/analytics`
|
||||
- Requires `shop_editor_required` (same as shop settings)
|
||||
- Link from shop settings page (near existing nav)
|
||||
|
||||
### Access
|
||||
## Page Sections
|
||||
|
||||
New route `/shop/analytics` — only visible to shop owner/mods. Link from shop
|
||||
settings or dashboard nav.
|
||||
### 1. Overview Strip (shop-wide, last 7 days)
|
||||
|
||||
### Privacy
|
||||
A single row of key numbers at the top:
|
||||
|
||||
All data is anonymous aggregates computed from `mps_page_session` rows. No
|
||||
individual viewer data exists to display even if someone wanted to.
|
||||
| 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:
|
||||
|
||||
```python
|
||||
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 |
|
||||
| `templates/analytics.j2` | New template |
|
||||
| `static/css/common.css` | Analytics table styles |
|
||||
| `templates/snippets/shop_nav.j2` | Link to analytics (if exists) |
|
||||
| `tests/test_functional.py` | Analytics page access tests |
|
||||
| `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)
|
||||
MPS-2 (signal gathering + `mps_page_session` table + `view_count` column)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue