M4: every delivery attempt is recorded

DeliveryJobHandler records each attempt as an 'out' event: the activity, object type and
audience read from the body (ActivityShape, shared with the inbox side), the receiving
server, the status, the time the POST took, the wait since it was queued, the bytes, the
attempt number and the signer's kind (none for circles or private traffic). The outcome is
ok, deferred (429 or 503 with Retry-After, or an open breaker: host-unavailable), retry
(5xx, 408, timeout, network), or dead: another 4xx, private-address, not-deliverable,
signer-gone, or a retry at the last attempt. Until now none of this outlived the job's
seven-day TTL, and the last error was overwritten on success.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-03 11:06:24 +02:00
1 parent ac7cbd136b
commit e247001bdb
4 files changed
+249 -21

No files matched your search

@@ -0,0 +1,48 @@
using PrivaPub.Infrastructure.Statistics;
using System.Text.Json.Nodes;
using static PrivaPub.Federation.Objects.ActivityJson;
namespace PrivaPub.Federation.Objects
{
public sealed record ActivityShape(string Type, string ObjectType, string Audience)
{
public static ActivityShape Of(JsonNode activity)
{
var embedded = activity?["object"] as JsonObject;
return new ActivityShape(Value(activity, "type"), Value(embedded, "type"), AudienceOf(activity));
}
public static ActivityShape Of(string json)
{
try
{
return Of(JsonNode.Parse(json));
}
catch (System.Text.Json.JsonException)
{
return new ActivityShape(default, default, default);
}
}
static string AudienceOf(JsonNode activity)
{
var source = activity?["to"] != default || activity?["cc"] != default ? activity : activity?["object"] as JsonObject;
var to = Addresses(source?["to"]);
var cc = Addresses(source?["cc"]);
if (to.Any(Addressing.IsPublic))
return Interactions.Public;
if (cc.Any(Addressing.IsPublic))
return Interactions.Unlisted;
return to.Count + cc.Count == 0 ? Interactions.None : Interactions.Private;
}
static List<string> Addresses(JsonNode node) => node switch
{
JsonArray array => array.Select(Id).Where(id => id != default).ToList(),
JsonNode single when Id(single) is { } id => new List<string> { id },
_ => new List<string>()
};
}
}