survival_experiment_report.py 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. from __future__ import annotations
  2. import argparse
  3. import json
  4. from pathlib import Path
  5. from statistics import median
  6. from typing import Any
  7. def _bucket_key(member: dict[str, Any]) -> tuple[str, str, str]:
  8. return (
  9. str(member.get("registration_proxy_region") or "unknown").strip() or "unknown",
  10. str(member.get("registration_post_create_gate") or "none").strip() or "none",
  11. "consistent" if bool(member.get("fingerprint_consistent")) else "mismatch_or_unknown",
  12. )
  13. def _median_survival_seconds(members: list[dict[str, Any]]) -> float | None:
  14. values = [
  15. int(member.get("survival_seconds"))
  16. for member in members
  17. if member.get("survival_seconds") is not None
  18. ]
  19. if not values:
  20. return None
  21. return float(median(values))
  22. def build_report(payload: dict[str, Any]) -> dict[str, Any]:
  23. members = [item for item in payload.get("members") or [] if isinstance(item, dict)]
  24. buckets: dict[tuple[str, str, str], list[dict[str, Any]]] = {}
  25. for member in members:
  26. buckets.setdefault(_bucket_key(member), []).append(member)
  27. grouped: list[dict[str, Any]] = []
  28. for (proxy_region, post_create_gate, fingerprint_consistency), group_members in sorted(buckets.items()):
  29. invalid_members = [item for item in group_members if str(item.get("first_invalid_at") or "").strip()]
  30. grouped.append(
  31. {
  32. "registration_proxy_region": proxy_region,
  33. "registration_post_create_gate": post_create_gate,
  34. "fingerprint_consistency": fingerprint_consistency,
  35. "tracked": len(group_members),
  36. "invalid": len(invalid_members),
  37. "median_survival_seconds": _median_survival_seconds(invalid_members),
  38. }
  39. )
  40. return {
  41. "updated_at": payload.get("updated_at"),
  42. "probe_mode": payload.get("probe_mode"),
  43. "probe_fingerprint_profile": payload.get("probe_fingerprint_profile"),
  44. "summary": payload.get("summary") or {},
  45. "groups": grouped,
  46. }
  47. def main() -> int:
  48. parser = argparse.ArgumentParser(description="Summarize survival experiment buckets from tracker state")
  49. parser.add_argument(
  50. "--state-file",
  51. default="/home/sophomores/zhuce6/state/responses_survival_tracker.json",
  52. help="responses survival state file",
  53. )
  54. args = parser.parse_args()
  55. state_file = Path(args.state_file).expanduser().resolve()
  56. payload = json.loads(state_file.read_text(encoding="utf-8"))
  57. print(json.dumps(build_report(payload), ensure_ascii=False, indent=2))
  58. return 0
  59. if __name__ == "__main__":
  60. raise SystemExit(main())