"""Summarise measure.cjs output: one row per game. python3 analyze.py raw_dir/ out.csv [entities.json] Standard library only. Domains are reduced to their registrable domain (eTLD+1) with the Public Suffix List in ref/. A request is FIRST PARTY when its registrable domain belongs to the same organisation as the page itself: the page's own domain, plus any domain the optional entity map assigns to the same owner (so poki-cdn.com counts as Poki). Everything else is THIRD PARTY. entities.json (optional) maps registrable domain -> {"owner": ..., "categories": [...]}. Without it, ownership falls back to "same registrable domain" and nothing is labelled advertising or tracking. """ import csv, json, os, sys, urllib.parse HERE = os.path.dirname(os.path.abspath(__file__)) def load_psl(path=os.path.join(HERE, "ref", "public_suffix_list.dat")): rules, exceptions, wild = set(), set(), set() for line in open(path, encoding="utf-8"): line = line.strip() if not line or line.startswith("//"): continue rule = line.split()[0].lower() if rule.startswith("!"): exceptions.add(rule[1:]) elif rule.startswith("*."): wild.add(rule[2:]) else: rules.add(rule) return rules, exceptions, wild PSL = load_psl() # Categories counted as advertising/tracking. The first four are third-party-web's # own category names (see CLASSIFICATION.md for why these and not the others); # the rest keep compatibility with Tracker-Radar-style labels. AD_TRACKING = {"ad", "analytics", "marketing", "tag-manager", "advertising", "ad motivated tracking", "audience measurement", "third-party analytics marketing", "action pixels", "ad fraud", "tracking"} def registrable(host): """eTLD+1 of a hostname, per the Public Suffix List.""" host = (host or "").lower().rstrip(".") if not host or host.replace(".", "").isdigit(): return host rules, exceptions, wild = PSL labels = host.split(".") for i in range(len(labels)): cand = ".".join(labels[i:]) if cand in exceptions: return ".".join(labels[i:]) parent = ".".join(labels[i + 1:]) if cand in rules or (parent in wild): return ".".join(labels[i - 1:]) if i > 0 else host return ".".join(labels[-2:]) def summarise(game, entities): page_reg = registrable(urllib.parse.urlsplit(game["url"]).hostname) owner_of = lambda reg: (entities.get(reg) or {}).get("owner") or reg page_owner = owner_of(page_reg) reqs = [r for r in game["requests"] if not r["url"].startswith(("data:", "blob:"))] third, ad_track = set(), set() third_owners = set() n_third = n_adtrack = 0 bytes_total = bytes_third = 0 for r in reqs: reg = registrable(urllib.parse.urlsplit(r["url"]).hostname) b = (r.get("body_bytes") or 0) + (r.get("header_bytes") or 0) bytes_total += b if owner_of(reg) == page_owner: continue third.add(reg) third_owners.add(owner_of(reg)) n_third += 1 bytes_third += b cats = {c.lower() for c in (entities.get(reg) or {}).get("categories", [])} if cats & AD_TRACKING: ad_track.add(reg) n_adtrack += 1 cookie_regs = {registrable(c["domain"].lstrip(".")) for c in game["cookies"]} third_cookie_regs = {c for c in cookie_regs if owner_of(c) != page_owner} return { "label": game["label"], "url": game["url"], "measured_at": game["measured_at"], "wait_s": game["wait_s"], "status": game["status"], "requests": len(reqs), "megabytes": round(bytes_total / 1e6, 2), "third_party_requests": n_third, "third_party_megabytes": round(bytes_third / 1e6, 2), "third_party_domains": len(third), "third_party_owners": len(third_owners), "ad_tracking_domains": len(ad_track) if entities else "", "ad_tracking_requests": n_adtrack if entities else "", "cookies": len(game["cookies"]), "cookie_domains": len(cookie_regs), "third_party_cookie_domains": len(third_cookie_regs), "error": game.get("error") or "", } def main(raw_dir, out_csv, entities_path=None): entities = json.load(open(entities_path)) if entities_path else {} rows = [] for name in sorted(os.listdir(raw_dir)): if name.endswith(".json"): rows.append(summarise(json.load(open(os.path.join(raw_dir, name))), entities)) with open(out_csv, "w", newline="") as fh: w = csv.DictWriter(fh, fieldnames=list(rows[0].keys())) w.writeheader() w.writerows(rows) for r in rows: print(f"{r['label']:34} {r['requests']:>6} req {r['megabytes']:>6} MB " f"{r['third_party_domains']:>4} 3p domains {r['third_party_cookie_domains']:>4} 3p cookie domains") if __name__ == "__main__": main(*sys.argv[1:])