Skip to content

Commit

Permalink
Added Integration Middleware capabilities (#879)
Browse files Browse the repository at this point in the history
* Added integration middleware support.

* Fixed warnings; Updated project to recommended settings.

* only signal the runner if there’s actually middleware to be processed.

* Added & Updated tests.

* Added experimental raw filter block.

* Removed unnecessary logs.

* Added logic to allow tests to function.

Co-authored-by: Brandon Sneed <[email protected]>
  • Loading branch information
bsneed and Brandon Sneed authored Apr 22, 2020
1 parent 8158687 commit ac07c5f
Show file tree
Hide file tree
Showing 18 changed files with 295 additions and 79 deletions.
16 changes: 13 additions & 3 deletions Analytics.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */;
Expand Down Expand Up @@ -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++";
Expand All @@ -831,13 +836,15 @@
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;
CLANG_WARN_ENUM_CONVERSION = YES;
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;
Expand Down Expand Up @@ -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++";
Expand All @@ -890,13 +898,15 @@
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;
CLANG_WARN_ENUM_CONVERSION = YES;
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
141 changes: 93 additions & 48 deletions Analytics/Classes/Integrations/SEGIntegrationsManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#import "SEGGroupPayload.h"
#import "SEGScreenPayload.h"
#import "SEGAliasPayload.h"
#import "SEGUtils.h"

NSString *SEGAnalyticsIntegrationDidStart = @"io.segment.analytics.integration.did.start";
static NSString *const SEGAnonymousIdKey = @"SEGAnonymousId";
Expand Down Expand Up @@ -66,6 +67,7 @@ @interface SEGIntegrationsManager ()
@property (nonatomic, strong) NSArray *factories;
@property (nonatomic, strong) NSMutableDictionary *integrations;
@property (nonatomic, strong) NSMutableDictionary *registeredIntegrations;
@property (nonatomic, strong) NSMutableDictionary *integrationMiddleware;
@property (nonatomic) volatile BOOL initialized;
@property (nonatomic, copy) NSString *cachedAnonymousId;
@property (nonatomic, strong) SEGHTTPClient *httpClient;
Expand Down Expand Up @@ -105,6 +107,7 @@ - (instancetype _Nonnull)initWithAnalytics:(SEGAnalytics *_Nonnull)analytics
self.factories = [factories copy];
self.integrations = [NSMutableDictionary dictionaryWithCapacity:factories.count];
self.registeredIntegrations = [NSMutableDictionary dictionaryWithCapacity:factories.count];
self.integrationMiddleware = [NSMutableDictionary dictionaryWithCapacity:factories.count];

// Update settings on each integration immediately
[self refreshSettings];
Expand Down Expand Up @@ -188,38 +191,6 @@ - (void)identify:(SEGIdentifyPayload *)payload
options:payload.options
sync:false];
}
/*
- (void)identify:(NSString *)userId traits:(NSDictionary *)traits options:(NSDictionary *)options
{
NSCAssert2(userId.length > 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

Expand Down Expand Up @@ -383,6 +354,17 @@ - (void)setCachedSettings:(NSDictionary *)settings
[self updateIntegrationsWithSettings:settings[@"integrations"]];
}

- (nonnull NSArray<id<SEGMiddleware>> *)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, ^{
Expand All @@ -392,11 +374,18 @@ - (void)updateIntegrationsWithSettings:(NSDictionary *)projectSettings
for (id<SEGIntegrationFactory> factory in self.factories) {
NSString *key = [factory key];
NSDictionary *integrationSettings = [projectSettings objectForKey:key];
if (isUnitTesting()) {
integrationSettings = @{};
}
if (integrationSettings) {
id<SEGIntegration> integration = [factory createWithSettings:integrationSettings forAnalytics:self.analytics];
if (integration != nil) {
self.integrations[key] = integration;
self.registeredIntegrations[key] = @NO;

// setup integration middleware
NSArray<id<SEGMiddleware>> *middleware = [self middlewareForIntegrationKey:key];
self.integrationMiddleware[key] = [[SEGMiddlewareRunner alloc] initWithMiddleware:middleware];
}
[[NSNotificationCenter defaultCenter] postNotificationName:SEGAnalyticsIntegrationDidStart object:key userInfo:nil];
} else {
Expand Down Expand Up @@ -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<SEGIntegration>)integration key:(NSString *)key selector:(SEL)selector arguments:(NSArray *)arguments options:(NSDictionary *)options
{
if (![integration respondsToSelector:selector]) {
Expand All @@ -510,9 +542,9 @@ - (void)invokeIntegration:(id<SEGIntegration>)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) {
Expand All @@ -521,8 +553,31 @@ - (void)invokeIntegration:(id<SEGIntegration>)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<SEGMutableContext> _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];
}

Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 13 additions & 1 deletion Analytics/Classes/Internal/SEGSegmentIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}];
}

Expand Down
2 changes: 2 additions & 0 deletions Analytics/Classes/Internal/SEGUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
+ (id _Nullable)traverseJSON:(id _Nullable)object andReplaceWithFilters:(nonnull NSDictionary<NSString*, NSString*>*)patterns;

@end

BOOL isUnitTesting();
11 changes: 11 additions & 0 deletions Analytics/Classes/Internal/SEGUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,14 @@ +(id)traverseJSON:(id)object andReplaceWithFilters:(NSDictionary<NSString*, NSSt
}

@end

BOOL isUnitTesting()
{
static dispatch_once_t pred = 0;
static BOOL _isUnitTesting = NO;
dispatch_once(&pred, ^{
NSDictionary *env = [NSProcessInfo processInfo].environment;
_isUnitTesting = (env[@"XCTestConfigurationFilePath"] != nil);
});
return _isUnitTesting;
}
2 changes: 1 addition & 1 deletion Analytics/Classes/Middlewares/SEGContext.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ @implementation SEGContext

- (instancetype)init
{
@throw [NSException exceptionWithName:@"Bad Initization"
@throw [NSException exceptionWithName:@"Bad Initialization"
reason:@"Please use initWithAnalytics:"
userInfo:nil];
}
Expand Down
11 changes: 9 additions & 2 deletions Analytics/Classes/Middlewares/SEGMiddleware.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,15 @@ typedef void (^RunMiddlewaresCallback)(BOOL earlyExit, NSArray<id<SEGMiddleware>
// 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<id<SEGMiddleware>> *middlewares;

- (void)run:(SEGContext *_Nonnull)context callback:(RunMiddlewaresCallback _Nullable)callback;
- (SEGContext * _Nonnull)run:(SEGContext *_Nonnull)context callback:(RunMiddlewaresCallback _Nullable)callback;

- (instancetype _Nonnull)initWithMiddlewares:(NSArray<id<SEGMiddleware>> *_Nonnull)middlewares;
- (instancetype _Nonnull)initWithMiddleware:(NSArray<id<SEGMiddleware>> *_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<id<SEGMiddleware>> *middleware;
- (instancetype _Nonnull)initWithKey:(NSString * _Nonnull)integrationKey middleware:(NSArray<id<SEGMiddleware>> * _Nonnull)middleware;
@end
Loading

0 comments on commit ac07c5f

Please sign in to comment.