Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Debug wrong thread date #824

Merged
merged 4 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Mail/Helpers/PreviewHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import MailCore
import RealmSwift
import SwiftUI

struct PreviewHelper {
#if DEBUG
enum PreviewHelper {
static let sampleMailboxManager = MailboxManager(mailbox: sampleMailbox, apiFetcher: MailApiFetcher())

static let sampleMailbox = Mailbox(uuid: "",
Expand Down Expand Up @@ -132,3 +133,4 @@ struct PreviewHelper {
expirationDate: Date()
))
}
#endif
51 changes: 31 additions & 20 deletions MailCore/Cache/MailboxManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class MailboxManager: ObservableObject {
let realmName = "\(mailbox.userId)-\(mailbox.mailboxId).realm"
realmConfiguration = Realm.Configuration(
fileURL: MailboxManager.constants.rootDocumentsURL.appendingPathComponent(realmName),
schemaVersion: 11,
schemaVersion: 12,
deleteRealmIfMigrationNeeded: true,
objectTypes: [
Folder.self,
Expand Down Expand Up @@ -266,7 +266,7 @@ public class MailboxManager: ObservableObject {
}
}

private func deleteMessages(uids: [String], folder: Folder) async {
private func deleteMessages(uids: [String]) async {
guard !uids.isEmpty && !Task.isCancelled else { return }

await backgroundRealm.execute { realm in
Expand Down Expand Up @@ -302,14 +302,7 @@ public class MailboxManager: ObservableObject {
try thread.recomputeOrFail()
} catch {
threadsToDelete.insert(thread)
SentrySDK.capture(message: "Thread has nil lastMessageFromFolderDate") { scope in
scope.setContext(value: ["dates": "\(thread.messages.map { $0.date })",
"ids": "\(thread.messages.map { $0.id })"],
key: "all messages")
scope.setContext(value: ["id": "\(thread.lastMessageFromFolder?.uid ?? "nil")"],
key: "lastMessageFromFolder")
scope.setContext(value: ["date before error": thread.date], key: "thread")
}
SentryDebug.threadHasNilLastMessageFromFolderDate(thread: thread)
}
}
}
Expand Down Expand Up @@ -763,9 +756,34 @@ public class MailboxManager: ObservableObject {
}

private func handleMessagesUids(messageUids: MessagesUids, folder: Folder) async throws {
await deleteMessages(uids: messageUids.deletedUids, folder: folder)
let alreadyWrongIds = folder.fresh(using: getRealm())?.threads
.where { $0.date == SentryDebug.knownDebugDate }
.map { $0.uid } ?? []
await deleteMessages(uids: messageUids.deletedUids)
var shouldIgnoreNextEvents = SentryDebug.captureWrongDate(
step: "After delete",
folder: folder,
alreadyWrongIds: alreadyWrongIds,
realm: getRealm()
)
await updateMessages(updates: messageUids.updated, folder: folder)
if !shouldIgnoreNextEvents {
shouldIgnoreNextEvents = SentryDebug.captureWrongDate(
step: "After updateMessages",
folder: folder,
alreadyWrongIds: alreadyWrongIds,
realm: getRealm()
)
}
try await addMessages(shortUids: messageUids.addedShortUids, folder: folder, newCursor: messageUids.cursor)
if !shouldIgnoreNextEvents {
_ = SentryDebug.captureWrongDate(
step: "After addMessages",
folder: folder,
alreadyWrongIds: alreadyWrongIds,
realm: getRealm()
)
}
}

private func addMessages(shortUids: [String], folder: Folder, newCursor: String?) async throws {
Expand Down Expand Up @@ -892,16 +910,9 @@ public class MailboxManager: ObservableObject {
let folders = Set(threads.compactMap(\.folder))
for thread in threads {
do {
try thread.recomputeOrFail()
try thread.recomputeOrFail()
} catch {
SentrySDK.capture(message: "Thread has nil lastMessageFromFolderDate") { scope in
scope.setContext(value: ["dates": "\(thread.messages.map { $0.date })",
"ids": "\(thread.messages.map { $0.id })"],
key: "all messages")
scope.setContext(value: ["id": "\(thread.lastMessageFromFolder?.uid ?? "nil")"],
key: "lastMessageFromFolder")
scope.setContext(value: ["date before error": thread.date], key: "thread")
}
SentryDebug.threadHasNilLastMessageFromFolderDate(thread: thread)
realm.delete(thread)
}
}
Expand Down
24 changes: 18 additions & 6 deletions MailCore/Models/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import MailResources
import RealmSwift
import Sentry

// TODO move to core
// TODO: move to core
public extension String {
/// Max length of a string before we need to truncate it.
static let closeToMaxRealmSize = 14_000_000
Expand All @@ -33,7 +33,7 @@ public extension String {
var truncatedForRealmIfNeeded: Self {
Self.truncatedForRealmIfNeeded(self)
}

/// Truncate a string for compatibility with Realm if needed
///
/// The string will be terminated by " [truncated]" if it was
Expand Down Expand Up @@ -303,14 +303,27 @@ public final class Message: Object, Decodable, Identifiable {

public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
uid = try values.decode(String.self, forKey: .uid)
let uid = try values.decode(String.self, forKey: .uid)
self.uid = uid
if let msgId = try? values.decode(String.self, forKey: .messageId) {
messageId = msgId
linkedUids = [msgId].toRealmSet()
}
subject = try values.decodeIfPresent(String.self, forKey: .subject)
priority = try values.decode(MessagePriority.self, forKey: .priority)
date = (try? values.decode(Date.self, forKey: .date)) ?? Date()
if let date = (try? values.decode(Date.self, forKey: .date)) {
self.date = date
} else {
// FIXME: Remove after thread date bug fix
date = SentryDebug.knownDebugDate
SentrySDK
.addBreadcrumb(SentryDebug.createBreadcrumb(
level: .warning,
category: "Thread algo",
message: "Nil message date decoded",
data: ["uid": uid]
))
}
size = try values.decode(Int.self, forKey: .size)
from = try values.decode(List<Recipient>.self, forKey: .from)
to = try values.decode(List<Recipient>.self, forKey: .to)
Expand Down Expand Up @@ -454,10 +467,9 @@ final class ProxyBody: Codable {

/// Generate a new persisted realm object on the fly
public func realmObject() -> Body {

// truncate message if needed
let truncatedValue = value?.truncatedForRealmIfNeeded

let body = Body()
body.value = truncatedValue
body.type = type
Expand Down
2 changes: 1 addition & 1 deletion MailCore/Models/Thread.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class Thread: Object, Decodable, Identifiable {
@Persisted public var cc: List<Recipient>
@Persisted public var bcc: List<Recipient>
@Persisted public var subject: String?
@Persisted public var date: Date
@Persisted(indexed: true) public var date: Date
@Persisted public var hasAttachments: Bool
@Persisted public var hasSwissTransferAttachments: Bool
@Persisted public var hasDrafts: Bool
Expand Down
48 changes: 47 additions & 1 deletion MailCore/Utils/SentryDebug.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import Foundation
import RealmSwift
import Sentry

struct SentryDebug {
enum SentryDebug {
static let knownDebugDate = Date(timeIntervalSince1970: 1_893_456_000)
static func sendMissingMessagesSentry(sentUids: [String], receivedMessages: [Message], folder: Folder, newCursor: String?) {
if receivedMessages.count != sentUids.count {
let receivedUids = Set(receivedMessages.map { Constants.shortUid(from: $0.uid) })
Expand Down Expand Up @@ -72,4 +73,49 @@ struct SentryDebug {
}
}
}

static func threadHasNilLastMessageFromFolderDate(thread: Thread) {
SentrySDK.capture(message: "Thread has nil lastMessageFromFolderDate") { scope in
scope.setContext(value: ["dates": "\(thread.messages.map { $0.date })",
"ids": "\(thread.messages.map { $0.id })"],
key: "all messages")
scope.setContext(value: ["id": "\(thread.lastMessageFromFolder?.uid ?? "nil")"],
key: "lastMessageFromFolder")
scope.setContext(value: ["date before error": thread.date], key: "thread")
}
}

static func createBreadcrumb(level: SentryLevel,
category: String,
message: String,
data: [String: Any]? = nil) -> Breadcrumb {
let crumb = Breadcrumb(level: level, category: category)
crumb.type = level == .info ? "info" : "error"
crumb.message = message
crumb.data = data
return crumb
}

static func captureWrongDate(step: String, folder: Folder, alreadyWrongIds: [String], realm: Realm) -> Bool {
guard let freshFolder = folder.fresh(using: realm) else { return false }

let threads = freshFolder.threads.where { $0.date == knownDebugDate }.filter { !alreadyWrongIds.contains($0.uid) }
guard !threads.isEmpty else { return false }

SentrySDK.capture(message: "Threads with wrong date on step \(step)") { scope in
scope.setLevel(.error)
scope.setContext(value: ["threads": Array(threads).map {
[
"uid": "\($0.uid)",
"subject": $0.subject ?? "No subject",
"messageIds": "\($0.messageIds.joined(separator: ","))",
"lastMessageFromFolder": $0.lastMessageFromFolder?.uid ?? "nil",
"messages": Array($0.messages)
.map { ["message uid": $0.uid, "message subject": $0.subject ?? "No subject", "message date": $0.date] }
]
}],
key: "threads")
}
return true
}
}