microsoft / appcenter

Central repository for App Center open source resources and planning.
https://appcenter.ms
Creative Commons Attribution 4.0 International
1.01k stars 223 forks source link

Support for error reporting for non-crashing errors on iOS #192

Closed christianselig closed 3 years ago

christianselig commented 5 years ago

Describe the solution you'd like Currently there doesn't appear to be support for logging errors with Diagnostics for errors that are harmful but don't actually cause a crash. Here's an example of how it's handled in Crashlytics, for instance.

Describe alternatives you've considered The Analytics solution could work here, but having it properly tied in with the errors/Diagnostics side of things would be more optimal.

Additional context N/A

winnie commented 5 years ago

Hi @christianselig - thanks for the request. At this time, we only support handled errors for Xamarin and Unity apps. We don't have any immediate plans to bring this to iOS native but this is on our radar and we hope to bring this feature to other platforms in the future.

I'll post here if I have any updates. In the meantime, keep an eye out on our roadmap :)

tonyarnold commented 5 years ago

Would you mind making this a little wider and supporting this for all Cocoa apps (iOS, macOS, tvOS and watchOS)? Thanks!

sindresorhus commented 5 years ago

This is a blocker for me and other macOS devs I know for moving from Crashlytics.

zigfreid5 commented 5 years ago

This is a hang-up for us as well. I'm loving the new features recently brought into AppCenter, but it's things like this that still make Firebase a little better option.

gfinden commented 5 years ago

We would like to see this as well. And it would be great if it could capture a stack trace of where the error was handled at, to provide more insight into what code path resulted in the error.

winnie commented 5 years ago

Hi all - thanks for the feedback. We understand how crucial this feature is for app developers and this is absolutely something we plan to support in the future. Unfortunately, our team is focused on other priorities for the next few months and won't have the bandwidth to work on this. That said, I'll continue to monitor this thread and will take everyone's comments into consideration when we prioritize our future work.

Thanks!

valerianb commented 5 years ago

