#!/usr/bin/env python3 """Offline example validator. Does not send analytics or require credentials.""" import json,re,sys from pathlib import Path BASE=Path(__file__).resolve().parent EVENTS={'app_screen_ready','api_request_finished','client_error_displayed','support_entry_opened','app_recovery_completed'} ENUMS={'schema_version':{'1.0.0'},'platform':{'ios','android'},'app_build':{'qa_104','qa_105'},'feature_area':{'help','general','account_ui'},'result_class':{'success','timeout','error'},'duration_bucket_ms':{'lt_100','100_499','500_999','ge_1000'},'error_class':{'network','render','unknown'}} BASE_KEYS={'name','schema_version','platform','app_build','feature_area'} EXTRAS={'app_screen_ready':set(),'api_request_finished':{'result_class','duration_bucket_ms'},'client_error_displayed':{'error_class'},'support_entry_opened':set(),'app_recovery_completed':{'result_class'}} REQUIRED={'app_screen_ready':set(),'api_request_finished':{'result_class'},'client_error_displayed':{'error_class'},'support_entry_opened':set(),'app_recovery_completed':{'result_class'}} def check(e): if not isinstance(e,dict):return 'not a JSON object' name=e.get('name') if name not in EVENTS:return 'unknown event name' if not re.fullmatch(r'[a-z][a-z0-9_]{0,39}',name):return 'name format' expected=BASE_KEYS|EXTRAS[name] if set(e)-expected:return f'unknown properties: {sorted(set(e)-expected)}' missing=(BASE_KEYS|REQUIRED[name])-set(e) if missing:return f'missing fields: {sorted(missing)}' for k,v in e.items(): if not isinstance(v,str):return f'{k} must be string' if len(v)>100:return f'{k} over length' if k!='name' and v not in ENUMS[k]:return f'{k} invalid enum' return None def main(): path=Path(sys.argv[1]) if len(sys.argv)>1 else BASE/'synthetic-events.ndjson' errors=0;count=0 for i,line in enumerate(path.read_text(encoding='utf-8').splitlines(),1): if not line.strip():continue count+=1 try:reason=check(json.loads(line)) except Exception as e:reason=f'invalid JSON: {e}' if reason: print(f'FAIL line {i}: {reason}');errors+=1 print(f'{count-errors}/{count} synthetic events pass the local allowlist') return 1 if errors else 0 if __name__=='__main__':sys.exit(main())