mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Rageshake Service (#70)
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// BugReportService.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 16.05.2022.
|
||||
// Copyright © 2022 element.io. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
import GZIP
|
||||
import Sentry
|
||||
|
||||
enum BugReportServiceError: Error {
|
||||
case invalidBaseUrlString
|
||||
case invalidSentryEndpoint
|
||||
}
|
||||
|
||||
class BugReportService: BugReportServiceProtocol {
|
||||
private let baseURL: URL
|
||||
private let sentryEndpoint: String
|
||||
private let applicationId: String
|
||||
private let session: URLSession
|
||||
|
||||
init(withBaseUrlString baseUrlString: String,
|
||||
sentryEndpoint: String,
|
||||
applicationId: String = BuildSettings.bugReportApplicationId,
|
||||
session: URLSession = .shared) throws {
|
||||
guard let url = URL(string: baseUrlString) else {
|
||||
throw BugReportServiceError.invalidBaseUrlString
|
||||
}
|
||||
guard !sentryEndpoint.isEmpty else {
|
||||
throw BugReportServiceError.invalidSentryEndpoint
|
||||
}
|
||||
self.baseURL = url
|
||||
self.sentryEndpoint = sentryEndpoint
|
||||
self.applicationId = applicationId
|
||||
self.session = session
|
||||
|
||||
// enable SentrySDK
|
||||
SentrySDK.start { options in
|
||||
options.dsn = sentryEndpoint
|
||||
#if DEBUG
|
||||
options.debug = true
|
||||
#endif
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production.
|
||||
options.tracesSampleRate = 1.0
|
||||
|
||||
options.beforeSend = { event in
|
||||
MXLog.error("Sentry detected crash: \(event)")
|
||||
return event
|
||||
}
|
||||
|
||||
options.onCrashedLastRun = { event in
|
||||
MXLog.debug("Sentry detected application was crashed: \(event)")
|
||||
}
|
||||
}
|
||||
|
||||
// also enable logging crashes, to send them with bug reports
|
||||
MXLogger.logCrashes(true)
|
||||
// set build version for logger
|
||||
MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString)
|
||||
}
|
||||
|
||||
// MARK: - BugReportServiceProtocol
|
||||
|
||||
var crashedLastRun: Bool {
|
||||
return SentrySDK.crashedLastRun
|
||||
}
|
||||
|
||||
func crash() {
|
||||
SentrySDK.crash()
|
||||
}
|
||||
|
||||
func submitBugReport(text: String,
|
||||
includeLogs: Bool,
|
||||
includeCrashLog: Bool,
|
||||
githubLabels: [String],
|
||||
files: [URL]) async throws -> SubmitBugReportResponse {
|
||||
MXLog.debug("[BugReportService] submitBugReport")
|
||||
|
||||
var params = [
|
||||
MultipartFormData(key: "text", type: .text(value: text))
|
||||
]
|
||||
params.append(contentsOf: defaultParams)
|
||||
for label in githubLabels {
|
||||
params.append(MultipartFormData(key: "label", type: .text(value: label)))
|
||||
}
|
||||
let zippedFiles = try await zipFiles(includeLogs: includeLogs,
|
||||
includeCrashLog: includeCrashLog)
|
||||
// log or compressed-log
|
||||
if !zippedFiles.isEmpty {
|
||||
for url in zippedFiles {
|
||||
params.append(MultipartFormData(key: "compressed-log", type: .file(url: url)))
|
||||
}
|
||||
}
|
||||
for url in files {
|
||||
params.append(MultipartFormData(key: "file", type: .file(url: url)))
|
||||
}
|
||||
|
||||
let boundary = "Boundary-\(UUID().uuidString)"
|
||||
var body = Data()
|
||||
for param in params {
|
||||
body.appendString(string: "--\(boundary)\r\n")
|
||||
body.appendString(string: "Content-Disposition:form-data; name=\"\(param.key)\"")
|
||||
switch param.type {
|
||||
case .text(let value):
|
||||
body.appendString(string: "\r\n\r\n\(value)\r\n")
|
||||
case .file(let url):
|
||||
body.appendString(string: "; filename=\"\(url.lastPathComponent)\"\r\n")
|
||||
body.appendString(string: "Content-Type: \"content-type header\"\r\n\r\n")
|
||||
body.append(try Data(contentsOf: url))
|
||||
body.appendString(string: "\r\n")
|
||||
}
|
||||
}
|
||||
body.appendString(string: "--\(boundary)--\r\n")
|
||||
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent("submit"))
|
||||
request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = body as Data
|
||||
|
||||
let (data, _) = try await session.data(for: request)
|
||||
|
||||
// Parse the JSON data
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let result = try decoder.decode(SubmitBugReportResponse.self, from: data)
|
||||
|
||||
if !result.reportUrl.isEmpty {
|
||||
MXLogger.deleteCrashLog()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var defaultParams: [MultipartFormData] {
|
||||
[
|
||||
MultipartFormData(key: "user_agent", type: .text(value: "iOS")),
|
||||
MultipartFormData(key: "app", type: .text(value: applicationId)),
|
||||
MultipartFormData(key: "version", type: .text(value: version)),
|
||||
MultipartFormData(key: "os", type: .text(value: os)),
|
||||
MultipartFormData(key: "client", type: .text(value: "Element-X"))
|
||||
]
|
||||
}
|
||||
|
||||
private var os: String {
|
||||
"\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)"
|
||||
}
|
||||
|
||||
private var version: String {
|
||||
ElementInfoPlist.cfBundleShortVersionString
|
||||
}
|
||||
|
||||
private func zipFiles(includeLogs: Bool,
|
||||
includeCrashLog: Bool) async throws -> [URL] {
|
||||
MXLog.debug("[BugReportService] zipFiles: includeLogs: \(includeLogs), includeCrashLog: \(includeCrashLog)")
|
||||
|
||||
var filesToCompress: [URL] = []
|
||||
if includeLogs, let logFiles = MXLogger.logFiles() {
|
||||
let urls = logFiles.compactMap { URL(fileURLWithPath: $0) }
|
||||
filesToCompress.append(contentsOf: urls)
|
||||
}
|
||||
if includeCrashLog, let crashLogFile = MXLogger.crashLog() {
|
||||
filesToCompress.append(URL(fileURLWithPath: crashLogFile))
|
||||
}
|
||||
|
||||
var totalSize: Int = 0
|
||||
var totalZippedSize: Int = 0
|
||||
var zippedFiles: [URL] = []
|
||||
|
||||
for url in filesToCompress {
|
||||
let zippedFileURL = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
.appendingPathComponent(url.lastPathComponent)
|
||||
|
||||
// remove old zipped file if exists
|
||||
try? FileManager.default.removeItem(at: zippedFileURL)
|
||||
|
||||
let rawData = try Data(contentsOf: url)
|
||||
if rawData.isEmpty {
|
||||
continue
|
||||
}
|
||||
guard let zippedData = (rawData as NSData).gzipped() else {
|
||||
continue
|
||||
}
|
||||
|
||||
totalSize += rawData.count
|
||||
totalZippedSize += zippedData.count
|
||||
|
||||
try zippedData.write(to: zippedFileURL)
|
||||
|
||||
zippedFiles.append(zippedFileURL)
|
||||
}
|
||||
|
||||
MXLog.debug("[BugReportService] zipFiles: totalSize: \(totalSize), totalZippedSize: \(totalZippedSize)")
|
||||
|
||||
return zippedFiles
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension Data {
|
||||
mutating func appendString(string: String, encoding: String.Encoding = .utf8) {
|
||||
if let data = string.data(using: encoding) {
|
||||
append(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MultipartFormData {
|
||||
let key: String
|
||||
let type: MultipartFormDataType
|
||||
}
|
||||
|
||||
private enum MultipartFormDataType {
|
||||
case text(value: String)
|
||||
case file(url: URL)
|
||||
}
|
||||
Reference in New Issue
Block a user