import base64 import logging import json import boto3 from datetime import datetime, timezone # Android Client event names ANDROID_CLIENT_EVENT_OPEN_OMNIPLAYER_NAME = "openOmniplayerPauseHook" ANDROID_CLIENT_EVENT_HOOK_REPEAT_WATCH_NAME = "autoRepeatPlayHook" # iOS Client event names IOS_CLIENT_EVENT_OPEN_OMNIPLAYER_NAME = "TapSongPillPauseHook" IOS_CLIENT_EVENT_HOOK_REPEAT_WATCH_NAME = "AutoRepeatPlaySameHook" # Events that are the same on Android and iOS CLIENT_EVENT_REMIX_TAPPED_NAME = "remix_tapped" # Names of the event we log to rec-events-stream REC_EVENT_STREAM_HOOKS_OPEN_OMNIPLAYER_NAME = "HooksOpenOmniPlayer" REC_EVENT_STREAM_HOOKS_TAP_REMIX_NAME = "HooksTapRemix" REC_EVENT_STREAM_HOOKS_REPEAT_WATCH_NAME = "HooksRepeatWatch" # List of events we know unambiguously are hook events UNAMBIGUOUS_HOOK_EVENTS = [ # Open Omniplayer related events IOS_CLIENT_EVENT_OPEN_OMNIPLAYER_NAME, # iOS ANDROID_CLIENT_EVENT_OPEN_OMNIPLAYER_NAME, # Android # Repeat watch events IOS_CLIENT_EVENT_HOOK_REPEAT_WATCH_NAME, # iOS ANDROID_CLIENT_EVENT_HOOK_REPEAT_WATCH_NAME, # Android ] CLIENT_EVENT_NAME_TO_REC_EVENT_NAME = { # Open Omniplayer events IOS_CLIENT_EVENT_OPEN_OMNIPLAYER_NAME: REC_EVENT_STREAM_HOOKS_OPEN_OMNIPLAYER_NAME, ANDROID_CLIENT_EVENT_OPEN_OMNIPLAYER_NAME: REC_EVENT_STREAM_HOOKS_OPEN_OMNIPLAYER_NAME, # Tap remix event CLIENT_EVENT_REMIX_TAPPED_NAME: REC_EVENT_STREAM_HOOKS_TAP_REMIX_NAME, # Repeat watch events IOS_CLIENT_EVENT_HOOK_REPEAT_WATCH_NAME: REC_EVENT_STREAM_HOOKS_REPEAT_WATCH_NAME, ANDROID_CLIENT_EVENT_HOOK_REPEAT_WATCH_NAME: REC_EVENT_STREAM_HOOKS_REPEAT_WATCH_NAME, } RECS_EVENTS_KINESIS_CLIENT = boto3.client( "kinesis", region_name="us-east-2", ) logger = logging.getLogger(__name__) def _is_hook_event(properties): action_name = properties.get("actionName", "") # We want to guard against the case where the client has not # set the elementType properly. If we know the event name corresponds # to a hook event, just return True. if action_name in UNAMBIGUOUS_HOOK_EVENTS: return True # Otherwise, see if the client has set the elementType properly. elementType = properties.get("elementType", "") return elementType == "hook" def lambda_handler(event, context): """ Lambda handler to check if an event name is in hook_events list and log to RECS_EVENTS_KINESIS stream """ try: if "Records" not in event: return for record in event["Records"]: process_record(record) return {"statusCode": 200, "body": json.dumps("Success")} except Exception as e: print(f"Error in lambda_handler: {str(e)}") return {"statusCode": 500, "body": json.dumps(f"Error: {str(e)}")} def process_record(record): """ Process a single record and check if it should be logged to RECS_EVENTS_KINESIS Unlike the web hook events, all mobile events have the event name 'App-Event', so we can't filter out there. We can just filter out events if they have a hook ID. """ if "kinesis" not in record: return kinesis = record["kinesis"] if "data" not in kinesis: return payload = base64.b64decode(record["kinesis"]["data"]) payload_as_json = json.loads(payload) request_body = payload_as_json.get("request_body", "") if not request_body: return decoded_request_body = json.loads(base64.b64decode(request_body)) properties = decoded_request_body.get("properties", {}) if not properties: return if not _is_hook_event(properties): return action_name = properties.get("actionName", "") rec_event_name = CLIENT_EVENT_NAME_TO_REC_EVENT_NAME.get(action_name, "") if not rec_event_name: return hook_id = properties.get("elementId", "") if not hook_id: return user_id = properties.get("userId", "") if not user_id: return recs_event = {} recs_event["name"] = rec_event_name recs_event["source"] = "mobile" recs_event["user_id"] = user_id recs_event["timestamp"] = str(datetime.now(timezone.utc).isoformat()) recs_event["properties"] = { "hook_id": hook_id, } logger.info(f"Logging {rec_event_name} event: {recs_event}") RECS_EVENTS_KINESIS_CLIENT.put_record( StreamName="rec-events-stream", Data=json.dumps(recs_event), PartitionKey=str(datetime.now(timezone.utc).isoformat()), )