Skip to content

Commit d4ad59c

Browse files
authored
feat: Adds support for client-side prerequisite events (#409)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/v9/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** SDK-684 **Describe the solution you've provided** Added prerequisites to flag model Variation calls now recurse on prerequisites Updated iOS FlagRequestTracker to use "first default wins" instead of "last default wins" to be more consistent with other SDK implementations. Also updated it to not serialize a null default.
1 parent 3c48d7c commit d4ad59c

File tree

9 files changed

+119
-60
lines changed

9 files changed

+119
-60
lines changed

ContractTests/Source/Controllers/SdkController.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ final class SdkController: RouteCollection {
3131
"anonymous-redaction",
3232
"evaluation-hooks",
3333
"event-gzip",
34-
"optional-event-gzip"
34+
"optional-event-gzip",
35+
"client-prereq-events"
3536
]
3637

3738
return StatusResponse(

LaunchDarkly/GeneratedCode/mocks.generated.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,12 @@ final class EventReportingMock: EventReporting {
214214
}
215215

216216
var recordFlagEvaluationEventsCallCount = 0
217-
var recordFlagEvaluationEventsCallback: (() throws -> Void)?
217+
var recordFlagEvaluationEventsCallback: ((_ event: FeatureEvent) throws -> Void)?
218218
var recordFlagEvaluationEventsReceivedArguments: (flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool)?
219219
func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, context: LDContext, includeReason: Bool) {
220220
recordFlagEvaluationEventsCallCount += 1
221221
recordFlagEvaluationEventsReceivedArguments = (flagKey: flagKey, value: value, defaultValue: defaultValue, featureFlag: featureFlag, context: context, includeReason: includeReason)
222-
try! recordFlagEvaluationEventsCallback?()
222+
try! recordFlagEvaluationEventsCallback?(FeatureEvent(key: flagKey, context: context, value: value, defaultValue: defaultValue, featureFlag: featureFlag, includeReason: includeReason, isDebug: false))
223223
}
224224

225225
var flushCallCount = 0

LaunchDarkly/LaunchDarkly/LDClientVariation.swift

+6
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ extension LDClient {
173173
var result: LDEvaluationDetail<T>
174174
let featureFlag = flagStore.featureFlag(for: flagKey)
175175
if let featureFlag = featureFlag {
176+
featureFlag.prerequisites?.forEach{ prereqFlagKey in
177+
// recurse on prerequisites to emulate prereq evaluations occurring with desirable side effects such as events for prereqs
178+
_ = variationDetailInternal(prereqFlagKey, LDValue.null, needsReason: needsReason, methodName: methodName)
179+
}
180+
176181
if featureFlag.value == .null {
177182
result = LDEvaluationDetail(value: defaultValue, variationIndex: featureFlag.variation, reason: featureFlag.reason)
178183
} else {
@@ -188,6 +193,7 @@ extension LDClient {
188193
os_log("%s Unknown feature flag %s; returning default value", log: config.logger, type: .debug, typeName(and: #function), flagKey.description)
189194
result = LDEvaluationDetail(value: defaultValue, variationIndex: nil, reason: ["kind": "ERROR", "errorKind": "FLAG_NOT_FOUND"])
190195
}
196+
191197
eventReporter.recordFlagEvaluationEvents(flagKey: flagKey,
192198
value: result.value.toLDValue(),
193199
defaultValue: defaultValue.toLDValue(),

LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FeatureFlag.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Foundation
33
struct FeatureFlag: Codable {
44

55
enum CodingKeys: String, CodingKey, CaseIterable {
6-
case flagKey = "key", value, variation, version, flagVersion, trackEvents, debugEventsUntilDate, reason, trackReason
6+
case flagKey = "key", value, variation, version, flagVersion, trackEvents, debugEventsUntilDate, reason, trackReason, prerequisites
77
}
88

99
let flagKey: LDFlagKey
@@ -17,6 +17,7 @@ struct FeatureFlag: Codable {
1717
let debugEventsUntilDate: Date?
1818
let reason: [String: LDValue]?
1919
let trackReason: Bool
20+
let prerequisites: [String]?
2021

2122
var versionForEvents: Int? { flagVersion ?? version }
2223

@@ -28,7 +29,8 @@ struct FeatureFlag: Codable {
2829
trackEvents: Bool = false,
2930
debugEventsUntilDate: Date? = nil,
3031
reason: [String: LDValue]? = nil,
31-
trackReason: Bool = false) {
32+
trackReason: Bool = false,
33+
prerequisites: [String]? = nil) {
3234
self.flagKey = flagKey
3335
self.value = value
3436
self.variation = variation
@@ -38,6 +40,7 @@ struct FeatureFlag: Codable {
3840
self.debugEventsUntilDate = debugEventsUntilDate
3941
self.reason = reason
4042
self.trackReason = trackReason
43+
self.prerequisites = prerequisites
4144
}
4245

4346
init(from decoder: Decoder) throws {
@@ -61,6 +64,7 @@ struct FeatureFlag: Codable {
6164
self.debugEventsUntilDate = Date(millisSince1970: try container.decodeIfPresent(Int64.self, forKey: .debugEventsUntilDate))
6265
self.reason = try container.decodeIfPresent([String: LDValue].self, forKey: .reason)
6366
self.trackReason = (try container.decodeIfPresent(Bool.self, forKey: .trackReason)) ?? false
67+
self.prerequisites = (try container.decodeIfPresent([String].self, forKey: .prerequisites))
6468
}
6569

6670
func encode(to encoder: Encoder) throws {
@@ -76,6 +80,9 @@ struct FeatureFlag: Codable {
7680
}
7781
if reason != nil { try container.encode(reason, forKey: .reason) }
7882
if trackReason { try container.encode(true, forKey: .trackReason) }
83+
if let prerequisites = prerequisites, !prerequisites.isEmpty {
84+
try container.encodeIfPresent(prerequisites, forKey: .prerequisites)
85+
}
7986
}
8087

8188
func shouldCreateDebugEvents(lastEventReportResponseTime: Date?) -> Bool {

LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift

+12-6
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ struct FlagRequestTracker {
1212

1313
mutating func trackRequest(flagKey: LDFlagKey, reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue, context: LDContext) {
1414
if flagCounters[flagKey] == nil {
15-
flagCounters[flagKey] = FlagCounter()
15+
flagCounters[flagKey] = FlagCounter(defaultValue: defaultValue)
1616
}
1717
guard let flagCounter = flagCounters[flagKey]
1818
else { return }
19-
flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, defaultValue: defaultValue, context: context)
19+
flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, context: context)
2020

2121
os_log("%s \n\tflagKey: %s\n\treportedValue: %s\n\tvariation: %s\n\tversion: %s\n\tdefaultValue: %s", log: logger, type: .debug,
2222
typeName(and: #function),
@@ -41,12 +41,16 @@ final class FlagCounter: Encodable {
4141
case value, variation, version, unknown, count
4242
}
4343

44-
private(set) var defaultValue: LDValue = .null
44+
private(set) var defaultValue: LDValue
4545
private(set) var flagValueCounters: [CounterKey: CounterValue] = [:]
4646
private(set) var contextKinds: Set<String> = Set()
47-
48-
func trackRequest(reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue, context: LDContext) {
47+
48+
init(defaultValue: LDValue) {
49+
// default value follows a "first one wins" approach where the first evaluation for a flag key sets the default value for the summary events
4950
self.defaultValue = defaultValue
51+
}
52+
53+
func trackRequest(reportedValue: LDValue, featureFlag: FeatureFlag?, context: LDContext) {
5054
let key = CounterKey(variation: featureFlag?.variation, version: featureFlag?.versionForEvents)
5155
if let counter = flagValueCounters[key] {
5256
counter.increment()
@@ -61,7 +65,9 @@ final class FlagCounter: Encodable {
6165

6266
func encode(to encoder: Encoder) throws {
6367
var container = encoder.container(keyedBy: CodingKeys.self)
64-
try container.encode(defaultValue, forKey: .defaultValue)
68+
if defaultValue != .null {
69+
try container.encode(defaultValue, forKey: .defaultValue)
70+
}
6571
try container.encode(contextKinds, forKey: .contextKinds)
6672
var countersContainer = container.nestedUnkeyedContainer(forKey: .counters)
6773
try flagValueCounters.forEach { (key, value) in

LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift

+26
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,32 @@ final class LDClientSpec: QuickSpec {
714714
}
715715
}
716716
}
717+
context("flag store contains flags with prerequisites") {
718+
it("records evaluation events for the prerequisites that exist") {
719+
let flagA = FeatureFlag(flagKey: "flagA", value: LDValue.bool(true), trackEvents: false, trackReason: false)
720+
let flagAB = FeatureFlag(flagKey: "flagAB", value: LDValue.bool(true), trackEvents: false, trackReason: false, prerequisites: ["flagA"])
721+
let flagAC = FeatureFlag(flagKey: "flagAC", value: LDValue.bool(true), trackEvents: false, trackReason: false, prerequisites: ["flagA"])
722+
let flagABD = FeatureFlag(flagKey: "flagABD", value: LDValue.bool(true), trackEvents: false, trackReason: false, prerequisites: ["flagAB"])
723+
let flags: [LDFlagKey: FeatureFlag] = ["flagA": flagA, "flagAB": flagAB, "flagAC": flagAC, "flagABD": flagABD]
724+
var storedItems = StoredItems(items: flags)
725+
testContext.flagStoreMock.replaceStore(newStoredItems: storedItems)
726+
var events = [FeatureEvent]()
727+
testContext.eventReporterMock.recordFlagEvaluationEventsCallback = { events.append($0) }
728+
_ = testContext.subject.boolVariation(forKey: "flagA", defaultValue: DefaultFlagValues.bool)
729+
_ = testContext.subject.boolVariation(forKey: "flagAB", defaultValue: DefaultFlagValues.bool)
730+
_ = testContext.subject.boolVariation(forKey: "flagAC", defaultValue: DefaultFlagValues.bool)
731+
_ = testContext.subject.boolVariation(forKey: "flagABD", defaultValue: DefaultFlagValues.bool)
732+
expect(events.count) == 8
733+
expect(events[0].key) == "flagA"
734+
expect(events[1].key) == "flagA"
735+
expect(events[2].key) == "flagAB"
736+
expect(events[3].key) == "flagA"
737+
expect(events[4].key) == "flagAC"
738+
expect(events[5].key) == "flagA"
739+
expect(events[6].key) == "flagAB"
740+
expect(events[7].key) == "flagABD"
741+
}
742+
}
717743
}
718744
}
719745

LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,9 @@ final class EventSpec: XCTestCase {
219219
XCTAssertEqual(dict["endDate"], .number(Double(event.endDate.millisSince1970)))
220220
valueIsObject(dict["features"]) { features in
221221
XCTAssertEqual(features.count, 1)
222-
let counter = FlagCounter()
223-
counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub())
224-
counter.trackRequest(reportedValue: false, featureFlag: flag, defaultValue: true, context: LDContext.stub())
222+
let counter = FlagCounter(defaultValue: true)
223+
counter.trackRequest(reportedValue: false, featureFlag: flag, context: LDContext.stub())
224+
counter.trackRequest(reportedValue: false, featureFlag: flag, context: LDContext.stub())
225225
XCTAssertEqual(features["bool-flag"], encodeToLDValue(counter))
226226
}
227227
}

0 commit comments

Comments
 (0)