aboutsummaryrefslogtreecommitdiff
path: root/scripts/course-weekly-report.py
blob: de1845771af9e3a621fac8f5f0df4db36c7b14d1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/usr/bin/env python3
"""Weekly newOS4 course telemetry report (last 7 days). No PII, aggregate only."""
import json, glob, os
from collections import Counter
from datetime import datetime, timezone, timedelta

ROOT = '/var/www/webx11.duckdns.org/spaces/newos4'
DATA = os.path.join(ROOT, 'data', 'course')
COURSE = os.path.join(ROOT, 'course')

def load(p, d):
    try:
        with open(p, encoding='utf-8') as f: return json.load(f)
    except Exception: return d

def main():
    cutoff = (datetime.now(timezone.utc) - timedelta(days=7)).strftime('%Y-%m-%dT%H:%M:%S')
    opens, completes, fails, questions, cmds = Counter(), Counter(), Counter(), Counter(), Counter()
    sessions = set(); projects = Counter(); total = 0
    for path in sorted(glob.glob(os.path.join(DATA, 'events-*.jsonl')))[-2:]:
        for line in open(path, encoding='utf-8', errors='ignore'):
            try: e = json.loads(line)
            except Exception: continue
            sid = e.get('sid') or ''
            if sid.startswith('test') or (e.get('ts') or '') < cutoff: continue
            total += 1; sessions.add(sid)
            t, d = e.get('type'), e.get('detail') or {}
            lesson = (d.get('lesson') or '')[:20]
            if t == 'lesson_open' and lesson: opens[lesson] += 1
            elif t == 'lesson_complete' and lesson: completes[lesson] += 1
            elif t == 'exercise_fail':
                if lesson: fails[lesson] += 1
                c = (d.get('command') or '').strip().split(' ')[0][:30]
                if c: cmds[c] += 1
            elif t == 'question':
                q = ' '.join((d.get('q') or '').lower().split())[:120]
                if len(q) >= 8: questions[q] += 1
            elif t in ('project_open', 'project_view') and d.get('project'):
                projects[d['project'][:30]] += 1

    feed = load(os.path.join(COURSE, 'feed.json'), {'items': []})
    week = (datetime.now(timezone.utc) - timedelta(days=7)).strftime('%Y-%m-%d')
    new_items = [i for i in feed.get('items', []) if (i.get('date') or '') >= week]
    lessons = []
    for lid in sorted(set(opens) | set(completes)):
        o, c = opens[lid], completes[lid]
        lessons.append((lid, o, c, round(c / o, 2) if o else 0.0))
    weak = [x for x in lessons if x[1] >= 3 and x[3] < 0.3]

    c_total = sum(completes.values())
    print('๐Ÿ“š newOS4 Course โ€” weekly report (' + datetime.now(timezone.utc).strftime('%Y-%m-%d') + ')')
    print('Sessions: ' + str(len(sessions)) + ' | events: ' + str(total) + ' | opens: ' + str(sum(opens.values())) + ' | completes: ' + str(c_total))
    rate = round(c_total / sum(opens.values()), 2) if sum(opens.values()) else 0.0
    print('Overall completion: ' + str(rate) + ' | exercise failures: ' + str(sum(fails.values())) + ' | questions: ' + str(sum(questions.values())))
    if lessons:
        print('Lessons: ' + ' ยท '.join(f'{l} {c}/{o}' for l, o, c, r in lessons[:10]))
    print('Projects: ' + (', '.join(f'{k} {v}' for k, v in projects.items()) or 'none'))
    print('Content flags: ' + (', '.join(f'{l} {r:.0%} of {o}' for l, o, c, r in weak) or 'none'))
    print('Top questions: ' + (' | '.join(f'"{q}" x{n}' for q, n in questions.most_common(3)) or 'none'))
    print('Failed commands: ' + (', '.join(f'{c} x{n}' for c, n in cmds.most_common(3)) or 'none'))
    print('Daily feed: ' + str(len(feed.get('items', []))) + '/60 (' + str(len(new_items)) + ' new this week)')
    if len(sessions) < 20:
        print('Gate: paid Pro track still OFF โ€” need >=20 sessions and >=35% completion through 3.1 over 14 days.')

if __name__ == '__main__':
    main()