Support runtime customisation of the rageshake URL. (#4267)

This commit is contained in:
Doug
2025-06-30 10:18:25 +01:00
committed by GitHub
parent 08744fde73
commit e882b9e7b8
10 changed files with 143 additions and 19 deletions

View File

@@ -372,7 +372,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private static func setupServiceLocator(appSettings: AppSettings, appHooks: AppHooks) { private static func setupServiceLocator(appSettings: AppSettings, appHooks: AppHooks) {
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController()) ServiceLocator.shared.register(userIndicatorController: UserIndicatorController())
ServiceLocator.shared.register(appSettings: appSettings) ServiceLocator.shared.register(appSettings: appSettings)
ServiceLocator.shared.register(bugReportService: BugReportService(baseURL: appSettings.bugReportServiceBaseURL, ServiceLocator.shared.register(bugReportService: BugReportService(rageshakeURL: appSettings.bugReportRageshakeURL,
applicationID: appSettings.bugReportApplicationID, applicationID: appSettings.bugReportApplicationID,
sdkGitSHA: sdkGitSha(), sdkGitSHA: sdkGitSha(),
maxUploadSize: appSettings.bugReportMaxUploadSize, maxUploadSize: appSettings.bugReportMaxUploadSize,

View File

@@ -249,7 +249,7 @@ final class AppSettings {
// MARK: - Bug report // MARK: - Bug report
let bugReportServiceBaseURL: URL? = Secrets.rageshakeServerURL.map { URL(string: $0)! } // swiftlint:disable:this force_unwrapping let bugReportRageshakeURL: URL? = Secrets.rageshakeURL.map { URL(string: $0)! } // swiftlint:disable:this force_unwrapping
let bugReportSentryURL: URL? = Secrets.sentryDSN.map { URL(string: $0)! } // swiftlint:disable:this force_unwrapping let bugReportSentryURL: URL? = Secrets.sentryDSN.map { URL(string: $0)! } // swiftlint:disable:this force_unwrapping
let bugReportSentryRustURL: URL? = Secrets.sentryRustDSN.map { URL(string: $0)! } // swiftlint:disable:this force_unwrapping let bugReportSentryRustURL: URL? = Secrets.sentryRustDSN.map { URL(string: $0)! } // swiftlint:disable:this force_unwrapping
/// The name allocated by the bug report server /// The name allocated by the bug report server

View File

@@ -2142,6 +2142,47 @@ class BugReportServiceMock: BugReportServiceProtocol, @unchecked Sendable {
var underlyingCrashedLastRun: Bool! var underlyingCrashedLastRun: Bool!
var lastCrashEventID: String? var lastCrashEventID: String?
//MARK: - applyConfiguration
var applyConfigurationUnderlyingCallsCount = 0
var applyConfigurationCallsCount: Int {
get {
if Thread.isMainThread {
return applyConfigurationUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = applyConfigurationUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
applyConfigurationUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
applyConfigurationUnderlyingCallsCount = newValue
}
}
}
}
var applyConfigurationCalled: Bool {
return applyConfigurationCallsCount > 0
}
var applyConfigurationReceivedConfiguration: RageshakeConfiguration?
var applyConfigurationReceivedInvocations: [RageshakeConfiguration] = []
var applyConfigurationClosure: ((RageshakeConfiguration) -> Void)?
func applyConfiguration(_ configuration: RageshakeConfiguration) {
applyConfigurationCallsCount += 1
applyConfigurationReceivedConfiguration = configuration
DispatchQueue.main.async {
self.applyConfigurationReceivedInvocations.append(configuration)
}
applyConfigurationClosure?(configuration)
}
//MARK: - submitBugReport //MARK: - submitBugReport
var submitBugReportProgressListenerUnderlyingCallsCount = 0 var submitBugReportProgressListenerUnderlyingCallsCount = 0

View File

@@ -191,7 +191,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
switch await widgetDriver.start(baseURL: baseURL, switch await widgetDriver.start(baseURL: baseURL,
clientID: clientID, clientID: clientID,
colorScheme: colorScheme, colorScheme: colorScheme,
rageshakeURL: appSettings.bugReportServiceBaseURL?.absoluteString, rageshakeURL: appSettings.bugReportRageshakeURL?.absoluteString,
analyticsConfiguration: analyticsConfiguration) { analyticsConfiguration: analyticsConfiguration) {
case .success(let url): case .success(let url):
state.url = url state.url = url

View File

@@ -12,27 +12,30 @@ import Sentry
import UIKit import UIKit
class BugReportService: NSObject, BugReportServiceProtocol { class BugReportService: NSObject, BugReportServiceProtocol {
private let baseURL: URL? /// The rageshake URL as provided in the init.
private let defaultRageshakeURL: URL?
/// The rageshake URL currently being used by the service.
private var rageshakeURL: URL?
private let applicationID: String private let applicationID: String
private let sdkGitSHA: String private let sdkGitSHA: String
private let maxUploadSize: Int private let maxUploadSize: Int
private let session: URLSession private let session: URLSession
private let appHooks: AppHooks private let appHooks: AppHooks
private let progressSubject = PassthroughSubject<Double, Never>() private let progressSubject = PassthroughSubject<Double, Never>()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
var isEnabled: Bool { baseURL != nil } var isEnabled: Bool { rageshakeURL != nil }
var lastCrashEventID: String? var lastCrashEventID: String?
init(baseURL: URL?, init(rageshakeURL: URL?,
applicationID: String, applicationID: String,
sdkGitSHA: String, sdkGitSHA: String,
maxUploadSize: Int, maxUploadSize: Int,
session: URLSession = .shared, session: URLSession = .shared,
appHooks: AppHooks) { appHooks: AppHooks) {
self.baseURL = baseURL defaultRageshakeURL = rageshakeURL
self.rageshakeURL = rageshakeURL
self.applicationID = applicationID self.applicationID = applicationID
self.sdkGitSHA = sdkGitSHA self.sdkGitSHA = sdkGitSHA
self.maxUploadSize = maxUploadSize self.maxUploadSize = maxUploadSize
@@ -46,11 +49,22 @@ class BugReportService: NSObject, BugReportServiceProtocol {
var crashedLastRun: Bool { var crashedLastRun: Bool {
SentrySDK.crashedLastRun SentrySDK.crashedLastRun
} }
func applyConfiguration(_ configuration: RageshakeConfiguration) {
switch configuration {
case .url(let url):
rageshakeURL = url
case .disabled:
rageshakeURL = nil
case .default:
rageshakeURL = defaultRageshakeURL
}
}
// swiftlint:disable:next cyclomatic_complexity // swiftlint:disable:next cyclomatic_complexity
func submitBugReport(_ bugReport: BugReport, func submitBugReport(_ bugReport: BugReport,
progressListener: CurrentValueSubject<Double, Never>) async -> Result<SubmitBugReportResponse, BugReportServiceError> { progressListener: CurrentValueSubject<Double, Never>) async -> Result<SubmitBugReportResponse, BugReportServiceError> {
guard let baseURL else { guard let rageshakeURL else {
fatalError("No bug report URL set, the screen should not be shown in this case.") fatalError("No bug report URL set, the screen should not be shown in this case.")
} }
@@ -107,7 +121,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
} }
body.appendString(string: "--\(boundary)--\r\n") body.appendString(string: "--\(boundary)--\r\n")
var request = URLRequest(url: baseURL) var request = URLRequest(url: rageshakeURL)
request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST" request.httpMethod = "POST"

View File

@@ -46,6 +46,15 @@ enum BugReportServiceError: LocalizedError {
} }
} }
enum RageshakeConfiguration {
/// Rageshakes should be sent to the provided URL
case url(URL)
/// Rageshakes are disabled.
case disabled
/// No customisations are made, use the default configuration.
case `default`
}
// sourcery: AutoMockable // sourcery: AutoMockable
protocol BugReportServiceProtocol: AnyObject { protocol BugReportServiceProtocol: AnyObject {
var isEnabled: Bool { get } var isEnabled: Bool { get }
@@ -53,6 +62,8 @@ protocol BugReportServiceProtocol: AnyObject {
var lastCrashEventID: String? { get set } var lastCrashEventID: String? { get set }
func applyConfiguration(_ configuration: RageshakeConfiguration)
func submitBugReport(_ bugReport: BugReport, func submitBugReport(_ bugReport: BugReport,
progressListener: CurrentValueSubject<Double, Never>) async -> Result<SubmitBugReportResponse, BugReportServiceError> progressListener: CurrentValueSubject<Double, Never>) async -> Result<SubmitBugReportResponse, BugReportServiceError>
} }

View File

@@ -13,7 +13,7 @@ sentryDSN: String? = read?("env:SENTRY_DSN")
sentryRustDSN: String? = read?("env:SENTRY_RUST_DSN") sentryRustDSN: String? = read?("env:SENTRY_RUST_DSN")
postHogHost: String? = read?("env:POSTHOG_HOST") postHogHost: String? = read?("env:POSTHOG_HOST")
postHogAPIKey: String? = read?("env:POSTHOG_API_KEY") postHogAPIKey: String? = read?("env:POSTHOG_API_KEY")
rageshakeServerURL: String? = read?("env:RAGESHAKE_SERVER_URL") rageshakeURL: String? = read?("env:RAGESHAKE_URL")
mapLibreAPIKey: String? = read?("env:MAPLIBRE_API_KEY") mapLibreAPIKey: String? = read?("env:MAPLIBRE_API_KEY")
output { output {

View File

@@ -3,7 +3,7 @@ enum Secrets {
static let sentryRustDSN: String? = "https://username@sentry.localhost/project_id" static let sentryRustDSN: String? = "https://username@sentry.localhost/project_id"
static let postHogHost: String? = "https://posthog.localhost" static let postHogHost: String? = "https://posthog.localhost"
static let postHogAPIKey: String? = "your_key" static let postHogAPIKey: String? = "your_key"
static let rageshakeServerURL: String? = "https://rageshake.localhost" static let rageshakeURL: String? = "https://rageshake.localhost/submit"
static let mapLibreAPIKey: String? = "your_key" static let mapLibreAPIKey: String? = "your_key"
} }

View File

@@ -42,7 +42,7 @@ class BugReportServiceTests: XCTestCase {
} }
func testInitialStateWithRealService() throws { func testInitialStateWithRealService() throws {
let service = BugReportService(baseURL: "https://www.example.com", let service = BugReportService(rageshakeURL: "https://example.com/submit",
applicationID: "mock_app_id", applicationID: "mock_app_id",
sdkGitSHA: "1234", sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize, maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
@@ -53,7 +53,7 @@ class BugReportServiceTests: XCTestCase {
} }
func testInitialStateWithRealServiceAndNoURL() throws { func testInitialStateWithRealServiceAndNoURL() throws {
let service = BugReportService(baseURL: nil, let service = BugReportService(rageshakeURL: nil,
applicationID: "mock_app_id", applicationID: "mock_app_id",
sdkGitSHA: "1234", sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize, maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
@@ -64,7 +64,7 @@ class BugReportServiceTests: XCTestCase {
} }
@MainActor func testSubmitBugReportWithRealService() async throws { @MainActor func testSubmitBugReportWithRealService() async throws {
let service = BugReportService(baseURL: "https://www.example.com", let service = BugReportService(rageshakeURL: "https://example.com/submit",
applicationID: "mock_app_id", applicationID: "mock_app_id",
sdkGitSHA: "1234", sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize, maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
@@ -86,6 +86,62 @@ class BugReportServiceTests: XCTestCase {
XCTAssertEqual(response.reportURL, "https://example.com/123") XCTAssertEqual(response.reportURL, "https://example.com/123")
} }
@MainActor func testConfigurations() async throws {
let service = BugReportService(rageshakeURL: "https://example.com/submit",
applicationID: "mock_app_id",
sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
session: .mock,
appHooks: AppHooks())
XCTAssertTrue(service.isEnabled)
service.applyConfiguration(.disabled)
XCTAssertFalse(service.isEnabled)
service.applyConfiguration(.url("https://bugs.server.net/submit"))
XCTAssertTrue(service.isEnabled)
let bugReport = BugReport(userID: "@mock:client.com",
deviceID: nil,
ed25519: nil,
curve25519: nil,
text: "i cannot send message",
logFiles: Tracing.logFiles,
canContact: false,
githubLabels: [],
files: [])
let progressSubject = CurrentValueSubject<Double, Never>(0.0)
let customConfigurationResponse = try await service.submitBugReport(bugReport, progressListener: progressSubject).get()
XCTAssertEqual(customConfigurationResponse.reportURL, "https://bugs.server.net/123")
service.applyConfiguration(.default)
XCTAssertTrue(service.isEnabled)
let defaultConfigurationResponse = try await service.submitBugReport(bugReport, progressListener: progressSubject).get()
XCTAssertEqual(defaultConfigurationResponse.reportURL, "https://example.com/123")
}
func testDisabledConfigurations() {
let service = BugReportService(rageshakeURL: nil,
applicationID: "mock_app_id",
sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
session: .mock,
appHooks: AppHooks())
XCTAssertFalse(service.isEnabled)
service.applyConfiguration(.disabled)
XCTAssertFalse(service.isEnabled)
service.applyConfiguration(.url("https://bugs.server.net/submit"))
XCTAssertTrue(service.isEnabled)
service.applyConfiguration(.default)
XCTAssertFalse(service.isEnabled)
}
func testLogsMaxSize() { func testLogsMaxSize() {
// Given a new set of logs // Given a new set of logs
var logs = BugReportService.Logs(maxFileSize: 1000) var logs = BugReportService.Logs(maxFileSize: 1000)
@@ -114,9 +170,11 @@ class BugReportServiceTests: XCTestCase {
private class MockURLProtocol: URLProtocol { private class MockURLProtocol: URLProtocol {
override func startLoading() { override func startLoading() {
let response = "{\"report_url\":\"https://example.com/123\"}" guard let url = request.url else { return }
let reportURL = url.deletingLastPathComponent().appending(path: "123")
let response = "{\"report_url\":\"\(reportURL.absoluteString)\"}"
if let data = response.data(using: .utf8), if let data = response.data(using: .utf8),
let url = request.url,
let urlResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) { let urlResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) {
client?.urlProtocol(self, didReceive: urlResponse, cacheStoragePolicy: .allowedInMemoryOnly) client?.urlProtocol(self, didReceive: urlResponse, cacheStoragePolicy: .allowedInMemoryOnly)
client?.urlProtocol(self, didLoad: data) client?.urlProtocol(self, didLoad: data)