Skip to content

Commit 66db67e

Browse files
author
Rodrigo Gomez Palacio
authored
Merge pull request #1486 from OneSignal/consistency-manager
`OSConsistencyManager` & IAM fetch read-your-write consistency implementation
2 parents 91ad5e6 + 75eab0f commit 66db67e

25 files changed

+1344
-73
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 282 additions & 1 deletion
Large diffs are not rendered by default.

iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,24 +189,29 @@ - (void)prettyPrintDebugStatementWithRequest:(OneSignalRequest *)request {
189189
}
190190

191191
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
192-
193-
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with parameters: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, jsonString]];
192+
193+
[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]];
194194
}
195195

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

198198
NSHTTPURLResponse* HTTPResponse = (NSHTTPURLResponse*)response;
199199
NSInteger statusCode = [HTTPResponse statusCode];
200+
NSDictionary *headers = [HTTPResponse allHeaderFields];
200201
NSError* jsonError = nil;
201202
NSMutableDictionary* innerJson;
202203

203204
if (data != nil && [data length] > 0) {
204205
innerJson = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError];
205206
innerJson[@"httpStatusCode"] = [NSNumber numberWithLong:statusCode];
207+
innerJson[@"headers"] = headers;
208+
209+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"network request (%@) with URL %@ and headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, request.additionalHeaders]];
210+
206211
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"network response (%@) with URL %@: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, innerJson]];
207212
if (jsonError) {
208213
if (failureBlock != nil)
209-
failureBlock([NSError errorWithDomain:@"OneSignal Error" code:statusCode userInfo:@{@"returned" : jsonError}]);
214+
failureBlock([NSError errorWithDomain:@"OneSignal Error" code:statusCode userInfo:@{@"returned" : jsonError, @"headers": headers}]); // Add headers to error block
210215
return;
211216
}
212217
}
@@ -224,14 +229,15 @@ - (void)handleJSONNSURLResponse:(NSURLResponse*)response data:(NSData*)data erro
224229
} else if (failureBlock != nil) {
225230
// Make sure to send all the infomation available to the client
226231
if (innerJson != nil && error != nil)
227-
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson, @"error": error}]);
232+
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson, @"error": error, @"headers": headers}]);
228233
else if (innerJson != nil)
229-
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson}]);
234+
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"returned" : innerJson, @"headers": headers}]);
230235
else if (error != nil)
231-
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"error" : error}]);
236+
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"error" : error, @"headers": headers}]);
232237
else
233-
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:nil]);
238+
failureBlock([NSError errorWithDomain:@"OneSignalError" code:statusCode userInfo:@{@"headers": headers}]);
234239
}
235240
}
236241

242+
237243
@end

iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m

Lines changed: 148 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Modified MIT License
33
*
4-
* Copyright 2017 OneSignal
4+
* Copyright 2024 OneSignal
55
*
66
* Permission is hereby granted, free of charge, to any person obtaining a copy
77
* of this software and associated documentation files (the "Software"), to deal
@@ -35,8 +35,14 @@
3535
#import "OSInAppMessagePrompt.h"
3636
#import "OSInAppMessagingRequests.h"
3737
#import "OneSignalWebViewManager.h"
38+
#import "OneSignalTracker.h"
3839
#import <OneSignalOutcomes/OneSignalOutcomes.h>
3940
#import "OSSessionManager.h"
41+
#import "OneSignalOSCore/OneSignalOSCore-Swift.h"
42+
43+
static NSInteger const DEFAULT_RETRY_AFTER_SECONDS = 1; // Default 1 second retry delay
44+
static NSInteger const DEFAULT_RETRY_LIMIT = 0; // If not returned by backend, don't retry
45+
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
4046

4147
@implementation OSInAppMessageWillDisplayEvent
4248

@@ -242,22 +248,70 @@ - (void)updateInAppMessagesFromCache {
242248
}
243249

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

