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) {
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController())
ServiceLocator.shared.register(appSettings: appSettings)
ServiceLocator.shared.register(bugReportService: BugReportService(baseURL: appSettings.bugReportServiceBaseURL,
ServiceLocator.shared.register(bugReportService: BugReportService(rageshakeURL: appSettings.bugReportRageshakeURL,
applicationID: appSettings.bugReportApplicationID,
sdkGitSHA: sdkGitSha(),
maxUploadSize: appSettings.bugReportMaxUploadSize,

View File

@@ -249,7 +249,7 @@ final class AppSettings {
// 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 bugReportSentryRustURL: URL? = Secrets.sentryRustDSN.map { URL(string: $0)! } // swiftlint:disable:this force_unwrapping
/// The name allocated by the bug report server

View File

@@ -2142,6 +2142,47 @@ class BugReportServiceMock: BugReportServiceProtocol, @unchecked Sendable {
var underlyingCrashedLastRun: Bool!
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
var submitBugReportProgressListenerUnderlyingCallsCount = 0

View File

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

View File

@@ -12,27 +12,30 @@ import Sentry
import UIKit
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 sdkGitSHA: String
private let maxUploadSize: Int
private let session: URLSession
private let appHooks: AppHooks
private let progressSubject = PassthroughSubject<Double, Never>()
private var cancellables = Set<AnyCancellable>()
var isEnabled: Bool { baseURL != nil }
var isEnabled: Bool { rageshakeURL != nil }
var lastCrashEventID: String?
init(baseURL: URL?,
init(rageshakeURL: URL?,
applicationID: String,
sdkGitSHA: String,
maxUploadSize: Int,
session: URLSession = .shared,
appHooks: AppHooks) {
self.baseURL = baseURL
defaultRageshakeURL = rageshakeURL
self.rageshakeURL = rageshakeURL
self.applicationID = applicationID
self.sdkGitSHA = sdkGitSHA
self.maxUploadSize = maxUploadSize
@@ -46,11 +49,22 @@ class BugReportService: NSObject, BugReportServiceProtocol {
var crashedLastRun: Bool {
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
func submitBugReport(_ bugReport: BugReport,
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.")
}
@@ -107,7 +121,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
}
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.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
protocol BugReportServiceProtocol: AnyObject {
var isEnabled: Bool { get }
@@ -53,6 +62,8 @@ protocol BugReportServiceProtocol: AnyObject {
var lastCrashEventID: String? { get set }
func applyConfiguration(_ configuration: RageshakeConfiguration)
func submitBugReport(_ bugReport: BugReport,
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")
postHogHost: String? = read?("env:POSTHOG_HOST")
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")
output {

View File

@@ -3,7 +3,7 @@ enum Secrets {
static let sentryRustDSN: String? = "https://username@sentry.localhost/project_id"
static let postHogHost: String? = "https://posthog.localhost"
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"
}

View File

@@ -42,7 +42,7 @@ class BugReportServiceTests: XCTestCase {
}
func testInitialStateWithRealService() throws {
let service = BugReportService(baseURL: "https://www.example.com",
let service = BugReportService(rageshakeURL: "https://example.com/submit",
applicationID: "mock_app_id",
sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
@@ -53,7 +53,7 @@ class BugReportServiceTests: XCTestCase {
}
func testInitialStateWithRealServiceAndNoURL() throws {
let service = BugReportService(baseURL: nil,
let service = BugReportService(rageshakeURL: nil,
applicationID: "mock_app_id",
sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
@@ -64,7 +64,7 @@ class BugReportServiceTests: XCTestCase {
}
@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",
sdkGitSHA: "1234",
maxUploadSize: ServiceLocator.shared.settings.bugReportMaxUploadSize,
@@ -86,6 +86,62 @@ class BugReportServiceTests: XCTestCase {
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() {
// Given a new set of logs
var logs = BugReportService.Logs(maxFileSize: 1000)
@@ -114,9 +170,11 @@ class BugReportServiceTests: XCTestCase {
private class MockURLProtocol: URLProtocol {
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),
let url = request.url,
let urlResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) {
client?.urlProtocol(self, didReceive: urlResponse, cacheStoragePolicy: .allowedInMemoryOnly)
client?.urlProtocol(self, didLoad: data)