Skip to content

Commit d36439d

Browse files
jamesnroktclaude
andauthored
feat: Global CNAME support and Rokt Kit passthrough (#760)
* feat: CNAME support * Consolidate logic and add tests * refactor: address PR review feedback on CNAME support - Replace MPCustomHost() static C function by inlining networkOptions.customBaseURL.host at each call site, consistent with how all other host properties are accessed - Cache [MParticle sharedInstance] in a local variable in configURL, audienceURL, identityURL:, and modifyURL to eliminate duplicate calls Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update URL path to match routing rules * Bump minimum Rokt version * Update mParticle-Rokt.podspec --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ba68b4c commit d36439d

9 files changed

Lines changed: 268 additions & 32 deletions

File tree

Kits/rokt/rokt/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ let package = Package(
3232
mParticleAppleSDK,
3333
.package(
3434
url: "https://github.com/ROKT/rokt-sdk-ios",
35-
.upToNextMajor(from: "5.1.0")
35+
.upToNextMajor(from: "5.2.0")
3636
),
3737
.package(
3838
url: "https://github.com/ROKT/rokt-contracts-apple.git",

Kits/rokt/rokt/Sources/mParticle-Rokt/MPKitRokt.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu
9191
}
9292
}];
9393

94+
NSURL *customBaseURL = [MParticle sharedInstance].networkOptions.customBaseURL;
95+
if (customBaseURL) {
96+
[Rokt setCustomBaseURL:customBaseURL];
97+
}
98+
9499
[Rokt initWithRoktTagId:partnerId mParticleSdkVersion:sdkVersion mParticleKitVersion:kMPRoktKitVersion];
95100

96101
return [self execStatus:MPKitReturnCodeSuccess];

Kits/rokt/rokt/mParticle-Rokt.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
1313
s.ios.deployment_target = "15.6"
1414
s.ios.source_files = 'Sources/mParticle-Rokt/**/*.{h,m}', 'Sources/mParticle-Rokt-Swift/**/*.swift'
1515
s.ios.resource_bundles = { 'mParticle-Rokt-Privacy' => ['Sources/mParticle-Rokt/PrivacyInfo.xcprivacy'] }
16-
s.ios.dependency 'mParticle-Apple-SDK', '~> 9.0'
16+
s.ios.dependency 'mParticle-Apple-SDK', '~> 9.1'
1717
s.ios.dependency 'RoktContracts', '~> 2.0'
18-
s.ios.dependency 'Rokt-Widget', '~> 5.1'
18+
s.ios.dependency 'Rokt-Widget', '~> 5.2'
1919
end

UnitTests/ObjCTests/MPNetworkCommunicationTests.m

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,138 @@ - (void)testAliasURLWithOptionsAndOverrideAndEventsOnlyAndTrackingHost {
328328
XCTAssert([aliasURL.accessibilityHint isEqualToString:@"identity"]);
329329
}
330330

