mirror of
https://github.com/Sea-Haven-Industries/apm-wo-analysis.git
synced 2026-09-30 04:13:13 +00:00
Two issues only a real deploy/run surfaced (synth + offline tests passed): 1. Classifier exceeded Lambda's 250 MB unzipped limit (bundled awswrangler + pandas + pyarrow + numpy). Move them to the AWS-managed SDK-for-pandas layer (AWSSDKPandas-Python312-Arm64:27, awswrangler 3.16.1, pre-stripped to fit); bundle only openpyxl. Drop the unused anthropic SDK — _call_haiku uses stdlib urllib. Function package now ~890 KB. 2. Slack rejected the daily post with invalid_blocks: every category drill button shared action_id "drill_category". Qualify it as "drill_category:<cat>" for uniqueness; the interactions handler now matches on the prefix. Add a regression test asserting all daily-summary action_ids are unique. Verified in prod: classifier writes Parquet + summary.json + details.json; slack-post posts the daily summary + 3rd-escalation alert; the interactions endpoint (apm-wo.seahaven.com) returns 401 on a bad signature. 58/58 tests pass. NOTE: these fixes sit on the phase-5 branch but logically belong to earlier phases — the layer fix to #8 (classifier), the Slack fix to #10 — and must be moved/cherry-picked there before those PRs merge independently. See cleanup.
72 lines
2.7 KiB
Python
72 lines
2.7 KiB
Python
"""Slack interactivity Lambda: drill-down modals behind API Gateway.
|
|
|
|
Fronted by the ``apm-wo.seahaven.com`` HTTP API (Slack interactivity request URL).
|
|
Verifies the Slack request signature, then for a category/site drill button reads
|
|
the day's ``details.json``, filters it, and opens a WO-list modal via ``views.open``
|
|
within Slack's 3-second ``trigger_id`` window. WO rows link to Grafana only — no
|
|
APM deep-links (see Phase 4 decisions).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from urllib.parse import parse_qs
|
|
|
|
import blockkit
|
|
import slackio
|
|
|
|
_FILTER_FIELD = {"drill_category": "category", "drill_site": "site"}
|
|
|
|
|
|
def _raw_body(event: dict) -> str:
|
|
body = event.get("body") or ""
|
|
if event.get("isBase64Encoded"):
|
|
body = base64.b64decode(body).decode("utf-8")
|
|
return body
|
|
|
|
|
|
def _headers(event: dict) -> dict:
|
|
# API Gateway HTTP API lowercases header names; be defensive either way.
|
|
return {k.lower(): v for k, v in (event.get("headers") or {}).items()}
|
|
|
|
|
|
def handler(event, context):
|
|
"""Verify the Slack signature and open the requested drill-down modal."""
|
|
body = _raw_body(event)
|
|
headers = _headers(event)
|
|
timestamp = headers.get("x-slack-request-timestamp", "")
|
|
signature = headers.get("x-slack-signature", "")
|
|
|
|
if not slackio.verify_signature(body, timestamp, signature):
|
|
print("Rejected: invalid Slack signature.")
|
|
return {"statusCode": 401, "body": "invalid signature"}
|
|
|
|
parsed = parse_qs(body)
|
|
payload = json.loads(parsed.get("payload", ["{}"])[0])
|
|
if payload.get("type") != "block_actions":
|
|
return {"statusCode": 200, "body": ""}
|
|
|
|
action = (payload.get("actions") or [{}])[0]
|
|
# action_id is qualified for Slack uniqueness, e.g. "drill_category:Report /
|
|
# Docs Needed" — match on the prefix before ":"; the filter value is in `value`.
|
|
action_kind = (action.get("action_id") or "").split(":", 1)[0]
|
|
field = _FILTER_FIELD.get(action_kind)
|
|
value = action.get("value")
|
|
trigger_id = payload.get("trigger_id")
|
|
if not field or not value or not trigger_id:
|
|
return {"statusCode": 200, "body": ""}
|
|
|
|
# The daily post is same-day; default the drill to today's snapshot.
|
|
dt = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
details = slackio.read_analytics_json(dt, "details.json") or []
|
|
wos = [d for d in details if d.get(field) == value]
|
|
|
|
view = blockkit.build_wo_modal(
|
|
title=f"{value} ({len(wos)})",
|
|
wos=wos,
|
|
dashboard_url=slackio.get_dashboard_url(),
|
|
)
|
|
slackio.web_client().views_open(trigger_id=trigger_id, view=view)
|
|
return {"statusCode": 200, "body": ""}
|