Skip to content
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
17 changes: 13 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,29 @@ import PackageDescription

let package = Package(
name: "FTAPIKit",
platforms: [.iOS(.v12), .macOS(.v10_10), .tvOS(.v12), .watchOS(.v5)],
platforms: [
.iOS(.v14),
.macOS(.v11),
.tvOS(.v14),
.watchOS(.v7)
],
products: [
.library(
name: "FTAPIKit",
targets: ["FTAPIKit"])
],
dependencies: [
.package(url: "https://github.com/futuredapp/FTNetworkTracer", from: "0.2.1")
],
targets: [
.target(
name: "FTAPIKit",
dependencies: []
dependencies: [
.product(name: "FTNetworkTracer", package: "FTNetworkTracer")
]
),
.testTarget(
name: "FTAPIKitTests",
dependencies: ["FTAPIKit"]
)
dependencies: ["FTAPIKit"])
]
)
15 changes: 13 additions & 2 deletions Package@swift-5.5.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,27 @@ import PackageDescription

let package = Package(
name: "FTAPIKit",
platforms: [.iOS(.v12), .macOS(.v10_10), .tvOS(.v12), .watchOS(.v5)],
platforms: [
.iOS(.v14),
.macOS(.v11),
.tvOS(.v14),
.watchOS(.v7)
],
products: [
.library(
name: "FTAPIKit",
targets: ["FTAPIKit"])
],
dependencies: [
.package(url: "https://github.com/futuredapp/FTNetworkTracer", from: "0.2.0")
],
targets: [
.target(
name: "FTAPIKit",
dependencies: []),
dependencies: [
.product(name: "FTNetworkTracer", package: "FTNetworkTracer")
]
),
.testTarget(
name: "FTAPIKitTests",
dependencies: ["FTAPIKit"])
Expand Down
75 changes: 72 additions & 3 deletions Sources/FTAPIKit/URLServer+Task.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,30 @@ extension URLServer {
process: @escaping (Data?, URLResponse?, Error?) -> Result<R, ErrorType>,
completion: @escaping (Result<R, ErrorType>) -> Void
) -> URLSessionDataTask? {
let requestId = UUID().uuidString
let startTime = Date()

networkTracer?.logAndTrackRequest(request: request, requestId: requestId)

let task = urlSession.dataTask(with: request) { data, response, error in
completion(process(data, response, error))
networkTracer?.logAndTrackResponse(
request: request,
response: response,
data: data,
requestId: requestId,
startTime: startTime
)

let result = process(data, response, error)

if case let .failure(error) = result {
networkTracer?.logAndTrackError(
request: request,
error: error,
requestId: requestId
)
}
completion(result)
}
task.resume()
return task
Expand All @@ -35,8 +57,32 @@ extension URLServer {
process: @escaping (Data?, URLResponse?, Error?) -> Result<R, ErrorType>,
completion: @escaping (Result<R, ErrorType>) -> Void
) -> URLSessionUploadTask? {
let requestId = UUID().uuidString
let startTime = Date()

networkTracer?.logAndTrackRequest(request: request, requestId: requestId)

let task = urlSession.uploadTask(with: request, fromFile: file) { data, response, error in
completion(process(data, response, error))
networkTracer?.logAndTrackResponse(
request: request,
response: response,
data: data,
requestId: requestId,
startTime: startTime
)

let result = process(data, response, error)

// Log and track error if any
if case let .failure(error) = result {
networkTracer?.logAndTrackError(
request: request,
error: error,
requestId: requestId
)
}

completion(result)
}
task.resume()
return task
Expand All @@ -47,8 +93,31 @@ extension URLServer {
process: @escaping (URL?, URLResponse?, Error?) -> Result<URL, ErrorType>,
completion: @escaping (Result<URL, ErrorType>) -> Void
) -> URLSessionDownloadTask? {
let requestId = UUID().uuidString
let startTime = Date()

networkTracer?.logAndTrackRequest(request: request, requestId: requestId)

let task = urlSession.downloadTask(with: request) { url, response, error in
completion(process(url, response, error))
networkTracer?.logAndTrackResponse(
request: request,
response: response,
data: nil,
requestId: requestId,
startTime: startTime
)

let result = process(url, response, error)

if case let .failure(error) = result {
networkTracer?.logAndTrackError(
request: request,
error: error,
requestId: requestId
)
}

completion(result)
}
task.resume()
return task
Expand Down
6 changes: 6 additions & 0 deletions Sources/FTAPIKit/URLServer.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import FTNetworkTracer

#if os(Linux)
import FoundationNetworking
Expand Down Expand Up @@ -43,12 +44,17 @@ public protocol URLServer: Server where Request == URLRequest {
/// `URLSession` instance, which is used for task execution
/// - Note: Provided default implementation.
var urlSession: URLSession { get }

/// Optional network tracer for request logging and tracking
/// - Note: Provided default implementation returns nil.
var networkTracer: FTNetworkTracer? { get }
}

public extension URLServer {
var urlSession: URLSession { .shared }
var decoding: Decoding { JSONDecoding() }
var encoding: Encoding { JSONEncoding() }
var networkTracer: FTNetworkTracer? { nil }

func buildRequest(endpoint: Endpoint) throws -> URLRequest {
try buildStandardRequest(endpoint: endpoint)
Expand Down
30 changes: 30 additions & 0 deletions Tests/FTAPIKitTests/Mockups/Analytics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation
import FTNetworkTracer

class MockAnalytics: AnalyticsProtocol {
var requestCount = 0
var responseCount = 0
var errorCount = 0
var lastRequestId: String?
var lastDuration: TimeInterval?

let configuration: AnalyticsConfiguration = AnalyticsConfiguration(
privacy: .none,
unmaskedHeaders: [],
unmaskedUrlQueries: [],
unmaskedBodyParams: []
)

func track(_ entry: AnalyticEntry) {
switch entry.type {
case .request:
requestCount += 1
case .response:
responseCount += 1
case .error:
errorCount += 1
}
lastRequestId = entry.requestId
lastDuration = entry.duration
}
}
11 changes: 11 additions & 0 deletions Tests/FTAPIKitTests/Mockups/Servers.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import FTAPIKit
import FTNetworkTracer

#if os(Linux)
import FoundationNetworking
Expand Down Expand Up @@ -29,3 +30,13 @@ struct ErrorThrowingServer: URLServer {
let urlSession = URLSession(configuration: .ephemeral)
let baseUri = URL(string: "http://httpbin.org/")!
}

struct HTTPBinServerWithTracer: URLServer {
let urlSession = URLSession(configuration: .ephemeral)
let baseUri = URL(string: "http://httpbin.org/")!
let networkTracer: FTNetworkTracer?

init(tracer: FTNetworkTracer?) {
self.networkTracer = tracer
}
}
75 changes: 75 additions & 0 deletions Tests/FTAPIKitTests/NetworkTracerIntegrationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import FTAPIKit
import FTNetworkTracer
import XCTest

#if os(Linux)
import FoundationNetworking
#endif

final class NetworkTracerIntegrationTests: XCTestCase {
private let timeout: TimeInterval = 30.0

// MARK: - Unit Tests (no network required)

func testTracerIsCalledForRequest() {
let mockAnalytics = MockAnalytics()
let tracer = FTNetworkTracer(logger: nil, analytics: mockAnalytics)
let server = HTTPBinServerWithTracer(tracer: tracer)
let endpoint = GetEndpoint()

// Build request to verify tracer integration
_ = try? server.buildRequest(endpoint: endpoint)

// Note: Just building request doesn't trigger logging,
// but this verifies the tracer property is properly integrated
XCTAssertNotNil(server.networkTracer, "NetworkTracer should be set")
}

func testNilTracerDoesNotCauseIssues() {
let server = HTTPBinServer() // Default tracer is nil
let endpoint = GetEndpoint()

// Verify nil tracer doesn't cause problems during request building
XCTAssertNoThrow(try server.buildRequest(endpoint: endpoint))
XCTAssertNil(server.networkTracer, "Default networkTracer should be nil")
}

func testMockAnalyticsTracking() {
let mockAnalytics = MockAnalytics()
let analyticEntry = AnalyticEntry(
type: .request(method: "GET", url: "https://test.com"),
headers: [:],
body: nil,
duration: nil,
requestId: "test-123",
configuration: mockAnalytics.configuration
)

mockAnalytics.track(analyticEntry)

XCTAssertEqual(mockAnalytics.requestCount, 1)
XCTAssertEqual(mockAnalytics.lastRequestId, "test-123")
}

// MARK: - Integration Tests (requires network)
// Note: These tests may fail if httpbin.org is unavailable

func testTracerLogsFailedRequest() {
let mockAnalytics = MockAnalytics()
let tracer = FTNetworkTracer(logger: nil, analytics: mockAnalytics)
let server = HTTPBinServerWithTracer(tracer: tracer)
let endpoint = NotFoundEndpoint()
let expectation = self.expectation(description: "Result")

server.call(endpoint: endpoint) { _ in
expectation.fulfill()
}

wait(for: [expectation], timeout: timeout)

// Verify tracer was called (request is always logged, even on failure)
XCTAssertEqual(mockAnalytics.requestCount, 1, "Request should be logged once")
XCTAssertGreaterThanOrEqual(mockAnalytics.responseCount + mockAnalytics.errorCount, 1,
"Either response or error should be logged")
}
}
Loading