Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c511ac9
feat: Add initial implementation of FlagsEvaluationContext and precom…
sameerank Sep 18, 2025
d3d786b
fix: Add missing newlines at the end of several source and test files
sameerank Sep 18, 2025
a6e4cfd
chore: Update TODO comment and clean up whitespace in Flags module files
sameerank Sep 18, 2025
a14fc18
chore: Update TODO comments and remove unnecessary whitespace in Flag…
sameerank Sep 18, 2025
31a335b
chore: Clean up whitespace and formatting in Flags module test files
sameerank Sep 18, 2025
6da7907
refactor: Introduce temporary FlagsConfiguration struct to resolve co…
sameerank Sep 18, 2025
9996cdf
chore: Add Flags module source and test files to project configuration
sameerank Sep 18, 2025
84cc10d
fix: Enhance mock HTTP client in FlagsClientTests with proper respons…
sameerank Sep 18, 2025
f6e9726
chore: Update TODO comments in Flags module to remove redundant refer…
sameerank Sep 19, 2025
00632fe
fix: Changed FlagsEvaluationContext.attributes to a stricter type
sameerank Sep 19, 2025
0e5554a
feat: Implement dedicated URLSession in NetworkFlagsHttpClient and en…
sameerank Sep 19, 2025
707e6cd
feat: Add PrecomputeAssignmentsRequest model and integrate it into Fl…
sameerank Sep 19, 2025
228299f
refactor: Replace site string with DatadogSite enum in FlagsClientCon…
sameerank Sep 19, 2025
30dfee0
chore: Clean up whitespace and formatting in FlagsHttpClient and rela…
sameerank Sep 19, 2025
a4c9f21
FFL-1015 Fix tests coverage setup for `DatadogFlags`
ncreated Sep 18, 2025
a4dd1db
FFL-1015 Add `Flags.enable()` and register `FlagsFeature` to core
ncreated Sep 18, 2025
db5ca5f
FFLL-1015 Simplify FlagsClient config by using DatadogContext
ncreated Sep 19, 2025
3ca1981
FFLL-1015 Add namespace to flags client configuration
ncreated Sep 19, 2025
705fdbc
FFL-1015 CR feedback
ncreated Sep 19, 2025
80fcedd
Merge pull request #2484 from DataDog/ncreated/FFL-1015/improve-flags…
dd-mergequeue[bot] Sep 19, 2025
dc1f7ba
Merge pull request #2480 from DataDog/ncreated/FFL-1015/enable-flags-…
dd-mergequeue[bot] Sep 19, 2025
f5ce5d0
refactor: Fix Swift naming convention violations for acronyms in Data…
sameerank Sep 20, 2025
6fce004
chore: Remove FlagsHttpClient
sameerank Sep 20, 2025
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
106 changes: 106 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D29A9F3329DD84AA005C54A4"
BuildableName = "DatadogRUM.framework"
BlueprintName = "DatadogRUM iOS"
BlueprintIdentifier = "5BA8C2EC2E784B3C00B1DA80"
BuildableName = "DatadogFlags.framework"
BlueprintName = "DatadogFlags iOS"
ReferencedContainer = "container:Datadog.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D23F8E4D29DDCD28001CFAE8"
BuildableName = "DatadogRUM.framework"
BlueprintName = "DatadogRUM tvOS"
BlueprintIdentifier = "5BA8C3182E785B6F00B1DA80"
BuildableName = "DatadogFlags.framework"
BlueprintName = "DatadogFlags tvOS"
ReferencedContainer = "container:Datadog.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
Expand Down
2 changes: 1 addition & 1 deletion DatadogCore/Sources/PerformancePreset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ internal extension PerformancePreset {
maxFileAgeForWrite: override.maxFileAgeForWrite ?? maxFileAgeForWrite,
minFileAgeForRead: override.minFileAgeForRead ?? minFileAgeForRead,
maxFileAgeForRead: override.maxFileAgeForRead ?? maxFileAgeForRead,
maxObjectsInFile: maxObjectsInFile,
maxObjectsInFile: override.maxObjectsInFile ?? maxObjectsInFile,
maxObjectSize: override.maxObjectSize ?? maxObjectSize,
initialUploadDelay: override.initialUploadDelay ?? initialUploadDelay,
minUploadDelay: override.minUploadDelay ?? minUploadDelay,
Expand Down
3 changes: 3 additions & 0 deletions DatadogCore/Tests/Datadog/Core/PerformancePresetTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class PerformancePresetTests: XCTestCase {
changeRate: .mockRandom()
)
let maxFileAgeForRead: TimeInterval = .mockRandom(min: 100, max: 1_000)
let maxObjectsInFile: Int = .mockRandom(min: 1, max: 100)

// When
let preset = PerformancePreset(
Expand All @@ -175,6 +176,7 @@ class PerformancePresetTests: XCTestCase {
maxObjectSize: maxObjectSizeOverride,
meanFileAge: meanFileAgeOverride,
maxFileAgeForRead: maxFileAgeForRead,
maxObjectsInFile: maxObjectsInFile,
uploadDelay: uploadDelayOverride
)
)
Expand All @@ -185,6 +187,7 @@ class PerformancePresetTests: XCTestCase {
XCTAssertEqual(updatedPreset.maxFileAgeForWrite, meanFileAgeOverride * 0.95, accuracy: 0.01)
XCTAssertEqual(updatedPreset.minFileAgeForRead, meanFileAgeOverride * 1.05, accuracy: 0.01)
XCTAssertEqual(updatedPreset.maxFileAgeForRead, maxFileAgeForRead)
XCTAssertEqual(updatedPreset.maxObjectsInFile, maxObjectsInFile)
XCTAssertEqual(updatedPreset.uploaderWindow, meanFileAgeOverride, accuracy: 0.01)
XCTAssertEqual(updatedPreset.initialUploadDelay, uploadDelayOverride.initial)
XCTAssertEqual(updatedPreset.minUploadDelay, uploadDelayOverride.range.lowerBound)
Expand Down
41 changes: 41 additions & 0 deletions DatadogFlags/Sources/Feature/ExposureRequestBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import Foundation
import DatadogInternal

internal struct ExposureRequestBuilder: FeatureRequestBuilder {
/// A custom RUM intake.
let customIntakeURL: URL?
/// Telemetry interface.
let telemetry: Telemetry

func request(for events: [Event], with context: DatadogContext, execution: ExecutionContext) throws -> URLRequest {
guard let exposureEventData = events.first?.data else {
throw InternalError(description: "Found no event in Flags batch")
}

let builder = URLRequestBuilder(
url: url(with: context),
queryItems: [
// TODO: FFL-1022 Send Exposure event data to /api/v2/exposures
],
headers: [
// TODO: FFL-1022 Send Exposure event data to /api/v2/exposures
],
telemetry: telemetry
)

return builder.uploadRequest(
with: exposureEventData,
compress: true // TODO: FFL-1022 If /api/v2/exposures supports compression
)
}

private func url(with context: DatadogContext) -> URL {
customIntakeURL ?? context.site.endpoint.appendingPathComponent("api/v2/exposures")
}
}
31 changes: 31 additions & 0 deletions DatadogFlags/Sources/Feature/FlagsFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import Foundation
import DatadogInternal

internal struct FlagsFeature: DatadogRemoteFeature {
static let name = "flags"

let requestBuilder: FeatureRequestBuilder
let messageReceiver: FeatureMessageReceiver

let performanceOverride: PerformancePresetOverride

init(
configuration: Flags.Configuration,
featureScope: FeatureScope
) {
requestBuilder = ExposureRequestBuilder(
customIntakeURL: configuration.customExposureEndpoint,
telemetry: featureScope.telemetry
)
messageReceiver = NOPFeatureMessageReceiver()
performanceOverride = PerformancePresetOverride(
maxObjectsInFile: 1 // to send only one Exposure event per request
)
}
}
49 changes: 48 additions & 1 deletion DatadogFlags/Sources/Flags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,51 @@
import Foundation
import DatadogInternal

public enum Flags {}
public enum Flags {
public struct Configuration {
/// Custom server url for sending Flags exposure data.
///
/// Default: `nil`.
public var customExposureEndpoint: URL?

public init() {}
}

/// Enables the Datadog Flags feature.
///
/// - Parameters:
/// - configuration: Flags configuration options.
/// - core: The Datadog SDK instance to enable Flags in (defaults to the global core instance).
public static func enable(
with configuration: Flags.Configuration = .init(),
in core: DatadogCoreProtocol = CoreRegistry.default
) {
do {
// To ensure the correct registration order between Core and Features,
// the entire initialization flow is synchronized on the main thread.
try runOnMainThreadSync {
try enableOrThrow(with: configuration, in: core)
}
} catch let error {
consolePrint("\(error)", .error)
}
}

internal static func enableOrThrow(
with configuration: Flags.Configuration,
in core: DatadogCoreProtocol
) throws {
guard !(core is NOPDatadogCore) else {
throw ProgrammerError(
description: "Datadog SDK must be initialized before calling `Flags.enable(with:)`."
)
}

let featureScope = core.scope(for: FlagsFeature.self) // safe to obtain scope before feature registration; scope is lazily evaluated
let feature = FlagsFeature(
configuration: configuration,
featureScope: featureScope
)
try core.register(feature: feature)
}
}
136 changes: 136 additions & 0 deletions DatadogFlags/Sources/FlagsClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import Foundation
import DatadogInternal

public class FlagsClient {
private let configuration: FlagsClient.Configuration
private let httpClient: FlagsHTTPClient
private let store: FlagsStore
private let featureScope: FeatureScope

internal init(configuration: FlagsClient.Configuration, httpClient: FlagsHTTPClient, store: FlagsStore, featureScope: FeatureScope) {
self.configuration = configuration
self.httpClient = httpClient
self.store = store
self.featureScope = featureScope
}

public static func create(with configuration: FlagsClient.Configuration, in core: DatadogCoreProtocol = CoreRegistry.default) -> FlagsClient {
do {
// To ensure the correct registration order between Core and Features,
// the entire initialization flow is synchronized on the main thread.
return try runOnMainThreadSync {
try createOrThrow(with: configuration, in: core)
}
} catch let error {
consolePrint("\(error)", .error)
fatalError("TODO: FFL-1016 Fallback to NOP Client")
}
}

internal static func createOrThrow(with configuration: FlagsClient.Configuration, in core: DatadogCoreProtocol) throws -> FlagsClient {
guard core.get(feature: FlagsFeature.self) != nil else {
throw ProgrammerError(
description: "`FlagsClient.create()` produces a non-functional client because the `Flags` feature was not enabled."
)
}

let httpClient = NetworkFlagsHTTPClient()
let store = FlagsStore()
let featureScope = core.scope(for: FlagsFeature.self)
return FlagsClient(configuration: configuration, httpClient: httpClient, store: store, featureScope: featureScope)
}

public func setEvaluationContext(_ context: FlagsEvaluationContext, completion: @escaping (Result<Void, FlagsError>) -> Void) {
featureScope.context { [httpClient, configuration] sdkContext in
httpClient.postPrecomputeAssignments(
context: context,
configuration: configuration,
sdkContext: sdkContext
) { [weak self] result in
guard let self = self else {
completion(.failure(.clientNotInitialized))
return
}

switch result {
case .success(let (data, response)):
guard let httpResponse = response as? HTTPURLResponse,
200...299 ~= httpResponse.statusCode else {
completion(.failure(.invalidResponse))
return
}

do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]

if let responseData = json?["data"] as? [String: Any],
let attributes = responseData["attributes"] as? [String: Any],
let flags = attributes["flags"] as? [String: Any] {
self.store.setFlags(flags, context: context)
completion(.success(()))
} else {
completion(.failure(.invalidResponse))
}
} catch {
completion(.failure(.networkError(error)))
}

case .failure(let error):
completion(.failure(.networkError(error)))
}
}
}
}

public func getBooleanValue(key: String, defaultValue: Bool) -> Bool {
let flags = store.getFlags()
if let flagData = flags[key] as? [String: Any],
let value = flagData["variationValue"] as? Bool {
return value
}
return defaultValue
}

public func getStringValue(key: String, defaultValue: String) -> String {
let flags = store.getFlags()
if let flagData = flags[key] as? [String: Any],
let value = flagData["variationValue"] as? String {
return value
}
return defaultValue
}

public func getIntegerValue(key: String, defaultValue: Int64) -> Int64 {
let flags = store.getFlags()
if let flagData = flags[key] as? [String: Any],
let value = flagData["variationValue"] as? NSNumber {
return value.int64Value
}
return defaultValue
}

public func getDoubleValue(key: String, defaultValue: Double) -> Double {
let flags = store.getFlags()
if let flagData = flags[key] as? [String: Any],
let value = flagData["variationValue"] as? NSNumber {
return value.doubleValue
}
return defaultValue
}

// TODO: FFL-1047 Replace [String: Any] with OpenFeature.Value-compatible type
public func getObjectValue(key: String, defaultValue: [String: Any]) -> [String: Any] {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking, comment/ [String: Any] is a pragmatic first choice, so let’s stick with it for now. That said, we should keep OpenFeature.Value in mind - eventually, we’ll want to mirror this container in our SDK to ease migration from Datadog FlagsClient to OpenFeatureClient. Let’s mark this as a "to do" (maybe add a JIRA?) 👍 and move on, WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let flags = store.getFlags()
if let flagData = flags[key] as? [String: Any],
let value = flagData["variationValue"] as? [String: Any] {
return value
}
return defaultValue
}
}
26 changes: 26 additions & 0 deletions DatadogFlags/Sources/FlagsClientConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import Foundation
import DatadogInternal

extension FlagsClient {
public struct Configuration {
public let baseURL: String?
public let customHeaders: [String: String]
public let flaggingProxy: String?

public init(
baseURL: String? = nil,
customHeaders: [String: String] = [:],
flaggingProxy: String? = nil
) {
self.baseURL = baseURL
self.customHeaders = customHeaders
self.flaggingProxy = flaggingProxy
}
}
}
Loading