Commit 7b880ea410

7b880ea410f37a7df22594ed9b268448621db4d8

parent: 8b661d9f36

Unsigned

cmc <hello@cleberg.net> · 2026-07-20 20:33 UTC

fix: report unreachable domains instead of 'No meaningful changes'

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.

Layout: unified · split

DomainDig/DomainMonitoringService.swift +41 −2
@@ -291,6 +291,12 @@ final class DomainMonitoringService {
291291 previousSnapshot: previousSnapshot
292292 )
293293 let snapshot = Self.resolvedSnapshotAfterFallback(inspectedSnapshot, previousSnapshot: previousSnapshot)
294 // When the fallback fires, `snapshot` *is* `previousSnapshot`, so
295 // every comparison below is old-against-old and would otherwise
296 // report "No meaningful changes" for a domain we never reached.
297 let unreachableReason = (previousSnapshot != nil && Self.shouldFallbackToSnapshot(inspectedSnapshot))
298 ? Self.firstErrorMessage(in: inspectedSnapshot)
299 : nil
294300 let savedEntry: HistoryEntry?
295301 if snapshot.statusMessage == nil {
296302 savedEntry = persistSnapshot(
@@ -330,14 +336,16 @@ final class DomainMonitoringService {
330336 historyEntryID: savedEntry?.id ?? snapshot.historyEntryID,
331337 checkedAt: now,
332338 didChange: alertDescriptor != nil,
333 summaryMessage: snapshot.statusMessage
339 summaryMessage: unreachableReason.map { "Could not check — kept the previous result. \($0)" }
340 ?? snapshot.statusMessage
334341 ?? alertDescriptor?.message
335342 ?? savedEntry?.changeSummary?.message
336343 ?? "No meaningful changes",
337344 alertSeverity: alertDescriptor?.severity,
338345 certificateWarningLevel: DomainDiffService.certificateWarningLevel(for: snapshot),
339346 resultSource: snapshot.resultSource,
340 errorMessage: snapshot.statusMessage
347 errorMessage: snapshot.statusMessage,
348 unreachableReason: unreachableReason
341349 )
342350 results.append(result)
343351
@@ -397,6 +405,21 @@ final class DomainMonitoringService {
397405 )
398406 }
399407
408 if let unreachableReason = result.unreachableReason {
409 return MonitoringEvent(
410 type: .monitoringFailure,
411 severity: .warning,
412 domain: result.domain,
413 timestamp: result.checkedAt,
414 summary: result.summaryMessage,
415 details: [
416 "reason": unreachableReason,
417 "trigger": log.trigger.rawValue,
418 "resultSource": result.resultSource.rawValue
419 ]
420 )
421 }
422
400423 if result.certificateWarningLevel == .critical {
401424 return MonitoringEvent(
402425 type: .certificateExpiring,
@@ -943,6 +966,22 @@ final class DomainMonitoringService {
943966 )
944967 }
945968
969 /// First reported error on a snapshot, used to explain a fallback. Mirrors
970 /// the ordering `shouldFallbackToSnapshot` inspects.
971 private static func firstErrorMessage(in snapshot: LookupSnapshot) -> String {
972 [
973 snapshot.dnsError,
974 snapshot.httpHeadersError,
975 snapshot.sslError,
976 snapshot.ownershipError,
977 snapshot.subdomainsError,
978 snapshot.redirectChainError,
979 snapshot.ipGeolocationError
980 ]
981 .compactMap { $0 }
982 .first ?? "The lookup could not reach the domain."
983 }
984
946985 private static func shouldFallbackToSnapshot(_ snapshot: LookupSnapshot) -> Bool {
947986 let candidateMessages = [
948987 snapshot.dnsError,
DomainDig/Models.swift +7 −1
@@ -1544,6 +1544,10 @@ struct MonitoringDomainResult: Codable, Identifiable, Equatable {
15441544 let certificateWarningLevel: CertificateWarningLevel
15451545 let resultSource: LookupResultSource
15461546 let errorMessage: String?
1547 /// Non-nil when the lookup failed and the previous snapshot was reused, so
1548 /// this run compared the old data against itself and cannot claim the
1549 /// domain is unchanged. Optional so already-persisted logs still decode.
1550 let unreachableReason: String?
15471551
15481552 init(
15491553 id: UUID = UUID(),
@@ -1555,7 +1559,8 @@ struct MonitoringDomainResult: Codable, Identifiable, Equatable {
15551559 alertSeverity: MonitoringAlertSeverity?,
15561560 certificateWarningLevel: CertificateWarningLevel,
15571561 resultSource: LookupResultSource,
1558 errorMessage: String? = nil
1562 errorMessage: String? = nil,
1563 unreachableReason: String? = nil
15591564 ) {
15601565 self.id = id
15611566 self.domain = domain
@@ -1567,6 +1572,7 @@ struct MonitoringDomainResult: Codable, Identifiable, Equatable {
15671572 self.certificateWarningLevel = certificateWarningLevel
15681573 self.resultSource = resultSource
15691574 self.errorMessage = errorMessage
1575 self.unreachableReason = unreachableReason
15701576 }
15711577}
15721578