Skip to content

Commit 512ffbe

Browse files
Merge pull request #8825 from woocommerce/merge/12.1-final-into-trunk
Merge 12.1 final into trunk
2 parents a532628 + c31b21b commit 512ffbe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+679
-493
lines changed

Experiments/Experiments.xcodeproj/project.pbxproj

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
C8E16F0EE6954B58A1C402F0 /* Pods_ExperimentsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AAC7C082DD376957B4676401 /* Pods_ExperimentsTests.framework */; };
1818
CC53FB48275E426900C4CA4F /* ABTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC53FB47275E426900C4CA4F /* ABTest.swift */; };
1919
EE2EDFDF29879331004E702B /* ABTestVariationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2EDFDE29879331004E702B /* ABTestVariationProvider.swift */; };
20+
EEC8C0ED298A92F10047B4CB /* CachedABTestVariationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC8C0EC298A92F10047B4CB /* CachedABTestVariationProvider.swift */; };
21+
EEC8C0EF298A939C0047B4CB /* VariationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC8C0EE298A939C0047B4CB /* VariationCache.swift */; };
22+
EEC8C0F1298B5AFB0047B4CB /* VariationCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC8C0F0298B5AFB0047B4CB /* VariationCacheTests.swift */; };
23+
EEC8C0F3298B5F950047B4CB /* CachedABTestVariationProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC8C0F2298B5F950047B4CB /* CachedABTestVariationProviderTests.swift */; };
2024
/* End PBXBuildFile section */
2125

2226
/* Begin PBXContainerItemProxy section */
@@ -49,6 +53,10 @@
4953
AF72D9DB7771E7A5105C88B0 /* Pods-Experiments.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Experiments.release.xcconfig"; path = "Target Support Files/Pods-Experiments/Pods-Experiments.release.xcconfig"; sourceTree = "<group>"; };
5054
CC53FB47275E426900C4CA4F /* ABTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ABTest.swift; sourceTree = "<group>"; };
5155
EE2EDFDE29879331004E702B /* ABTestVariationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ABTestVariationProvider.swift; sourceTree = "<group>"; };
56+
EEC8C0EC298A92F10047B4CB /* CachedABTestVariationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedABTestVariationProvider.swift; sourceTree = "<group>"; };
57+
EEC8C0EE298A939C0047B4CB /* VariationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariationCache.swift; sourceTree = "<group>"; };
58+
EEC8C0F0298B5AFB0047B4CB /* VariationCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariationCacheTests.swift; sourceTree = "<group>"; };
59+
EEC8C0F2298B5F950047B4CB /* CachedABTestVariationProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedABTestVariationProviderTests.swift; sourceTree = "<group>"; };
5260
/* End PBXFileReference section */
5361

