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

chore(ios): track and export http events #1612

Open
wants to merge 1 commit into
base: main
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
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.
44 changes: 44 additions & 0 deletions ios/MeasureSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,17 @@
52159E292CC8091E00486F54 /* EventSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52159E282CC8091E00486F54 /* EventSerializerTests.swift */; };
52159E2B2CC81A2D00486F54 /* TestDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52159E2A2CC81A2D00486F54 /* TestDataGenerator.swift */; };
52159E2D2CC8FAA500486F54 /* TimeProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52159E2C2CC8FAA500486F54 /* TimeProviderTests.swift */; };
5222C9E82D14605000B198DA /* NetworkInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222C9E72D14605000B198DA /* NetworkInterceptor.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 @@ -337,8 +346,17 @@
52159E282CC8091E00486F54 /* EventSerializerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSerializerTests.swift; sourceTree = "<group>"; };
52159E2A2CC81A2D00486F54 /* TestDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDataGenerator.swift; sourceTree = "<group>"; };
52159E2C2CC8FAA500486F54 /* TimeProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeProviderTests.swift; sourceTree = "<group>"; };
5222C9E72D14605000B198DA /* NetworkInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInterceptor.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 +576,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 +631,20 @@
path = Helper;
sourceTree = "<group>";
};
5225D02D2D088B7100FD240D /* Http */ = {
isa = PBXGroup;
children = (
5225D0272D088B7100FD240D /* HttpData.swift */,
5225D0282D088B7100FD240D /* HttpEventCollector.swift */,
5225D0292D088B7100FD240D /* HttpInterceptorCallbacks.swift */,
5222C9E72D14605000B198DA /* NetworkInterceptor.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 +744,7 @@
52A853382C994AD100B2A39F /* Exception */,
524576632CBFD18600B288E5 /* Exporter */,
52AE72002CABAE9000F2830A /* Gestures */,
5225D02D2D088B7100FD240D /* Http */,
52816B682CCE390800B160A4 /* Lifecycle */,
52A3C0752CDB732F00C8F047 /* Performance */,
524CC5DB2C6A4B48001AB506 /* Utils */,
Expand Down Expand Up @@ -1322,6 +1357,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 +1375,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,12 +1387,15 @@
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 */,
5222C9E82D14605000B198DA /* NetworkInterceptor.swift in Sources */,
52159E272CC802A800486F54 /* EventSerializer.swift in Sources */,
528EAB892C804AA100CB1574 /* SessionManager.swift in Sources */,
52CD91262C7C3D90000189BA /* MeasureInitializer.swift in Sources */,
Expand All @@ -1367,6 +1407,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 +1419,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
8 changes: 8 additions & 0 deletions ios/MeasureSDK/Config/BaseConfigProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ final class BaseConfigProvider: ConfigProvider {
return getMergedConfig(\.cpuTrackingIntervalMs)
}

var httpContentTypeAllowlist: [String] {
return getMergedConfig(\.httpContentTypeAllowlist)
}

var defaultHttpHeadersBlocklist: [String] {
return getMergedConfig(\.defaultHttpHeadersBlocklist)
}

private func getMergedConfig<T>(_ keyPath: KeyPath<Config, T>) -> T {
if let networkConfig = networkConfig {
return networkConfig[keyPath: keyPath]
Expand Down
9 changes: 9 additions & 0 deletions ios/MeasureSDK/Config/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ struct Config: InternalConfig, MeasureConfig {
let maxSessionDurationMs: Number
let cpuTrackingIntervalMs: UnsignedNumber
let memoryTrackingIntervalMs: UnsignedNumber
let httpContentTypeAllowlist: [String]
let defaultHttpHeadersBlocklist: [String]

internal init(enableLogging: Bool = DefaultConfig.enableLogging,
trackScreenshotOnCrash: Bool = DefaultConfig.trackScreenshotOnCrash,
Expand All @@ -45,5 +47,12 @@ struct Config: InternalConfig, MeasureConfig {
self.maxSessionDurationMs = 6 * 60 * 60 * 1000 // 6 hours
self.cpuTrackingIntervalMs = 3 * 1000 // 3 seconds
self.memoryTrackingIntervalMs = 2 * 1000 // 2 seconds
self.httpContentTypeAllowlist = ["application/json"]
self.defaultHttpHeadersBlocklist = ["Authorization",
"Cookie",
"Set-Cookie",
"Proxy-Authorization",
"WWW-Authenticate",
"X-Api-Key"]
}
}
6 changes: 6 additions & 0 deletions ios/MeasureSDK/Config/InternalConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ protocol InternalConfig {

/// The interval at which memory related data is collected. Defaults to 2 seconds.
var memoryTrackingIntervalMs: UnsignedNumber { get }

/// This determines whether to capture the body or not based on the content type of the request/response. Defaults to `application/json`.
var httpContentTypeAllowlist: [String] { get }

/// Default list of HTTP headers to not capture for network request and response.
var defaultHttpHeadersBlocklist: [String] { get }
}
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
}
Loading
Loading