Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSConsistencyManager & IAM fetch read-your-write consistency implementation #1486

Merged
merged 14 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 282 additions & 1 deletion iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

20 changes: 13 additions & 7 deletions iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -189,24 +189,29 @@ - (void)prettyPrintDebugStatementWithRequest:(OneSignalRequest *)request {
}

NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with parameters: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, jsonString]];

[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with parameters: %@ and headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, jsonString, request.additionalHeaders]];
}

- (void)handleJSONNSURLResponse:(NSURLResponse*)response data:(NSData*)data error:(NSError*)error isAsync:(BOOL)async withRequest:(OneSignalRequest *)request onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock {

NSHTTPURLResponse* HTTPResponse = (NSHTTPURLResponse*)response;
NSInteger statusCode = [HTTPResponse statusCode];
NSDictionary *headers = [HTTPResponse allHeaderFields];
NSError* jsonError = nil;
NSMutableDictionary* innerJson;

if (data != nil && [data length] > 0) {
innerJson = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError];
innerJson[@"httpStatusCode"] = [NSNumber numberWithLong:statusCode];
innerJson[@"headers"] = headers;

[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"network request (%@) with URL %@ and headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, request.additionalHeaders]];
rgomezp marked this conversation as resolved.
Show resolved Hide resolved

[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"network response (%@) with URL %@: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, innerJson]];
if (jsonError) {
if (failureBlock != nil)
failureBlock([NSError errorWithDomain:@"OneSignal Error" code:statusCode userInfo:@{@"returned" : jsonError}]);
failureBlock([NSError errorWithDomain:@"OneSignal Error" code:statusCode userInfo:@{@"returned" : jsonError, @"headers": headers}]); // Add headers to error block
return;
}
}
Expand All @@ -224,14 +229,15 @@ - (void)handleJSONNSURLResponse:(NSURLResponse*)response data:(NSData*)data erro
} else if (failureBlock != nil) {
// Make sure to send all the infomation available to the client
if (innerJson != nil && error != nil)
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson, @"error": error}]);
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson, @"error": error, @"headers": headers}]);
else if (innerJson != nil)
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson}]);
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson, @"headers": headers}]);
else if (error != nil)
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"error" : error}]);
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"error" : error, @"headers": headers}]);
else
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:nil]);
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"headers": headers}]);
}
}


@end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Modified MIT License
*
* Copyright 2017 OneSignal
* Copyright 2024 OneSignal
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -35,8 +35,14 @@
#import "OSInAppMessagePrompt.h"
#import "OSInAppMessagingRequests.h"
#import "OneSignalWebViewManager.h"
#import "OneSignalTracker.h"
#import <OneSignalOutcomes/OneSignalOutcomes.h>
#import "OSSessionManager.h"
#import "OneSignalOSCore/OneSignalOSCore-Swift.h"

static NSInteger const DEFAULT_RETRY_AFTER_SECONDS = 1; // Default 1 second retry delay
static NSInteger const DEFAULT_RETRY_LIMIT = 0; // If not returned by backend, don't retry
static NSInteger const IAM_FETCH_DELAY_BUFFER = 0.5; // Fallback value if ryw_delay is nil: delay by 500 ms to increase the probability of getting a 200 & not having to retry

@implementation OSInAppMessageWillDisplayEvent

Expand Down Expand Up @@ -242,22 +248,70 @@ - (void)updateInAppMessagesFromCache {
}