5462
/* Begin PBXFrameworksBuildPhase section */
@@ -103,6 +111,8 @@
103111
0270C0A627069BA500FC799F /* BuildConfiguration.swift */,
104112
CC53FB47275E426900C4CA4F /* ABTest.swift */,
105113
EE2EDFDE29879331004E702B /* ABTestVariationProvider.swift */,
114+
EEC8C0EC298A92F10047B4CB /* CachedABTestVariationProvider.swift */,
115+
EEC8C0EE298A939C0047B4CB /* VariationCache.swift */,
106116
);
107117
path = Experiments;
108118
sourceTree = "<group>";
@@ -111,6 +121,8 @@
111121
isa = PBXGroup;
112122
children = (
113123
0270C09127069A8900FC799F /* Info.plist */,
124+
EEC8C0F0298B5AFB0047B4CB /* VariationCacheTests.swift */,
125+
EEC8C0F2298B5F950047B4CB /* CachedABTestVariationProviderTests.swift */,
114126
);
115127
path = ExperimentsTests;
116128
sourceTree = "<group>";
@@ -206,6 +218,7 @@
206218
};
207219
0270C08927069A8900FC799F = {
208220
CreatedOnToolsVersion = 12.5.1;
221+
LastSwiftMigration = 1420;
209222
};
210223
};
211224
};
@@ -314,11 +327,13 @@
314327
isa = PBXSourcesBuildPhase;
315328
buildActionMask = 2147483647;
316329
files = (
330+
EEC8C0EF298A939C0047B4CB /* VariationCache.swift in Sources */,
317331
0270C0A327069B7800FC799F /* FeatureFlagService.swift in Sources */,
318332
0270C0A527069B8900FC799F /* DefaultFeatureFlagService.swift in Sources */,
319333
0270C09C27069AE700FC799F /* FeatureFlag.swift in Sources */,
320334
EE2EDFDF29879331004E702B /* ABTestVariationProvider.swift in Sources */,
321335
0270C0A727069BA500FC799F /* BuildConfiguration.swift in Sources */,
336+
EEC8C0ED298A92F10047B4CB /* CachedABTestVariationProvider.swift in Sources */,
322337
CC53FB48275E426900C4CA4F /* ABTest.swift in Sources */,
323338
);
324339
runOnlyForDeploymentPostprocessing = 0;
@@ -327,6 +342,8 @@
327342
isa = PBXSourcesBuildPhase;
328343
buildActionMask = 2147483647;
329344
files = (
345+
EEC8C0F1298B5AFB0047B4CB /* VariationCacheTests.swift in Sources */,
346+
EEC8C0F3298B5F950047B4CB /* CachedABTestVariationProviderTests.swift in Sources */,
330347
);
331348
runOnlyForDeploymentPostprocessing = 0;
332349
};
@@ -521,6 +538,7 @@
521538
baseConfigurationReference = 3022E2766134CE2735C73FC6 /* Pods-ExperimentsTests.debug.xcconfig */;
522539
buildSettings = {
523540
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "${inherited}";
541+
CLANG_ENABLE_MODULES = YES;
524542
CODE_SIGN_STYLE = Automatic;
525543
INFOPLIST_FILE = ExperimentsTests/Info.plist;
526544
LD_RUNPATH_SEARCH_PATHS = (
@@ -530,6 +548,7 @@
530548
);
531549
PRODUCT_BUNDLE_IDENTIFIER = com.automattic.ExperimentsTests;
532550
PRODUCT_NAME = "$(TARGET_NAME)";
551+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
533552
SWIFT_VERSION = 5.0;
534553
TARGETED_DEVICE_FAMILY = "1,2";
535554
};
@@ -540,6 +559,7 @@
540559
baseConfigurationReference = 7C831644164B49828A485590 /* Pods-ExperimentsTests.release.xcconfig */;
541560
buildSettings = {
542561
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "${inherited}";
562+
CLANG_ENABLE_MODULES = YES;
543563
CODE_SIGN_STYLE = Automatic;
544564
INFOPLIST_FILE = ExperimentsTests/Info.plist;
545565
LD_RUNPATH_SEARCH_PATHS = (
@@ -644,6 +664,7 @@
644664
baseConfigurationReference = 3F9DB5FBFF7A42EFBCB746F3 /* Pods-ExperimentsTests.release-alpha.xcconfig */;
645665
buildSettings = {
646666
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "${inherited}";
667+
CLANG_ENABLE_MODULES = YES;
647668
CODE_SIGN_STYLE = Automatic;
648669
INFOPLIST_FILE = ExperimentsTests/Info.plist;
649670
LD_RUNPATH_SEARCH_PATHS = (

Experiments/Experiments/ABTest.swift

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import AutomatticTracks
22

33
/// ABTest adds A/B testing experiments and runs the tests based on their variations from the ExPlat service.
44
///
5-
public enum ABTest: String, CaseIterable {
6-
/// Throwaway case, to prevent a compiler error:
7-
/// `An enum with no cases cannot declare a raw type`
8-
case null
5+
public enum ABTest: String, Codable, CaseIterable {
6+
/// Mocks for unit testing
7+
case mockLoggedIn, mockLoggedOut
98

109
/// A/A test to make sure there is no bias in the logged out state.
1110
/// Experiment ref: pbxNRc-1QS-p2
@@ -21,8 +20,8 @@ public enum ABTest: String, CaseIterable {
2120

2221
/// Returns a variation for the given experiment
2322
///
24-
var variation: Variation {
25-
ExPlat.shared?.experiment(rawValue) ?? .control
23+
public var variation: Variation? {
24+
ExPlat.shared?.experiment(rawValue)
2625
}
2726

2827
/// Returns the context for the given experiment.
@@ -34,18 +33,27 @@ public enum ABTest: String, CaseIterable {
3433
return .loggedIn
3534
case .aaTestLoggedOut, .applicationPasswordAuthentication:
3635
return .loggedOut
37-
case .null:
38-
return .none
36+
// Mocks
37+
case .mockLoggedIn:
38+
return .loggedIn
39+
case .mockLoggedOut:
40+
return .loggedOut
3941
}
4042
}
43+
44+
// Returns only the genuine ABTest cases. (After removing unit test mocks)
45+
//
46+
static var genuineCases: [ABTest] {
47+
ABTest.allCases.filter { [.mockLoggedIn, .mockLoggedOut].contains($0) == false }
48+
}
4149
}
4250

4351
public extension ABTest {
4452
/// Start the AB Testing platform if any experiment exists for the provided context
4553
///
4654
@MainActor
4755
static func start(for context: ExperimentContext) async {
48-
let experiments = ABTest.allCases.filter { $0.context == context }
56+
let experiments = ABTest.genuineCases.filter { $0.context == context }
4957

5058
await withCheckedContinuation { continuation in
5159
guard !experiments.isEmpty else {

Experiments/Experiments/ABTestVariationProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ public struct DefaultABTestVariationProvider: ABTestVariationProvider {
1111
public init() { }
1212

1313
public func variation(for abTest: ABTest) -> Variation {
14-
abTest.variation
14+
abTest.variation ?? .control
1515
}
1616
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import AutomatticTracks
2+
3+
/// Cache based implementation of `ABTestVariationProvider`
4+
///
5+
public struct CachedABTestVariationProvider: ABTestVariationProvider {
6+
7+
private let cache: VariationCache
8+
9+
public init(cache: VariationCache = VariationCache(userDefaults: .standard)) {
10+
self.cache = cache
11+
}
12+
13+
public func variation(for abTest: ABTest) -> Variation {
14+
// We cache only logged out ABTests as they are assigned based on `anonId` by `ExPlat`.
15+
// There will be one value assigned to one device and it won't change.
16+
//
17+
guard abTest.context == .loggedOut else {
18+
return abTest.variation ?? .control
19+
}
20+
21+
if let variation = abTest.variation {
22+
try? cache.assign(variation: variation, for: abTest)
23+
return variation
24+
} else if let cachedVariation = cache.variation(for: abTest) {
25+
return cachedVariation
26+
} else {
27+
return .control
28+
}
29+
}
30+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import AutomatticTracks
2+
3+
enum VariationCacheError: Error {
4+
case onlyLoggedOutExperimentsShouldBeCached
5+
}
6+
7+
public struct VariationCache {
8+
private let variationKey = "VariationCacheKey"
9+
10+
private let userDefaults: UserDefaults
11+
12+
public init(userDefaults: UserDefaults) {
13+
self.userDefaults = userDefaults
14+
}
15+
16+
func variation(for abTest: ABTest) -> Variation? {
17+
guard abTest.context == .loggedOut else {
18+
return nil
19+
}
20+
21+
guard let data = userDefaults.object(forKey: variationKey) as? Data,
22+
let cachedVariations = try? JSONDecoder().decode([CachedVariation].self, from: data),
23+
case let variation = cachedVariations.first(where: { $0.abTest == abTest })?.variation
24+
else {
25+
return nil
26+
}
27+
28+
return variation
29+
}
30+
31+
func assign(variation: Variation, for abTest: ABTest) throws {
32+
guard abTest.context == .loggedOut else {
33+
throw VariationCacheError.onlyLoggedOutExperimentsShouldBeCached
34+
}
35+
36+
var variations = userDefaults.object(forKey: variationKey) as? [CachedVariation] ?? []
37+
variations.append(CachedVariation(abTest: abTest, variation: variation))
38+
39+
let encodedVariation = try JSONEncoder().encode(variations)
40+
userDefaults.set(encodedVariation, forKey: variationKey)
41+
}
42+
}
43+
44+
public struct CachedVariation: Codable {
45+
let abTest: ABTest
46+
let variation: Variation
47+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import XCTest
2+
@testable import Experiments
3+
4+
final class CachedABTestVariationProviderTests: XCTestCase {
5+
func test_variation_is_control_when_the_value_does_not_exist() throws {
6+
// Given
7+
let userDefaults = try XCTUnwrap(UserDefaults(suiteName: UUID().uuidString))
8+
9+
// When
10+
let cache = VariationCache(userDefaults: userDefaults)
11+
let provider = CachedABTestVariationProvider(cache: cache)
12+
13+
// Then
14+
XCTAssertEqual(provider.variation(for: .mockLoggedOut), .control)
15+
}
16+
17+
func test_correct_variation_is_returned_after_caching_it() throws {
18+
// Given
19+
let userDefaults = try XCTUnwrap(UserDefaults(suiteName: UUID().uuidString))
20+
let cache = VariationCache(userDefaults: userDefaults)
21+
let provider = CachedABTestVariationProvider(cache: cache)
22+
23+
// When
24+
try cache.assign(variation: .treatment, for: .mockLoggedOut)
25+
26+
// Then
27+
XCTAssertEqual(provider.variation(for: .mockLoggedOut), .treatment)
28+
}
29+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import XCTest
2+
@testable import Experiments
3+
4+
final class VariationCacheTests: XCTestCase {
5+
func test_variation_is_nil_when_the_value_does_not_exist() throws {
6+
// Given
7+
let userDefaults = try XCTUnwrap(UserDefaults(suiteName: UUID().uuidString))
8+
9+
// When
10+
let cache = VariationCache(userDefaults: userDefaults)
11+
12+
// Then
13+
XCTAssertNil(cache.variation(for: .mockLoggedOut))
14+
}
15+
16+
func test_correct_variation_is_returned_after_setting_it() throws {
17+
// Given
18+
let userDefaults = try XCTUnwrap(UserDefaults(suiteName: UUID().uuidString))
19+
let cache = VariationCache(userDefaults: userDefaults)
20+
21+
// When
22+
try cache.assign(variation: .treatment, for: .mockLoggedOut)
23+
24+
// Then
25+
XCTAssertEqual(cache.variation(for: .mockLoggedOut), .treatment)
26+
}
27+
28+
func test_it_throws_when_trying_to_cache_logged_in_experiment() throws {
29+
// Given
30+
let userDefaults = try XCTUnwrap(UserDefaults(suiteName: UUID().uuidString))
31+
let cache = VariationCache(userDefaults: userDefaults)
32+
33+
// When
34+
XCTAssertThrowsError(try cache.assign(variation: .treatment, for: .mockLoggedIn))
35+
}
36+
}

Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def aztec
2727
end
2828

2929
def tracks
30-
pod 'Automattic-Tracks-iOS', '~> 1.0.0'
30+
pod 'Automattic-Tracks-iOS', '~> 2.0.0-beta.1'
3131
# pod 'Automattic-Tracks-iOS', :git => 'https://github.com/Automattic/Automattic-Tracks-iOS.git', :branch => 'trunk'
3232
# pod 'Automattic-Tracks-iOS', :git => 'https://github.com/Automattic/Automattic-Tracks-iOS.git', :commit => ''
3333
# pod 'Automattic-Tracks-iOS', :path => '../Automattic-Tracks-iOS'

Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ PODS:
66
- AppAuth/Core (1.6.0)
77
- AppAuth/ExternalUserAgent (1.6.0):
88
- AppAuth/Core
9-
- Automattic-Tracks-iOS (1.0.0):
9+
- Automattic-Tracks-iOS (2.0.0-beta.1):
1010
- Sentry (~> 7.25)
1111
- Sodium (>= 0.9.1)
1212
- UIDeviceIdentifier (~> 2.0)
@@ -75,7 +75,7 @@ PODS:
7575

7676
DEPENDENCIES:
7777
- Alamofire (~> 4.8)
78-
- Automattic-Tracks-iOS (~> 1.0.0)
78+
- Automattic-Tracks-iOS (~> 2.0.0-beta.1)
7979
- CocoaLumberjack (~> 3.7.4)
8080
- CocoaLumberjack/Swift (~> 3.7.4)
8181
- Gridicons (~> 1.2.0)
@@ -144,7 +144,7 @@ CHECKOUT OPTIONS:
144144
SPEC CHECKSUMS:
145145
Alamofire: 3ec537f71edc9804815215393ae2b1a8ea33a844
146146
AppAuth: 8fca6b5563a5baef2c04bee27538025e4ceb2add
147-
Automattic-Tracks-iOS: 93df154824af31eba947718110023acce1ce7905
147+
Automattic-Tracks-iOS: 20220d63a075787a890513ae56214a17a160ec43
148148
CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646
149149
GoogleSignIn: fd381840dbe7c1137aa6dc30849a5c3e070c034a
150150
Gridicons: 4455b9f366960121430e45997e32112ae49ffe1d
@@ -178,6 +178,6 @@ SPEC CHECKSUMS:
178178
ZendeskSupportProvidersSDK: 2bdf8544f7cd0fd4c002546f5704b813845beb2a
179179
ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba
180180

181-
PODFILE CHECKSUM: 74b7cf5dee0b25888c6c3b52ade928cf52818407
181+
PODFILE CHECKSUM: edd03ed08aa5c68531f27bdd04f1c0b846f98749
182182

183183
COCOAPODS: 1.11.3

0 commit comments

Comments
 (0)