Skip to content

Commit

Permalink
chore(ios): track and export http events
Browse files Browse the repository at this point in the history
  • Loading branch information
Adwin Ronald Ross committed Dec 18, 2024
1 parent 92f1f03 commit af14778
Show file tree
Hide file tree
Showing 29 changed files with 862 additions and 29 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 monitoring](features/feature_network_monitoring.md)
21 changes: 21 additions & 0 deletions docs/ios/features/feature_network_monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Feature - Network Monitoring

Measure SDK can capture network requests, responses, and failures along with useful metrics to help understand how APIs are performing in production from an end-user perspective. Network monitoring is currently supported for URLSession's data tasks.

### How It Works

Measure uses two techniques to intercept network requests. The first method relies on swizzling `NSURLSessionTask`'s `setState:` method, while the second method involves adding a custom implementation of `URLProtocol` to `URLSessionConfiguration`'s `protocolClasses`.

While the swizzling of the `setState:` method provides sufficient information about network requests, such as the request URL, headers, and status, it does not give access to the response data. However, the advantage of this approach is that no additional code needs to be added on the app side.

To address the limitation of not being able to track response objects, Measure also provides an option for developers to enable network tracking for a given `URLSession`. All you need to do is add the following code:

```swift
NetworkInterceptor.enable(on: configuration)
```

If the NetworkInterceptor is enabled for a particular URLSession, automated network tracking is disabled, and only the network requests of the enabled URLSession are tracked.

### Data collected

