commit a89d6a8f2e312bd4968b5d5cf7ad4a0c4caab2c2
parent 1eed8b9c855053a148a2f8d208b9bce93b3f0f16
Author: Michael Camilleri <[email protected]>
Date: Fri, 12 Jun 2026 12:18:31 +0900
Log the worker's publish delivery counts in diagnostics
PushClient logged 'worker accepted' but discarded the response body,
so the new muted count — and delivered/removed/failed — were invisible
on-device, which is the only place Production delivery can often be
observed. sendAuthorized now returns the body and publish logs e.g.
'push(play): worker accepted (delivered=2 muted=1 removed=0 failed=0)'.
A pre-mutedKinds worker response or an unparseable body degrades to
the old log line.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat:
2 files changed, 44 insertions(+), 8 deletions(-)
diff --git a/Crossmate/Services/PushClient.swift b/Crossmate/Services/PushClient.swift
@@ -206,7 +206,7 @@ final class PushClient {
do {
let body = try JSONSerialization.data(withJSONObject: payload)
request.httpBody = body
- let response = try await sendAuthorized(
+ let (data, response) = try await sendAuthorized(
request,
method: "POST",
path: "/publish",
@@ -215,7 +215,7 @@ final class PushClient {
guard response.statusCode == 200 else {
throw URLError(.badServerResponse)
}
- log("push(\(kind)): worker accepted")
+ log("push(\(kind)): worker accepted\(Self.deliverySummary(from: data))")
} catch {
log("push(\(kind)) failed: \(error.localizedDescription)")
}
@@ -242,6 +242,22 @@ final class PushClient {
)
}
+ /// Formats the worker's publish response counts for the diagnostics log,
+ /// e.g. " (delivered=2 muted=1 removed=0 failed=0)". Production delivery
+ /// is only observable through the on-device log, so this is where "why
+ /// didn't that push arrive?" gets answered — `muted` in particular means
+ /// a recipient device turned that notification kind off in settings.
+ /// Returns "" for an unparseable body (e.g. a worker predating a count).
+ nonisolated static func deliverySummary(from data: Data) -> String {
+ guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ return ""
+ }
+ let counts = ["delivered", "muted", "removed", "failed"].compactMap { key in
+ (json[key] as? Int).map { "\(key)=\($0)" }
+ }
+ return counts.isEmpty ? "" : " (\(counts.joined(separator: " ")))"
+ }
+
private static let iso8601: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -265,7 +281,7 @@ final class PushClient {
]
let data = try JSONSerialization.data(withJSONObject: body)
request.httpBody = data
- let response = try await sendAuthorized(
+ let (_, response) = try await sendAuthorized(
request,
method: "POST",
path: "/register",
@@ -283,7 +299,7 @@ final class PushClient {
])) ?? Data()
request.httpBody = data
do {
- let response = try await sendAuthorized(
+ let (_, response) = try await sendAuthorized(
request,
method: "DELETE",
path: "/register",
@@ -300,7 +316,7 @@ final class PushClient {
method: String,
path: String,
body: Data
- ) async throws -> HTTPURLResponse {
+ ) async throws -> (Data, HTTPURLResponse) {
try await sendAuthorized(
request,
method: method,
@@ -316,7 +332,7 @@ final class PushClient {
path: String,
body: Data,
retryAfterRegistrationReset: Bool
- ) async throws -> HTTPURLResponse {
+ ) async throws -> (Data, HTTPURLResponse) {
var request = request
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let headers = try await authenticator.signedHeaders(
@@ -327,7 +343,7 @@ final class PushClient {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
- let (_, response) = try await session.data(for: request)
+ let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
@@ -341,7 +357,7 @@ final class PushClient {
retryAfterRegistrationReset: false
)
}
- return http
+ return (data, http)
}
private func assert204(_ response: HTTPURLResponse) throws {
diff --git a/Tests/Unit/PuzzleNotificationTextTests.swift b/Tests/Unit/PuzzleNotificationTextTests.swift
@@ -197,4 +197,24 @@ struct NotificationMutedKindsTests {
#expect(!muted.contains("accountJoined"))
#expect(!muted.contains("accountSeen"))
}
+
+ @Test("Publish delivery summary formats the worker counts")
+ func deliverySummaryFormatsCounts() {
+ let body = Data(#"{"delivered":2,"removed":0,"muted":1,"failed":0}"#.utf8)
+
+ #expect(PushClient.deliverySummary(from: body) == " (delivered=2 muted=1 removed=0 failed=0)")
+ }
+
+ @Test("Publish delivery summary tolerates a pre-muted worker response")
+ func deliverySummaryOldWorker() {
+ let body = Data(#"{"delivered":3,"removed":1,"failed":0}"#.utf8)
+
+ #expect(PushClient.deliverySummary(from: body) == " (delivered=3 removed=1 failed=0)")
+ }
+
+ @Test("Publish delivery summary is empty for an unparseable body")
+ func deliverySummaryUnparseable() {
+ #expect(PushClient.deliverySummary(from: Data()) == "")
+ #expect(PushClient.deliverySummary(from: Data("ok".utf8)) == "")
+ }
}