diff --git a/Analytics.xcodeproj/project.pbxproj b/Analytics.xcodeproj/project.pbxproj index 724b881b8..84e51d7b0 100644 --- a/Analytics.xcodeproj/project.pbxproj +++ b/Analytics.xcodeproj/project.pbxproj @@ -536,26 +536,30 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0810; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 1130; ORGANIZATIONNAME = Segment; TargetAttributes = { + 9D8CE58B23EE014E00197D0C = { + LastSwiftMigration = 1130; + }; EADEB85A1DECD080005322DA = { CreatedOnToolsVersion = 8.1; ProvisioningStyle = Automatic; }; EADEB8691DECD0EF005322DA = { CreatedOnToolsVersion = 8.1; + LastSwiftMigration = 1130; ProvisioningStyle = Automatic; }; }; }; buildConfigurationList = EADEB8551DECD080005322DA /* Build configuration list for PBXProject "Analytics" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( - English, en, + Base, ); mainGroup = EADEB8511DECD080005322DA; productRefGroup = EADEB85C1DECD080005322DA /* Products */; @@ -822,6 +826,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -831,6 +836,7 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -838,6 +844,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -881,6 +888,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -890,6 +898,7 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -897,6 +906,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; diff --git a/Analytics.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme b/Analytics.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme index 8c0862b7c..f19d121d5 100644 --- a/Analytics.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme +++ b/Analytics.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme @@ -1,6 +1,6 @@ 0 || traits.count > 0, @"either userId (%@) or traits (%@) must be provided.", userId, traits); - - NSDictionary *options; - if (p.anonymousId) { - NSMutableDictionary *mutableOptions = [[NSMutableDictionary alloc] initWithDictionary:p.options]; - mutableOptions[@"anonymousId"] = p.anonymousId; - options = [mutableOptions copy]; - } else { - options = p.options; - } - - NSString *anonymousId = [options objectForKey:@"anonymousId"]; - if (anonymousId) { - [self saveAnonymousId:anonymousId]; - } else { - anonymousId = self.cachedAnonymousId; - } - - SEGIdentifyPayload *payload = [[SEGIdentifyPayload alloc] initWithUserId:userId - anonymousId:anonymousId - traits:SEGCoerceDictionary(traits) - context:SEGCoerceDictionary([options objectForKey:@"context"]) - integrations:[options objectForKey:@"integrations"]]; - - [self callIntegrationsWithSelector:NSSelectorFromString(@"identify:") - arguments:@[ payload ] - options:options - sync:false]; -}*/ #pragma mark - Track @@ -383,6 +354,17 @@ - (void)setCachedSettings:(NSDictionary *)settings [self updateIntegrationsWithSettings:settings[@"integrations"]]; } +- (nonnull NSArray> *)middlewareForIntegrationKey:(NSString *)key +{ + NSMutableArray *result = [[NSMutableArray alloc] init]; + for (SEGIntegrationMiddleware *container in self.configuration.integrationMiddleware) { + if ([container.integrationKey isEqualToString:key]) { + [result addObjectsFromArray:container.middleware]; + } + } + return result; +} + - (void)updateIntegrationsWithSettings:(NSDictionary *)projectSettings { seg_dispatch_specific_sync(_serialQueue, ^{ @@ -392,11 +374,18 @@ - (void)updateIntegrationsWithSettings:(NSDictionary *)projectSettings for (id factory in self.factories) { NSString *key = [factory key]; NSDictionary *integrationSettings = [projectSettings objectForKey:key]; + if (isUnitTesting()) { + integrationSettings = @{}; + } if (integrationSettings) { id integration = [factory createWithSettings:integrationSettings forAnalytics:self.analytics]; if (integration != nil) { self.integrations[key] = integration; self.registeredIntegrations[key] = @NO; + + // setup integration middleware + NSArray> *middleware = [self middlewareForIntegrationKey:key]; + self.integrationMiddleware[key] = [[SEGMiddlewareRunner alloc] initWithMiddleware:middleware]; } [[NSNotificationCenter defaultCenter] postNotificationName:SEGAnalyticsIntegrationDidStart object:key userInfo:nil]; } else { @@ -499,6 +488,49 @@ - (void)forwardSelector:(SEL)selector arguments:(NSArray *)arguments options:(NS }]; } +/* + This kind of sucks, but we wrote ourselves into a corner here. A larger refactor will need to happen. + I also opted to not put this as a utility function because we shouldn't be doing this in the first place, + so consider it a one-off. If you find yourself needing to do this again, lets talk about a refactor. + */ +- (SEGEventType)eventTypeFromSelector:(SEL)selector +{ + NSString *selectorString = NSStringFromSelector(selector); + SEGEventType result = SEGEventTypeUndefined; + + if ([selectorString hasPrefix:@"identify"]) { + result = SEGEventTypeIdentify; + } else if ([selectorString hasPrefix:@"track"]) { + result = SEGEventTypeTrack; + } else if ([selectorString hasPrefix:@"screen"]) { + result = SEGEventTypeScreen; + } else if ([selectorString hasPrefix:@"group"]) { + result = SEGEventTypeGroup; + } else if ([selectorString hasPrefix:@"alias"]) { + result = SEGEventTypeAlias; + } else if ([selectorString hasPrefix:@"reset"]) { + result = SEGEventTypeReset; + } else if ([selectorString hasPrefix:@"flush"]) { + result = SEGEventTypeFlush; + } else if ([selectorString hasPrefix:@"receivedRemoteNotification"]) { + result = SEGEventTypeReceivedRemoteNotification; + } else if ([selectorString hasPrefix:@"failedToRegisterForRemoteNotificationsWithError"]) { + result = SEGEventTypeFailedToRegisterForRemoteNotifications; + } else if ([selectorString hasPrefix:@"registeredForRemoteNotificationsWithDeviceToken"]) { + result = SEGEventTypeRegisteredForRemoteNotifications; + } else if ([selectorString hasPrefix:@"handleActionWithIdentifier"]) { + result = SEGEventTypeHandleActionWithForRemoteNotification; + } else if ([selectorString hasPrefix:@"continueUserActivity"]) { + result = SEGEventTypeContinueUserActivity; + } else if ([selectorString hasPrefix:@"openURL"]) { + result = SEGEventTypeOpenURL; + } else if ([selectorString hasPrefix:@"application"]) { + result = SEGEventTypeApplicationLifecycle; + } + + return result; +} + - (void)invokeIntegration:(id)integration key:(NSString *)key selector:(SEL)selector arguments:(NSArray *)arguments options:(NSDictionary *)options { if (![integration respondsToSelector:selector]) { @@ -510,9 +542,9 @@ - (void)invokeIntegration:(id)integration key:(NSString *)key se SEGLog(@"Not sending call to %@ because it is disabled in options.", key); return; } - - NSString *eventType = NSStringFromSelector(selector); - if ([eventType hasPrefix:@"track:"]) { + + SEGEventType eventType = [self eventTypeFromSelector:selector]; + if (eventType == SEGEventTypeTrack) { SEGTrackPayload *eventPayload = arguments[0]; BOOL enabled = [[self class] isTrackEvent:eventPayload.event enabledForIntegration:key inPlan:self.cachedSettings[@"plan"]]; if (!enabled) { @@ -521,8 +553,31 @@ - (void)invokeIntegration:(id)integration key:(NSString *)key se } } - SEGLog(@"Running: %@ with arguments %@ on integration: %@", eventType, arguments, key); - NSInvocation *invocation = [self invocationForSelector:selector arguments:arguments]; + NSMutableArray *newArguments = [arguments mutableCopy]; + + if (eventType != SEGEventTypeUndefined) { + SEGMiddlewareRunner *runner = self.integrationMiddleware[key]; + if (runner.middlewares.count > 0) { + SEGPayload *payload = nil; + // things like flush have no args. + if (arguments.count > 0) { + payload = arguments[0]; + } + SEGContext *context = [[[SEGContext alloc] initWithAnalytics:self.analytics] modify:^(id _Nonnull ctx) { + ctx.eventType = eventType; + ctx.payload = payload; + }]; + + context = [runner run:context callback:nil]; + // if we weren't given args, don't set them. + if (arguments.count > 0) { + newArguments[0] = context.payload; + } + } + } + + SEGLog(@"Running: %@ with arguments %@ on integration: %@", NSStringFromSelector(selector), newArguments, key); + NSInvocation *invocation = [self invocationForSelector:selector arguments:newArguments]; [invocation invokeWithTarget:integration]; } @@ -584,16 +639,6 @@ - (void)context:(SEGContext *)context next:(void (^_Nonnull)(SEGContext *_Nullab case SEGEventTypeIdentify: { SEGIdentifyPayload *p = (SEGIdentifyPayload *)context.payload; [self identify:p]; - /* - NSDictionary *options; - if (p.anonymousId) { - NSMutableDictionary *mutableOptions = [[NSMutableDictionary alloc] initWithDictionary:p.options]; - mutableOptions[@"anonymousId"] = p.anonymousId; - options = [mutableOptions copy]; - } else { - options = p.options; - } - [self identify:p.userId traits:p.traits options:options];*/ break; } case SEGEventTypeTrack: { @@ -655,7 +700,7 @@ - (void)context:(SEGContext *)context next:(void (^_Nonnull)(SEGContext *_Nullab } case SEGEventTypeUndefined: NSAssert(NO, @"Received context with undefined event type %@", context); - NSLog(@"[ERROR]: Received context with undefined event type %@", context); + SEGLog(@"[ERROR]: Received context with undefined event type %@", context); break; } next(context); diff --git a/Analytics/Classes/Internal/SEGSegmentIntegration.m b/Analytics/Classes/Internal/SEGSegmentIntegration.m index f623259ca..a74b94487 100644 --- a/Analytics/Classes/Internal/SEGSegmentIntegration.m +++ b/Analytics/Classes/Internal/SEGSegmentIntegration.m @@ -486,7 +486,19 @@ - (void)enqueueAction:(NSString *)action dictionary:(NSMutableDictionary *)paylo [payload setValue:[context copy] forKey:@"context"]; SEGLog(@"%@ Enqueueing action: %@", self, payload); - [self queuePayload:[payload copy]]; + + NSDictionary *queuePayload = [payload copy]; + + if (self.configuration.experimental.rawSegmentModificationBlock != nil) { + NSDictionary *tempPayload = self.configuration.experimental.rawSegmentModificationBlock(queuePayload); + if (tempPayload == nil) { + SEGLog(@"rawSegmentModificationBlock cannot be used to drop events!"); + } else { + // prevent anything else from modifying it at this point. + queuePayload = [tempPayload copy]; + } + } + [self queuePayload:queuePayload]; }]; } diff --git a/Analytics/Classes/Internal/SEGUtils.h b/Analytics/Classes/Internal/SEGUtils.h index 877fbe193..1317a350a 100644 --- a/Analytics/Classes/Internal/SEGUtils.h +++ b/Analytics/Classes/Internal/SEGUtils.h @@ -15,3 +15,5 @@ + (id _Nullable)traverseJSON:(id _Nullable)object andReplaceWithFilters:(nonnull NSDictionary*)patterns; @end + +BOOL isUnitTesting(); diff --git a/Analytics/Classes/Internal/SEGUtils.m b/Analytics/Classes/Internal/SEGUtils.m index b6019a109..bce74502a 100644 --- a/Analytics/Classes/Internal/SEGUtils.m +++ b/Analytics/Classes/Internal/SEGUtils.m @@ -89,3 +89,14 @@ +(id)traverseJSON:(id)object andReplaceWithFilters:(NSDictionary // gonna support that for now to keep things simple. If there is a real need later we'll see then. @property (nonnull, nonatomic, readonly) NSArray> *middlewares; -- (void)run:(SEGContext *_Nonnull)context callback:(RunMiddlewaresCallback _Nullable)callback; +- (SEGContext * _Nonnull)run:(SEGContext *_Nonnull)context callback:(RunMiddlewaresCallback _Nullable)callback; -- (instancetype _Nonnull)initWithMiddlewares:(NSArray> *_Nonnull)middlewares; +- (instancetype _Nonnull)initWithMiddleware:(NSArray> *_Nonnull)middlewares; @end + +// Container object for middlewares for a specific integration. +@interface SEGIntegrationMiddleware : NSObject +@property (nonatomic, strong, nonnull, readonly) NSString *integrationKey; +@property (nonatomic, strong, nullable, readonly) NSArray> *middleware; +- (instancetype _Nonnull)initWithKey:(NSString * _Nonnull)integrationKey middleware:(NSArray> * _Nonnull)middleware; +@end diff --git a/Analytics/Classes/Middlewares/SEGMiddleware.m b/Analytics/Classes/Middlewares/SEGMiddleware.m index d5dd998fe..88dc50943 100644 --- a/Analytics/Classes/Middlewares/SEGMiddleware.m +++ b/Analytics/Classes/Middlewares/SEGMiddleware.m @@ -10,6 +10,17 @@ #import "SEGMiddleware.h" +@implementation SEGIntegrationMiddleware +- (instancetype)initWithKey:(NSString *)integrationKey middleware:(NSArray> *)middleware +{ + if (self = [super init]) { + _integrationKey = integrationKey; + _middleware = middleware; + } + return self; +} +@end + @implementation SEGBlockMiddleware - (instancetype)initWithBlock:(SEGMiddlewareBlock)block @@ -30,7 +41,7 @@ - (void)context:(SEGContext *)context next:(SEGMiddlewareNext)next @implementation SEGMiddlewareRunner -- (instancetype)initWithMiddlewares:(NSArray> *_Nonnull)middlewares +- (instancetype)initWithMiddleware:(NSArray> *_Nonnull)middlewares { if (self = [super init]) { _middlewares = middlewares; @@ -38,29 +49,33 @@ - (instancetype)initWithMiddlewares:(NSArray> *_Nonnull)middle return self; } -- (void)run:(SEGContext *_Nonnull)context callback:(RunMiddlewaresCallback _Nullable)callback +- (SEGContext *)run:(SEGContext *_Nonnull)context callback:(RunMiddlewaresCallback _Nullable)callback { - [self runMiddlewares:self.middlewares context:context callback:callback]; + return [self runMiddlewares:self.middlewares context:context callback:callback]; } // TODO: Maybe rename SEGContext to SEGEvent to be a bit more clear? // We could also use some sanity check / other types of logging here. -- (void)runMiddlewares:(NSArray> *_Nonnull)middlewares +- (SEGContext *)runMiddlewares:(NSArray> *_Nonnull)middlewares context:(SEGContext *_Nonnull)context callback:(RunMiddlewaresCallback _Nullable)callback { + __block SEGContext * _Nonnull result = context; + BOOL earlyExit = context == nil; if (middlewares.count == 0 || earlyExit) { if (callback) { callback(earlyExit, middlewares); } - return; + return context; } - - [middlewares[0] context:context next:^(SEGContext *_Nullable newContext) { + + [middlewares[0] context:result next:^(SEGContext *_Nullable newContext) { NSArray *remainingMiddlewares = [middlewares subarrayWithRange:NSMakeRange(1, middlewares.count - 1)]; - [self runMiddlewares:remainingMiddlewares context:newContext callback:callback]; + result = [self runMiddlewares:remainingMiddlewares context:newContext callback:callback]; }]; + + return result; } @end diff --git a/Analytics/Classes/SEGAnalytics.m b/Analytics/Classes/SEGAnalytics.m index 462ffdf90..cea865468 100644 --- a/Analytics/Classes/SEGAnalytics.m +++ b/Analytics/Classes/SEGAnalytics.m @@ -52,8 +52,8 @@ - (instancetype)initWithConfiguration:(SEGAnalyticsConfiguration *)configuration // TODO: Figure out if this is really the best way to do things here. self.integrationsManager = [[SEGIntegrationsManager alloc] initWithAnalytics:self]; - self.runner = [[SEGMiddlewareRunner alloc] initWithMiddlewares: - [configuration.middlewares ?: @[] arrayByAddingObject:self.integrationsManager]]; + self.runner = [[SEGMiddlewareRunner alloc] initWithMiddleware: + [configuration.sourceMiddleware ?: @[] arrayByAddingObject:self.integrationsManager]]; // Attach to application state change hooks NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; diff --git a/Analytics/Classes/SEGAnalyticsConfiguration.h b/Analytics/Classes/SEGAnalyticsConfiguration.h index 22400ab86..d878cc18c 100644 --- a/Analytics/Classes/SEGAnalyticsConfiguration.h +++ b/Analytics/Classes/SEGAnalyticsConfiguration.h @@ -26,6 +26,7 @@ typedef NSMutableURLRequest *_Nonnull (^SEGRequestFactory)(NSURL *_Nonnull); @protocol SEGMiddleware; @class SEGAnalyticsExperimental; +@class SEGIntegrationMiddleware; /** * This object provides a set of properties to control various policies of the analytics client. Other than `writeKey`, these properties can be changed at any time. @@ -128,9 +129,20 @@ typedef NSMutableURLRequest *_Nonnull (^SEGRequestFactory)(NSURL *_Nonnull); @property (nonatomic, strong, nullable) id crypto; /** - * Set custom middlewares. Will be run before all integrations + * Set custom middlewares. Will be run before all integrations. + * This property is deprecated in favor of the `sourceMiddleware` property. */ -@property (nonatomic, strong, nullable) NSArray> *middlewares; +@property (nonatomic, strong, nullable) NSArray> *middlewares DEPRECATED_MSG_ATTRIBUTE("Use .sourceMiddleware instead."); + +/** + * Set custom source middleware. Will be run before all integrations + */ +@property (nonatomic, strong, nullable) NSArray> *sourceMiddleware; + +/** + * Set custom integration middleware. Will be run before the associated integration. + */ +@property (nonatomic, strong, nullable) NSArray *integrationMiddleware; /** * Register a factory that can be used to create an integration. @@ -180,6 +192,9 @@ typedef NSMutableURLRequest *_Nonnull (^SEGRequestFactory)(NSURL *_Nonnull); @end +#pragma mark - Experimental + +typedef NSDictionary * _Nonnull (^SEGRawModificationBlock)( NSDictionary * _Nonnull rawPayload); @interface SEGAnalyticsExperimental : NSObject /** @@ -192,4 +207,12 @@ typedef NSMutableURLRequest *_Nonnull (^SEGRequestFactory)(NSURL *_Nonnull); received. */ @property (nonatomic, assign) BOOL nanosecondTimestamps; +/** + Experimental support for transformation of raw dictionaries prior to being sent to segment. + This should generally NOT be used, but is a current stop-gap measure for some customers who need to filter + payload data prior to being received by segment.com. This property will go away in future versions when context + object data is made available earlier in the event pipeline. + */ +@property (nonatomic, strong, nullable) SEGRawModificationBlock rawSegmentModificationBlock; + @end diff --git a/Analytics/Classes/SEGAnalyticsConfiguration.m b/Analytics/Classes/SEGAnalyticsConfiguration.m index 46d712351..1f461dcaf 100644 --- a/Analytics/Classes/SEGAnalyticsConfiguration.m +++ b/Analytics/Classes/SEGAnalyticsConfiguration.m @@ -27,7 +27,6 @@ - (void)seg_endBackgroundTask:(UIBackgroundTaskIdentifier)identifier @implementation SEGAnalyticsExperimental @end - @interface SEGAnalyticsConfiguration () @property (nonatomic, copy, readwrite) NSString *writeKey; @@ -87,4 +86,16 @@ - (NSString *)description return [NSString stringWithFormat:@"<%p:%@, %@>", self, self.class, [self dictionaryWithValuesForKeys:@[ @"writeKey", @"shouldUseLocationServices", @"flushAt" ]]]; } +// MARK: remove these when `middlewares` property is removed. + +- (void)setMiddlewares:(NSArray> *)middlewares +{ + self.sourceMiddleware = middlewares; +} + +- (NSArray> *)middlewares +{ + return self.sourceMiddleware; +} + @end diff --git a/AnalyticsTests/AnalyticsTests.swift b/AnalyticsTests/AnalyticsTests.swift index a3deafc89..b15be213f 100644 --- a/AnalyticsTests/AnalyticsTests.swift +++ b/AnalyticsTests/AnalyticsTests.swift @@ -26,7 +26,7 @@ class AnalyticsTests: QuickSpec { beforeEach { testMiddleware = TestMiddleware() - config.middlewares = [testMiddleware] + config.sourceMiddleware = [testMiddleware] testApplication = TestApplication() config.application = testApplication config.trackApplicationLifecycleEvents = true diff --git a/AnalyticsTests/MiddlewareTests.swift b/AnalyticsTests/MiddlewareTests.swift index 2d8a3c9bd..8655de103 100644 --- a/AnalyticsTests/MiddlewareTests.swift +++ b/AnalyticsTests/MiddlewareTests.swift @@ -38,12 +38,12 @@ let customizeAllTrackCalls = SEGBlockMiddleware { (context, next) in let eatAllCalls = SEGBlockMiddleware { (context, next) in } -class MiddlewareTests: QuickSpec { +class SourceMiddlewareTests: QuickSpec { override func spec() { it("receives events") { let config = SEGAnalyticsConfiguration(writeKey: "TESTKEY") let passthrough = SEGPassthroughMiddleware() - config.middlewares = [ + config.sourceMiddleware = [ passthrough, ] let analytics = SEGAnalytics(configuration: config) @@ -56,7 +56,7 @@ class MiddlewareTests: QuickSpec { it("modifies and passes event to next") { let config = SEGAnalyticsConfiguration(writeKey: "TESTKEY") let passthrough = SEGPassthroughMiddleware() - config.middlewares = [ + config.sourceMiddleware = [ customizeAllTrackCalls, passthrough, ] @@ -73,7 +73,7 @@ class MiddlewareTests: QuickSpec { it("expects event to be swallowed if next is not called") { let config = SEGAnalyticsConfiguration(writeKey: "TESTKEY") let passthrough = SEGPassthroughMiddleware() - config.middlewares = [ + config.sourceMiddleware = [ eatAllCalls, passthrough, ] @@ -83,3 +83,83 @@ class MiddlewareTests: QuickSpec { } } } + +class IntegrationMiddlewareTests: QuickSpec { + override func spec() { + it("receives events") { + let config = SEGAnalyticsConfiguration(writeKey: "TESTKEY") + let passthrough = SEGPassthroughMiddleware() + config.integrationMiddleware = [SEGIntegrationMiddleware(key: SEGSegmentIntegrationFactory().key(), middleware: [passthrough])] + let analytics = SEGAnalytics(configuration: config) + analytics.identify("testUserId1") + + // pump the runloop until we have a last context. + // integration middleware is held up until initialization is completed. + waitUntil(timeout: 60) { done in + let queue = DispatchQueue(label: "test") + queue.async { + while(passthrough.lastContext == nil) { + sleep(1); + } + done() + } + } + + expect(passthrough.lastContext?.eventType) == SEGEventType.identify + let identify = passthrough.lastContext?.payload as? SEGIdentifyPayload + expect(identify?.userId) == "testUserId1" + } + + it("modifies and passes event to next") { + let config = SEGAnalyticsConfiguration(writeKey: "TESTKEY") + let passthrough = SEGPassthroughMiddleware() + config.integrationMiddleware = [SEGIntegrationMiddleware(key: SEGSegmentIntegrationFactory().key(), middleware: [customizeAllTrackCalls, passthrough])] + let analytics = SEGAnalytics(configuration: config) + analytics.track("Purchase Success") + + // pump the runloop until we have a last context. + // integration middleware is held up until initialization is completed. + waitUntil(timeout: 60) { done in + let queue = DispatchQueue(label: "test") + queue.async { + while(passthrough.lastContext == nil) { + sleep(1) + } + done() + } + } + + expect(passthrough.lastContext?.eventType) == SEGEventType.track + let track = passthrough.lastContext?.payload as? SEGTrackPayload + expect(track?.event) == "[New] Purchase Success" + expect(track?.properties?["customAttribute"] as? String) == "Hello" + let isNull = (track?.properties?["nullTest"] is NSNull) + expect(isNull) == true + } + + it("expects event to be swallowed if next is not called") { + let config = SEGAnalyticsConfiguration(writeKey: "TESTKEY") + let passthrough = SEGPassthroughMiddleware() + config.integrationMiddleware = [SEGIntegrationMiddleware(key: SEGSegmentIntegrationFactory().key(), middleware: [eatAllCalls, passthrough])] + let analytics = SEGAnalytics(configuration: config) + analytics.track("Purchase Success") + + // Since we're testing that an event is dropped, the previously used run loop pump won't work here. + var initialized = false; + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: SEGAnalyticsIntegrationDidStart), object: nil, queue: nil) { (notification) in + initialized = true; + } + waitUntil(timeout: 60) { done in + let queue = DispatchQueue(label: "test") + queue.async { + while (initialized != true) { + sleep(1) + } + done() + } + } + + expect(passthrough.lastContext).to(beNil()) + } + } +} diff --git a/AnalyticsTests/StoreKitTrackerTests.swift b/AnalyticsTests/StoreKitTrackerTests.swift index 4fd979427..0a67decb1 100644 --- a/AnalyticsTests/StoreKitTrackerTests.swift +++ b/AnalyticsTests/StoreKitTrackerTests.swift @@ -49,7 +49,7 @@ class StoreKitTrackerTests: QuickSpec { beforeEach { let config = SEGAnalyticsConfiguration(writeKey: "foobar") test = TestMiddleware() - config.middlewares = [test] + config.sourceMiddleware = [test] analytics = SEGAnalytics(configuration: config) tracker = SEGStoreKitTracker.trackTransactions(for: analytics) } diff --git a/AnalyticsTests/TrackingTests.swift b/AnalyticsTests/TrackingTests.swift index b91bbcc1e..29275ca14 100644 --- a/AnalyticsTests/TrackingTests.swift +++ b/AnalyticsTests/TrackingTests.swift @@ -19,7 +19,7 @@ class TrackingTests: QuickSpec { beforeEach { let config = SEGAnalyticsConfiguration(writeKey: "QUI5ydwIGeFFTa1IvCBUhxL9PyW5B0jE") passthrough = SEGPassthroughMiddleware() - config.middlewares = [ + config.sourceMiddleware = [ passthrough, ] analytics = SEGAnalytics(configuration: config) diff --git a/AnalyticsTests/Utils/TestUtils.swift b/AnalyticsTests/Utils/TestUtils.swift index c15490c47..f4ce75716 100644 --- a/AnalyticsTests/Utils/TestUtils.swift +++ b/AnalyticsTests/Utils/TestUtils.swift @@ -85,8 +85,8 @@ class JsonGzippedBody : LSMatcher, LSMatcheable { } func matchesJson(_ json: AnyObject) -> Bool { - let actualValue : () -> NSObject! = { - return json as! NSObject + let actualValue : () -> NSObject? = { + return (json as! NSObject) } let failureMessage = FailureMessage() let location = SourceLocation()