- (void)getInAppMessagesFromServer:(NSString *)subscriptionId {
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer"];

if (!subscriptionId) {
[self updateInAppMessagesFromCache];
return;
}

OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId];
[OneSignalCoreImpl.sharedClient executeRequest:request onSuccess:^(NSDictionary *result) {
if (!subscriptionId) {
[self updateInAppMessagesFromCache];
return;
}

OSConsistencyManager *consistencyManager = [OSConsistencyManager shared];
NSString *onesignalId = OneSignalUserManagerImpl.sharedInstance.onesignalId;

if (!onesignalId) {
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Failed to get in app messages due to no OneSignal ID"];
return;
}

OSIamFetchReadyCondition *condition = [OSIamFetchReadyCondition sharedInstanceWithId:onesignalId];
OSReadYourWriteData *rywData = [consistencyManager getRywTokenFromAwaitableCondition:condition forId:onesignalId];

// We need to delay the first request by however long the backend is telling us (`ryw_delay`)
// This will help avoid unnecessary retries & can be easily adjusted from the backend
NSTimeInterval rywDelayInSeconds;
if (rywData.rywDelay) {
rywDelayInSeconds = [rywData.rywDelay doubleValue] / 1000.0;
} else {
rywDelayInSeconds = IAM_FETCH_DELAY_BUFFER;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rywDelayInSeconds * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

// Initial request
[self attemptFetchWithRetries:subscriptionId
rywData:rywData
attempts:@0 // Starting with 0 attempts
retryLimit:nil]; // Retry limit to be set dynamically on first failure
});
});
}


- (void)attemptFetchWithRetries:(NSString *)subscriptionId
rywData:(OSReadYourWriteData *)rywData
attempts:(NSNumber *)attempts
retryLimit:(NSNumber *)retryLimit {
NSNumber *sessionDuration = @([OSSessionManager.sharedSessionManager getTimeFocusedElapsed]);
NSString *rywToken = rywData.rywToken;
NSNumber *rywDelay = rywData.rywDelay;

// Create the request with the current attempt count
OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId
withSessionDuration:sessionDuration
withRetryCount:attempts
withRywToken:rywToken];

__block NSNumber *blockRetryLimit = retryLimit;

[OneSignalCoreImpl.sharedClient executeRequest:request
onSuccess:^(NSDictionary *result) {
dispatch_async(dispatch_get_main_queue(), ^{
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer success"];
if (result[@"in_app_messages"]) { // when there are no IAMs, will this still be there?
let messages = [NSMutableArray new];
if (result[@"in_app_messages"]) {
NSMutableArray *messages = [NSMutableArray new];

for (NSDictionary *messageJson in result[@"in_app_messages"]) {
let message = [OSInAppMessageInternal instanceWithJson:messageJson];
OSInAppMessageInternal *message = [OSInAppMessageInternal instanceWithJson:messageJson];
if (message) {
[messages addObject:message];
}
Expand All @@ -266,11 +320,89 @@ - (void)getInAppMessagesFromServer:(NSString *)subscriptionId {
[self updateInAppMessagesFromServer:messages];
return;
}
});
}
onFailure:^(NSError *error) {
NSDictionary *errorInfo = error.userInfo[@"returned"];
NSNumber *statusCode = errorInfo[@"httpStatusCode"];
NSDictionary* responseHeaders = errorInfo[@"headers"];

if (!statusCode) {
[self updateInAppMessagesFromCache];
return;
}

[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"getInAppMessagesFromServer failure: %@", error.localizedDescription]];

NSInteger code = [statusCode integerValue];
if (code == 425 || code == 429) { // 425 Too Early or 429 Too Many Requests
NSInteger retryAfter = [responseHeaders[@"Retry-After"] integerValue] ?: DEFAULT_RETRY_AFTER_SECONDS;

// TODO: Check this request and response. If no IAMs returned, should we really get from cache?
// This is the existing implementation but it could mean this user has no IAMs?

// Default is using cached IAMs in the messaging controller
// Dynamically set the retry limit from the header, if not already set
if (!blockRetryLimit) {
blockRetryLimit = @([responseHeaders[@"OneSignal-Retry-Limit"] integerValue] ?: DEFAULT_RETRY_LIMIT);
}

if ([attempts integerValue] < [blockRetryLimit integerValue]) {
NSInteger nextAttempt = [attempts integerValue] + 1; // Increment attempts
[self retryAfterDelay:retryAfter
subscriptionId:subscriptionId
rywData:rywData
attempts:@(nextAttempt)
retryLimit:blockRetryLimit];
} else {
// Final attempt without rywToken
[self fetchInAppMessagesWithoutToken:subscriptionId];
}
} else if (code >= 500 && code <= 599) {
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Server error, skipping retries"];
[self updateInAppMessagesFromCache];
} else {
[self updateInAppMessagesFromCache];
}
}];
}

- (void)retryAfterDelay:(NSInteger)retryAfter
subscriptionId:(NSString *)subscriptionId
rywData:(OSReadYourWriteData *)rywData
attempts:(NSNumber *)attempts
retryLimit:(NSNumber *)retryLimit {

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryAfter * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[self attemptFetchWithRetries:subscriptionId
rywData:rywData
attempts:attempts
retryLimit:retryLimit];
});
}

