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

Requests life cycle #1411

Open
wants to merge 23 commits into
base: develop
Choose a base branch
from
Open
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
54 changes: 54 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/RelayerTests.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "RelayerTests"
BuildableName = "RelayerTests"
BlueprintName = "RelayerTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@
"repositoryURL": "https://github.com/getsentry/sentry-cocoa.git",
"state": {
"branch": null,
"revision": "d2ced2d961b34573ebd2ea0567a2f1408e90f0ae",
"version": "8.34.0"
"revision": "5575af93efb776414f243e93d6af9f6258dc539a",
"version": "8.36.0"
}
},
{
Expand Down
6 changes: 4 additions & 2 deletions Example/RelayIntegrationTests/RelayClientEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,16 @@ final class RelayClientEndToEndTests: XCTestCase {
socketAuthenticator: socketAuthenticator
)

let socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, logger: logger)
let socketStatusProvider = SocketStatusProvider(socket: socket, logger: logger)
let socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, subscriptionsTracker: SubscriptionsTracker(), logger: logger, socketStatusProvider: socketStatusProvider)
let dispatcher = Dispatcher(
socketFactory: webSocketFactory,
relayUrlFactory: urlFactory,
networkMonitor: networkMonitor,
socket: socket,
logger: logger,
socketConnectionHandler: socketConnectionHandler
socketConnectionHandler: socketConnectionHandler,
socketStatusProvider: socketStatusProvider
)
let keychain = KeychainStorageMock()
let relayClient = RelayClientFactory.create(
Expand Down
47 changes: 38 additions & 9 deletions Sources/Events/EventsClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,28 @@ public class EventsClient: EventsClientProtocol {
private let logger: ConsoleLogging
private var stateStorage: TelemetryStateStorage
private let messageEventsStorage: MessageEventsStorage
private let initEventsStorage: InitEventsStorage

init(
eventsCollector: EventsCollector,
eventsDispatcher: EventsDispatcher,
logger: ConsoleLogging,
stateStorage: TelemetryStateStorage,
messageEventsStorage: MessageEventsStorage
messageEventsStorage: MessageEventsStorage,
initEventsStorage: InitEventsStorage
) {
self.eventsCollector = eventsCollector
self.eventsDispatcher = eventsDispatcher
self.logger = logger
self.stateStorage = stateStorage
self.messageEventsStorage = messageEventsStorage
self.initEventsStorage = initEventsStorage

if stateStorage.telemetryEnabled {
Task { await sendStoredEvents() }
} else {
if !stateStorage.telemetryEnabled {
self.eventsCollector.storage.clearErrorEvents()
}
saveInitEvent()
Task { await sendStoredEvents() }
}

public func setLogging(level: LoggingLevel) {
Expand Down Expand Up @@ -63,6 +66,30 @@ public class EventsClient: EventsClientProtocol {
messageEventsStorage.saveMessageEvent(event)
}

public func saveInitEvent() {
logger.debug("Will store an init event")

let bundleId = Bundle.main.bundleIdentifier ?? "Unknown"
let clientId = (try? Networking.interactor.getClientId()) ?? "Unknown"
let userAgent = EnvironmentInfo.userAgent

let props = InitEvent.Props(
properties: InitEvent.Properties(
clientId: clientId,
userAgent: userAgent
)
)

let event = InitEvent(
eventId: UUID().uuidString,
bundleId: bundleId,
timestamp: Int64(Date().timeIntervalSince1970 * 1000),
props: props
)

initEventsStorage.saveInitEvent(event)
}

// Public method to set telemetry enabled or disabled
public func setTelemetryEnabled(_ enabled: Bool) {
stateStorage.telemetryEnabled = enabled
Expand All @@ -78,24 +105,26 @@ public class EventsClient: EventsClientProtocol {

let traceEvents = eventsCollector.storage.fetchErrorEvents()
let messageEvents = messageEventsStorage.fetchMessageEvents()
let initEvents = initEventsStorage.fetchInitEvents()

guard !traceEvents.isEmpty || !messageEvents.isEmpty else { return }
guard !traceEvents.isEmpty || !messageEvents.isEmpty || !initEvents.isEmpty else { return }

var combinedEvents: [AnyCodable] = []

// Wrap trace events
combinedEvents.append(contentsOf: traceEvents.map { AnyCodable($0) })

// Wrap message events
combinedEvents.append(contentsOf: messageEvents.map { AnyCodable($0) })

combinedEvents.append(contentsOf: initEvents.map { AnyCodable($0) })

logger.debug("Will send combined events")
do {
let success: Bool = try await eventsDispatcher.executeWithRetry(events: combinedEvents)
if success {
logger.debug("Combined events sent successfully")
self.eventsCollector.storage.clearErrorEvents()
self.messageEventsStorage.clearMessageEvents()
eventsCollector.storage.clearErrorEvents()
messageEventsStorage.clearMessageEvents()
initEventsStorage.clearInitEvents()
}
} catch {
logger.debug("Failed to send events after multiple attempts: \(error)")
Expand Down
3 changes: 2 additions & 1 deletion Sources/Events/EventsClientFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public class EventsClientFactory {
eventsDispatcher: eventsDispatcher,
logger: logger,
stateStorage: UserDefaultsTelemetryStateStorage(),
messageEventsStorage: UserDefaultsMessageEventsStorage()
messageEventsStorage: UserDefaultsMessageEventsStorage(),
initEventsStorage: UserDefaultsInitEventsStorage()
)
}
}
Expand Down
26 changes: 26 additions & 0 deletions Sources/Events/InitEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

import Foundation

struct InitEvent: Codable {
struct Props: Codable {
let event: String = "INIT"

Check warning on line 6 in Sources/Events/InitEvent.swift

View workflow job for this annotation

GitHub Actions / prepare

immutable property will not be decoded because it is declared with an initial value which cannot be overwritten

Check warning on line 6 in Sources/Events/InitEvent.swift

View workflow job for this annotation

GitHub Actions / prepare

immutable property will not be decoded because it is declared with an initial value which cannot be overwritten
let type: String = "None"

Check warning on line 7 in Sources/Events/InitEvent.swift

View workflow job for this annotation

GitHub Actions / prepare

immutable property will not be decoded because it is declared with an initial value which cannot be overwritten

Check warning on line 7 in Sources/Events/InitEvent.swift

View workflow job for this annotation

GitHub Actions / prepare

immutable property will not be decoded because it is declared with an initial value which cannot be overwritten
let properties: Properties
}

struct Properties: Codable {
let clientId: String
let userAgent: String

// Custom CodingKeys to map Swift property names to JSON keys
enum CodingKeys: String, CodingKey {
case clientId = "client_id"
case userAgent = "user_agent"
}
}

let eventId: String
let bundleId: String
let timestamp: Int64
let props: Props
}
42 changes: 42 additions & 0 deletions Sources/Events/InitEventsStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

protocol InitEventsStorage {
func saveInitEvent(_ event: InitEvent)
func fetchInitEvents() -> [InitEvent]
func clearInitEvents()
}


class UserDefaultsInitEventsStorage: InitEventsStorage {
private let initEventsKey = "com.walletconnect.sdk.initEvents"
private let maxEvents = 100

func saveInitEvent(_ event: InitEvent) {
// Fetch existing events from UserDefaults
var existingEvents = fetchInitEvents()
existingEvents.append(event)

// Ensure we keep only the last 100 events
if existingEvents.count > maxEvents {
existingEvents = Array(existingEvents.suffix(maxEvents))
}

// Save updated events back to UserDefaults
if let encoded = try? JSONEncoder().encode(existingEvents) {
UserDefaults.standard.set(encoded, forKey: initEventsKey)
}
}

func fetchInitEvents() -> [InitEvent] {
if let data = UserDefaults.standard.data(forKey: initEventsKey),
let events = try? JSONDecoder().decode([InitEvent].self, from: data) {
// Return only the last 100 events
return Array(events.suffix(maxEvents))
}
return []
}

func clearInitEvents() {
UserDefaults.standard.removeObject(forKey: initEventsKey)
}
}
63 changes: 24 additions & 39 deletions Sources/WalletConnectRelay/Dispatching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ final class Dispatcher: NSObject, Dispatching {
var socket: WebSocketConnecting
var socketConnectionHandler: SocketConnectionHandler

private let defaultTimeout: Int = 5
private let defaultTimeout: Int = 15
private let relayUrlFactory: RelayUrlFactory
private let networkMonitor: NetworkMonitoring
private let logger: ConsoleLogging

private let socketConnectionStatusPublisherSubject = CurrentValueSubject<SocketConnectionStatus, Never>(.disconnected)
private let socketStatusProvider: SocketStatusProviding

var socketConnectionStatusPublisher: AnyPublisher<SocketConnectionStatus, Never> {
socketConnectionStatusPublisherSubject.eraseToAnyPublisher()
socketStatusProvider.socketConnectionStatusPublisher
}

var networkConnectionStatusPublisher: AnyPublisher<NetworkConnectionStatus, Never> {
Expand All @@ -45,18 +44,18 @@ final class Dispatcher: NSObject, Dispatching {
networkMonitor: NetworkMonitoring,
socket: WebSocketConnecting,
logger: ConsoleLogging,
socketConnectionHandler: SocketConnectionHandler
socketConnectionHandler: SocketConnectionHandler,
socketStatusProvider: SocketStatusProviding
) {
self.socketConnectionHandler = socketConnectionHandler
self.relayUrlFactory = relayUrlFactory
self.networkMonitor = networkMonitor
self.logger = logger

self.socket = socket
self.socketStatusProvider = socketStatusProvider

super.init()
setUpWebSocketSession()
setUpSocketConnectionObserving()
}

func send(_ string: String, completion: @escaping (Error?) -> Void) {
Expand All @@ -70,28 +69,27 @@ final class Dispatcher: NSObject, Dispatching {
}

func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) {
guard !socket.isConnected || !networkMonitor.isConnected else {
return send(string, completion: completion)
// Check if the socket is already connected and ready to send
if socket.isConnected && networkMonitor.isConnected {
send(string, completion: completion)
return
}

var cancellable: AnyCancellable?
cancellable = Publishers.CombineLatest(socketConnectionStatusPublisher, networkConnectionStatusPublisher)
.filter { $0.0 == .connected && $0.1 == .connected }
.setFailureType(to: NetworkError.self)
.timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .connectionFailed })
.sink(receiveCompletion: { [unowned self] result in
switch result {
case .failure(let error):
cancellable?.cancel()
completion(error)
case .finished: break
}
}, receiveValue: { [unowned self] _ in
cancellable?.cancel()
// Start the connection process if not already connected
Task {
do {
// Await the connection handler to establish the connection
try await socketConnectionHandler.handleInternalConnect()

// If successful, send the message
send(string, completion: completion)
})
} catch {
// If an error occurs during connection, complete with that error
completion(error)
}
}
}


func protectedSend(_ string: String) async throws {
var isResumed = false
Expand Down Expand Up @@ -128,18 +126,5 @@ extension Dispatcher {
}
}

private func setUpSocketConnectionObserving() {
socket.onConnect = { [unowned self] in
self.socketConnectionStatusPublisherSubject.send(.connected)
}
socket.onDisconnect = { [unowned self] error in
self.socketConnectionStatusPublisherSubject.send(.disconnected)
if error != nil {
self.socket.request.url = relayUrlFactory.create()
}
Task(priority: .high) {
await self.socketConnectionHandler.handleDisconnection()
}
}
}

}
Loading
Loading