Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: better log in case of error

Hugo bd3b3f8a bdfc9b3d

+138 -22
+9
app/icons.ts
··· 6 6 // @ts-expect-error deep import 7 7 import ActivityIcon from "lucide-preact/icons/activity.js"; 8 8 // @ts-expect-error deep import 9 + import ChevronDownIcon from "lucide-preact/icons/chevron-down.js"; 10 + // @ts-expect-error deep import 11 + import ChevronRightIcon from "lucide-preact/icons/chevron-right.js"; 12 + // @ts-expect-error deep import 13 + import CircleAlertIcon from "lucide-preact/icons/circle-alert.js"; 14 + // @ts-expect-error deep import 9 15 import ArrowLeftIcon from "lucide-preact/icons/arrow-left.js"; 10 16 // @ts-expect-error deep import 11 17 import DatabaseIcon from "lucide-preact/icons/database.js"; ··· 41 47 42 48 export const Activity = cast(ActivityIcon); 43 49 export const ArrowLeft = cast(ArrowLeftIcon); 50 + export const ChevronDown = cast(ChevronDownIcon); 51 + export const ChevronRight = cast(ChevronRightIcon); 52 + export const CircleAlert = cast(CircleAlertIcon); 44 53 export const Database = cast(DatabaseIcon); 45 54 export const Eye = cast(EyeIcon); 46 55 export const FilePlus2 = cast(FilePlus2Icon);
+50
app/islands/DeliveryLog.css.ts
··· 122 122 borderBlockStart: `1px solid ${vars.color.borderSubtle}`, 123 123 }); 124 124 125 + export const timeCell = style({ 126 + display: "inline-flex", 127 + alignItems: "center", 128 + gap: space[2], 129 + }); 130 + 131 + export const chevronIcon = style({ 132 + color: vars.color.textMuted, 133 + display: "inline-flex", 134 + }); 135 + 136 + export const errorIcon = style({ 137 + color: vars.color.error, 138 + display: "inline-flex", 139 + }); 140 + 141 + export const clickableRow = style({ 142 + cursor: "pointer", 143 + ":hover": { 144 + backgroundColor: vars.color.surfaceHover, 145 + }, 146 + }); 147 + 148 + export const expandedRow = style({ 149 + backgroundColor: vars.color.surfaceHover, 150 + selectors: { 151 + "&:hover": { 152 + backgroundColor: vars.color.surfaceHover, 153 + }, 154 + }, 155 + }); 156 + 125 157 export const dryRunRow = style({ 126 158 fontStyle: "italic", 127 159 opacity: 0.75, 160 + }); 161 + 162 + export const detailRow = style({}); 163 + 164 + export const detailCell = style({ 165 + paddingBlockStart: 0, 166 + paddingBlockEnd: space[3], 167 + paddingInlineStart: space[8], 168 + paddingInlineEnd: space[4], 169 + fontSize: fontSize.sm, 170 + lineHeight: 1.5, 171 + wordBreak: "break-word", 172 + backgroundColor: vars.color.surfaceHover, 173 + borderBlockStart: "none", 174 + }); 175 + 176 + export const detailError = style({ 177 + color: vars.color.error, 128 178 }); 129 179 130 180 export const loadMoreWrapper = style({
+50 -13
app/islands/DeliveryLog.tsx
··· 1 1 import { useState, useCallback, useRef } from "hono/jsx"; 2 - import { Power, FlaskConical, Trash2, RefreshCw } from "../icons.js"; 2 + import { Power, FlaskConical, Trash2, RefreshCw, ChevronRight, ChevronDown, CircleAlert } from "../icons.js"; 3 3 import * as s from "./DeliveryLog.css.ts"; 4 4 import { variant as badgeVariant } from "../components/Badge/styles.css.ts"; 5 5 ··· 39 39 const [isActive, setIsActive] = useState(active); 40 40 const [isDryRun, setIsDryRun] = useState(dryRun); 41 41 const [logs, setLogs] = useState(initialLogs); 42 + const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set()); 42 43 const [loading, setLoading] = useState(false); 43 44 const [loadingMore, setLoadingMore] = useState(false); 44 45 const [hasMore, setHasMore] = useState(initialHasMore); ··· 189 190 <th class={s.th}>Action</th> 190 191 <th class={s.th}>Status</th> 191 192 <th class={s.th}>Attempt</th> 192 - <th class={s.th}>Message</th> 193 - <th class={s.th}>Error</th> 194 193 </tr> 195 194 </thead> 196 195 <tbody> 197 - {logs.map((log) => ( 198 - <tr key={log.id} class={log.dryRun ? s.dryRunRow : undefined}> 199 - <td class={s.td}>{new Date(log.createdAt).toLocaleString()}</td> 200 - <td class={s.td}>{log.actionIndex + 1}</td> 201 - <td class={s.td}>{log.dryRun ? "dry run" : (log.statusCode ?? "\u2014")}</td> 202 - <td class={s.td}>{log.attempt}</td> 203 - <td class={s.td}>{log.message || "\u2014"}</td> 204 - <td class={s.td}>{log.error || "\u2014"}</td> 205 - </tr> 206 - ))} 196 + {logs.map((log) => { 197 + const expanded = expandedIds.has(log.id); 198 + const hasDetails = !!(log.message || log.error); 199 + const toggleExpand = () => 200 + setExpandedIds((prev) => { 201 + const next = new Set(prev); 202 + if (next.has(log.id)) next.delete(log.id); 203 + else next.add(log.id); 204 + return next; 205 + }); 206 + return ( 207 + <> 208 + <tr 209 + key={log.id} 210 + class={`${log.dryRun ? s.dryRunRow : ""} ${hasDetails ? s.clickableRow : ""} ${expanded ? s.expandedRow : ""}`.trim() || undefined} 211 + onClick={hasDetails ? toggleExpand : undefined} 212 + > 213 + <td class={s.td}> 214 + <span class={s.timeCell}> 215 + {hasDetails && ( 216 + <span class={log.error ? s.errorIcon : s.chevronIcon}> 217 + {log.error ? ( 218 + <CircleAlert size={14} /> 219 + ) : expanded ? ( 220 + <ChevronDown size={14} /> 221 + ) : ( 222 + <ChevronRight size={14} /> 223 + )} 224 + </span> 225 + )} 226 + {new Date(log.createdAt).toLocaleString()} 227 + </span> 228 + </td> 229 + <td class={s.td}>{log.actionIndex + 1}</td> 230 + <td class={s.td}>{log.dryRun ? "dry run" : (log.statusCode ?? "\u2014")}</td> 231 + <td class={s.td}>{log.attempt}</td> 232 + </tr> 233 + {expanded && ( 234 + <tr key={`${log.id}-detail`} class={s.detailRow}> 235 + <td class={s.detailCell} colSpan={4}> 236 + {log.message && <p><strong>Message:</strong> {log.message}</p>} 237 + {log.error && <p class={s.detailError}><strong>Error:</strong> {log.error}</p>} 238 + </td> 239 + </tr> 240 + )} 241 + </> 242 + ); 243 + })} 207 244 </tbody> 208 245 </table> 209 246 </div>
+15 -5
lib/automations/pds.ts
··· 73 73 ): Promise<Record<string, unknown>> { 74 74 const client = await getOAuthClient(); 75 75 const session = await client.restore(did); 76 - const res = await session.fetchHandler(`/xrpc/${nsid}`, { 77 - method: "POST", 78 - headers: { "Content-Type": "application/json" }, 79 - body: JSON.stringify(body), 80 - }); 76 + const serialized = JSON.stringify(body); 77 + let res: Response; 78 + try { 79 + res = await session.fetchHandler(`/xrpc/${nsid}`, { 80 + method: "POST", 81 + headers: { "Content-Type": "application/json" }, 82 + body: serialized, 83 + }); 84 + } catch (err) { 85 + console.error(`PDS ${nsid} fetch error for ${did}:`, err); 86 + console.error(`Request body was:`, serialized); 87 + throw new Error( 88 + `PDS ${nsid} fetch failed for ${did}: ${err instanceof Error ? err.message : String(err)}`, 89 + ); 90 + } 81 91 if (!res.ok) { 82 92 const text = await res.text(); 83 93 throw new Error(`PDS ${nsid} failed (${res.status}): ${text}`);
+12 -4
lib/pds/resolver.ts
··· 44 44 url.searchParams.set("collection", collection); 45 45 url.searchParams.set("rkey", rkey); 46 46 47 - const res = await fetch(url, { 48 - headers: { Accept: "application/json" }, 49 - signal: AbortSignal.timeout(10_000), 50 - }); 47 + let res: Response; 48 + try { 49 + res = await fetch(url, { 50 + headers: { Accept: "application/json" }, 51 + signal: AbortSignal.timeout(10_000), 52 + }); 53 + } catch (err) { 54 + console.error(`PDS getRecord fetch error for ${atUri} (${url}):`, err); 55 + throw new Error( 56 + `PDS getRecord fetch failed for ${atUri}: ${err instanceof Error ? err.message : String(err)}`, 57 + ); 58 + } 51 59 52 60 if (!res.ok) { 53 61 throw new Error(`PDS getRecord failed (${res.status}) for ${atUri}`);
+2
lib/webhooks/dispatcher.ts
··· 76 76 }); 77 77 return { statusCode: res.status }; 78 78 } catch (err) { 79 + console.error(`Webhook delivery error to ${callbackUrl} for ${automationUri}:`, err); 80 + console.error(`Request body was:`, body); 79 81 return { statusCode: 0, error: String(err) }; 80 82 } 81 83 }