- (void)fetchInAppMessagesWithoutToken:(NSString *)subscriptionId {
NSNumber *sessionDuration = @([OSSessionManager.sharedSessionManager getTimeFocusedElapsed]);

OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId
withSessionDuration:sessionDuration
withRetryCount:nil
withRywToken:nil]; // No retries for the final attempt

[OneSignalCoreImpl.sharedClient executeRequest:request
onSuccess:^(NSDictionary *result) {
dispatch_async(dispatch_get_main_queue(), ^{
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Final attempt without token success"];
if (result[@"in_app_messages"]) {
NSMutableArray *messages = [NSMutableArray new];

for (NSDictionary *messageJson in result[@"in_app_messages"]) {
OSInAppMessageInternal *message = [OSInAppMessageInternal instanceWithJson:messageJson];
if (message) {
[messages addObject:message];
}
}

[self updateInAppMessagesFromServer:messages];
return;
}
[self updateInAppMessagesFromCache];
});
} onFailure:^(NSError *error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
#import "OSInAppMessageClickResult.h"

@interface OSRequestGetInAppMessages : OneSignalRequest
+ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId;
+ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId withSessionDuration:(NSNumber * _Nonnull)sessionDuration withRetryCount:(NSNumber *)retryCount withRywToken:(NSString *)rywToken;
@end

@interface OSRequestInAppMessageViewed : OneSignalRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,25 @@ of this software and associated documentation files (the "Software"), to deal
#import "OSInAppMessagingRequests.h"

@implementation OSRequestGetInAppMessages
+ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId {
+ (instancetype _Nonnull) withSubscriptionId:(NSString * _Nonnull)subscriptionId
withSessionDuration:(NSNumber * _Nonnull)sessionDuration
withRetryCount:(NSNumber *)retryCount
withRywToken:(NSString *)rywToken
{
let request = [OSRequestGetInAppMessages new];
request.method = GET;
let headers = [NSMutableDictionary new];

if (sessionDuration != nil) {
// convert to ms & round
sessionDuration = @(round([sessionDuration doubleValue] * 1000));
headers[@"OneSignal-Session-Duration" ] = [sessionDuration stringValue];
}
headers[@"OneSignal-RYW-Token"] = rywToken;
headers[@"OneSignal-Retry-Count"] = [retryCount stringValue];

request.additionalHeaders = headers;

NSString *appId = [OneSignalConfigManager getAppId];
request.path = [NSString stringWithFormat:@"apps/%@/subscriptions/%@/iams", appId, subscriptionId];
return request;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Modified MIT License

Copyright 2024 OneSignal

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

2. All copies of substantial portions of the Software may only be used in connection
with services provided by OneSignal.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

import Foundation

public enum OSIamFetchOffsetKey: Int, OSConsistencyKeyEnum {
// We track user create tokens as well because on fresh installs, we don't have a user or subscription
// to update, which would lead to a 5 second delay until the subsequent user & subscription update calls
// give us RYW tokens
case userCreate = 0
case userUpdate = 1
case subscriptionUpdate = 2
}
Loading
Loading