247-
if (!subscriptionId) {
248-
[self updateInAppMessagesFromCache];
249-
return;
250-
}
251-
252-
OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId];
253-
[OneSignalCoreImpl.sharedClient executeRequest:request onSuccess:^(NSDictionary *result) {
254+
if (!subscriptionId) {
255+
[self updateInAppMessagesFromCache];
256+
return;
257+
}
258+
259+
OSConsistencyManager *consistencyManager = [OSConsistencyManager shared];
260+
NSString *onesignalId = OneSignalUserManagerImpl.sharedInstance.onesignalId;
261+
262+
if (!onesignalId) {
263+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Failed to get in app messages due to no OneSignal ID"];
264+
return;
265+
}
266+
267+
OSIamFetchReadyCondition *condition = [OSIamFetchReadyCondition sharedInstanceWithId:onesignalId];
268+
OSReadYourWriteData *rywData = [consistencyManager getRywTokenFromAwaitableCondition:condition forId:onesignalId];
269+
270+
// We need to delay the first request by however long the backend is telling us (`ryw_delay`)
271+
// This will help avoid unnecessary retries & can be easily adjusted from the backend
272+
NSTimeInterval rywDelayInSeconds;
273+
if (rywData.rywDelay) {
274+
rywDelayInSeconds = [rywData.rywDelay doubleValue] / 1000.0;
275+
} else {
276+
rywDelayInSeconds = IAM_FETCH_DELAY_BUFFER;
277+
}
278+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rywDelayInSeconds * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
279+
280+
// Initial request
281+
[self attemptFetchWithRetries:subscriptionId
282+
rywData:rywData
283+
attempts:@0 // Starting with 0 attempts
284+
retryLimit:nil]; // Retry limit to be set dynamically on first failure
285+
});
286+
});
287+
}
288+
289+
290+
- (void)attemptFetchWithRetries:(NSString *)subscriptionId
291+
rywData:(OSReadYourWriteData *)rywData
292+
attempts:(NSNumber *)attempts
293+
retryLimit:(NSNumber *)retryLimit {
294+
NSNumber *sessionDuration = @([OSSessionManager.sharedSessionManager getTimeFocusedElapsed]);
295+
NSString *rywToken = rywData.rywToken;
296+
NSNumber *rywDelay = rywData.rywDelay;
297+
298+
// Create the request with the current attempt count
299+
OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId
300+
withSessionDuration:sessionDuration
301+
withRetryCount:attempts
302+
withRywToken:rywToken];
303+
304+
__block NSNumber *blockRetryLimit = retryLimit;
305+
306+
[OneSignalCoreImpl.sharedClient executeRequest:request
307+
onSuccess:^(NSDictionary *result) {
254308
dispatch_async(dispatch_get_main_queue(), ^{
255309
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer success"];
256-
if (result[@"in_app_messages"]) { // when there are no IAMs, will this still be there?
257-
let messages = [NSMutableArray new];
310+
if (result[@"in_app_messages"]) {
311+
NSMutableArray *messages = [NSMutableArray new];
258312

259313
for (NSDictionary *messageJson in result[@"in_app_messages"]) {
260-
let message = [OSInAppMessageInternal instanceWithJson:messageJson];
314+
OSInAppMessageInternal *message = [OSInAppMessageInternal instanceWithJson:messageJson];
261315
if (message) {
262316
[messages addObject:message];
263317
}
@@ -266,11 +320,89 @@ - (void)getInAppMessagesFromServer:(NSString *)subscriptionId {
266320
[self updateInAppMessagesFromServer:messages];
267321
return;
268322
}
323+
});
324+
}
325+
onFailure:^(NSError *error) {
326+
NSDictionary *errorInfo = error.userInfo[@"returned"];
327+
NSNumber *statusCode = errorInfo[@"httpStatusCode"];
328+
NSDictionary* responseHeaders = errorInfo[@"headers"];
329+
330+
if (!statusCode) {
331+
[self updateInAppMessagesFromCache];
332+
return;
333+
}
334+
335+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"getInAppMessagesFromServer failure: %@", error.localizedDescription]];
336+
337+
NSInteger code = [statusCode integerValue];
338+
if (code == 425 || code == 429) { // 425 Too Early or 429 Too Many Requests
339+
NSInteger retryAfter = [responseHeaders[@"Retry-After"] integerValue] ?: DEFAULT_RETRY_AFTER_SECONDS;
269340

270-
// TODO: Check this request and response. If no IAMs returned, should we really get from cache?
271-
// This is the existing implementation but it could mean this user has no IAMs?
272-
273-
// Default is using cached IAMs in the messaging controller
341+
// Dynamically set the retry limit from the header, if not already set
342+
if (!blockRetryLimit) {
343+
blockRetryLimit = @([responseHeaders[@"OneSignal-Retry-Limit"] integerValue] ?: DEFAULT_RETRY_LIMIT);
344+
}
345+
346+
if ([attempts integerValue] < [blockRetryLimit integerValue]) {
347+
NSInteger nextAttempt = [attempts integerValue] + 1; // Increment attempts
348+
[self retryAfterDelay:retryAfter
349+
subscriptionId:subscriptionId
350+
rywData:rywData
351+
attempts:@(nextAttempt)
352+
retryLimit:blockRetryLimit];
353+
} else {
354+
// Final attempt without rywToken
355+
[self fetchInAppMessagesWithoutToken:subscriptionId];
356+
}
357+
} else if (code >= 500 && code <= 599) {
358+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Server error, skipping retries"];
359+
[self updateInAppMessagesFromCache];
360+
} else {
361+
[self updateInAppMessagesFromCache];
362+
}
363+
}];
364+
}
365+
366+
- (void)retryAfterDelay:(NSInteger)retryAfter
367+
subscriptionId:(NSString *)subscriptionId
368+
rywData:(OSReadYourWriteData *)rywData
369+
attempts:(NSNumber *)attempts
370+
retryLimit:(NSNumber *)retryLimit {
371+
372+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryAfter * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
373+
374+
[self attemptFetchWithRetries:subscriptionId
375+
rywData:rywData
376+
attempts:attempts
377+
retryLimit:retryLimit];
378+
});
379+
}
380+
381+
- (void)fetchInAppMessagesWithoutToken:(NSString *)subscriptionId {
382+
NSNumber *sessionDuration = @([OSSessionManager.sharedSessionManager getTimeFocusedElapsed]);
383+
384+
OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId
385+
withSessionDuration:sessionDuration
386+
withRetryCount:nil
387+
withRywToken:nil]; // No retries for the final attempt
388+
389+
[OneSignalCoreImpl.sharedClient executeRequest:request
390+
onSuccess:^(NSDictionary *result) {
391+
dispatch_async(dispatch_get_main_queue(), ^{
392+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Final attempt without token success"];
393+
if (result[@"in_app_messages"]) {
394+
NSMutableArray *messages = [NSMutableArray new];
395+
396+
for (NSDictionary *messageJson in result[@"in_app_messages"]) {
397+
OSInAppMessageInternal *message = [OSInAppMessageInternal instanceWithJson:messageJson];
398+
if (message) {
399+
[messages addObject:message];
400+
}
401+
}
402+
403+
[self updateInAppMessagesFromServer:messages];
404+
return;
405+
}
274406
[self updateInAppMessagesFromCache];
275407
});
276408
} onFailure:^(NSError *error) {

iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
#import "OSInAppMessageClickResult.h"
3030

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

3535
@interface OSRequestInAppMessageViewed : OneSignalRequest

iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,25 @@ of this software and associated documentation files (the "Software"), to deal
2828
#import "OSInAppMessagingRequests.h"
2929

3030
@implementation OSRequestGetInAppMessages
31-
+ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId {
31+
+ (instancetype _Nonnull) withSubscriptionId:(NSString * _Nonnull)subscriptionId
32+
withSessionDuration:(NSNumber * _Nonnull)sessionDuration
33+
withRetryCount:(NSNumber *)retryCount
34+
withRywToken:(NSString *)rywToken
35+
{
3236
let request = [OSRequestGetInAppMessages new];
3337
request.method = GET;
38+
let headers = [NSMutableDictionary new];
39+
40+
if (sessionDuration != nil) {
41+
// convert to ms & round
42+
sessionDuration = @(round([sessionDuration doubleValue] * 1000));
43+
headers[@"OneSignal-Session-Duration" ] = [sessionDuration stringValue];
44+
}
45+
headers[@"OneSignal-RYW-Token"] = rywToken;
46+
headers[@"OneSignal-Retry-Count"] = [retryCount stringValue];
47+
48+
request.additionalHeaders = headers;
49+
3450
NSString *appId = [OneSignalConfigManager getAppId];
3551
request.path = [NSString stringWithFormat:@"apps/%@/subscriptions/%@/iams", appId, subscriptionId];
3652
return request;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2024 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
27+
28+
import Foundation
29+
30+
public enum OSIamFetchOffsetKey: Int, OSConsistencyKeyEnum {
31+
// We track user create tokens as well because on fresh installs, we don't have a user or subscription
32+
// to update, which would lead to a 5 second delay until the subsequent user & subscription update calls
33+
// give us RYW tokens
34+
case userCreate = 0
35+
case userUpdate = 1
36+
case subscriptionUpdate = 2
37+
}

0 commit comments

Comments
 (0)