a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

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

add observability span around list_notifications

Wrap the bsky get_notifications call in a logfire span that captures
the raw response — total_count, unread_count, and per-unread-item
structured data (uri, cid, author handle, reason, reason_subject,
indexed_at, is_read). This makes "what did bsky actually return"
answerable from the trace instead of inferable from downstream code.

Motivation: hit a debugging wall trying to figure out why phi only
saw 1 of 3 valid mention notifications in a poll cycle. The existing
spans only showed phi's interpretation of the response (post-filter
batch size), not the raw bsky output. Without raw response capture
we can't distinguish "bsky returned only 1" from "bsky returned 3
but the filter dropped 2" from "bsky returned 3 with one already-read."

Only emits the unread_items attribute when unread > 0, so normal
quiet polls don't bloat the trace with empty arrays. Doesn't capture
read notifications since they're not the load-bearing part for this
question.

zzstoatzz 848fd08f aad66ff0

+33 -3
+33 -3
src/bot/services/notification_poller.py
··· 4 4 import logging 5 5 from datetime import UTC, date, datetime 6 6 7 + import logfire 8 + 7 9 from bot.config import settings 8 10 from bot.core.atproto_client import BotClient 9 11 from bot.services.message_handler import MessageHandler ··· 107 109 """ 108 110 check_time = self.client.client.get_current_time_iso() 109 111 110 - response = await self.client.get_notifications() 111 - notifications = response.notifications 112 + # Wrap the bsky list_notifications call in an observability span so we 113 + # can see the raw response — counts AND the actual unread items. 114 + # Without this we only see phi's downstream interpretation (post-filter 115 + # batch size), which makes "why did bsky return only N" unanswerable 116 + # from logs alone. 117 + with logfire.span("fetch notifications", check_time=check_time) as fetch_span: 118 + response = await self.client.get_notifications() 119 + notifications = response.notifications 120 + 121 + unread = [n for n in notifications if not n.is_read] 112 122 113 - unread = [n for n in notifications if not n.is_read] 123 + fetch_span.set_attribute("total_count", len(notifications)) 124 + fetch_span.set_attribute("unread_count", len(unread)) 125 + if unread: 126 + # Capture each unread entry as a structured dict so we can 127 + # answer questions like "did bsky return all 3 mentions or 128 + # just 1" without re-running the test. 129 + fetch_span.set_attribute( 130 + "unread_items", 131 + [ 132 + { 133 + "uri": n.uri, 134 + "cid": getattr(n, "cid", "") or "", 135 + "author_handle": n.author.handle, 136 + "reason": n.reason, 137 + "reason_subject": getattr(n, "reason_subject", None) or "", 138 + "indexed_at": str(getattr(n, "indexed_at", "") or ""), 139 + "is_read": n.is_read, 140 + } 141 + for n in unread 142 + ], 143 + ) 114 144 115 145 # First poll: show initial state 116 146 if self._first_poll: