Skip to content

Commit 817036d

Browse files
Merge pull request #687 from mParticle/chore/merge-main-into-workstation-9.0-2026-03-25
chore: Merge from Main 3-25-26
2 parents f09930a + a7247d4 commit 817036d

File tree

17 files changed

+369
-61
lines changed

17 files changed

+369
-61
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,27 @@ All notable changes to the mParticle Apple SDK (core and integration kits) are d
1616

1717
---
1818

19+
# [8.44.3](https://github.com/mParticle/mparticle-apple-sdk/compare/v8.44.2...v8.44.3) (2026-03-23)
20+
21+
### Bug Fixes
22+
23+
- fix: serialize updateLastUseDate with messageQueue on background entry (#680) ([ba76afc5](https://github.com/mParticle/mparticle-apple-sdk/commit/ba76afc5c619e3fcf06acbb976e2e9e072845a7e))
24+
25+
# [8.44.2](https://github.com/mParticle/mparticle-apple-sdk/compare/v8.44.1...v8.44.2) (2026-03-17)
26+
27+
### Bug Fixes
28+
29+
- fix: serialize backgroundTimeRemaining with cancellation check (#667) ([beccd65a](https://github.com/mParticle/mparticle-apple-sdk/commit/beccd65a2d3ba77a548ca1d104a14d511174e528))
30+
31+
# [8.44.1](https://github.com/mParticle/mparticle-apple-sdk/compare/v8.44.0...v8.44.1) (2026-03-11)
32+
33+
### Bug Fixes
34+
35+
- fix: Add Defensive Code when Building URL Signatures (#656) ([d84ef13e](https://github.com/mParticle/mparticle-apple-sdk/commit/d84ef13ed1547c8e28196727ea2f183941debf1b))
36+
- fix: WKWebView Logging Crash (#662) ([edaa25c0](https://github.com/mParticle/mparticle-apple-sdk/commit/edaa25c0b3b230207c5344d06c5879f51a756b6e))
37+
- fix: Crash in MPSession description (#661) ([dd6c80f9](https://github.com/mParticle/mparticle-apple-sdk/commit/dd6c80f9942c2ea7863b945a1a47844444e89f4a))
38+
- fix: Remove Outdated previousSessionSuccessfullyClosed Logic (#655) ([84ad0875](https://github.com/mParticle/mparticle-apple-sdk/commit/84ad08757b5c845dfefe9b9e1bef2655b5e8c9ed))
39+
1940
# [8.44.0](https://github.com/mParticle/mparticle-apple-sdk/compare/v8.43.1...v8.44.0) (2026-02-19)
2041

2142
### Bug Fixes

Framework/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<key>CFBundlePackageType</key>
1616
<string>FMWK</string>
1717
<key>CFBundleShortVersionString</key>
18-
<string>8.44.0</string>
18+
<string>8.44.3</string>
1919
<key>CFBundleSignature</key>
2020
<string>????</string>
2121
<key>CFBundleVersion</key>

Kits/braze/braze-14/Tests/mParticle-BrazeTests/mParticle_BrazeTests.m

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ - (void)setBrazeInstanceLocal:(Braze *)instance;
1818
- (void)setEnableTypeDetection:(BOOL)enableTypeDetection;
1919
+ (BOOL)shouldDisableNotificationHandling;
2020
+ (Braze *)brazeInstance;
21-
+ (MPKitExecStatus *)updateUser:(FilteredMParticleUser *)user request:(NSDictionary<NSNumber *,NSString *> *)userIdentities;
22-
+ (MPKitExecStatus *)setUserAttribute:(NSString *)key value:(NSString *)value;
21+
- (MPKitExecStatus *)updateUser:(FilteredMParticleUser *)user request:(NSDictionary<NSNumber *,NSString *> *)userIdentities;
22+
- (MPKitExecStatus *)setUserAttribute:(NSString *)key value:(NSString *)value;
2323

2424
@end
2525

@@ -34,10 +34,19 @@ - (void)setUp {
3434
// Put setup code here. This method is called before the invocation of each test method in the class.
3535
[MPKitBraze setBrazeInstance:nil];
3636
[MPKitBraze setURLDelegate:nil];
37+
#if TARGET_OS_IOS
38+
[MPKitBraze setInAppMessageControllerDelegate:nil];
39+
[MPKitBraze setShouldDisableNotificationHandling:NO];
40+
#endif
3741
}
3842

3943
- (void)tearDown {
40-
// Put teardown code here. This method is called after the invocation of each test method in the class.
44+
[MPKitBraze setBrazeInstance:nil];
45+
[MPKitBraze setURLDelegate:nil];
46+
#if TARGET_OS_IOS
47+
[MPKitBraze setInAppMessageControllerDelegate:nil];
48+
[MPKitBraze setShouldDisableNotificationHandling:NO];
49+
#endif
4150
[super tearDown];
4251
}
4352

@@ -75,10 +84,10 @@ - (void)testStartwithAdvancedConfig {
7584

7685
NSDictionary *testOptionsDictionary = @{ABKEnableAutomaticLocationCollectionKey:@(YES),
7786
ABKSDKFlavorKey:@7,
78-
@"ABKRquestProcessingPolicy": @(1),
79-
@"ABKFlushInterval":@(2),
80-
@"ABKSessionTimeout":@(3),
81-
@"ABKMinimumTriggerTimeInterval":@(4)
87+
ABKRequestProcessingPolicyOptionKey: @(1),
88+
ABKFlushIntervalOptionKey: @(2),
89+
ABKSessionTimeoutKey: @(3),
90+
ABKMinimumTriggerTimeIntervalKey: @(4)
8291
};
8392

8493
NSDictionary *optionsDictionary = [braze optionsDictionary];
@@ -254,15 +263,15 @@ - (void)testSubscriptionGroupIdsMappedUserAttributes {
254263
id mockClient = OCMPartialMock(testClient);
255264
[kitInstance setBrazeInstanceLocal:mockClient];
256265
XCTAssertEqualObjects(mockClient, [kitInstance brazeInstanceLocal]);
257-
258-
// Should succeed since Bool false is a valid value
266+
// subscriptionGroupMapping is applied in -start only; without -start mapped keys are handled as custom attributes (invalid subscription values would incorrectly return success).
267+
[kitInstance start];
268+
259269
MPKitExecStatus *execStatus1 = [kitInstance setUserAttribute:@"testAttribute1" value:@NO];
260-
XCTAssertEqual(execStatus1.returnCode, MPKitReturnCodeSuccess);
261-
// Should succeed since Bool true is a valid value
262270
MPKitExecStatus *execStatus2 = [kitInstance setUserAttribute:@"testAttribute2" value:@YES];
263-
XCTAssertEqual(execStatus2.returnCode, MPKitReturnCodeSuccess);
264-
// Should fail since testValue is not type BOOL
265271
MPKitExecStatus *execStatus3 = [kitInstance setUserAttribute:@"testAttribute2" value:@"testValue"];
272+
273+
XCTAssertEqual(execStatus1.returnCode, MPKitReturnCodeSuccess);
274+
XCTAssertEqual(execStatus2.returnCode, MPKitReturnCodeSuccess);
266275
XCTAssertEqual(execStatus3.returnCode, MPKitReturnCodeFail);
267276

268277
[mockClient verify];

Kits/rokt/rokt/Sources/mParticle-Rokt-Swift/MPRoktLayout.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public class MPRoktLayout {
5858
"locationName:\(locationName), " +
5959
"attributes:\(preparedAttributes)"
6060
)
61+
6162
self.roktLayout = RoktLayout.init(
6263
sdkTriggered: sdkTriggered,
6364
identifier: identifier,

UnitTests/Mocks/SceneDelegateHandlerMock.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ class OpenURLHandlerProtocolMock: NSObject, OpenURLHandlerProtocol {
66
var openURLWithOptionsURLParam: URL?
77
var openURLWithOptionsOptionsParam: [String: Any]?
88

9-
@objc(openURL:options:)
109
func open(_ url: URL, options: [String: Any]?) {
1110
openURLWithOptionsCalled = true
1211
openURLWithOptionsURLParam = url
@@ -18,11 +17,8 @@ class OpenURLHandlerProtocolMock: NSObject, OpenURLHandlerProtocol {
1817
var continueUserActivityRestorationHandlerParam: (([UIUserActivityRestoring]?) -> Void)?
1918
var continueUserActivityReturnValue: Bool = false
2019

21-
@objc(continueUserActivity:restorationHandler:)
22-
func `continue`(
23-
_ userActivity: NSUserActivity,
24-
restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void
25-
) -> Bool {
20+
func `continue`(_ userActivity: NSUserActivity,
21+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
2622
continueUserActivityCalled = true
2723
continueUserActivityUserActivityParam = userActivity
2824
continueUserActivityRestorationHandlerParam = restorationHandler

UnitTests/ObjCTests/MPBackendControllerTests.m

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2442,6 +2442,107 @@ - (void)testBackgroundTimeCheckLoopStoresTimeRemainingInLocalVariable {
24422442
[MPApplication_PRIVATE setMockApplication:nil];
24432443
}
24442444

2445+
- (void)testForegroundHandlerStopsBackgroundTimeRemainingCalls {
2446+
// Verify that when the app returns to foreground (simulated via
2447+
// cancelBackgroundTimeCheckLoop + endBackgroundTask, the same
2448+
// sequence as handleApplicationWillEnterForeground), the loop
2449+
// stops calling backgroundTimeRemaining. This is the scenario
2450+
// where the TOCTOU race was observed (2ms window in production).
2451+
2452+
__block NSInteger backgroundTimeRemainingCallCount = 0;
2453+
2454+
id mockApplication = OCMClassMock([UIApplication class]);
2455+
OCMStub([mockApplication applicationState]).andReturn(UIApplicationStateBackground);
2456+
OCMStub([mockApplication backgroundTimeRemaining]).andDo(^(NSInvocation *invocation) {
2457+
backgroundTimeRemainingCallCount++;
2458+
NSTimeInterval remaining = 25.0;
2459+
[invocation setReturnValue:&remaining];
2460+
});
2461+
OCMStub([mockApplication beginBackgroundTaskWithExpirationHandler:OCMOCK_ANY]).andReturn((UIBackgroundTaskIdentifier)42);
2462+
OCMStub([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]);
2463+
2464+
[MPApplication_PRIVATE setMockApplication:mockApplication];
2465+
2466+
// Start the background task
2467+
XCTestExpectation *taskStarted = [self expectationWithDescription:@"Background task started"];
2468+
[self.backendController beginBackgroundTask];
2469+
dispatch_async(dispatch_get_main_queue(), ^{
2470+
[taskStarted fulfill];
2471+
});
2472+
[self waitForExpectations:@[taskStarted] timeout:2.0];
2473+
2474+
// Start the background time check loop
2475+
dispatch_async(messageQueue, ^{
2476+
[self.backendController beginBackgroundTimeCheckLoop];
2477+
});
2478+
2479+
// Let the loop run a few iterations
2480+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
2481+
NSInteger callsBeforeForeground = backgroundTimeRemainingCallCount;
2482+
XCTAssertGreaterThan(callsBeforeForeground, 0, @"Loop should have called backgroundTimeRemaining at least once");
2483+
2484+
// Simulate the foreground handler (same sequence as handleApplicationWillEnterForeground)
2485+
[self.backendController cancelBackgroundTimeCheckLoop];
2486+
[self.backendController endBackgroundTask];
2487+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
2488+
2489+
// Record calls and wait to confirm no new calls arrive
2490+
NSInteger callsAtForeground = backgroundTimeRemainingCallCount;
2491+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
2492+
NSInteger callsAfterForeground = backgroundTimeRemainingCallCount;
2493+
2494+
XCTAssertEqual(callsAfterForeground, callsAtForeground,
2495+
@"backgroundTimeRemaining should not be called after foreground handler. "
2496+
"Calls at foreground: %ld, calls after waiting: %ld",
2497+
(long)callsAtForeground, (long)callsAfterForeground);
2498+
2499+
XCTAssertEqual(self.backendController.backgroundCheckQueue.operationCount, 0,
2500+
@"Background check queue should have no running operations after foreground");
2501+
2502+
[self.backendController cancelBackgroundTimeCheckLoop];
2503+
[MPApplication_PRIVATE setMockApplication:nil];
2504+
}
2505+
2506+
- (void)testBackgroundTimeRemainingIsAccessedOnMainThread {
2507+
__block BOOL allAccessesOnMainThread = YES;
2508+
2509+
id mockApplication = OCMClassMock([UIApplication class]);
2510+
2511+
__block NSInteger stateCallCount = 0;
2512+
OCMStub([mockApplication applicationState]).andDo(^(NSInvocation *invocation) {
2513+
UIApplicationState state = (stateCallCount < 3)
2514+
? UIApplicationStateBackground
2515+
: UIApplicationStateActive;
2516+
stateCallCount++;
2517+
[invocation setReturnValue:&state];
2518+
});
2519+
2520+
OCMStub([mockApplication backgroundTimeRemaining]).andDo(^(NSInvocation *invocation) {
2521+
if (![NSThread isMainThread]) {
2522+
allAccessesOnMainThread = NO;
2523+
}
2524+
NSTimeInterval remaining = 25.0;
2525+
[invocation setReturnValue:&remaining];
2526+
});
2527+
OCMStub([mockApplication beginBackgroundTaskWithExpirationHandler:OCMOCK_ANY]).andReturn((UIBackgroundTaskIdentifier)42);
2528+
OCMStub([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]);
2529+
2530+
[MPApplication_PRIVATE setMockApplication:mockApplication];
2531+
2532+
dispatch_async(messageQueue, ^{
2533+
[self.backendController beginBackgroundTimeCheckLoop];
2534+
});
2535+
2536+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]];
2537+
2538+
XCTAssertTrue(allAccessesOnMainThread,
2539+
@"backgroundTimeRemaining must be accessed on the main thread "
2540+
"to prevent XPC race conditions during app suspension");
2541+
2542+
[self.backendController cancelBackgroundTimeCheckLoop];
2543+
[MPApplication_PRIVATE setMockApplication:nil];
2544+
}
2545+
24452546
- (void)testEndSessionIfTimedOutDispatchesToMessageQueue {
24462547
// Verify that endSessionIfTimedOut called from a non-message-queue thread
24472548
// does not mutate session properties directly on that thread, but instead

UnitTests/ObjCTests/MPRoktTests.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ - (void)testSelectPlacementsExpandedWithValidParameters {
196196

197197
// Wait for async operation
198198
[self waitForExpectationsWithTimeout:0.2 handler:nil];
199-
199+
200200
// Verify
201201
OCMVerifyAll(self.mockContainer);
202202
}

UnitTests/ObjCTests/MPStateMachineTests.m

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ @interface MParticle ()
2222

2323
@property (nonatomic, strong) MPStateMachine_PRIVATE *stateMachine;
2424
@property (nonatomic, strong) MPKitContainer_PRIVATE *kitContainer_PRIVATE;
25+
@property (nonatomic, strong, nonnull) MPBackendController_PRIVATE *backendController;
26+
27+
+ (dispatch_queue_t)messageQueue;
2528

2629
@end
2730

@@ -271,4 +274,97 @@ - (void)testApiKeySecretThreadSafety {
271274
[self waitForExpectationsWithTimeout:30 handler:nil];
272275
}
273276

277+
#pragma mark - Background UserDefaults Serialization Tests
278+
279+
- (void)testUpdateLastUseDateSerializedWithMessageQueueWork {
280+
XCTestExpectation *expectation = [self expectationWithDescription:@"Serialized background access"];
281+
282+
[MParticle sharedInstance].backendController = [[MPBackendController_PRIVATE alloc] initWithDelegate:(id<MPBackendControllerDelegate>)[MParticle sharedInstance]];
283+
[MPPersistenceController_PRIVATE setMpid:@12345];
284+
285+
MPUserDefaults *defaults = MPUserDefaultsConnector.userDefaults;
286+
287+
dispatch_queue_t sdkMessageQueue = [MParticle messageQueue];
288+
dispatch_group_t group = dispatch_group_create();
289+
NSInteger iterations = 500;
290+
291+
for (NSInteger i = 0; i < iterations; i++) {
292+
dispatch_group_async(group, sdkMessageQueue, ^{
293+
[MPApplication_PRIVATE updateLastUseDate:[NSDate date]];
294+
});
295+
296+
dispatch_group_async(group, sdkMessageQueue, ^{
297+
[defaults setMPObject:@(i) forKey:@"testBg" userId:[MPPersistenceController_PRIVATE mpId]];
298+
});
299+
300+
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
301+
NSNumber *mpId = [MPPersistenceController_PRIVATE mpId];
302+
(void)mpId;
303+
});
304+
}
305+
306+
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
307+
MPUserDefaults *ud = MPUserDefaultsConnector.userDefaults;
308+
NSNumber *lastUseDate = ud[kMPAppLastUseDateKey];
309+
XCTAssertNotNil(lastUseDate, @"lastUseDate must be persisted after background transition");
310+
[expectation fulfill];
311+
});
312+
313+
[self waitForExpectationsWithTimeout:30 handler:nil];
314+
}
315+
316+
- (void)testSubscriptAccessorThreadSafety {
317+
XCTestExpectation *expectation = [self expectationWithDescription:@"Subscript thread safety"];
318+
319+
[MParticle sharedInstance].backendController = [[MPBackendController_PRIVATE alloc] initWithDelegate:(id<MPBackendControllerDelegate>)[MParticle sharedInstance]];
320+
[MPPersistenceController_PRIVATE setMpid:@42];
321+
322+
MPUserDefaults *defaults = MPUserDefaultsConnector.userDefaults;
323+
324+
dispatch_group_t group = dispatch_group_create();
325+
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.mparticle.test.subscript.concurrent", DISPATCH_QUEUE_CONCURRENT);
326+
NSInteger iterations = 1000;
327+
328+
for (NSInteger i = 0; i < iterations; i++) {
329+
dispatch_group_async(group, concurrentQueue, ^{
330+
id value = defaults[@"lud"];
331+
(void)value;
332+
});
333+
334+
dispatch_group_async(group, concurrentQueue, ^{
335+
defaults[@"lud"] = @(1234567890 + i);
336+
});
337+
338+
dispatch_group_async(group, concurrentQueue, ^{
339+
id mpidValue = defaults[@"mpid"];
340+
(void)mpidValue;
341+
});
342+
343+
dispatch_group_async(group, concurrentQueue, ^{
344+
defaults[@"mpid"] = @(i % 100);
345+
});
346+
}
347+
348+
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
349+
[expectation fulfill];
350+
});
351+
352+
[self waitForExpectationsWithTimeout:30 handler:nil];
353+
}
354+
355+
- (void)testUpdateLastUseDateWithNilDate {
356+
[MParticle sharedInstance].backendController = [[MPBackendController_PRIVATE alloc] initWithDelegate:(id<MPBackendControllerDelegate>)[MParticle sharedInstance]];
357+
[MPPersistenceController_PRIVATE setMpid:@1];
358+
359+
#pragma clang diagnostic push
360+
#pragma clang diagnostic ignored "-Wnonnull"
361+
[MPApplication_PRIVATE updateLastUseDate:nil];
362+
#pragma clang diagnostic pop
363+
364+
MPUserDefaults *defaults = MPUserDefaultsConnector.userDefaults;
365+
NSNumber *lastUseDate = defaults[kMPAppLastUseDateKey];
366+
XCTAssertNotNil(lastUseDate);
367+
XCTAssertEqualObjects(lastUseDate, @0);
368+
}
369+
274370
@end

UnitTests/ObjCTests/MPURLRequestBuilderTests.m

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,25 @@ - (void)testURLRequestComposition {
226226
}
227227
}
228228

229+
- (void)testBuildReturnsNilWhenConfigQueryExceedsMaxLength {
230+
NSMutableString *longQueryValue = [NSMutableString stringWithCapacity:9000];
231+
for (NSInteger i = 0; i < 9000; i++) {
232+
[longQueryValue appendString:@"a"];
233+
}
234+
235+
NSString *urlString = [NSString stringWithFormat:@"https://config2.mparticle.com/v4/unit_test_app_key/config?plan_id=%@", longQueryValue];
236+
NSURL *defaultURL = [NSURL URLWithString:urlString];
237+
NSURL *modifiedURL = [NSURL URLWithString:urlString];
238+
XCTAssertNotNil(defaultURL);
239+
XCTAssertNotNil(modifiedURL);
240+
241+
MPURL *url = [[MPURL alloc] initWithURL:modifiedURL defaultURL:defaultURL];
242+
MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:url message:nil httpMethod:@"GET"];
243+
NSMutableURLRequest *request = [builder build];
244+
245+
XCTAssertNil(request);
246+
}
247+
229248
- (void)testEtag {
230249
NSDictionary *configuration1 = @{
231250
@"id":@42,

mParticle-Apple-SDK.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = "mParticle-Apple-SDK"
3-
s.version = "8.44.0"
3+
s.version = "8.44.3"
44
s.summary = "mParticle Apple SDK."
55

66
s.description = <<-DESC

0 commit comments

Comments
 (0)