Skip to content

Commit ed6ff9d

Browse files
fix: throttle retriable transport upload failures (#770)
* fix: throttle retriable transport upload failures Detect retriable transport errors via a shared Swift detector and apply throttling for message/alias retry paths while preserving existing HTTP retry ordering. * fix: simplify transport retry throttling path Remove the dedicated transport-throttle helper and handle transport retry throttling inline for message and alias uploads to keep the retry flow straightforward. * fix: avoid throttling on offline and timeout errors Keep transport throttling focused on host/connectivity failures by excluding not-connected and timed-out NSURL errors, and update tests to lock in the new retry classification behavior. * fix: route throttle logging through MPLog Use the SDK logger in throttleWithHTTPResponse to align with the new logging path and ensure custom logger handling is applied consistently. * test: add retry-after dictionary parser coverage Add a Swift NSDictionary extension for parsing Retry-After seconds and cover NSNumber, String, invalid, and missing header scenarios with unit tests. * fix: simplify retry-after throttle parsing Use dictionary retry-after helpers for date and seconds parsing in throttling logic and add Swift tests for both parsing paths. * fix: centralize retry-after calculation in network helper Move retry-after interval calculation into a dedicated Swift network helper and cover the helper behavior with unit tests for seconds and date-based headers. * fix: throttle transport errors with internal backoff state Add transport-error retry backoff state to the detector, route retriable transport failures through a dedicated throttling path, and reset the counter after successful uploads. * refactor: use a single throttle method with computed retry time Unify throttling into one method that accepts retryAfter and compute retry intervals at each call site for HTTP response and transport error paths. * refactor: reuse shared mparticle instance in throttling Use a local shared instance reference in throttleWithRetryAfter to keep logger and state machine access consistent and avoid repeated singleton lookups. * fix method call * fix target for tests * fix: restore SDK timeout detection with shared connector constants Promote the connector semaphore timeout domain/code to shared constants and reuse them in connector timeout errors. Restore transport detector handling for the SDK timeout signal and align unit expectations with the updated behavior. * fix: treat offline and timeout as retriable transport errors Classify NSURLErrorNotConnectedToInternet and NSURLErrorTimedOut as retriable transport failures and update detector tests to match the restored behavior. * fix: clamp transport retry schedule indexing Guard against empty retry schedules and clamp the calculated backoff index to the last available entry to avoid out-of-bounds access if constants drift. * fix: reduce transport backoff schedule to short windows Lower transport retry intervals to a 5s/15s/60s/120s/300s progression and cap at 5 minutes to avoid long upload freezes after transient connectivity failures. * deduplicate constants * fix: tighten transport retries and align OneTrust selector Exclude ATS secure-connection failures from retriable transport errors with matching test coverage, and update OneTrust vendor details call to the current vendorId selector. * fix: use OneTrust vendorID selector for consent details Switch the OneTrust vendor consent lookup to the exported getVendorDetailsWithVendorID:for: selector so kit builds succeed with the current SDK headers. * fix: namespace retry-after NSDictionary extension methods Prefix retry-after category methods with mp_ to avoid Objective-C selector collisions in host apps, and update helper and tests to use the namespaced API. --------- Co-authored-by: James Newman <james.newman@rokt.com>
1 parent 662377f commit ed6ff9d

9 files changed

Lines changed: 379 additions & 41 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import XCTest
2+
internal import mParticle_Apple_SDK_Swift
3+
4+
final class MPTransportErrorDetectorTests: XCTestCase {
5+
override func setUp() {
6+
super.setUp()
7+
MPTransportErrorDetector.resetTransportErrorCounter()
8+
}
9+
10+
func test_isRetriableTransportError_returnsFalse_whenErrorIsNil() {
11+
XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(nil))
12+
}
13+
14+
func test_isRetriableTransportError_returnsFalse_whenNoConnectionCode() {
15+
let error = NSError(domain: "any-domain", code: 1)
16+
XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(error))
17+
}
18+
19+
func test_isRetriableTransportError_returnsTrue_whenNSURLErrorIsRetriable() {
20+
let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotConnectToHost)
21+
XCTAssertTrue(MPTransportErrorDetector.isRetriableTransportError(error))
22+
}
23+
24+
func test_isRetriableTransportError_returnsFalse_whenNSURLErrorIsNotRetriable() {
25+
let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled)
26+
XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(error))
27+
}
28+
29+
func test_isRetriableTransportError_returnsTrue_whenNSURLErrorIsNotConnectedToInternet() {
30+
let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet)
31+
XCTAssertTrue(MPTransportErrorDetector.isRetriableTransportError(error))
32+
}
33+
34+
func test_isRetriableTransportError_returnsTrue_whenNSURLErrorIsTimedOut() {
35+
let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut)
36+
XCTAssertTrue(MPTransportErrorDetector.isRetriableTransportError(error))
37+
}
38+
39+
func test_isRetriableTransportError_returnsFalse_whenNSURLErrorIsAppTransportSecurityRequiresSecureConnection() {
40+
let error = NSError(
41+
domain: NSURLErrorDomain,
42+
code: NSURLErrorAppTransportSecurityRequiresSecureConnection
43+
)
44+
XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(error))
45+
}
46+
47+
func test_isRetriableTransportError_returnsTrue_forMParticleTimeoutError() {
48+
let error = NSError(
49+
domain: MPTransportErrorDetector.semaphoreTimeoutErrorDomain() as String,
50+
code: MPTransportErrorDetector.semaphoreTimeoutErrorCode().intValue
51+
)
52+
XCTAssertTrue(MPTransportErrorDetector.isRetriableTransportError(error))
53+
}
54+
55+
func test_isRetriableTransportError_returnsFalse_forUnknownError() {
56+
let error = NSError(domain: "custom-domain", code: 42)
57+
XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(error))
58+
}
59+
60+
func test_calculateRetryTimeForTransportError_usesSmallValueForFirstError() {
61+
XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 5)
62+
}
63+
64+
func test_calculateRetryTimeForTransportError_reachesMaxAtFiveErrors() {
65+
for _ in 0..<4 {
66+
_ = MPTransportErrorDetector.calculateRetryTimeForTransportError()
67+
}
68+
69+
XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 300)
70+
}
71+
72+
func test_calculateRetryTimeForTransportError_usesExpectedBackoffSchedule() {
73+
XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 5)
74+
XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 15)
75+
XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 60)
76+
XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 120)
77+
XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 300)
78+
}
79+
80+
func test_calculateRetryTimeForTransportError_staysAtMaxAfterThreshold() {
81+
for _ in 0..<5 {
82+
_ = MPTransportErrorDetector.calculateRetryTimeForTransportError()
83+
}
84+
85+
XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 300)
86+
}
87+
88+
func test_resetTransportErrorCounter_resetsBackoff() {
89+
_ = MPTransportErrorDetector.calculateRetryTimeForTransportError()
90+
_ = MPTransportErrorDetector.calculateRetryTimeForTransportError()
91+
MPTransportErrorDetector.resetTransportErrorCounter()
92+
93+
XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 5)
94+
}
95+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Foundation
2+
3+
@objc public final class MPNetworkCommunicationHelper: NSObject {
4+
@objc(calculateRetryTimeForHeaders:)
5+
public static func calculateRetryTime(for headers: NSDictionary) -> NSNumber {
6+
let retryAfterDate = headers.mp_retryDate()
7+
let retryAfterSeconds = headers.mp_retrySeconds()
8+
let defaultRetryAfter: Double = 7200
9+
let maxRetryAfter: Double = 86400
10+
11+
if let retryAfterDate {
12+
let now = Date()
13+
let retryAfter = min(retryAfterDate.timeIntervalSince1970 - now.timeIntervalSince1970, maxRetryAfter)
14+
return NSNumber(value: retryAfter > 0 ? retryAfter : defaultRetryAfter)
15+
}
16+
17+
if let retryAfterSeconds {
18+
return NSNumber(value: min(retryAfterSeconds.doubleValue, maxRetryAfter))
19+
}
20+
21+
return NSNumber(value: defaultRetryAfter)
22+
}
23+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Foundation
2+
3+
@objc public class MPTransportErrorDetector: NSObject {
4+
private static let maxRetryAfter: Double = 300
5+
private static let maxErrorCountBeforeMaxRetry = 5
6+
private static let retryAfterSchedule: [Double] = [5, 15, 60, 120, 300]
7+
private static var consecutiveTransportErrorCount = 0
8+
private static let backoffQueue = DispatchQueue(label: "com.mparticle.transport-error-backoff")
9+
private static let semaphoreTimeoutErrorDomainValue = "com.mparticle"
10+
private static let semaphoreTimeoutErrorCodeValue = 0
11+
12+
@objc(semaphoreTimeoutErrorDomain)
13+
public static func semaphoreTimeoutErrorDomain() -> NSString {
14+
semaphoreTimeoutErrorDomainValue as NSString
15+
}
16+
17+
@objc(semaphoreTimeoutErrorCode)
18+
public static func semaphoreTimeoutErrorCode() -> NSNumber {
19+
NSNumber(value: semaphoreTimeoutErrorCodeValue)
20+
}
21+
22+
@objc(isRetriableTransportError:)
23+
public static func isRetriableTransportError(_ error: NSError?) -> Bool {
24+
guard let error else {
25+
return false
26+
}
27+
28+
if error.domain == NSURLErrorDomain {
29+
switch error.code {
30+
case NSURLErrorCannotFindHost,
31+
NSURLErrorCannotConnectToHost,
32+
NSURLErrorNotConnectedToInternet,
33+
NSURLErrorNetworkConnectionLost,
34+
NSURLErrorDNSLookupFailed,
35+
NSURLErrorTimedOut,
36+
NSURLErrorCannotLoadFromNetwork,
37+
NSURLErrorSecureConnectionFailed,
38+
NSURLErrorInternationalRoamingOff,
39+
NSURLErrorDataNotAllowed,
40+
NSURLErrorCallIsActive:
41+
return true
42+
default:
43+
return false
44+
}
45+
}
46+
47+
return error.domain == semaphoreTimeoutErrorDomainValue
48+
&& error.code == semaphoreTimeoutErrorCodeValue
49+
}
50+
51+
@objc(calculateRetryTimeForTransportError)
52+
public static func calculateRetryTimeForTransportError() -> NSNumber {
53+
return backoffQueue.sync {
54+
consecutiveTransportErrorCount += 1
55+
56+
if consecutiveTransportErrorCount >= maxErrorCountBeforeMaxRetry {
57+
return NSNumber(value: maxRetryAfter)
58+
}
59+
60+
guard !retryAfterSchedule.isEmpty else {
61+
return NSNumber(value: maxRetryAfter)
62+
}
63+
64+
let scheduleIndex = min(
65+
max(0, consecutiveTransportErrorCount - 1),
66+
retryAfterSchedule.count - 1
67+
)
68+
return NSNumber(value: retryAfterSchedule[scheduleIndex])
69+
}
70+
}
71+
72+
@objc(resetTransportErrorCounter)
73+
public static func resetTransportErrorCounter() {
74+
backoffQueue.sync {
75+
consecutiveTransportErrorCount = 0
76+
}
77+
}
78+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Foundation
2+
3+
@objc public extension NSDictionary {
4+
@objc(mp_retryDate)
5+
func mp_retryDate() -> Date? {
6+
guard let headerValue = self["Retry-After"] as? String else {
7+
return nil
8+
}
9+
10+
return MPDateFormatter.date(fromStringRFC1123: headerValue)
11+
}
12+
13+
@objc(mp_retrySeconds)
14+
func mp_retrySeconds() -> NSNumber? {
15+
let headerValue = self["Retry-After"]
16+
17+
if let number = headerValue as? NSNumber {
18+
return NSNumber(value: number.doubleValue)
19+
}
20+
if let string = headerValue as? String {
21+
return Double(string).map { NSNumber(value: $0) }
22+
}
23+
24+
return nil
25+
}
26+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import XCTest
2+
@testable import mParticle_Apple_SDK_Swift
3+
4+
final class MPNetworkCommunicationHelperTests: XCTestCase {
5+
func testCalculateRetryTimeUsesNumberHeader() {
6+
let headers: NSDictionary = ["Retry-After": NSNumber(value: 42)]
7+
8+
XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 42)
9+
}
10+
11+
func testCalculateRetryTimeUsesStringSecondsHeader() {
12+
let headers: NSDictionary = ["Retry-After": "123.5"]
13+
14+
XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 123.5)
15+
}
16+
17+
func testCalculateRetryTimeFallsBackToDefaultForInvalidString() {
18+
let headers: NSDictionary = ["Retry-After": "not-a-number"]
19+
20+
XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 7200)
21+
}
22+
23+
func testCalculateRetryTimeFallsBackToDefaultWhenHeaderMissing() {
24+
let headers: NSDictionary = ["Other-Header": "1"]
25+
26+
XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 7200)
27+
}
28+
29+
func testCalculateRetryTimeClampsToMax() {
30+
let headers: NSDictionary = ["Retry-After": NSNumber(value: 999999)]
31+
32+
XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 86400)
33+
}
34+
35+
func testCalculateRetryTimeParsesDateHeader() {
36+
let futureDate = Date().addingTimeInterval(300)
37+
let retryAfterHeader = MPDateFormatter.string(fromDateRFC1123: futureDate)
38+
let headers: NSDictionary = ["Retry-After": retryAfterHeader as Any]
39+
40+
XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 300, accuracy: 2.0)
41+
}
42+
43+
func testCalculateRetryTimeUsesDefaultForPastDateHeader() {
44+
let pastDate = Date().addingTimeInterval(-300)
45+
let retryAfterHeader = MPDateFormatter.string(fromDateRFC1123: pastDate)
46+
let headers: NSDictionary = ["Retry-After": retryAfterHeader as Any]
47+
48+
XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 7200)
49+
}
50+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import XCTest
2+
@testable import mParticle_Apple_SDK_Swift
3+
4+
final class NSDictionaryMPRetryAfterTests: XCTestCase {
5+
func testRetryDateParsesRFC1123String() {
6+
let headers: NSDictionary = ["Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT"]
7+
8+
XCTAssertNotNil(headers.mp_retryDate())
9+
}
10+
11+
func testRetryDateReturnsNilForInvalidString() {
12+
let headers: NSDictionary = ["Retry-After": "not-a-date"]
13+
14+
XCTAssertNil(headers.mp_retryDate())
15+
}
16+
17+
func testRetrySecondsReturnsNumberValue() {
18+
let headers: NSDictionary = ["Retry-After": NSNumber(value: 42)]
19+
20+
XCTAssertEqual(headers.mp_retrySeconds()?.doubleValue, 42)
21+
}
22+
23+
func testRetrySecondsParsesStringValue() {
24+
let headers: NSDictionary = ["Retry-After": "123.5"]
25+
26+
XCTAssertEqual(headers.mp_retrySeconds()?.doubleValue, 123.5)
27+
}
28+
29+
func testRetrySecondsReturnsNilForInvalidString() {
30+
let headers: NSDictionary = ["Retry-After": "not-a-number"]
31+
32+
XCTAssertNil(headers.mp_retrySeconds())
33+
}
34+
35+
func testRetrySecondsReturnsNilWhenHeaderMissing() {
36+
let headers: NSDictionary = ["Other-Header": "1"]
37+
38+
XCTAssertNil(headers.mp_retrySeconds())
39+
}
40+
}

