import argparse import os import json import requests import time import sys import pandas as pd from config import ( SPLITIO_ADMIN_API_KEY, SPLITIO_BASE_URL, SPLITIO_HEADERS, SPLITIO_WORKSPACE_ID, FF_SAFE_TO_DELETE_XLSX, FEATURE_FLAGS_JSON, FEATURE_FLAGS_XLSX, USERS_JSON, GROUPS_JSON, GITHUB_CACHE_FILE, GITHUB_HEADERS, GITHUB_ORG ) def get_paginated_results(url): """Helper to paginate through Split API results.""" results = [] offset = 0 limit = 50 while True: paged_url = f"{url}?offset={offset}&limit={limit}" response = requests.get(paged_url, headers=SPLITIO_HEADERS) if response.status_code != 200: raise Exception(f"Failed to fetch: {paged_url} - {response.status_code}\n{response.text}") data = response.json() batch = data.get("objects", []) results.extend(batch) if len(batch) < limit: break offset += limit return results def get_all_users_with_markers(): """Fetch all users using marker-based pagination.""" users = [] after = None limit = 200 i = 1 while True: params = { "limit": limit, } if after: params["after"] = after response = requests.get(f"{SPLITIO_BASE_URL}/users", headers=SPLITIO_HEADERS, params=params) if response.status_code != 200: raise Exception(f"Failed to fetch users: {response.status_code}\n{response.text}") data = response.json() batch = data.get("data", []) users.extend(batch) after = data.get("nextMarker") if not after: break return users def save_json(data, filename): with open(filename, "w") as f: json.dump(data, f, indent=2) def load_json(filename): if os.path.exists(filename): with open(filename, "r") as f: return json.load(f) return {} def get_owner_user_names(owners, users_by_id): return [ users_by_id[owner["id"]]["name"] for owner in owners if owner["type"] == "user" and owner["id"] in users_by_id ] def get_owner_group_names(owners, groups_by_id): return [ groups_by_id[owner["id"]]["name"] for owner in owners if owner["type"] == "group" and owner["id"] in groups_by_id ] def export_flags_to_excel(flags, users, groups, output_path=FEATURE_FLAGS_XLSX): users_by_id = {u["id"]: u for u in users} groups_by_id = {g["id"]: g for g in groups} flag_rows = [] for flag in flags: tags = flag.get("tags") or [] tag_str = ", ".join(tag["name"] for tag in tags if tag and "name" in tag) owners = flag.get("owners", []) user_names = get_owner_user_names(owners, users_by_id) group_names = get_owner_group_names(owners, groups_by_id) creation_time = "" if flag.get("creationTime"): try: creation_time = pd.to_datetime(flag["creationTime"], unit="ms").strftime("%Y-%m-%d %H:%M:%S") except Exception: creation_time = flag["creationTime"] flag_rows.append({ "Name": flag.get("name"), "Description": flag.get("description"), "Traffic Type": flag.get("trafficType", {}).get("name"), "Rollout Status": flag.get("rolloutStatus", {}).get("name"), "Creation Time": creation_time, "Owners": ", ".join(user_names), "Groups": ", ".join(group_names), "Tags": tag_str, }) flags_df = pd.DataFrame(flag_rows) users_df = pd.DataFrame(users)[["name", "email", "status", "2fa", "id"]] groups_df = pd.DataFrame(groups)[["name", "description", "id"]] with pd.ExcelWriter(output_path, engine='openpyxl') as writer: flags_df.to_excel(writer, sheet_name="Feature Flags", index=False) users_df.to_excel(writer, sheet_name="Users", index=False) groups_df.to_excel(writer, sheet_name="Groups", index=False) print(f"Exported data to {output_path} with 3 sheets.") def get_github_references_for_flags(flags, delay=2): session = requests.Session() session.headers.update(GITHUB_HEADERS) cache = load_json(GITHUB_CACHE_FILE) results = cache.copy() for flag in flags: flag_name = flag.get("name") if not flag_name or flag_name in results: continue query = f'{flag_name}+in:file+org:{GITHUB_ORG}' url = f'https://api.github.com/search/code?q={query}' print(f"Searching GitHub for flag: {flag_name}") response = session.get(url) # Retry once if rate limited if response.status_code == 403: print("Rate limit hit. Sleeping for 60s...") time.sleep(60) response = session.get(url) if response.status_code != 200: print(f"Failed to search GitHub for {flag_name}: {response.status_code}") continue data = response.json() repos = {item["repository"]["name"] for item in data.get("items", [])} results[flag_name] = list(repos) save_json(results, GITHUB_CACHE_FILE) time.sleep(delay) # Prevent rate limit return results def export_safe_to_delete_flags(flags, users, groups, github_refs=None, output_path=FF_SAFE_TO_DELETE_XLSX): users_by_id = {u["id"]: u for u in users} groups_by_id = {g["id"]: g for g in groups} rows = [] for flag in flags: name = flag.get("name") if not name: continue tags = flag.get("tags") or [] tag_str = ", ".join(tag["name"] for tag in tags if tag and "name" in tag) owners = flag.get("owners", []) user_names = get_owner_user_names(owners, users_by_id) group_names = get_owner_group_names(owners, groups_by_id) creation_time = "" if flag.get("creationTime"): try: creation_time = pd.to_datetime(flag["creationTime"], unit="ms").strftime("%Y-%m-%d %H:%M:%S") except Exception: creation_time = flag["creationTime"] referenced_repos = github_refs.get(name, []) if github_refs else [] ref_str = ", ".join(referenced_repos) can_be_deleted = "Yes" if not referenced_repos else "No" rows.append({ "Name": name, "Description": flag.get("description"), "Traffic Type": flag.get("trafficType", {}).get("name"), "Rollout Status": flag.get("rolloutStatus", {}).get("name"), "Creation Time": creation_time, "Owners": ", ".join(user_names), "Groups": ", ".join(group_names), "Tags": tag_str, "Referenced In": ref_str, "Can Be Safely Deleted": can_be_deleted, }) df = pd.DataFrame(rows) with pd.ExcelWriter(output_path, engine='openpyxl') as writer: df.to_excel(writer, sheet_name="Safe to Delete", index=False) print(f"Exported {len(df)} deletable flags to {output_path}") def main(fetch_fresh=False, check_github=False): # Clean old output files files_to_cleanup = [FEATURE_FLAGS_XLSX, FF_SAFE_TO_DELETE_XLSX] if fetch_fresh: files_to_cleanup.extend([GITHUB_CACHE_FILE, FEATURE_FLAGS_JSON, USERS_JSON, GROUPS_JSON]) for file in files_to_cleanup: if os.path.exists(file): os.remove(file) print(f"🧹 Deleted old file: {file}") # Ensure cache files are available or fetched def ensure_json(path, fetch_fn, label): if fetch_fresh or not os.path.exists(path): if not fetch_fresh: print(f"⚠️ {label} cache missing. Fetching fresh...") fetch_fn() ensure_json(FEATURE_FLAGS_JSON, lambda: save_json(get_paginated_results(f"{SPLITIO_BASE_URL}/splits/ws/{SPLITIO_WORKSPACE_ID}"), FEATURE_FLAGS_JSON), "Feature Flags") ensure_json(USERS_JSON, lambda: save_json(get_all_users_with_markers(), USERS_JSON), "Users") ensure_json(GROUPS_JSON, lambda: save_json(get_paginated_results(f"{SPLITIO_BASE_URL}/groups"), GROUPS_JSON), "Groups") flags = load_json(FEATURE_FLAGS_JSON) users = load_json(USERS_JSON) groups = load_json(GROUPS_JSON) print("📤 Exporting Excel...") export_flags_to_excel(flags, users, groups) if check_github: ensure_json(GITHUB_CACHE_FILE, save_json(get_github_references_for_flags(flags), GITHUB_CACHE_FILE), "GitHub references") github_refs = load_json(GITHUB_CACHE_FILE) export_safe_to_delete_flags(flags, users, groups, github_refs) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Split.io FF Export Tool") parser.add_argument( "--fetch-fresh", action="store_true", help="Fetch latest data from Split.io and GitHub APIs" ) parser.add_argument( "--check-github", action="store_true", help="Search GitHub org for references to each feature flag" ) args = parser.parse_args() if not SPLITIO_ADMIN_API_KEY or not SPLITIO_WORKSPACE_ID: print("❌ Please set SPLIT_ADMIN_API_KEY and WORKSPACE_ID in .env or environment variables.") sys.exit(1) main(fetch_fresh=args.fetch_fresh, check_github=args.check_github)