diff options
Diffstat (limited to 'scripts/course-insights.py')
| -rw-r--r-- | scripts/course-insights.py | 57 |
1 files changed, 57 insertions, 0 deletions
diff --git a/scripts/course-insights.py b/scripts/course-insights.py new file mode 100644 index 0000000..0ee891e --- /dev/null +++ b/scripts/course-insights.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Aggregate course events -> course/insights.json (counts + capped question text, no sids/IPs).""" +import json, os, glob, re +from collections import Counter +from datetime import datetime, timezone + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DATA = os.path.join(ROOT, 'data', 'course') +OUT = os.path.join(ROOT, 'course', 'insights.json') + +def norm_q(q): + q = re.sub(r'https?://\S+', '', q or '') + return re.sub(r'\s+', ' ', q).strip().lower()[:200] + +def main(): + opens, completes, fails, cmds, questions = Counter(), Counter(), Counter(), Counter(), Counter() + projects = Counter() + files = sorted(glob.glob(os.path.join(DATA, 'events-*.jsonl')))[-2:] + for path in files: + with open(path, encoding='utf-8', errors='ignore') as f: + for line in f: + try: e = json.loads(line) + except Exception: continue + t, d = e.get('type'), e.get('detail') or {} + lesson = (d.get('lesson') or '')[:40] + 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][:40] + if c: cmds[c] += 1 + elif t == 'question': + q = norm_q(d.get('q')) + if len(q) >= 8: questions[q] += 1 + elif t in ('project_open', 'project_view'): + p = (d.get('project') or '')[:40] + if p: projects[p] += 1 + lessons = {} + for lid in set(opens) | set(completes): + o, c = opens[lid], completes[lid] + lessons[lid] = {'opens': o, 'completes': c, 'failures': fails.get(lid, 0), + 'completion_rate': round(c / o, 2) if o else 0.0} + payload = {'generated': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), + 'window_files': [os.path.basename(p) for p in files], + 'lessons': dict(sorted(lessons.items())), + 'top_failed_commands': [{'cmd': c, 'n': n} for c, n in cmds.most_common(20)], + 'top_questions': [{'q': q, 'n': n} for q, n in questions.most_common(20) if n >= 2], + 'projects': dict(projects), + 'totals': {'opens': sum(opens.values()), 'completes': sum(completes.values()), 'failures': sum(fails.values())}} + os.makedirs(os.path.dirname(OUT), exist_ok=True) + tmp = OUT + '.tmp' + with open(tmp, 'w', encoding='utf-8') as f: json.dump(payload, f, ensure_ascii=False, indent=1) + os.replace(tmp, OUT); os.chmod(OUT, 0o644) + print(json.dumps({'lessons': len(lessons), 'questions': len(payload['top_questions']), 'totals': payload['totals']})) + +if __name__ == '__main__': + main() |