Checkout the data collected by Measure for each HTTP request in the [HTTP Event](../../api/sdk/README.md#http) section.
40 changes: 40 additions & 0 deletions ios/MeasureSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@
52159E2D2CC8FAA500486F54 /* TimeProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52159E2C2CC8FAA500486F54 /* TimeProviderTests.swift */; };
5224ECE02C88057A00D1B1F7 /* FatalErrorUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5224ECDF2C88057A00D1B1F7 /* FatalErrorUtil.swift */; };
5224ECE32C880FA400D1B1F7 /* XCTextCase+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5224ECE22C880FA300D1B1F7 /* XCTextCase+Extension.swift */; };
5225D02E2D088B7100FD240D /* HttpData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D0272D088B7100FD240D /* HttpData.swift */; };
5225D02F2D088B7100FD240D /* HttpEventCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D0282D088B7100FD240D /* HttpEventCollector.swift */; };
5225D0302D088B7100FD240D /* HttpInterceptorCallbacks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D0292D088B7100FD240D /* HttpInterceptorCallbacks.swift */; };
5225D0312D088B7100FD240D /* NetworkInterceptorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D02A2D088B7100FD240D /* NetworkInterceptorProtocol.swift */; };
5225D0322D088B7100FD240D /* URLSessionTaskInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D02B2D088B7100FD240D /* URLSessionTaskInterceptor.swift */; };
5225D0332D088B7100FD240D /* URLSessionTaskSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D02C2D088B7100FD240D /* URLSessionTaskSwizzler.swift */; };
5225D0352D0AEB1A00FD240D /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D0342D0AEB1A00FD240D /* String+Extension.swift */; };
5225D0502D0FECFF00FD240D /* InputStream+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5225D04F2D0FECFF00FD240D /* InputStream+Extension.swift */; };
5229D16E2CCB533C00EFFE44 /* RecentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5229D16D2CCB533C00EFFE44 /* RecentSession.swift */; };
523287692C85E07B000EE268 /* LifecycleObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523287682C85E07B000EE268 /* LifecycleObserverTests.swift */; };
523287732C86195E000EE268 /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523287722C86195E000EE268 /* SessionManagerTests.swift */; };
Expand Down Expand Up @@ -339,6 +347,14 @@
52159E2C2CC8FAA500486F54 /* TimeProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeProviderTests.swift; sourceTree = "<group>"; };
5224ECDF2C88057A00D1B1F7 /* FatalErrorUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorUtil.swift; sourceTree = "<group>"; };
5224ECE22C880FA300D1B1F7 /* XCTextCase+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTextCase+Extension.swift"; sourceTree = "<group>"; };
5225D0272D088B7100FD240D /* HttpData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpData.swift; sourceTree = "<group>"; };
5225D0282D088B7100FD240D /* HttpEventCollector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpEventCollector.swift; sourceTree = "<group>"; };
5225D0292D088B7100FD240D /* HttpInterceptorCallbacks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpInterceptorCallbacks.swift; sourceTree = "<group>"; };
5225D02A2D088B7100FD240D /* NetworkInterceptorProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkInterceptorProtocol.swift; sourceTree = "<group>"; };
5225D02B2D088B7100FD240D /* URLSessionTaskInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskInterceptor.swift; sourceTree = "<group>"; };
5225D02C2D088B7100FD240D /* URLSessionTaskSwizzler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskSwizzler.swift; sourceTree = "<group>"; };
5225D0342D0AEB1A00FD240D /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = "<group>"; };
5225D04F2D0FECFF00FD240D /* InputStream+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InputStream+Extension.swift"; sourceTree = "<group>"; };
5229D16D2CCB533C00EFFE44 /* RecentSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentSession.swift; sourceTree = "<group>"; };
523287682C85E07B000EE268 /* LifecycleObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifecycleObserverTests.swift; sourceTree = "<group>"; };
523287722C86195E000EE268 /* SessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManagerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -558,8 +574,10 @@
5202BE442C89600200A3496E /* Extensions */ = {
isa = PBXGroup;
children = (
5225D04F2D0FECFF00FD240D /* InputStream+Extension.swift */,
52A1A9472CA31E8E00461103 /* NSManagedObjectContext+Extension.swift */,
52AE72072CABAEAB00F2830A /* NSObject+Extension.swift */,
5225D0342D0AEB1A00FD240D /* String+Extension.swift */,
5202BE432C89600200A3496E /* UIDevice+Extension.swift */,
52D51A5B2CCE77A5008F30A6 /* UIViewController+Extension.swift */,
52AE72062CABAEAB00F2830A /* UIWindow+Extension.swift */,
Expand Down Expand Up @@ -611,6 +629,19 @@
path = Helper;
sourceTree = "<group>";
};
5225D02D2D088B7100FD240D /* Http */ = {
isa = PBXGroup;
children = (
5225D0272D088B7100FD240D /* HttpData.swift */,
5225D0282D088B7100FD240D /* HttpEventCollector.swift */,
5225D0292D088B7100FD240D /* HttpInterceptorCallbacks.swift */,
5225D02A2D088B7100FD240D /* NetworkInterceptorProtocol.swift */,
5225D02B2D088B7100FD240D /* URLSessionTaskInterceptor.swift */,
5225D02C2D088B7100FD240D /* URLSessionTaskSwizzler.swift */,
);
path = Http;
sourceTree = "<group>";
};
5232876A2C85E277000EE268 /* Utils */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -710,6 +741,7 @@
52A853382C994AD100B2A39F /* Exception */,
524576632CBFD18600B288E5 /* Exporter */,
52AE72002CABAE9000F2830A /* Gestures */,
5225D02D2D088B7100FD240D /* Http */,
52816B682CCE390800B160A4 /* Lifecycle */,
52A3C0752CDB732F00C8F047 /* Performance */,
524CC5DB2C6A4B48001AB506 /* Utils */,
Expand Down Expand Up @@ -1322,6 +1354,7 @@
52A1A93A2CA0702600461103 /* SessionEntity.swift in Sources */,
52CC63C92C9DE71300F7CA0A /* SystemCrashReporter.swift in Sources */,
5202BE392C895FC800A3496E /* AppAttributeProcessor.swift in Sources */,
5225D0352D0AEB1A00FD240D /* String+Extension.swift in Sources */,
524576772CC1366E00B288E5 /* EventExporter.swift in Sources */,
5202BE472C89600200A3496E /* UIDevice+Extension.swift in Sources */,
52AE72012CABAE9000F2830A /* GestureCollector.swift in Sources */,
Expand All @@ -1339,6 +1372,7 @@
5202BE7B2C8B117900A3496E /* Event.swift in Sources */,
5202BE7C2C8B117900A3496E /* EventProcessor.swift in Sources */,
5202BE482C89600200A3496E /* UserDefaultStorage.swift in Sources */,
5225D0502D0FECFF00FD240D /* InputStream+Extension.swift in Sources */,
5202BE7A2C8B117900A3496E /* AttachmentType.swift in Sources */,
528EAB8D2C80824200CB1574 /* Logger.swift in Sources */,
528EAB8F2C81B4C700CB1574 /* TimeProvider.swift in Sources */,
Expand All @@ -1350,10 +1384,12 @@
524576752CC11FDA00B288E5 /* BatchEntity.swift in Sources */,
524576692CC0021B00B288E5 /* NetworkClient.swift in Sources */,
52A1A9482CA31E8E00461103 /* NSManagedObjectContext+Extension.swift in Sources */,
5225D0302D088B7100FD240D /* HttpInterceptorCallbacks.swift in Sources */,
52AE72022CABAE9000F2830A /* GestureDetector.swift in Sources */,
52CC63C12C9C608E00F7CA0A /* CrashDataPersistence.swift in Sources */,
52EB380C2C8C7334002D63EC /* SignPost.swift in Sources */,
52A8533E2C994CC200B2A39F /* ExceptionDetail.swift in Sources */,
5225D02F2D088B7100FD240D /* HttpEventCollector.swift in Sources */,
526E30F62CE77BCB00F484B4 /* AppLaunchEvents.swift in Sources */,
52A853402C994D7900B2A39F /* Exception.swift in Sources */,
52159E272CC802A800486F54 /* EventSerializer.swift in Sources */,
Expand All @@ -1367,6 +1403,7 @@
5245766D2CC0DF0200B288E5 /* Heartbeat.swift in Sources */,
5202BE3F2C895FC800A3496E /* NetworkStateAttributeProcessor.swift in Sources */,
5229D16E2CCB533C00EFFE44 /* RecentSession.swift in Sources */,
5225D02E2D088B7100FD240D /* HttpData.swift in Sources */,
5245766B2CC0D55500B288E5 /* HttpModels.swift in Sources */,
52CC63C32C9C609F00F7CA0A /* CrashDataWriter.swift in Sources */,
52D51A9B2CCF5780008F30A6 /* MeasureViewController.swift in Sources */,
Expand All @@ -1378,17 +1415,20 @@
524576732CC116DD00B288E5 /* BatchStore.swift in Sources */,
52816B6A2CCE399B00B160A4 /* LifecycleCollector.swift in Sources */,
528EAB962C84553500CB1574 /* LifecycleObserver.swift in Sources */,
5225D0322D088B7100FD240D /* URLSessionTaskInterceptor.swift in Sources */,
52AE72032CABAE9000F2830A /* GestureEvents.swift in Sources */,
52A3C0782CDB732F00C8F047 /* CpuUsageData.swift in Sources */,
5202BE7D2C8B117900A3496E /* EventType.swift in Sources */,
52A8533C2C994BCE00B2A39F /* StackFrame.swift in Sources */,
5225D0332D088B7100FD240D /* URLSessionTaskSwizzler.swift in Sources */,
52CD911E2C7B397C000189BA /* BaseConfigProvider.swift in Sources */,
52FA6A802CE212320091F089 /* SysCtl.swift in Sources */,
52A853422C9983B900B2A39F /* CrashDataFormatter.swift in Sources */,
52A853372C983FFC00B2A39F /* CrashReportingManager.swift in Sources */,
52A3C0902CDC73AB00C8F047 /* MemoryUsageCollector.swift in Sources */,
52A1A9462CA1786A00461103 /* MeasureQueue.swift in Sources */,
52AE72092CABAEAB00F2830A /* NSObject+Extension.swift in Sources */,
5225D0312D088B7100FD240D /* NetworkInterceptorProtocol.swift in Sources */,
52CD91202C7B39AE000189BA /* Config.swift in Sources */,
52AE72082CABAEAB00F2830A /* UIWindow+Extension.swift in Sources */,
52D51A5E2CCE7BE8008F30A6 /* LifecycleManager.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<CommandLineArguments>
<CommandLineArgument
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "NO">
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLDebug 1"
Expand Down
26 changes: 24 additions & 2 deletions ios/MeasureSDK/CoreData/Entities/EventEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

struct EventEntity {
struct EventEntity { // swiftlint:disable:this type_body_length
let id: String
let sessionId: String
let timestamp: String
Expand All @@ -30,6 +30,7 @@ struct EventEntity {
let attachmentSize: Number
let timestampInMillis: Number
var batchId: String?
let http: Data?

init<T: Codable>(_ event: Event<T>) { // swiftlint:disable:this cyclomatic_complexity function_body_length
self.id = event.id
Expand Down Expand Up @@ -173,6 +174,17 @@ struct EventEntity {
self.hotLaunch = nil
}

if let http = event.data as? HttpData {
do {
let data = try JSONEncoder().encode(http)
self.http = data
} catch {
self.http = nil
}
} else {
self.http = nil
}

if let attributes = event.attributes {
do {
let data = try JSONEncoder().encode(attributes)
Expand Down Expand Up @@ -213,7 +225,8 @@ struct EventEntity {
memoryUsage: Data?,
coldLaunch: Data?,
warmLaunch: Data?,
hotLaunch: Data?) {
hotLaunch: Data?,
http: Data?) {
self.id = id
self.sessionId = sessionId
self.timestamp = timestamp
Expand All @@ -236,6 +249,7 @@ struct EventEntity {
self.coldLaunch = coldLaunch
self.warmLaunch = warmLaunch
self.hotLaunch = hotLaunch
self.http = http
}

func getEvent<T: Codable>() -> Event<T> { // swiftlint:disable:this cyclomatic_complexity function_body_length
Expand Down Expand Up @@ -338,6 +352,14 @@ struct EventEntity {
decodedData = nil
}
}
case .http:
if let httpData = self.http {
do {
decodedData = try JSONDecoder().decode(T.self, from: httpData)
} 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.http = event.http

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,
http: eventOb.http)
}
} 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,
http: eventOb.http)
}
} 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,
http: eventOb.http))
}
} catch {
guard let self = self else {
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 http
}
61 changes: 60 additions & 1 deletion ios/MeasureSDK/Exporter/EventSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

struct EventSerializer {
struct EventSerializer { // swiftlint:disable:this type_body_length
private func getSerialisedData(for event: EventEntity) -> String? { // swiftlint:disable:this cyclomatic_complexity function_body_length
let eventType = EventType(rawValue: event.type)
switch eventType {
Expand Down Expand Up @@ -131,6 +131,16 @@ struct EventSerializer {
}
}
return nil
case .http:
if let httpData = event.http {
do {
let decodedData = try JSONDecoder().decode(HttpData.self, from: httpData)
return serialiseHttpData(decodedData)
} catch {
return nil
}
}
return nil
case nil:
return nil
}
Expand Down Expand Up @@ -321,6 +331,55 @@ struct EventSerializer {
return result
}

private func serialiseHttpData(_ httpData: HttpData) -> String {
var result = "{"
result += "\"url\":\"\(httpData.url)\","
result += "\"method\":\"\(httpData.method)\","

if let statusCode = httpData.statusCode {
result += "\"status_code\":\"\(statusCode)\","
}

if let startTime = httpData.startTime {
result += "\"start_time\":\"\(startTime)\","
}

if let endTime = httpData.endTime {
result += "\"end_time\":\"\(endTime)\","
}

if let failureReason = httpData.failureReason {
result += "\"failure_reason\":\"\(failureReason)\","
}

if let failureDescription = httpData.failureDescription {
result += "\"failure_description\":\"\(failureDescription)\","
}

if let requestHeaders = httpData.requestHeaders {
let headers = requestHeaders.map { "\"\($0.key)\":\"\($0.value)\"" }.joined(separator: ",")
result += "\"request_headers\":{\(headers)},"
}

if let responseHeaders = httpData.responseHeaders {
let headers = responseHeaders.map { "\"\($0.key)\":\"\($0.value)\"" }.joined(separator: ",")
result += "\"response_headers\":{\(headers)},"
}

if let requestBody = httpData.requestBody {
result += "\"request_body\":\"\(requestBody)\","
}

if let responseBody = httpData.responseBody {
result += "\"response_body\":\"\(responseBody)\","
}

result += "\"client\":\"\(httpData.client)\""
result += "}"

return result
}

private func getSerialisedAttributes(for event: EventEntity) -> String? {
let decodedAttributes: Attributes?
if let attributeData = event.attributes {
Expand Down
1 change: 1 addition & 0 deletions ios/MeasureSDK/Exporter/HttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ final class BaseHttpClient: HttpClient {
self.configProvider = configProvider
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = configProvider.timeoutIntervalForRequest
// NetworkInterceptor.enable(on: configuration)
self.session = URLSession(configuration: configuration)
}

Expand Down
Loading

0 comments on commit af14778

Please sign in to comment.