Skip to content

[v2] [5/X] Client Awareness Refactor #651

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

Draft
wants to merge 1 commit into
base: Apollo-Client-Sendable
Choose a base branch
from
Draft
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
13 changes: 4 additions & 9 deletions apollo-ios/Sources/Apollo/ApolloClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ public class ApolloClient: ApolloClientProtocol, @unchecked Sendable {

public let store: ApolloStore

private let sendEnhancedClientAwareness: Bool

public enum ApolloClientError: Error, LocalizedError, Hashable {
case noUploadTransport
case noSubscriptionTransport
Expand All @@ -62,33 +60,30 @@ public class ApolloClient: ApolloClientProtocol, @unchecked Sendable {
/// key. Client library metadata is the Apollo iOS library name and version. Defaults to `true`.
public init(
networkTransport: any NetworkTransport,
store: ApolloStore,
sendEnhancedClientAwareness: Bool = true
store: ApolloStore
) {
self.networkTransport = networkTransport
self.store = store
self.sendEnhancedClientAwareness = sendEnhancedClientAwareness
}

/// Creates a client with a `RequestChainNetworkTransport` connecting to the specified URL.
///
/// - Parameter url: The URL of a GraphQL server to connect to.
public convenience init(
url: URL,
sendEnhancedClientAwareness: Bool = true
clientAwarenessMetadata: ClientAwarenessMetadata = ClientAwarenessMetadata()
) {
let store = ApolloStore(cache: InMemoryNormalizedCache())
let provider = DefaultInterceptorProvider(store: store)
let transport = RequestChainNetworkTransport(
interceptorProvider: provider,
endpointURL: url,
sendEnhancedClientAwareness: sendEnhancedClientAwareness
clientAwarenessMetadata: clientAwarenessMetadata
)

self.init(
networkTransport: transport,
store: store,
sendEnhancedClientAwareness: sendEnhancedClientAwareness
store: store
)
}

Expand Down
143 changes: 143 additions & 0 deletions apollo-ios/Sources/Apollo/ClientAwarenessMetadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import Foundation
#if !COCOAPODS
import ApolloAPI
#endif

/// A data structure containing telemetry metadata about the client. This is used by GraphOS Studio's
/// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation)
/// feature.
public struct ClientAwarenessMetadata: Sendable {

/// The name of the application. This value is sent for the header "apollographql-client-name".
///
/// Defaults to `nil`.
public var clientApplicationName: String?

/// The version of the application. This value is sent for the header "apollographql-client-version".
///
/// Defaults to `nil`.
public var clientApplicationVersion: String?

/// Determines if the Apollo iOS library name and version should be sent with the telemetry data.
///
/// If `true`, the JSON body of the request will include a "clientLibrary" extension containing
/// the name of the Apollo iOS library and the version of Apollo iOS being used by the client
/// application.
///
/// Defaults to `true`.
public var includeApolloLibraryAwareness: Bool = true

public init(
clientApplicationName: String? = nil,
clientApplicationVersion: String? = nil,
includeApolloLibraryAwareness: Bool = true
) {
self.clientApplicationName = clientApplicationName
self.clientApplicationVersion = clientApplicationVersion
self.includeApolloLibraryAwareness = includeApolloLibraryAwareness
}

/// Disables all client awareness metadata.
public static var none: ClientAwarenessMetadata {
.init(
clientApplicationName: nil,
clientApplicationVersion: nil,
includeApolloLibraryAwareness: false
)
}

/// Enables all client awareness metadata with the following default values:
///
/// - `clientApplicationName`: The application's bundle identifier + "-apollo-ios".
/// - `clientApplicationVersion`: The bundle's short version string if available,
/// otherwise the build number.
/// - `includeApolloiOSLibraryVersion`: `true`
public static var enabledWithDefaults: ClientAwarenessMetadata {
.init(
clientApplicationName: defaultClientName,
clientApplicationVersion: defaultClientVersion,
includeApolloLibraryAwareness: true
)
}

/// The default client name to use when setting up the `clientName` property
public static var defaultClientName: String {
guard let identifier = Bundle.main.bundleIdentifier else {
return "apollo-ios-client"
}

return "\(identifier)-apollo-ios"
}

/// The default client version to use when setting up the `clientVersion` property.
public static var defaultClientVersion: String {
var version = String()
if let shortVersion = Bundle.main.shortVersion {
version.append(shortVersion)
}

if let buildNumber = Bundle.main.buildNumber {
if version.isEmpty {
version.append(buildNumber)
} else {
version.append("-\(buildNumber)")
}
}

if version.isEmpty {
version = "(unknown)"
}

return version
}

struct Constants {
/// The field name for the Apollo Client Name header
static let clientApplicationNameKey: StaticString = "apollographql-client-name"

/// The field name for the Apollo Client Version header
static let clientApplicationVersionKey: StaticString = "apollographql-client-version"
}

/// A helper method that adds the client awareness headers to the given request
/// This is used by GraphOS Studio's
/// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation)
/// feature.
///
/// - Parameters:
/// - clientAwarenessMetadata: The client name. The telemetry metadata about the client.
public func applyHeaders(
to request: inout URLRequest
) {
if let clientApplicationName = self.clientApplicationName {
request.addValue(
clientApplicationName,
forHTTPHeaderField: ClientAwarenessMetadata.Constants.clientApplicationNameKey.description
)
}

if let clientApplicationVersion = self.clientApplicationVersion {
request.addValue(
clientApplicationVersion,
forHTTPHeaderField: ClientAwarenessMetadata.Constants.clientApplicationVersionKey.description
)
}
}

/// Adds client metadata to the request body in the `extensions` key.
///
/// - Parameter body: The previously generated JSON body.
func applyExtension(to body: inout JSONEncodableDictionary) {
if self.includeApolloLibraryAwareness {
let clientLibraryMetadata: JSONEncodableDictionary = [
"name": Apollo.Constants.ApolloClientName,
"version": Apollo.Constants.ApolloClientVersion
]

var extensions = body["extensions"] as? JSONEncodableDictionary ?? JSONEncodableDictionary()
extensions["clientLibrary"] = clientLibraryMetadata

body["extensions"] = extensions
}
}
}
84 changes: 21 additions & 63 deletions apollo-ios/Sources/Apollo/GraphQLRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,52 @@ public protocol GraphQLRequest<Operation>: Sendable {
/// [optional] A context that is being passed through the request chain.
var context: (any RequestContext)? { get set }

/// The telemetry metadata about the client. This is used by GraphOS Studio's
/// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation)
/// feature.
var clientAwarenessMetadata: ClientAwarenessMetadata { get }

/// Converts the receiver into a `URLRequest` to be used for networking operations.
///
/// - Note: This function should call `createDefaultRequest()` to obtain a request with the
/// default configuration. The implementation may then modify that request. See the documentation
/// for ``GraphQLRequest/createDefaultRequest()`` for more information.
func toURLRequest() throws -> URLRequest
}

public extension GraphQLRequest {
var clientAwarenessMetadata: ClientAwarenessMetadata { .init() }
}

// MARK: - Helper Functions

extension GraphQLRequest {

/// Creates a default `URLRequest` for the receiver.
///
/// This can be called within the implementation of `toURLRequest()` and the returned request
/// can then be modified as necessary before being returned.
///
/// This function creates a `URLRequest` with the following behaviors:
/// - `url` set to the receiver's `graphQLEndpoint`
/// - `httpMethod` set to POST
/// - Client awareness headers from `clientAwarenessMetadata` added to `allHTTPHeaderFields`
/// - All header's from `additionalHeaders` added to `allHTTPHeaderFields`
/// - If the `context` conforms to `RequestContextTimeoutConfigurable`, the `timeoutInterval` is
/// set to the context's `requestTimeout`.
///
/// - Note: This should be called within the implementation of `toURLRequest()` and the returned request
/// can then be modified as necessary before being returned.
///
/// - Returns: A `URLRequest` configured as described above.
public func createDefaultRequest() -> URLRequest {
var request = URLRequest(url: self.graphQLEndpoint)

request.httpMethod = GraphQLHTTPMethod.POST.rawValue

for (fieldName, value) in additionalHeaders {
clientAwarenessMetadata.applyHeaders(to: &request)
for (fieldName, value) in self.additionalHeaders {
request.addValue(value, forHTTPHeaderField: fieldName)
}

if let configContext = context as? any RequestContextTimeoutConfigurable {
if let configContext = self.context as? any RequestContextTimeoutConfigurable {
request.timeoutInterval = configContext.requestTimeout
}

Expand All @@ -65,62 +81,4 @@ extension GraphQLRequest {
self.additionalHeaders.merge(headers) { (_, new) in new }
}

/// A helper method that dds the Apollo client headers to the given request
/// These header values are used for telemetry to track the source of client requests.
///
/// This should be called during setup of any implementation of `GraphQLRequest` to provide these
/// header values.
///
/// - Parameters:
/// - clientName: The client name. Defaults to the application's bundle identifier + "-apollo-ios".
/// - clientVersion: The client version. Defaults to the bundle's short version or build number.
public mutating func addApolloClientHeaders(
clientName: String? = Self.defaultClientName,
clientVersion: String? = Self.defaultClientVersion
) {
additionalHeaders[Self.headerFieldNameApolloClientName] = clientName
additionalHeaders[Self.headerFieldNameApolloClientVersion] = clientVersion
}

/// The field name for the Apollo Client Name header
static var headerFieldNameApolloClientName: String {
return "apollographql-client-name"
}

/// The field name for the Apollo Client Version header
static var headerFieldNameApolloClientVersion: String {
return "apollographql-client-version"
}

/// The default client name to use when setting up the `clientName` property
public static var defaultClientName: String {
guard let identifier = Bundle.main.bundleIdentifier else {
return "apollo-ios-client"
}

return "\(identifier)-apollo-ios"
}

/// The default client version to use when setting up the `clientVersion` property.
public static var defaultClientVersion: String {
var version = String()
if let shortVersion = Bundle.main.shortVersion {
version.append(shortVersion)
}

if let buildNumber = Bundle.main.buildNumber {
if version.isEmpty {
version.append(buildNumber)
} else {
version.append("-\(buildNumber)")
}
}

if version.isEmpty {
version = "(unknown)"
}

return version
}

}
30 changes: 10 additions & 20 deletions apollo-ios/Sources/Apollo/JSONRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ public struct JSONRequest<Operation: GraphQLOperation>: GraphQLRequest, AutoPers
/// Mutation operations always use POST, even when this is `false`
public let useGETForQueries: Bool

private let sendEnhancedClientAwareness: Bool
/// The telemetry metadata about the client. This is used by GraphOS Studio's
/// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation)
/// feature.
public var clientAwarenessMetadata: ClientAwarenessMetadata

/// Designated initializer
///
Expand All @@ -59,14 +62,12 @@ public struct JSONRequest<Operation: GraphQLOperation>: GraphQLRequest, AutoPers
operation: Operation,
graphQLEndpoint: URL,
contextIdentifier: UUID? = nil,
clientName: String? = Self.defaultClientName,
clientVersion: String? = Self.defaultClientVersion,
cachePolicy: CachePolicy = .default,
context: (any RequestContext)? = nil,
apqConfig: AutoPersistedQueryConfiguration = .init(),
useGETForQueries: Bool = false,
requestBodyCreator: any JSONRequestBodyCreator = DefaultRequestBodyCreator(),
sendEnhancedClientAwareness: Bool = true
clientAwarenessMetadata: ClientAwarenessMetadata = ClientAwarenessMetadata()
) {
self.operation = operation
self.graphQLEndpoint = graphQLEndpoint
Expand All @@ -77,20 +78,13 @@ public struct JSONRequest<Operation: GraphQLOperation>: GraphQLRequest, AutoPers

self.apqConfig = apqConfig
self.useGETForQueries = useGETForQueries
self.sendEnhancedClientAwareness = sendEnhancedClientAwareness
self.clientAwarenessMetadata = clientAwarenessMetadata

self.setupDefaultHeaders(
clientName: clientName,
clientVersion: clientVersion
)
self.setupDefaultHeaders()
}

private mutating func setupDefaultHeaders(
clientName: String? = Self.defaultClientName,
clientVersion: String? = Self.defaultClientVersion
) {
self.addHeader(name: "Content-Type", value: "application/json")
self.addApolloClientHeaders(clientName: clientName, clientVersion: clientVersion)
private mutating func setupDefaultHeaders() {
self.addHeader(name: "Content-Type", value: "application/json")

if Operation.operationType == .subscription {
self.addHeader(
Expand Down Expand Up @@ -181,11 +175,7 @@ public struct JSONRequest<Operation: GraphQLOperation>: GraphQLRequest, AutoPers
for: self,
sendQueryDocument: sendQueryDocument,
autoPersistQuery: autoPersistQueries
)

if self.sendEnhancedClientAwareness {
addEnhancedClientAwarenessExtension(to: &body)
}
)

return body
}
Expand Down
Loading
Loading