Skip to content
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>
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
25 changes: 25 additions & 0 deletions Sources/Events/InitEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

struct InitEvent: Codable {
struct Props: Codable {
let event: String = "INIT"
let type: String = "None"
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)
}
}
33 changes: 12 additions & 21 deletions Sources/WalletConnectRelay/Dispatching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ final class Dispatcher: NSObject, Dispatching {
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 @@ -74,12 +73,17 @@ final class Dispatcher: NSObject, Dispatching {
return send(string, completion: completion)
}

// Always connect when there is a message to be sent
if !socket.isConnected {
socketConnectionHandler.handleInternalConnect()
}

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
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
cancellable?.cancel()
Expand Down Expand Up @@ -128,18 +132,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