keenlabs / KeenClient-iOS

Official iOS client for the Keen IO API. Build analytics features directly into your iOS apps.
https://keen.io/docs
MIT License
78 stars 56 forks source link

runMultiAnalysisWithQueries limited to identical query properties #129

Closed huguesbr closed 9 years ago

huguesbr commented 9 years ago

It's more a limit of the backend, but I think it will be great to have this on the client (running multiple query).

I've write a dirty (should I say super dirty?) patch trying to be compatible with existing runMultiAnalysisWithQueries signature (using NSData, NSURLResponse, NSError) named runMultiAsyncQueries.

If idea is interesting, I could refactor to integrate in the current client (rather than category) and probably calling back with a dict rather than NSData.

Even if I liked the idea of keeping all Keen response using JSON rather than dict..


@interface KeenClient(MultiQuery)
-(void)runMultiAsyncQueries:(NSArray *)keenQueries block:(void (^)(NSData *, NSURLResponse *, NSError *))block;
@end

@implementation KeenClient(MultiQuery)

-(void)runMultiAsyncQueries:(NSArray *)keenQueries block:(void (^)(NSData *, NSURLResponse *, NSError *))block {
    dispatch_group_t group = dispatch_group_create();
    __block NSError *anyError;
    __block NSURLResponse *anyResponse;
    NSMutableDictionary *responses = [NSMutableDictionary new];
    [keenQueries enumerateObjectsUsingBlock:^(KIOQuery *  _Nonnull query, NSUInteger idx, BOOL * _Nonnull stop) {
        dispatch_group_enter(group);
        [self runAsyncQuery:query block:^(NSData *jsonData, NSURLResponse *urlReponse, NSError *error) {
            // check if error or error in data
            if(error) {
                anyError = error;
                return;
            }
            if(!anyResponse) {
                anyResponse = urlReponse;
            }
            id response = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
            if(![response isKindOfClass:[NSDictionary class]]) {
                anyError = [NSError errorWithDomain:kKeenErrorDomain code:1 userInfo:@{NSLocalizedDescriptionKey: @"invalidJSON"}];
                return;
            }

            responses[query.queryName] = response;

            dispatch_group_leave(group);
        }];
    }];

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSMutableDictionary *mergedResponse = [NSMutableDictionary new];
        NSMutableDictionary *mergedResults = [NSMutableDictionary new];

        [responses enumerateKeysAndObjectsUsingBlock:^(NSString *  _Nonnull queryKey, NSDictionary *  _Nonnull response, BOOL * _Nonnull stop) {
            NSMutableDictionary *mutableResponse = [response mutableCopy];
            NSArray *results = mutableResponse[@"result"];
            [mutableResponse removeObjectForKey:@"result"];
            [mergedResponse addEntriesFromDictionary:mutableResponse];
            if(results) {
                [results enumerateObjectsUsingBlock:^(NSDictionary *  _Nonnull result, NSUInteger idx, BOOL * _Nonnull stop) {
                    NSDictionary *timeframe = result[@"timeframe"];
                    id value = result[@"value"];
                    id uniqueKey = timeframe[@"start"];
                    NSMutableDictionary *mergeResult = mergedResults[uniqueKey];
                    if(!mergeResult) {
                        mergeResult = [@{@"value": [NSMutableDictionary new], @"timeframe": timeframe} mutableCopy];
                    }
                    mergeResult[@"value"][queryKey] = value;

                    mergedResults[uniqueKey] = mergeResult;
                }];
            }
        }];

        mergedResponse[@"result"] = [mergedResults allValues];

        NSError *error;
        id response = [NSJSONSerialization dataWithJSONObject:mergedResponse options:0 error:&error];
        if(error) {
            anyError = error;
        }

        if(block) {
            block(response, anyResponse, anyError);
        }

    });
}
@end
huguesbr commented 9 years ago

Any suggestion?

heitortsergent commented 9 years ago

Hey @huguesbr! Thanks for the suggestion. :)

I don't know how I feel about adding this method. What's the advantage of having it instead of just looping a KIOQuery array and calling the runAsyncQuery method for each one?

huguesbr commented 9 years ago

no pressure ^^ I said it was hacky and it's definitely a weird way to parse the data to recreate a json.. @heitortsergent I know it's kind of weird but the runMultiAnalysisWithQueries have so much limitation (backend) that I was trying to get a way to work around it. I know I could just do multiple queries but then I have to re-parse the data to make it behave the same way than runMultiAnalysisWithQueries... I would love just a way to run multiple query on the same timeframe but with different filters...

heitortsergent commented 9 years ago

@terryhorner do you have any suggestions here? :dancer:

huguesbr commented 9 years ago

honestly I do think you're right, it's really to ugly. I just want an easy way to query the same result over the same timeframe but with different filters. (I compare actions of different type of users). I thought multiQueries could do that but they are limited to same dataset (identical filters) and are more to run different analysis (average vs median).

heitortsergent commented 9 years ago

@huguesbr yeah, I definitely see the use case for it. I was just wondering if there's any easier way to handle that via Keen's backend, but I don't see anything in the docs.

terrhorn commented 9 years ago

@huguesbr @heitortsergent there's no way to do this via the API directly, they are required to be separate queries.

The benefit of multi-analysis is that it can calculate multiple aggregations from a single data set without the need to make a second pass on the data. Using multiple filters requires multiple passes because the underlying data required to calculate each result is different.

huguesbr commented 9 years ago

Yes. I understand the point of having the same dataset. I wanted to keep the keen signature ( json ) but I guess I will just add a convenience method which just return its own data structure. Thank you for putting me back in the righteous way :)

On Tue, Oct 27, 2015 at 8:12 AM -0700, "Terry Horner" notifications@github.com wrote:

@huguesbr @heitortsergent there's no way to do this via the API directly, they are required to be separate queries.

The benefit of multi-analysis is that it can calculate multiple aggregations from a single data set without the need to make a second pass on the data. Using multiple filters requires multiple passes because the underlying data required to calculate each result is different.

— Reply to this email directly or view it on GitHub.

heitortsergent commented 9 years ago

@huguesbr no worries, thanks for helping out with the SDK! :rocket: