From 7b880ea410f37a7df22594ed9b268448621db4d8 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 20 Jul 2026 15:33:50 -0500 Subject: fix: report unreachable domains instead of 'No meaningful changes' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #10. resolvedSnapshotAfterFallback replaces a failed lookup's snapshot with the previous one, so alertDescriptor compared the old snapshot against itself, found matching hashes, and the run reported 'No meaningful changes' for a domain that was never actually reached. Nothing in the UI or the monitoring log distinguished that from a genuine no-change. MonitoringDomainResult now carries unreachableReason, set when the fallback fires. It is Optional so already-persisted monitoring logs still decode. The run summary reads 'Could not check — kept the previous result' with the underlying error, and monitoringEvents emits a warning- severity monitoringFailure so configured integrations hear about it rather than seeing silence. --- DomainDig/DomainMonitoringService.swift | 43 +++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) (limited to 'DomainDig/DomainMonitoringService.swift') diff --git a/DomainDig/DomainMonitoringService.swift b/DomainDig/DomainMonitoringService.swift index 6fb5fc9..917ede0 100644 --- a/DomainDig/DomainMonitoringService.swift +++ b/DomainDig/DomainMonitoringService.swift @@ -291,6 +291,12 @@ final class DomainMonitoringService { previousSnapshot: previousSnapshot ) let snapshot = Self.resolvedSnapshotAfterFallback(inspectedSnapshot, previousSnapshot: previousSnapshot) + // When the fallback fires, `snapshot` *is* `previousSnapshot`, so + // every comparison below is old-against-old and would otherwise + // report "No meaningful changes" for a domain we never reached. + let unreachableReason = (previousSnapshot != nil && Self.shouldFallbackToSnapshot(inspectedSnapshot)) + ? Self.firstErrorMessage(in: inspectedSnapshot) + : nil let savedEntry: HistoryEntry? if snapshot.statusMessage == nil { savedEntry = persistSnapshot( @@ -330,14 +336,16 @@ final class DomainMonitoringService { historyEntryID: savedEntry?.id ?? snapshot.historyEntryID, checkedAt: now, didChange: alertDescriptor != nil, - summaryMessage: snapshot.statusMessage + summaryMessage: unreachableReason.map { "Could not check — kept the previous result. \($0)" } + ?? snapshot.statusMessage ?? alertDescriptor?.message ?? savedEntry?.changeSummary?.message ?? "No meaningful changes", alertSeverity: alertDescriptor?.severity, certificateWarningLevel: DomainDiffService.certificateWarningLevel(for: snapshot), resultSource: snapshot.resultSource, - errorMessage: snapshot.statusMessage + errorMessage: snapshot.statusMessage, + unreachableReason: unreachableReason ) results.append(result) @@ -397,6 +405,21 @@ final class DomainMonitoringService { ) } + if let unreachableReason = result.unreachableReason { + return MonitoringEvent( + type: .monitoringFailure, + severity: .warning, + domain: result.domain, + timestamp: result.checkedAt, + summary: result.summaryMessage, + details: [ + "reason": unreachableReason, + "trigger": log.trigger.rawValue, + "resultSource": result.resultSource.rawValue + ] + ) + } + if result.certificateWarningLevel == .critical { return MonitoringEvent( type: .certificateExpiring, @@ -943,6 +966,22 @@ final class DomainMonitoringService { ) } + /// First reported error on a snapshot, used to explain a fallback. Mirrors + /// the ordering `shouldFallbackToSnapshot` inspects. + private static func firstErrorMessage(in snapshot: LookupSnapshot) -> String { + [ + snapshot.dnsError, + snapshot.httpHeadersError, + snapshot.sslError, + snapshot.ownershipError, + snapshot.subdomainsError, + snapshot.redirectChainError, + snapshot.ipGeolocationError + ] + .compactMap { $0 } + .first ?? "The lookup could not reach the domain." + } + private static func shouldFallbackToSnapshot(_ snapshot: LookupSnapshot) -> Bool { let candidateMessages = [ snapshot.dnsError, -- cgit v1.2.3