import base64 import logging import json import boto3 from datetime import datetime, timezone HOOKS_PLAY_DURATION_NAME = "HooksPlayDuration" # Android events are currently lowerCamelCase and iOS events are UpperCamelCase. # TODO - Shorten list once we standardize the action names. HOOK_EVENT_ACTION_NAME_ALLOWLIST = [ "PlayNewHook", "playNewHook", "PlayHook", "playHook", "PauseHook", "pauseHook", "AutoRepeatPlaySameHook", "autoRepeatPlaySameHook", "ScrollUpPauseHook", "scrollUpPauseHook", "ScrollDownPauseHook", "scrollDownPauseHook", "ScrollUpPlayNewHook", "scrollUpPlayNewHook", "ScrollDownPlayNewHook", "scrollDownPlayNewHook", "OpenOmniPlayerPauseHook", "openOmniPlayerPauseHook", "CloseOmniPlayerPlayHook", "closeOmniPlayerPlayHook", "OpenDeeplinkPlayNewHook", "OpenDeeplinkPauseHook", "OpenNotificationPlayNewHook", "OpenNotificationPauseHook", "TapSongPillPauseHook", "TapSongPillPlayNewSong", "CloseOmniPlayerPauseSong", "TapCreateHookPauseHook", "CloseCreateHookPlayHook", "TapTabPlayHook", "TapTabPauseHook", "TapCreateSongPauseHook", "CloseCreateSongPlayHook", "TapBackPlayHook", "TapBackPauseHook", "TapProfilePauseHook", "ForegroundAppPlayHook", "BackgroundAppPauseHook", "CloseCoverPlayHook", "TapExtendPauseHook", "CloseExtendPlayHook", "TapCoverPauseHook", ] RECS_EVENTS_KINESIS_CLIENT = boto3.client( "kinesis", region_name="us-east-2", ) logger = logging.getLogger(__name__) 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 action_name = properties.get("actionName", "") if action_name not in HOOK_EVENT_ACTION_NAME_ALLOWLIST: return event_context = properties.get("context", {}) if not event_context: return # Whether event_context is a string or a dict depends # on how the client serialized the payload, so we need to # handle both cases. if isinstance(event_context, str): event_context_json = json.loads(event_context) else: event_context_json = event_context 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"] = HOOKS_PLAY_DURATION_NAME recs_event["source"] = "mobile" recs_event["user_id"] = user_id recs_event["timestamp"] = str(datetime.now(timezone.utc).isoformat()) play_duration = event_context_json.get("playDuration", 0) if play_duration == 0: return recs_event["properties"] = { "hook_id": hook_id, "hook_play_duration": play_duration, } logger.info(f"Logging hook play duration event: {recs_event}") print(f"Logging hook play duration 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()), )