fail upload when atproto sync fails (#297)

authored by zzstoatzz.io and committed by GitHub 02a620ad 32789dc3

Changed files
+51 -5
.claude
commands
src
backend
api
tracks
+15
.claude/commands/check-spans.md
···
··· 1 + --- 2 + description: Investigate Logfire spans and traces to answer questions about application behavior 3 + argument-hint: [your question about what happened] 4 + --- 5 + 6 + First read the Logfire documentation: @docs/tools/logfire.md 7 + 8 + Investigate: $ARGUMENTS 9 + 10 + Query the `records` table using SQL patterns from the docs. Key points: 11 + - Search by `message` for logfire.info() events, `span_name` for spans 12 + - Use `trace_id` to follow a full user action flow 13 + - Check `otel_status_message` for errors (not exception.message) 14 + - Filter by `start_timestamp` for time ranges 15 + - Extract metadata from `attributes` JSONB field
+36 -5
src/backend/api/tracks/uploads.py
··· 221 except json.JSONDecodeError: 222 pass # ignore malformed features 223 224 # create ATProto record 225 atproto_uri = None 226 atproto_cid = None ··· 243 features=featured_artists if featured_artists else None, 244 image_url=image_url, 245 ) 246 - if result: 247 - atproto_uri, atproto_cid = result 248 except Exception as e: 249 - logger.warning( 250 - f"failed to create ATProto record: {e}", exc_info=True 251 - ) 252 253 # create track record 254 upload_tracker.update_status(
··· 221 except json.JSONDecodeError: 222 pass # ignore malformed features 223 224 + async def fail_atproto_sync(reason: str) -> None: 225 + """mark upload as failed when ATProto sync cannot complete.""" 226 + 227 + logger.error( 228 + "upload %s failed during ATProto sync", 229 + upload_id, 230 + extra={ 231 + "file_id": file_id, 232 + "artist_did": artist_did, 233 + "reason": reason, 234 + }, 235 + ) 236 + upload_tracker.update_status( 237 + upload_id, 238 + UploadStatus.FAILED, 239 + "upload failed", 240 + error=f"failed to sync track to ATProto: {reason}", 241 + phase="atproto", 242 + ) 243 + 244 + # delete uploaded media so we don't leave orphaned files behind 245 + with contextlib.suppress(Exception): 246 + await storage.delete(file_id, audio_format.value) 247 + if image_id: 248 + with contextlib.suppress(Exception): 249 + await storage.delete(image_id) 250 + 251 # create ATProto record 252 atproto_uri = None 253 atproto_cid = None ··· 270 features=featured_artists if featured_artists else None, 271 image_url=image_url, 272 ) 273 + if not result: 274 + await fail_atproto_sync("PDS returned no record data") 275 + return 276 + atproto_uri, atproto_cid = result 277 except Exception as e: 278 + await fail_atproto_sync(str(e)) 279 + return 280 + else: 281 + await fail_atproto_sync("no public audio URL available") 282 + return 283 284 # create track record 285 upload_tracker.update_status(