331+
- (void)testCustomBaseURLRejectsNonHTTPS {
332+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
333+
options.customBaseURL = [NSURL URLWithString:@"http://rkt.example.com"];
334+
XCTAssertNil(options.customBaseURL, @"Non-HTTPS customBaseURL should be rejected");
335+
}
336+
337+
- (void)testConfigURLWithCustomBaseURL {
338+
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
339+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
340+
options.customBaseURL = [NSURL URLWithString:@"https://rkt.example.com"];
341+
[MParticle sharedInstance].networkOptions = options;
342+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
343+
NSURL *configURL = [networkCommunication configURL].url;
344+
[self deswizzle];
345+
XCTAssert([configURL.absoluteString rangeOfString:@"rkt.example.com/config/v4/"].location != NSNotFound);
346+
XCTAssert([configURL.absoluteString rangeOfString:@"config2.mparticle.com"].location == NSNotFound);
347+
}
348+
349+
- (void)testConfigURLCustomBaseURLOverridesConfigHost {
350+
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
351+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
352+
options.customBaseURL = [NSURL URLWithString:@"https://rkt.example.com"];
353+
options.configHost = @"config.mpproxy.example.com";
354+
[MParticle sharedInstance].networkOptions = options;
355+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
356+
NSURL *configURL = [networkCommunication configURL].url;
357+
[self deswizzle];
358+
XCTAssert([configURL.absoluteString rangeOfString:@"rkt.example.com/config/v4/"].location != NSNotFound);
359+
XCTAssert([configURL.absoluteString rangeOfString:@"config.mpproxy.example.com"].location == NSNotFound);
360+
}
361+
362+
- (void)testModifyURLWithCustomBaseURL {
363+
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
364+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
365+
options.customBaseURL = [NSURL URLWithString:@"https://rkt.example.com"];
366+
[MParticle sharedInstance].networkOptions = options;
367+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
368+
NSURL *modifyURL = [networkCommunication modifyURL].url;
369+
[self deswizzle];
370+
XCTAssert([modifyURL.absoluteString rangeOfString:@"https://rkt.example.com/identity/v1/"].location != NSNotFound);
371+
XCTAssert([modifyURL.absoluteString rangeOfString:@"identity.us1.mparticle.com"].location == NSNotFound);
372+
XCTAssert([modifyURL.accessibilityHint isEqualToString:@"identity"]);
373+
}
374+
375+
- (void)testModifyURLCustomBaseURLOverridesIdentityTrackingHost {
376+
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
377+
MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
378+
stateMachine.attAuthorizationStatus = @(MPATTAuthorizationStatusAuthorized);
379+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
380+
options.customBaseURL = [NSURL URLWithString:@"https://rkt.example.com"];
381+
options.identityTrackingHost = @"identity-tracking.mpproxy.example.com";
382+
[MParticle sharedInstance].networkOptions = options;
383+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
384+
NSURL *modifyURL = [networkCommunication modifyURL].url;
385+
stateMachine.attAuthorizationStatus = nil;
386+
[self deswizzle];
387+
XCTAssert([modifyURL.absoluteString rangeOfString:@"https://rkt.example.com/identity/v1/"].location != NSNotFound);
388+
XCTAssert([modifyURL.absoluteString rangeOfString:@"identity-tracking.mpproxy.example.com"].location == NSNotFound);
389+
}
390+
391+
- (void)testEventURLWithCustomBaseURL {
392+
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
393+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
394+
options.customBaseURL = [NSURL URLWithString:@"https://rkt.example.com"];
395+
[MParticle sharedInstance].networkOptions = options;
396+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
397+
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
398+
NSURL *eventURL = [networkCommunication eventURLForUpload:upload].url;
399+
[self deswizzle];
400+
XCTAssert([eventURL.absoluteString rangeOfString:@"rkt.example.com/nativeevents/v2/"].location != NSNotFound);
401+
XCTAssert([eventURL.absoluteString rangeOfString:@"nativesdks.us1.mparticle.com"].location == NSNotFound);
402+
}
403+
404+
- (void)testEventURLCustomBaseURLAppliesToTrackingHost {
405+
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
406+
MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
407+
stateMachine.attAuthorizationStatus = @(MPATTAuthorizationStatusAuthorized);
408+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
409+
options.customBaseURL = [NSURL URLWithString:@"https://rkt.example.com"];
410+
[MParticle sharedInstance].networkOptions = options;
411+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
412+
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
413+
NSURL *eventURL = [networkCommunication eventURLForUpload:upload].url;
414+
stateMachine.attAuthorizationStatus = nil;
415+
[self deswizzle];
416+
XCTAssert([eventURL.absoluteString rangeOfString:@"rkt.example.com/nativeevents/v2/"].location != NSNotFound);
417+
XCTAssert([eventURL.absoluteString rangeOfString:@"tracking-nativesdks"].location == NSNotFound);
418+
}
419+
420+
- (void)testAliasURLWithCustomBaseURL {
421+
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
422+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
423+
options.customBaseURL = [NSURL URLWithString:@"https://rkt.example.com"];
424+
[MParticle sharedInstance].networkOptions = options;
425+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
426+
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
427+
NSURL *aliasURL = [networkCommunication aliasURLForUpload:upload].url;
428+
[self deswizzle];
429+
XCTAssert([aliasURL.absoluteString rangeOfString:@"https://rkt.example.com/nativeevents/v1/identity/"].location != NSNotFound);
430+
XCTAssert([aliasURL.absoluteString rangeOfString:@"nativesdks.us1.mparticle.com"].location == NSNotFound);
431+
XCTAssert([aliasURL.accessibilityHint isEqualToString:@"identity"]);
432+
}
433+
434+
- (void)testAudienceURLWithCustomBaseURL {
435+
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
436+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
437+
options.customBaseURL = [NSURL URLWithString:@"https://rkt.example.com"];
438+
[MParticle sharedInstance].networkOptions = options;
439+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
440+
NSURL *audienceURL = [networkCommunication audienceURL].url;
441+
[self deswizzle];
442+
XCTAssert([audienceURL.absoluteString rangeOfString:@"rkt.example.com"].location != NSNotFound);
443+
XCTAssert([audienceURL.absoluteString rangeOfString:@"mparticle.com"].location == NSNotFound);
444+
}
445+
446+
- (void)testAliasURLWithCustomBaseURLAndATTAuthorized {
447+
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
448+
MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
449+
stateMachine.attAuthorizationStatus = @(MPATTAuthorizationStatusAuthorized);
450+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
451+
options.customBaseURL = [NSURL URLWithString:@"https://rkt.example.com"];
452+
[MParticle sharedInstance].networkOptions = options;
453+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
454+
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
455+
NSURL *aliasURL = [networkCommunication aliasURLForUpload:upload].url;
456+
stateMachine.attAuthorizationStatus = nil;
457+
[self deswizzle];
458+
XCTAssert([aliasURL.absoluteString rangeOfString:@"https://rkt.example.com/nativeevents/v1/identity/"].location != NSNotFound);
459+
XCTAssert([aliasURL.absoluteString rangeOfString:@"nativesdks.us1.mparticle.com"].location == NSNotFound);
460+
XCTAssert([aliasURL.accessibilityHint isEqualToString:@"identity"]);
461+
}
462+
331463
- (void)testEmptyUploadsArray {
332464
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
333465
NSArray *uploads = @[];

mParticle-Apple-SDK/Include/mParticle.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,23 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp
150150
*/
151151
@property (nonatomic) BOOL eventsOnly;
152152

153+
/**
154+
Routes all mParticle endpoint traffic (config, events, identity, alias) through
155+
a single CNAME domain. Must be an HTTPS URL, e.g. https://rkt.example.com.
156+
Non-HTTPS values are rejected with a warning log and the property is left unchanged.
157+
158+
When set, this property takes priority over all individual host properties
159+
(configHost, eventsHost, eventsTrackingHost, identityHost, identityTrackingHost,
160+
aliasHost, aliasTrackingHost). Setting both customBaseURL and any individual host
161+
property will log a warning and the individual property will be ignored.
162+
Use customBaseURL exclusively for CNAME-based traffic routing.
163+
164+
Certificate pinning: if certificate pinning is enabled (the default), you must
165+
either supply certificates for your CNAME domain via the @c certificates property,
166+
or disable pinning via @c pinningDisabled / @c pinningDisabledInDevelopment.
167+
*/
168+
@property (nonatomic, nullable) NSURL *customBaseURL;
169+
153170
@end
154171

155172
/**

mParticle-Apple-SDK/MPNetworkOptions+MParticlePrivate.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@import Foundation;
22
#import "mParticle.h"
3+
#import "MPILogger.h"
34

45

56
@implementation MPNetworkOptions
@@ -19,8 +20,19 @@ - (instancetype)init
1920
return self;
2021
}
2122

23+
- (void)setCustomBaseURL:(NSURL *)customBaseURL {
24+
if (customBaseURL && ![customBaseURL.scheme isEqualToString:@"https"]) {
25+
MPILogWarning(@"MPNetworkOptions: customBaseURL must use HTTPS — value ignored.");
26+
return;
27+
}
28+
_customBaseURL = customBaseURL;
29+
}
30+
2231
- (NSString *)description {
2332
NSMutableString *description = [[NSMutableString alloc] initWithString:@"MPNetworkOptions {\n"];
33+
if (_customBaseURL) {
34+
[description appendFormat:@" customBaseURL: %@\n", _customBaseURL];
35+
}
2436
[description appendFormat:@" configHost: %@\n", _configHost];
2537
[description appendFormat:@" overridesConfigSubdirectory: %s\n", _overridesConfigSubdirectory ? "true" : "false"];
2638
[description appendFormat:@" eventsHost: %@\n", _eventsHost];

mParticle-Apple-SDK/Network/MPConnector.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didRece
103103
MPNetworkOptions *networkOptions = [[MParticle sharedInstance] networkOptions];
104104

105105
BOOL isPinningHost = [host rangeOfString:@"mparticle.com"].location != NSNotFound ||
106+
(networkOptions.customBaseURL.host.length > 0 && [host isEqualToString:networkOptions.customBaseURL.host]) ||
106107
(networkOptions.configHost.pathComponents.count > 0 && [host isEqualToString:networkOptions.configHost.pathComponents[0]]) ||
107108
(networkOptions.identityHost.pathComponents.count > 0 && [host isEqualToString:networkOptions.identityHost.pathComponents[0]]) ||
108109
(networkOptions.eventsHost.pathComponents.count > 0 && [host isEqualToString:networkOptions.eventsHost.pathComponents[0]]) ||

0 commit comments

Comments
 (0)