mParticle-Apple-SDK.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
359BAFFC2E56335300A8A704 /* SettingsProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 359BAFFB2E56335300A8A704 /* SettingsProvider.m */; };
2626
359BAFFF2E575B0300A8A704 /* SettingsProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359BAFFE2E575AF500A8A704 /* SettingsProviderTests.swift */; };
2727
359BB0062E57A56800A8A704 /* MPUserDefaultsMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359BB0052E57A56300A8A704 /* MPUserDefaultsMock.swift */; };
28+
35A1B2C32F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35A1B2C22F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift */; };
2829
35C3DE702F2919B40077C0FD /* MPCCPAConsent.m in Sources */ = {isa = PBXBuildFile; fileRef = 35C3DE6F2F2919B40077C0FD /* MPCCPAConsent.m */; };
2930
35C3DE712F2919B40077C0FD /* MPCCPAConsent.h in Headers */ = {isa = PBXBuildFile; fileRef = 35C3DE6E2F2919B40077C0FD /* MPCCPAConsent.h */; settings = {ATTRIBUTES = (Public, ); }; };
3031
35C3DE732F291DA40077C0FD /* MPGDPRConsent.m in Sources */ = {isa = PBXBuildFile; fileRef = 35C3DE722F291DA40077C0FD /* MPGDPRConsent.m */; };
@@ -346,6 +347,7 @@
346347
359BAFFB2E56335300A8A704 /* SettingsProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsProvider.m; sourceTree = "<group>"; };
347348
359BAFFE2E575AF500A8A704 /* SettingsProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsProviderTests.swift; sourceTree = "<group>"; };
348349
359BB0052E57A56300A8A704 /* MPUserDefaultsMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPUserDefaultsMock.swift; sourceTree = "<group>"; };
350+
35A1B2C22F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPTransportErrorDetectorTests.swift; sourceTree = "<group>"; };
349351
35C3DE6E2F2919B40077C0FD /* MPCCPAConsent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPCCPAConsent.h; sourceTree = "<group>"; };
350352
35C3DE6F2F2919B40077C0FD /* MPCCPAConsent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPCCPAConsent.m; sourceTree = "<group>"; };
351353
35C3DE722F291DA40077C0FD /* MPGDPRConsent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPGDPRConsent.m; sourceTree = "<group>"; };
@@ -630,11 +632,13 @@
630632
Test/Utils/MPDateFormatterTests.swift,
631633
Test/Utils/MPDeviceTests.swift,
632634
Test/Utils/MPLaunchInfoTests.m,
635+
Test/Utils/MPNetworkCommunicationHelperTests.swift,
633636
Test/Utils/MPResponseConfigTests.swift,
634637
Test/Utils/MPUserAttributeChangeTests.m,
635638
Test/Utils/MPUserDefaultsTests.swift,
636639
Test/Utils/MPZipTests.m,
637640
"Test/Utils/NSArray+MPCaseInsensitiveTests.swift",
641+
"Test/Utils/NSDictionary+MPRetryAfterTests.swift",
638642
"Test/Utils/NSNumber+MPFormatterTests.m",
639643
"Test/Utils/NSString+MPPercentEscapeTests.swift",
640644
);
@@ -649,11 +653,13 @@
649653
Test/Utils/MPDateFormatterTests.swift,
650654
Test/Utils/MPDeviceTests.swift,
651655
Test/Utils/MPLaunchInfoTests.m,
656+
Test/Utils/MPNetworkCommunicationHelperTests.swift,
652657
Test/Utils/MPResponseConfigTests.swift,
653658
Test/Utils/MPUserAttributeChangeTests.m,
654659
Test/Utils/MPUserDefaultsTests.swift,
655660
Test/Utils/MPZipTests.m,
656661
"Test/Utils/NSArray+MPCaseInsensitiveTests.swift",
662+
"Test/Utils/NSDictionary+MPRetryAfterTests.swift",
657663
"Test/Utils/NSNumber+MPFormatterTests.m",
658664
"Test/Utils/NSString+MPPercentEscapeTests.swift",
659665
);
@@ -1178,6 +1184,7 @@
11781184
35E3FCC22E53B5C200DB5B18 /* MPAttributionResult+MParticlePrivateTests.swift */,
11791185
72FEBD162E86FE2D00B8341F /* MPIdentityTests.swift */,
11801186
72D356522E8460020012A0C2 /* MPEventTests.swift */,
1187+
35A1B2C22F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift */,
11811188
);
11821189
path = SwiftTests;
11831190
sourceTree = "<group>";
@@ -1562,6 +1569,7 @@
15621569
534CD28D29CE2CE1008452B3 /* MPUploadBuilderTests.m in Sources */,
15631570
534CD28E29CE2CE1008452B3 /* MPBaseTestCase.m in Sources */,
15641571
35329FEC2E54C483009AC4FD /* MPNetworkOptions+MParticlePrivateTests.swift in Sources */,
1572+
35A1B2C32F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift in Sources */,
15651573
7231B8352EB95F9F001565E5 /* MParticleBreadcrumbTests.swift in Sources */,
15661574
534CD28F29CE2CE1008452B3 /* MPIdentityApiRequestTests.m in Sources */,
15671575
534CD29029CE2CE1008452B3 /* MPKitAppsFlyerTest.m in Sources */,

0 commit comments

Comments
 (0)