A Swift package for comprehensive network request logging and analytics tracking with privacy-first design.
- 🔍 Dual-mode operation: Simultaneous logging and analytics tracking
- 🔒 Privacy-first design: Configurable data masking with three privacy levels
- 🌐 REST & GraphQL support: Specialized formatting for both API types
- 📊 Structured logging: Uses
os.logfor performance and privacy - 🎚️ Log level filtering: Configurable minimum threshold (debug, info, error, fault)
- 🐧 Linux support: Full cross-platform compatibility with CI/CD
- 🎯 Type-safe: Associated values eliminate impossible states
- ⚡ Zero dependencies: Pure Swift implementation
- 🧪 Fully tested: 65+ tests including comprehensive security tests
- 🔄 Swift 6 ready: Strict concurrency compliant with
Sendablesupport
- iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+
- Swift 6.1.0+
- Xcode 15.0+
.package(url: "https://github.com/futuredapp/FTNetworkTracer.git", from: "0.2.0")import FTNetworkTracer
// Create logger configuration
let logger = LoggerConfiguration(
subsystem: "com.yourapp",
category: "network"
)
// Create analytics tracker
class MyAnalytics: AnalyticsProtocol {
let configuration = AnalyticsConfiguration(privacy: .private)
func track(_ entry: AnalyticEntry) {
// Send to your analytics service
print("Tracking: \(entry.method) \(entry.url)")
}
}
// Initialize tracer
let tracer = FTNetworkTracer(
logger: logger,
analytics: MyAnalytics()
)// Track request
let request = URLRequest(url: URL(string: "https://api.example.com/users")!)
tracer.logAndTrackRequest(
request: request,
requestId: UUID().uuidString
)
// Track response
let response = HTTPURLResponse(...)
tracer.logAndTrackResponse(
request: request,
response: response,
data: responseData,
requestId: requestId,
startTime: startTime
)
// Track error
tracer.logAndTrackError(
request: request,
error: error,
requestId: requestId
)let query = """
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
"""
let variables: [String: any Sendable] = ["id": "123"]
// Track GraphQL request
tracer.logAndTrackRequest(
url: "https://api.example.com/graphql",
operationName: "GetUser",
query: query,
variables: variables,
headers: ["Authorization": "Bearer token"],
requestId: requestId
)
// Track GraphQL response
tracer.logAndTrackResponse(
url: "https://api.example.com/graphql",
operationName: "GetUser",
statusCode: 200,
requestId: requestId,
startTime: startTime
)// Default configuration with pretty-printed JSON
let logger = LoggerConfiguration(
subsystem: "com.yourapp",
category: "network",
privacy: .auto // .none, .auto, .private, .sensitive
)
// With log level filtering (only show errors and faults)
let logger = LoggerConfiguration(
subsystem: "com.yourapp",
category: "network",
logLevel: .error // .debug, .info, .error, .fault
)
// Custom data decoder (e.g., show only size)
let logger = LoggerConfiguration(
subsystem: "com.yourapp",
category: "network",
dataDecoder: LoggerConfiguration.sizeOnlyDataDecoder
)
// UTF8-only decoder (no JSON formatting)
let logger = LoggerConfiguration(
subsystem: "com.yourapp",
category: "network",
dataDecoder: LoggerConfiguration.utf8DataDecoder
)// Sensitive mode (most secure, default)
let config = AnalyticsConfiguration(privacy: .sensitive)
// Private mode with exceptions
let config = AnalyticsConfiguration(
privacy: .private,
unmaskedHeaders: ["content-type", "accept"],
unmaskedUrlQueries: ["page", "limit"],
unmaskedBodyParams: ["username", "email"]
)
// No privacy (development only)
let config = AnalyticsConfiguration(privacy: .none)| Level | Headers | URL Queries | Body | Use Case |
|---|---|---|---|---|
.none |
✅ Preserved | ✅ Preserved | ✅ Preserved | Development only |
.private |
Production with selective tracking | |||
.sensitive |
🔒 All masked | 🔒 All removed | 🔒 Removed | Production with maximum privacy |
FTNetworkTracer automatically masks sensitive data in analytics:
- Headers:
Authorization,Cookie,X-API-Key, etc. - URL Parameters: All query parameters (in
.sensitivemode) - Body Fields:
password,token,secret,creditCard,ssn, etc. - GraphQL Variables: All variables unless explicitly unmasked
Once data is masked with ***, the original value cannot be recovered. This ensures sensitive data never leaves your application.
Unmasked parameter lists use case-insensitive matching to prevent bypasses:
// These are all treated as the same key:
unmaskedHeaders: ["content-type"]
// Matches: "Content-Type", "CONTENT-TYPE", "content-type"FTNetworkTracer has been tested against common attack vectors:
- ✅ XSS attempts (
<script>alert('XSS')</script>) - ✅ SQL injection (
' OR '1'='1) - ✅ Path traversal (
../../../etc/passwd) - ✅ Very long strings (10,000+ characters)
- ✅ Unicode and special characters
[REQUEST] [abc12345]
Method POST
URL https://api.example.com/users
Timestamp 2025-11-04 15:42:30.123
Headers:
Content-Type application/json
Body:
{
"username": "john",
"email": "john@example.com"
}
[REQUEST] [xyz67890]
Method POST
URL https://api.example.com/graphql
Timestamp 2025-11-04 15:42:31.456
Operation GetUser
Headers:
Authorization Bearer ***
Query:
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
Variables:
{
"id": "123"
}
FTNetworkTracer uses a dual-mode architecture:
┌─────────────────┐
│ FTNetworkTracer │
└────────┬────────┘
│
┌────┴────┐
│ │
┌───▼───┐ ┌──▼────────┐
│Logging│ │ Analytics │
└───────┘ └───────────┘
FTNetworkTracer: Main coordinator for logging and analyticsEntryType: Type-safe enum with associated values (request/response/error)LogEntry: Internal logging data with formatted messagesAnalyticEntry: Public analytics data with automatic privacy maskingGraphQLFormatter: Specialized GraphQL query formattingRESTFormatter: REST body formatting with pluggable decoders
- Privacy by Design: Masking happens at initialization, not at usage
- Type Safety: Associated values eliminate optional-heavy code
- Separation of Concerns: Logging and analytics are independent
- Protocol-Based: Easy to extend and test
- AnalyticsTests (4 tests): Privacy masking for all levels
- GraphQLFormatterTests (11 tests): Query and variable formatting
- IntegrationTests (15 tests): End-to-end flows
- LoggingTests (4 tests): Log message building
- RESTFormatterTests (9 tests): Body formatting
- SecurityTests (22 tests): Comprehensive security validation
class NetworkClient {
let tracer: FTNetworkTracer
func fetch(url: URL) async throws -> Data {
let requestId = UUID().uuidString
let request = URLRequest(url: url)
let startTime = Date()
// Log request
tracer.logAndTrackRequest(request: request, requestId: requestId)
do {
let (data, response) = try await URLSession.shared.data(for: request)
// Log response
tracer.logAndTrackResponse(
request: request,
response: response,
data: data,
requestId: requestId,
startTime: startTime
)
return data
} catch {
// Log error
tracer.logAndTrackError(
request: request,
error: error,
requestId: requestId
)
throw error
}
}
}class ApolloNetworkInterceptor: ApolloInterceptor {
let tracer: FTNetworkTracer
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) {
let requestId = UUID().uuidString
if let operation = request.operation as? GraphQLQuery {
tracer.logAndTrackRequest(
url: request.graphQLEndpoint.absoluteString,
operationName: operation.operationName,
query: operation.queryDocument,
variables: operation.variables,
headers: request.additionalHeaders,
requestId: requestId
)
}
chain.proceedAsync(
request: request,
response: response,
completion: completion
)
}
}- Development:
.noneor.private - Staging:
.privatewith specific unmasked fields - Production:
.sensitive(default)
let requestId = UUID().uuidString
// Use the same requestId for request, response, and errorlet startTime = Date()
// Make request...
tracer.logAndTrackResponse(..., startTime: startTime)Even with masking, avoid logging:
- Payment card details
- Social security numbers
- Biometric data
- Health information
Always test your privacy configuration to ensure sensitive data is masked:
let config = AnalyticsConfiguration(privacy: .private, ...)
let entry = AnalyticEntry(type: .request(...), body: testData, configuration: config)
// Verify entry.body doesn't contain sensitive dataFor issues, questions, or contributions:
- Open an issue on GitHub
- Review the CLAUDE.md for architecture details
Made with ❤️ by Futured