We'd love this as well to be able to migrate away from Crashlytics. Plus CLS_LOG/CLSLogv which will only appear if there's a crash (see https://docs.fabric.io/apple/crashlytics/enhanced-reports.html) are useful. Thanks !

radekwilczak commented 5 years ago

I also need the equivalent of CLSLogv in AppCenter.

diogot commented 4 years ago

We're evaluating the Crashlytics substitute with the end of the service. AppCenter is a great option, but this is highly needed feature.

myell0w commented 4 years ago

The lack of this feature is one of the reasons PSPDFKit moved away from AppCenter: https://twitter.com/steipete/status/1226896190509285376

CJonesBuild commented 4 years ago

Yep, this would be great to have. Without it theres no reason for my team to use AppCenter. Not sure why it's been prioritized for Android, Xamarin, Unity, UWP, WPF and WinForms apps, but suddenly you're too busy to support iOS... which is not exactly a small platform. Any update regarding a timeline on this feature?

Noobish1 commented 4 years ago

This is a must-have feature for me also.

winnie commented 4 years ago

Hey I'm so sorry, I don't have an update at the moment. We are continuosly working to improve our product based on feedback and I will post here if I have a better timeline. Thanks!

justintoth commented 4 years ago

It has been a year and still no progress on this. App Center has been very disappointing to work with, I miss HockeyApp.

justintoth commented 4 years ago

We were able to get around this limitation in the App Center iOS SDK by calling their Web API instead: https://docs.microsoft.com/en-us/appcenter/diagnostics/upload-crashes#upload-a-crash-report

rist commented 4 years ago

@justintoth would you be so kind and share your workaround? thanks I mostly got NoValidLogsFoundInLogContainer when playing around with posting some JSON and when I got a success response I was unable to find the posted data in the appcenter UI

dtdw commented 4 years ago

Also for us the ability to log Error / NSError objects together with a call stack a precondition to be able to migrate from Crashlythics / Firebase. At the moment we have to work with Firebase for our iOS apps and with AppCenter for our Xamarin apps, which is not exactly convenient.

jbranc commented 4 years ago

There are so many circumstances where it would be great for a user to submit an issue that also brings along logs and device data that are part of a crash report. This seems like a trivial addition to the existing system, it's a bit unclear why MS is resisting this. It's not remarkably different than the test crash call.

ctoegi commented 4 years ago

Sounds like a very useful feature to me too.

andrejandrejko commented 4 years ago

Pls., go for it

keriati commented 4 years ago

Absolutely must-have, how is this not on high prio?

liamrudel commented 4 years ago

This is also something we consider a must have, especially when moving from Crashlytics. For now I'm putting the errors into the Analytics as a custom event, but they really belong with the crashes.

nindim commented 4 years ago

We have recently moved to App Center from Crashlytics and this is a huge problem. It's causing a lot of issues with bug visibility and missing info in its current form. The android bugs are so much easier to find and are much more useful (individually keyed, no truncation etc).

Please prioritise this.

justintoth commented 4 years ago

Sorry, I just saw @rist's question... Here is our Titanium code for logging custom App Center crashes, using their SDK for Android and their API for iOS.

// Report error to App Center.
    if (utils.android) {
        try {
            let properties = getProperties(true);
            properties = _.filter(properties, function(p) { return !!p.value; });
            console.log('Reporting Android error to app center: ' + evt.message + ' with properties: ' + JSON.stringify(properties));
            Crashes.trackError(evt.message, properties);
            console.log('Reported error to app center successfully!');
        }
        catch (err) {
            // well, that sucks, huh?
            console.error('Error not submitted to app center:');
            console.error(err);
        }
    } else {
        const user = settings.user();
        var data = {
            logs: [
                {
                    type: 'managedError',
                    processId: '123',
                    id: Ti.Platform.id,
                    fatal: false,
                    processName: Ti.App.id,
                    appLaunchTimestamp: moment.utc().format(),
                    device: {
                        appVersion: Ti.App.version,
                        appBuild: utils.android ? Ti.App.Android.appVersionCode : parseInt(Ti.App.version.replace(/\./g, '')),
                        sdkName: 'appcenter.custom',
                        sdkVersion: Ti.version,
                        osName: Ti.Platform.osname,
                        osVersion: Ti.Platform.version,
                        model: Ti.Platform.model,
                        locale: Ti.Locale.currentLocale
                    },
                    exception: {
                        message: evt.message,
                        type: 'Error'
                    },
                    userId: user && user.emailAddress,
                }
            ]
        };
        console.log('Reporting iOS error to app center: ' + evt.message + ' with data: ' + JSON.stringify(data));
        xhr.call({
            verb: 'POST',
            url: 'https://in.appcenter.ms/logs?Api-Version=1.0.0',
            headers: [
                { key: 'Content-Type', value: 'application/json' },
                { key: 'App-Secret', value: Ti.App.Properties.getString('ti.appcenter.secret.ios') },
                { key: 'Install-ID', value: Ti.App.installId }
            ],
            data: JSON.stringify(data),
            disableFlattenData: true,
            success: function xhrSuccess(result) {
                console.log('Reported error to app center successfully!');
            },
            error: function xhrError(result) {
                console.error('Error not submitted to app center: ' + result.status + ' status code with response: ' + result.responseText);
            },
        });
    }
ffittschen commented 4 years ago

Thanks @justintoth! I just tried it and the errors are showing up in App Center by using that payload structure. However, they are also logged as crashes and are indistinguishable from the real fatal errors. 😔

@winnie I hope this issue will make it into one of the next iteration plans. It is open for quite some time now and has a considerable amount of votes (it actually is in the top 10 of open feature requests) and it is already supported for other platforms than iOS.

sindresorhus commented 4 years ago

It looks like this is unlikely to happen for another year: https://github.com/microsoft/appcenter/wiki/Roadmap

[…] the App Center team will prioritize improving reliability and performance for the service through mid-2021. This means new feature work will be significantly reduced.

I'm really starting to regret having switched all my apps from Crashlytics to AppCenter...

gui17aume commented 4 years ago

Hi @MatkovIvan @winnie,

I made a little addition on the MSCrashes class to be able to send error reports. It makes use of the existing trackModelException:withProperties:withAttachments: internal method of the same class.

I can tell it works well: on the appcenter.ms website, in the Issues section of an iOS or macOS app, you just have to append &errorType=all to the URL to display the errors in the list along with the crashes. The website should be updated to display the "All" and "Errors" tabs as for Android apps.

appcenter-macos-errors

If you want to have a look, the commit is on my fork here: https://github.com/gui17aume/appcenter-sdk-apple/commit/e9818b235efa51574af3e4e6a186496f5e9ec37d (I don't know if you're open to pull requests for this kind of additions...)

Tommigun1980 commented 4 years ago

Is this going to be implemented in the base SDK? Is there any kind of roadmap for App Center? Will the entire App Center be deprecated soon, as there is almost zero communication from developers, nobody responds to tickets, and absolutely mandatory albeit small features are ignored. If it will be deprecated please step forward and tell us so we can look into alternatives.

Thanks.

osca commented 4 years ago

I built a plain HTTP version in Swift myself because I don't think this isn't going to be resolved soon. Here's my code to send crashes to AppCenter:

func sendError(_ message: String, properties: Dictionary<String, String>? = nil) {
    let url = URL(string: "https://in.appcenter.ms/logs?Api-Version=1.0.0")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("xxxxxxxx-yyyy-yyyy-yyyy-zzzzzzzzzzzz", forHTTPHeaderField: "app-secret")
    request.addValue(UIDevice.current.identifierForVendor!.uuidString, forHTTPHeaderField: "install-id")

    let errorId = UUID().uuidString
    let attachmentId = UUID().uuidString
    let processId = ProcessInfo.processInfo.processIdentifier
    let bundleIdentifier = Bundle.main.bundleIdentifier
    let appVersion = Bundle.main.infoDictionary!["CFBundleShortVersionString"]
    let appBuild = Bundle.main.infoDictionary!["CFBundleVersion"]
    let sdkName = "appcenter.custom"
    let osName = "iOS"
    let osVersion = UIDevice.current.systemVersion
    let model = modelName()
    let locale = Locale.current.identifier

    var errors: [Dictionary<String, Any>] = [[
        "type": "managedError",
        "processId": processId,
        "id": errorId,
        "fatal": false,
        "processName": bundleIdentifier,
        "appLaunchTimestamp": iso8601withFractionalSeconds(),
        "device": [
            "appVersion": appVersion,
            "appBuild": appBuild,
            "sdkName": sdkName,
            "sdkVersion": "1.0.0",
            "osName": osName,
            "osVersion": osVersion,
            "model": model,
            "locale": locale
        ],
        "exception": [
            "message": message,
            "type": "Error"
        ]
    ]]

    if properties != nil {
        let propertiesJson = try? JSONSerialization.data(withJSONObject: properties, options: [])
        let propertiesBase64: String = propertiesJson!.base64EncodedString()

        errors.append([
            "type": "errorAttachment",
            "contentType": "application/json",
            "timestamp": iso8601withFractionalSeconds(),
            "data": propertiesBase64,
            "errorId": errorId,
            "id": attachmentId,
            "device": [
            "appVersion": appVersion,
            "appBuild": appBuild,
            "sdkName": sdkName,
            "sdkVersion": "1.0.0",
            "osName": osName,
            "osVersion": osVersion,
            "model": model,
            "locale": locale
            ]
        ])

    }

    let jsonMap = ["logs": errors]

    if let jsonData = try? JSONSerialization.data(withJSONObject: jsonMap, options: []) {
        URLSession.shared.uploadTask(with: request, from: jsonData) { data, response, error in
            print(data)
            print(response)
            print(error)
        }.resume()
    }
}

private func iso8601withFractionalSeconds() -> String {
    let iso8601DateFormatter = ISO8601DateFormatter()
    iso8601DateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    return iso8601DateFormatter.string(from: Date())
}

private func modelName() -> String {
    var systemInfo = utsname()
    uname(&systemInfo)
    let machineMirror = Mirror(reflecting: systemInfo.machine)
    let identifier = machineMirror.children.reduce("") { identifier, element in
        guard let value = element.value as? Int8, value != 0 else { return identifier }
        return identifier + String(UnicodeScalar(UInt8(value)))
    }
    return identifier
}

Feel free to use, improve and share it back!

IPWright83 commented 4 years ago

@osca Could you share where you found the documentation for this endpoint? I don't see it in https://openapi.appcenter.ms/ but I'd like to do something similar!

osca commented 4 years ago

@IPWright83, it is linked in one of the comments before of @justintoth in https://github.com/microsoft/appcenter/issues/192#issuecomment-593028522

Link to the doc: https://docs.microsoft.com/en-us/appcenter/diagnostics/upload-crashes#upload-a-crash-report

IPWright83 commented 4 years ago

Has anyone managed to get this working with a react-native application? I'm trying to use fake crashes to record errors using the custom crash log functionality, but despite getting a success back from the API I can't view anything through the web dashboard :(

4brunu commented 3 years ago

Any news on this? This is really a blocker...

ptc-glugtenberg commented 3 years ago

For others having this issue where handled errors are being logged into App Center, but there is no overview dashboard or "Errors" tab in the web portal (appcenter.ms): Go to the Diagnostics > Issues page and paste the following behind the URL:

&errorType=handlederror
rpassis commented 3 years ago

For anyone still having issues with the lack of custom error logging, I've extended AppCenter to handle this in a type safe, user friendly way. Thank you @osca for the idea!

extension AppCenter {
    /// Sends a custom crash report using the App Center API
    /// API documentation available @ https://docs.microsoft.com/en-us/appcenter/diagnostics/upload-crashes#upload-a-crash-report
    /// - Parameters:
    ///   - message: The error message
    ///   - userID: An optional user identifier associated with the crash
    ///   - isFatal: Whether the crash is fatal or not. Defaults to false
    ///   - properties: An optional dictionary with additional properties to be sent as an attachment
    ///   - callback: An optional callback with the result of the operation.
    static func sendError(
        message: String,
        forUserID userID: String? = nil,
        isFatal: Bool = false,
        additionalProperties properties: [String: Any]? = nil,
        callback: ((Bool) -> Void)? = nil
    ) {
        let exception = AppCenter.Exception(domain: .customError, message: message, isFatal: isFatal)
        AppCenter.send(exception, forUserID: userID, additionalProperties: properties, callback: callback)
    }

    /// Sends a custom crash report using the App Center API
    /// API documentation available @ https://docs.microsoft.com/en-us/appcenter/diagnostics/upload-crashes#upload-a-crash-report
    /// - Parameters:
    ///   - exception: The exception payload containing information about the error
    ///   - userID: An optional user identifier associated with the crash
    ///   - properties: An optional dictionary with additional properties to be sent as an attachment
    ///   - callback: An optional callback with the result of the operation. The callback always runs on the main queue.
    static func send(
        _ exception: AppCenter.Exception,
        forUserID userID: String? = nil,
        additionalProperties properties: [String: Any]? = nil,
        callback: ((Bool) -> Void)? = nil
    ) {
        let error = CustomError(errorType: .managed, exception: exception, userID: userID)
        var errors = [error]
        if let properties = properties,
           let json = try? JSONSerialization.data(withJSONObject: properties, options: []) {
            let error = CustomError(errorType: .attachment(data: json, parentErrorID: error.identifier))
            errors.append(error)
        }
        let payload = ErrorsPayload(logs: errors)
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let payloadData = try? encoder.encode(payload)
        let task = URLSession.shared.uploadTask(
            with: .appCenterCustomErrorRequest,
            from: payloadData
        ) { _, response, error in
            guard
                error == nil,
                let response = response as? HTTPURLResponse,
                (200..<300).contains(response.statusCode)
            else {
                DispatchQueue.main.async { callback?(false) }
                return
            }
            DispatchQueue.main.async { callback?(true) }
        }
        task.resume()
    }
}

fileprivate extension AppCenter {
    struct CustomError: Encodable {
        let identifier = UUID()
        let timestamp: Date
        let appLaunchTimestamp: Date
        let processId: Int
        let processName: String
        let device: Device
        let userID: String?
        let exception: Exception?

        var isFatal: Bool { self.exception?.isFatal ?? false }
        var data: String? { self.errorType.data?.base64EncodedString() }
        var contentType: String? { self.errorType.contentType }
        var type: String { self.errorType.type }
        var errorID: UUID? { self.errorType.parentErrorID }

        private let errorType: ErrorType

        init(
            errorType: ErrorType,
            exception: Exception? = nil,
            date: Date = Date(),
            appLaunchTimestamp: Date = Date(),
            processId: Int = Int(ProcessInfo.processInfo.processIdentifier),
            processName: String = ProcessInfo.processInfo.processName,
            device: Device = Device(),
            userID: String? = nil
        ) {
            self.errorType = errorType
            self.exception = exception
            self.timestamp = date
            self.appLaunchTimestamp = appLaunchTimestamp
            self.processId = processId
            self.processName = processName
            self.device = device
            self.userID = userID
        }

        enum CodingKeys: String, CodingKey {
            case appLaunchTimestamp
            case contentType
            case data
            case device
            case errorID = "errorId"
            case exception
            case identifier = "id"
            case isFatal = "fatal"
            case processId
            case processName
            case timestamp
            case type
            case userID = "userId"
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(self.identifier, forKey: .identifier)
            try container.encode(self.timestamp, forKey: .timestamp)
            try container.encode(self.appLaunchTimestamp, forKey: .appLaunchTimestamp)
            try container.encode(self.processId, forKey: .processId)
            try container.encode(self.processName, forKey: .processName)
            try container.encode(self.exception, forKey: .exception)
            try container.encode(self.device, forKey: .device)
            try container.encode(self.userID, forKey: .userID)
            try container.encode(self.isFatal, forKey: .isFatal)
            try container.encode(self.data, forKey: .data)
            try container.encode(self.contentType, forKey: .contentType)
            try container.encode(self.type, forKey: .type)
            try container.encode(self.errorID, forKey: .errorID)
        }
    }
}

extension AppCenter {
    struct Exception: Encodable {
        let type: String?
        let message: String?
        let isFatal: Bool
        let strackTrace: String?
        let frames: [Frame]?
        let innerExceptions: [Exception]?
        let threads: [Thread]?

        init(
            domain: String? = nil,
            message: String? = nil,
            isFatal: Bool = false,
            strackTrace: String? = nil,
            frames: [Frame]? = nil,
            innerExceptions: [Exception]? = nil,
            threads: [Thread]? = nil
        ) {
            self.type = domain
            self.message = message
            self.isFatal = isFatal
            self.strackTrace = strackTrace
            self.frames = frames
            self.innerExceptions = innerExceptions
            self.threads = threads
        }

        struct Thread: Encodable {
            private let identifier: Int
            private let frames: [Frame]
            private let name: String
        }

        struct Frame: Encodable {
            private let className: String?
            private let fileName: String?
            private let lineNumber: Int?
            private let methodName: String?
        }
    }
}

extension AppCenter.CustomError {
    struct Device: Encodable {
        let appVersion: String
        let appBuild: String
        let sdkName: String
        let sdkVersion: String
        let osName: String
        let osVersion: String
        let model: String?
        let appNameSpace: String?
        let locale: String
        let timeZoneOffset: TimeInterval?

        init(
            appVersion: String = .appVersion,
            appBuild: String = .appBuild,
            sdkName: String = .appCenterSDKName,
            sdkVersion: String = .appCenterSDKVersion,
            osName: String = UIDevice.current.systemName,
            osVersion: String = UIDevice.current.systemVersion,
            model: String? = deviceModelName(),
            appNameSpace: String? = Bundle.main.bundleIdentifier,
            locale: String = Locale.current.identifier,
            timeZoneOffset: TimeInterval? = nil
        ) {
            self.appVersion = appVersion
            self.appBuild = appBuild
            self.sdkName = sdkName
            self.sdkVersion = sdkVersion
            self.osName = osName
            self.osVersion = osVersion
            self.model = model
            self.appNameSpace = appNameSpace
            self.locale = locale
            self.timeZoneOffset = timeZoneOffset
        }
    }

    struct ErrorType: Encodable {
        /// Default factories
        static let apple: ErrorType = { fatalError("Not implemented") }()
        static let handled: ErrorType = { fatalError("Not implemented") }()
        static let managed: ErrorType = { .init(type: .managedErrorType) }()
        static func attachment(data: Data, parentErrorID: UUID) -> ErrorType {
            .init(
                type: .attachmentErrorType,
                data: data,
                contentType: .attachmentErrorContentType,
                parentErrorID: parentErrorID
            )
        }

        let data: Data?
        let type: String
        let contentType: String?
        let parentErrorID: UUID?

        private init(
            type: String,
            data: Data? = nil,
            contentType: String? = nil,
            parentErrorID: UUID? = nil
        ) {
            self.type = type
            self.data = data
            self.contentType = contentType
            self.parentErrorID = parentErrorID
        }
    }
}

private extension URLRequest {
    /// A custom URLRequest configured with the url, method and headers required by the crash report upload API endpoint
    static let appCenterCustomErrorRequest: URLRequest = {
        let installID = UIDevice.current.identifierForVendor ?? UUID()
        var request = URLRequest(url: .appCenterAPIURL)
        request.httpMethod = .postHTTPMethod
        request.setValue(.contentTypeApplicationJSON, forHTTPHeaderField: .contentTypeHeaderKey)
        request.setValue(.appCenterAppID, forHTTPHeaderField: .appCenterSecretHeaderKey)
        request.setValue(installID.uuidString, forHTTPHeaderField: .appCenterInstallIDHeaderKey)
        return request
    }()
}

private extension URL {
    // swiftlint:disable:next force_unwrapping
    static let appCenterAPIURL = URL(string: "https://in.appcenter.ms/logs?Api-Version=1.0.0")!
}

private extension String {
    static let appCenterAppID = "YOUR_APPCENTER_APP_ID"
    static let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "No build number found"
    static let appCenterInstallIDHeaderKey = "Install-ID"
    static let appCenterSDKName = "appcenter.custom"
    static let appCenterSDKVersion = "1.0.0"
    static let appCenterSecretHeaderKey = "App-Secret"
    static let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "No version number found"
    static let attachmentErrorContentType = "application/octet-stream"
    static let attachmentErrorType = "errorAttachment"
    static let bundleIdentifier = Bundle.main.bundleIdentifier
    static let contentTypeApplicationJSON = "application/json"
    static let contentTypeHeaderKey = "Content-Type"
    static let customError = "CustomError"
    static let managedErrorType = "managedError"
    static let postHTTPMethod = "POST"
}

/// Returns the current device model name i.e. `iPhone12,1`
private func deviceModelName() -> String {
    var systemInfo = utsname()
    uname(&systemInfo)
    let machineMirror = Mirror(reflecting: systemInfo.machine)
    let identifier = machineMirror.children.reduce("") { identifier, element in
        guard let value = element.value as? Int8, value != 0 else { return identifier }
        return identifier + String(UnicodeScalar(UInt8(value)))
    }
    return identifier
}

/// Convenience encodable root payload used to send the crash report(s)
private struct ErrorsPayload: Encodable {
    let logs: [AppCenter.CustomError]
}
ghost commented 3 years ago

This issue has been automatically marked as stale because it has not had any activity for 60 days. It will be closed if no further activity occurs within 15 days of this comment.

4brunu commented 3 years ago

AFAIK this is still an issue and since there is no support for it

ghost commented 3 years ago

This issue has been automatically marked as stale because it has not had any activity for 60 days. It will be closed if no further activity occurs within 15 days of this comment.

nindim commented 3 years ago

Still an issue.

hrafnkellbaldurs commented 3 years ago

Please fix this, this is a major blocker

diogot commented 3 years ago

This issue is open for more than 2 years, it's one of the reasons we're moving to Sentry.

hrafnkellbaldurs commented 3 years ago

Just saw this: https://github.com/microsoft/appcenter-sdk-apple/releases/tag/4.3.0 [Feature] Add support for tracking handled errors with Crashes.trackError and Crashes.trackException APIs.

Has someone tried it?

DmitriyKirakosyan commented 3 years ago

Hi foks,

Yes, this feature is implemented and was included in the last release (4.3.0). So, I'm closing this issue.

There is a known bug though, with displaying a symbolicated error for a native iOS app. Until it is fixed, you can check the raw log in Error -> Reports -> Raw.