Skip to content

Commit

Permalink
chore(ios): track and export network change events
Browse files Browse the repository at this point in the history
  • Loading branch information
Adwin Ronald Ross committed Dec 24, 2024
1 parent cd54d5e commit 6edc541
Show file tree
Hide file tree
Showing 15 changed files with 343 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/ios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
All the features supported by the Measure SDK are listed below.

* [App launch](features/feature_app_launch.md)
* [Network changes](features/feature_network_changes.md)
13 changes: 13 additions & 0 deletions docs/ios/features/feature_network_changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Feature - Network Changes

Measure SDK captures changes to network connection state of the device. This includes changes to the type of network connection (WiFi, Mobile, etc.), and the network generation (2G, 3G, 4G, etc.).

## How it works

To detect network changes, Measure relies on NWPathMonitor's `pathUpdateHandler`. The `pathUpdateHandler` provides a callback whenever the network changes between cellular and Wi-Fi. Additionally, we perform independent checks to detect if the connection is accessed via VPN or if no internet is available.

We rely on `CTTelephonyNetworkInfo` to get carrier data. However, starting from iOS 16.4, access to the carrier name is no longer available.

## Data collected

Checkout the data collected by Measure for each network change in the [Network Event](../../api/sdk/README.md#networkchange) section.
24 changes: 24 additions & 0 deletions ios/MeasureSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
52D51AA02CCFB0F6008F30A6 /* MSRViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D51A9C2CCFA917008F30A6 /* MSRViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
52D51ABB2CD0E5C9008F30A6 /* MsrMoniterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D51ABA2CD0E5C9008F30A6 /* MsrMoniterView.swift */; };
52D51ABD2CD2060A008F30A6 /* LifecycleCollectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D51ABC2CD2060A008F30A6 /* LifecycleCollectorTests.swift */; };
52E303E02D1814BA008FE733 /* NetworkChangeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E303DF2D1814BA008FE733 /* NetworkChangeData.swift */; };
52E303E22D181519008FE733 /* NetworkChangeCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E303E12D181519008FE733 /* NetworkChangeCollector.swift */; };
52E303E42D1818EA008FE733 /* NetworkChangeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E303E32D1818EA008FE733 /* NetworkChangeDetector.swift */; };
52E303E62D191A05008FE733 /* NetworkChangeCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E303E52D191A05008FE733 /* NetworkChangeCallback.swift */; };
52EB380C2C8C7334002D63EC /* SignPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EB380B2C8C7334002D63EC /* SignPost.swift */; };
52F3773B2CB41DDF006147E8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F3773A2CB41DDF006147E8 /* AppDelegate.swift */; };
52F3773D2CB41DE0006147E8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F3773C2CB41DDF006147E8 /* SceneDelegate.swift */; };
Expand Down Expand Up @@ -459,6 +463,10 @@
52D51A9D2CCFA917008F30A6 /* MSRViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSRViewController.m; sourceTree = "<group>"; };
52D51ABA2CD0E5C9008F30A6 /* MsrMoniterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsrMoniterView.swift; sourceTree = "<group>"; };
52D51ABC2CD2060A008F30A6 /* LifecycleCollectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifecycleCollectorTests.swift; sourceTree = "<group>"; };
52E303DF2D1814BA008FE733 /* NetworkChangeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkChangeData.swift; sourceTree = "<group>"; };
52E303E12D181519008FE733 /* NetworkChangeCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkChangeCollector.swift; sourceTree = "<group>"; };
52E303E32D1818EA008FE733 /* NetworkChangeDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkChangeDetector.swift; sourceTree = "<group>"; };
52E303E52D191A05008FE733 /* NetworkChangeCallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkChangeCallback.swift; sourceTree = "<group>"; };
52EB380B2C8C7334002D63EC /* SignPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignPost.swift; sourceTree = "<group>"; };
52F377382CB41DDF006147E8 /* DemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
52F3773A2CB41DDF006147E8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -711,6 +719,7 @@
524576632CBFD18600B288E5 /* Exporter */,
52AE72002CABAE9000F2830A /* Gestures */,
52816B682CCE390800B160A4 /* Lifecycle */,
52E303DE2D1814A7008FE733 /* NetworkChange */,
52A3C0752CDB732F00C8F047 /* Performance */,
524CC5DB2C6A4B48001AB506 /* Utils */,
524CC5D52C6A4B48001AB506 /* XCDataModel */,
Expand Down Expand Up @@ -957,6 +966,17 @@
path = SwiftUIDemo;
sourceTree = "<group>";
};
52E303DE2D1814A7008FE733 /* NetworkChange */ = {
isa = PBXGroup;
children = (
52E303DF2D1814BA008FE733 /* NetworkChangeData.swift */,
52E303E12D181519008FE733 /* NetworkChangeCollector.swift */,
52E303E32D1818EA008FE733 /* NetworkChangeDetector.swift */,
52E303E52D191A05008FE733 /* NetworkChangeCallback.swift */,
);
path = NetworkChange;
sourceTree = "<group>";
};
52EB37FF2C8B3688002D63EC /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1318,6 +1338,7 @@
5242B2A02C7F4AF600BE19F7 /* ClientInfo.swift in Sources */,
52CC63C52C9D714200F7CA0A /* SystemFileManager.swift in Sources */,
5202BE3A2C895FC800A3496E /* Attribute.swift in Sources */,
52E303E02D1814BA008FE733 /* NetworkChangeData.swift in Sources */,
52CC63C72C9D870B00F7CA0A /* CrashReport.swift in Sources */,
52A1A93A2CA0702600461103 /* SessionEntity.swift in Sources */,
52CC63C92C9DE71300F7CA0A /* SystemCrashReporter.swift in Sources */,
Expand Down Expand Up @@ -1383,9 +1404,12 @@
5202BE7D2C8B117900A3496E /* EventType.swift in Sources */,
52A8533C2C994BCE00B2A39F /* StackFrame.swift in Sources */,
52CD911E2C7B397C000189BA /* BaseConfigProvider.swift in Sources */,
52E303E22D181519008FE733 /* NetworkChangeCollector.swift in Sources */,
52FA6A802CE212320091F089 /* SysCtl.swift in Sources */,
52E303E42D1818EA008FE733 /* NetworkChangeDetector.swift in Sources */,
52A853422C9983B900B2A39F /* CrashDataFormatter.swift in Sources */,
52A853372C983FFC00B2A39F /* CrashReportingManager.swift in Sources */,
52E303E62D191A05008FE733 /* NetworkChangeCallback.swift in Sources */,
52A3C0902CDC73AB00C8F047 /* MemoryUsageCollector.swift in Sources */,
52A1A9462CA1786A00461103 /* MeasureQueue.swift in Sources */,
52AE72092CABAEAB00F2830A /* NSObject+Extension.swift in Sources */,
Expand Down
23 changes: 22 additions & 1 deletion ios/MeasureSDK/CoreData/Entities/EventEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct EventEntity {
let coldLaunch: Data?
let warmLaunch: Data?
let hotLaunch: Data?
let networkChange: Data?
let userTriggered: Bool
let attachmentSize: Number
let timestampInMillis: Number
Expand Down Expand Up @@ -173,6 +174,16 @@ struct EventEntity {
self.hotLaunch = nil
}

if let networkChange = event.data as? NetworkChangeData {
do {
let data = try JSONEncoder().encode(networkChange)
self.networkChange = data
} catch {
self.networkChange = nil
}
} else {
self.networkChange = nil
}
if let attributes = event.attributes {
do {
let data = try JSONEncoder().encode(attributes)
Expand Down Expand Up @@ -213,7 +224,8 @@ struct EventEntity {
memoryUsage: Data?,
coldLaunch: Data?,
warmLaunch: Data?,
hotLaunch: Data?) {
hotLaunch: Data?,
networkChange: Data?) {
self.id = id
self.sessionId = sessionId
self.timestamp = timestamp
Expand All @@ -236,6 +248,7 @@ struct EventEntity {
self.coldLaunch = coldLaunch
self.warmLaunch = warmLaunch
self.hotLaunch = hotLaunch
self.networkChange = networkChange
}

func getEvent<T: Codable>() -> Event<T> { // swiftlint:disable:this cyclomatic_complexity function_body_length
Expand Down Expand Up @@ -338,6 +351,14 @@ struct EventEntity {
decodedData = nil
}
}
case .networkChange:
if let networkChangeData = self.networkChange {
do {
decodedData = try JSONDecoder().decode(T.self, from: networkChangeData)
} catch {
decodedData = nil
}
}
case nil:
decodedData = nil
}
Expand Down
10 changes: 7 additions & 3 deletions ios/MeasureSDK/CoreData/EventStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ final class BaseEventStore: EventStore {
eventOb.coldLaunch = event.coldLaunch
eventOb.warmLaunch = event.warmLaunch
eventOb.hotLaunch = event.hotLaunch
eventOb.networkChange = event.networkChange

do {
try context.saveIfNeeded()
Expand Down Expand Up @@ -93,7 +94,8 @@ final class BaseEventStore: EventStore {
memoryUsage: eventOb.memoryUsage,
coldLaunch: eventOb.coldLaunch,
warmLaunch: eventOb.warmLaunch,
hotLaunch: eventOb.hotLaunch)
hotLaunch: eventOb.hotLaunch,
networkChange: eventOb.networkChange)
}
} catch {
guard let self = self else { return }
Expand Down Expand Up @@ -134,7 +136,8 @@ final class BaseEventStore: EventStore {
memoryUsage: eventOb.memoryUsage,
coldLaunch: eventOb.coldLaunch,
warmLaunch: eventOb.warmLaunch,
hotLaunch: eventOb.hotLaunch)
hotLaunch: eventOb.hotLaunch,
networkChange: eventOb.networkChange)
}
} catch {
guard let self = self else { return }
Expand Down Expand Up @@ -194,7 +197,8 @@ final class BaseEventStore: EventStore {
memoryUsage: eventOb.memoryUsage,
coldLaunch: eventOb.coldLaunch,
warmLaunch: eventOb.warmLaunch,
hotLaunch: eventOb.hotLaunch))
hotLaunch: eventOb.hotLaunch,
networkChange: eventOb.networkChange))
}
} catch {
guard let self = self else {
Expand Down
1 change: 1 addition & 0 deletions ios/MeasureSDK/Events/EventProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ final class BaseEventProcessor: EventProcessor {
eventStore.insertEvent(event: eventEntity)
sessionManager.onEventTracked(eventEntity)
logger.log(level: .debug, message: "Event processed: \(type), \(event.id)", error: nil, data: data)
dump(data)
}

private func createEvent<T: Codable>( // swiftlint:disable:this function_parameter_count
Expand Down
1 change: 1 addition & 0 deletions ios/MeasureSDK/Events/EventType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ enum EventType: String, Codable {
case coldLaunch = "cold_launch"
case warmLaunch = "warm_launch"
case hotLaunch = "hot_launch"
case networkChange = "network_change"
}
20 changes: 20 additions & 0 deletions ios/MeasureSDK/Exporter/EventSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ struct EventSerializer {
}
}
return nil
case .networkChange:
if let networkChangeData = event.networkChange {
do {
let decodedData = try JSONDecoder().decode(NetworkChangeData.self, from: networkChangeData)
return serialiseNetworkChangeData(decodedData)
} catch {
return nil
}
}
return nil
case nil:
return nil
}
Expand Down Expand Up @@ -321,6 +331,16 @@ struct EventSerializer {
return result
}

private func serialiseNetworkChangeData(_ networkChangeData: NetworkChangeData) -> String {
var result = "{"
result += "\"network_type\":\(networkChangeData.networkType),"
result += "\"network_provider\":\(networkChangeData.networkProvider),"
result += "\"network_generation\":\"\(networkChangeData.networkGeneration)\","
result += "\"previous_network_type\":\(networkChangeData.previousNetworkType),"
result += "\"previous_network_generation\":\"\(networkChangeData.previousNetworkGeneration)\""
result += "}"
return result
}
private func getSerialisedAttributes(for event: EventEntity) -> String? {
let decodedAttributes: Attributes?
if let attributeData = event.attributes {
Expand Down
5 changes: 5 additions & 0 deletions ios/MeasureSDK/MeasureInitializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ protocol MeasureInitializer {
var memoryUsageCalculator: MemoryUsageCalculator { get }
var sysCtl: SysCtl { get }
var appLaunchCollector: AppLaunchCollector { get }
var networkChangeCollector: NetworkChangeCollector { get }
}

/// `BaseMeasureInitializer` is responsible for setting up the internal configuration
Expand Down Expand Up @@ -128,6 +129,7 @@ final class BaseMeasureInitializer: MeasureInitializer {
let memoryUsageCalculator: MemoryUsageCalculator
let sysCtl: SysCtl
let appLaunchCollector: AppLaunchCollector
let networkChangeCollector: NetworkChangeCollector

init(config: MeasureConfig, // swiftlint:disable:this function_body_length
client: Client) {
Expand Down Expand Up @@ -236,6 +238,9 @@ final class BaseMeasureInitializer: MeasureInitializer {
sysCtl: sysCtl,
userDefaultStorage: userDefaultStorage,
currentAppVersion: appVersion)
self.networkChangeCollector = BaseNetworkChangeCollector(logger: logger,
eventProcessor: eventProcessor,
timeProvider: timeProvider)
self.client = client
}
}
4 changes: 4 additions & 0 deletions ios/MeasureSDK/MeasureInternal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ final class MeasureInternal {
private var appLaunchCollector: AppLaunchCollector {
return measureInitializer.appLaunchCollector
}
private var networkChangeCollector: NetworkChangeCollector {
return measureInitializer.networkChangeCollector
}
private let lifecycleObserver: LifecycleObserver

init(_ measureInitializer: MeasureInitializer) {
Expand All @@ -123,6 +126,7 @@ final class MeasureInternal {
self.lifecycleCollector.enable()
self.cpuUsageCollector.enable()
self.memoryUsageCollector.enable()
self.networkChangeCollector.enable()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
if let window = UIApplication.shared.windows.first {
self.gestureCollector.enable(for: window)
Expand Down
16 changes: 16 additions & 0 deletions ios/MeasureSDK/NetworkChange/NetworkChangeCallback.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// NetworkChangeCallback.swift
// MeasureSDK
//
// Created by Adwin Ross on 23/12/24.
//

import Foundation

final class NetworkChangeCallback {
var onNetworkChangeCallback: ((_ data: NetworkChangeData) -> Void)?

func onNetworkChange(_ data: NetworkChangeData) {
onNetworkChangeCallback?(data)
}
}
43 changes: 43 additions & 0 deletions ios/MeasureSDK/NetworkChange/NetworkChangeCollector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// NetworkChangeCollector.swift
// MeasureSDK
//
// Created by Adwin Ross on 20/12/24.
//

import Foundation

protocol NetworkChangeCollector {
func enable()
}

final class BaseNetworkChangeCollector: NetworkChangeCollector {
private let logger: Logger
private let eventProcessor: EventProcessor
private let timeProvider: TimeProvider
private let networkChangeDetector: NetworkChangeDetector
private let networkChangeCallback: NetworkChangeCallback

init(logger: Logger, eventProcessor: EventProcessor, timeProvider: TimeProvider) {
self.logger = logger
self.eventProcessor = eventProcessor
self.timeProvider = timeProvider
self.networkChangeCallback = NetworkChangeCallback()
self.networkChangeDetector = BaseNetworkChangeDetector(networkChangeCallback: self.networkChangeCallback)
self.networkChangeCallback.onNetworkChangeCallback = onNetworkChangeCallback(_:)
}

func enable() {
logger.internalLog(level: .debug, message: "GestureCollector enabled", error: nil, data: nil)
networkChangeDetector.start()
}

func onNetworkChangeCallback(_ data: NetworkChangeData) {
eventProcessor.track(data: data,
timestamp: timeProvider.now(),
type: .networkChange,
attributes: nil,
sessionId: nil,
attachments: nil)
}
}
43 changes: 43 additions & 0 deletions ios/MeasureSDK/NetworkChange/NetworkChangeData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// NetworkChangeData.swift
// MeasureSDK
//
// Created by Adwin Ross on 20/12/24.
//

import Foundation

/// Represents the data related to network changes.
class NetworkChangeData: Codable {
/// The [NetworkType] of the network that was previously active. This is null if there was no previously active network.
let previousNetworkType: NetworkType

/// The [NetworkType] of the network that is now active.
let networkType: NetworkType

/// The [NetworkGeneration] of the network that was previously active. Only set for cellular networks.
let previousNetworkGeneration: NetworkGeneration

/// The [NetworkGeneration] of the network that is now active.
let networkGeneration: NetworkGeneration

/// The name of the network provider that is now active. Only set for cellular networks.
let networkProvider: String

/// Coding keys to map snake_case JSON keys to camelCase properties.
private enum CodingKeys: String, CodingKey {
case previousNetworkType = "previous_network_type"
case networkType = "network_type"
case previousNetworkGeneration = "previous_network_generation"
case networkGeneration = "network_generation"
case networkProvider = "network_provider"
}

init(previousNetworkType: NetworkType, networkType: NetworkType, previousNetworkGeneration: NetworkGeneration, networkGeneration: NetworkGeneration, networkProvider: String) {
self.previousNetworkType = previousNetworkType
self.networkType = networkType
self.previousNetworkGeneration = previousNetworkGeneration
self.networkGeneration = networkGeneration
self.networkProvider = networkProvider
}
}
Loading

0 comments on commit 6edc541

Please sign in to comment.