Skip to content

Commit fddfc95

Browse files
Merge pull request #431 from Iterable/feature/mob-2400-createdAt
[MOB-2400] - Add createdAt and sentAt
2 parents 74797da + 62b87d1 commit fddfc95

11 files changed

+236
-31
lines changed

swift-sdk/Constants.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ public enum JsonKey: String, JsonKeyRepresentable {
173173
static let sdkVersion = "SDK-Version"
174174
static let sdkPlatform = "SDK-Platform"
175175
static let authorization = "Authorization"
176+
static let sentAt = "sentAt"
177+
}
178+
179+
public enum Body {
180+
static let createdAt = "createdAt"
176181
}
177182

178183
public enum InApp {

swift-sdk/Internal/IterableAPICallRequest.swift

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@ struct IterableAPICallRequest {
1414
let deviceMetadata: DeviceMetadata
1515
let iterableRequest: IterableRequest
1616

17-
func convertToURLRequest() -> URLRequest? {
17+
func convertToURLRequest(currentDate: Date) -> URLRequest? {
1818
switch iterableRequest {
1919
case let .get(getRequest):
20-
return IterableRequestUtil.createGetRequest(forApiEndPoint: endPoint, path: getRequest.path, headers: createIterableHeaders(), args: getRequest.args)
20+
return IterableRequestUtil.createGetRequest(forApiEndPoint: endPoint,
21+
path: getRequest.path,
22+
headers: createIterableHeaders(currentDate: currentDate),
23+
args: getRequest.args)
2124
case let .post(postRequest):
22-
return IterableRequestUtil.createPostRequest(forApiEndPoint: endPoint, path: postRequest.path, headers: createIterableHeaders(), args: postRequest.args, body: postRequest.body)
25+
return IterableRequestUtil.createPostRequest(forApiEndPoint: endPoint,
26+
path: postRequest.path,
27+
headers: createIterableHeaders(currentDate: currentDate),
28+
args: postRequest.args,
29+
body: postRequest.body)
2330
}
2431
}
2532

@@ -32,18 +39,32 @@ struct IterableAPICallRequest {
3239
}
3340
}
3441

35-
private func createIterableHeaders() -> [String: String] {
42+
func addingBodyField(key: AnyHashable, value: Any) -> IterableAPICallRequest {
43+
IterableAPICallRequest(apiKey: apiKey,
44+
endPoint: endPoint,
45+
auth: auth,
46+
deviceMetadata: deviceMetadata,
47+
iterableRequest: iterableRequest.addingBodyField(key: key, value: value))
48+
}
49+
50+
private func createIterableHeaders(currentDate: Date) -> [String: String] {
3651
var headers = [JsonKey.contentType.jsonKey: JsonValue.applicationJson.jsonStringValue,
3752
JsonKey.Header.sdkPlatform: JsonValue.iOS.jsonStringValue,
3853
JsonKey.Header.sdkVersion: IterableAPI.sdkVersion,
39-
JsonKey.Header.apiKey: apiKey]
54+
JsonKey.Header.apiKey: apiKey,
55+
JsonKey.Header.sentAt: Self.format(sentAt: currentDate),
56+
]
4057

4158
if let authToken = auth.authToken {
4259
headers[JsonKey.Header.authorization] = "Bearer \(authToken)"
4360
}
4461

4562
return headers
4663
}
64+
65+
private static func format(sentAt: Date) -> String {
66+
return "\(IterableUtil.int(fromDate: sentAt))"
67+
}
4768
}
4869

4970
extension IterableAPICallRequest: Codable {}

swift-sdk/Internal/IterableAPICallTaskProcessor.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ import Foundation
88
struct IterableAPICallTaskProcessor: IterableTaskProcessor {
99
let networkSession: NetworkSessionProtocol
1010

11+
init(networkSession: NetworkSessionProtocol, dateProvider: DateProviderProtocol = SystemDateProvider()) {
12+
self.networkSession = networkSession
13+
self.dateProvider = dateProvider
14+
}
15+
1116
func process(task: IterableTask) throws -> Future<IterableTaskResult, IterableTaskError> {
1217
ITBInfo()
1318
guard let data = task.data else {
1419
return IterableTaskError.createErroredFuture(reason: "expecting data")
1520
}
1621

17-
let iterableRequest = try JSONDecoder().decode(IterableAPICallRequest.self, from: data)
18-
guard let urlRequest = iterableRequest.convertToURLRequest() else {
22+
let decodedIterableRequest = try JSONDecoder().decode(IterableAPICallRequest.self, from: data)
23+
let iterableRequest = decodedIterableRequest.addingBodyField(key: JsonKey.Body.createdAt,
24+
value: IterableUtil.int(fromDate: task.scheduledAt))
25+
26+
guard let urlRequest = iterableRequest.convertToURLRequest(currentDate: dateProvider.currentDate) else {
1927
return IterableTaskError.createErroredFuture(reason: "could not convert to url request")
2028
}
2129

@@ -38,6 +46,8 @@ struct IterableAPICallTaskProcessor: IterableTaskProcessor {
3846
return result
3947
}
4048

49+
private let dateProvider: DateProviderProtocol
50+
4151
private static func isNetworkUnavailable(sendRequestError: SendRequestError) -> Bool {
4252
if let originalError = sendRequestError.originalError {
4353
return originalError.localizedDescription.lowercased().contains("offline")

swift-sdk/Internal/IterableRequest.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ extension IterableRequest: Codable {
4545
}
4646
}
4747

48+
func addingBodyField(key: AnyHashable, value: Any) -> IterableRequest {
49+
if case .post(let postRequest) = self {
50+
return .post(postRequest.addingBodyField(key: key, value: value))
51+
} else {
52+
return self
53+
}
54+
}
55+
4856
private static let requestTypeGet = "get"
4957
private static let requestTypePost = "post"
5058
}
@@ -58,6 +66,12 @@ struct PostRequest {
5866
let path: String
5967
let args: [String: String]?
6068
let body: [AnyHashable: Any]?
69+
70+
func addingBodyField(key: AnyHashable, value: Any) -> PostRequest {
71+
var newBody = body ?? [AnyHashable: Any]()
72+
newBody[key] = value
73+
return PostRequest(path: path, args: args, body: newBody)
74+
}
6175
}
6276

6377
extension PostRequest: Codable {

swift-sdk/Internal/IterableRequestUtil.swift

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,35 @@
88
import Foundation
99

1010
struct IterableRequestUtil {
11-
static func createPostRequest(forApiEndPoint apiEndPoint: String, path: String, headers: [String: String]? = nil, args: [String: String]? = nil, body: [AnyHashable: Any]? = nil) -> URLRequest? {
12-
createPostRequest(forApiEndPoint: apiEndPoint, path: path, headers: headers, args: args, body: dictToJsonData(body))
11+
static func createPostRequest(forApiEndPoint apiEndPoint: String,
12+
path: String,
13+
headers: [String: String]? = nil,
14+
args: [String: String]? = nil,
15+
body: [AnyHashable: Any]? = nil) -> URLRequest? {
16+
createPostRequest(forApiEndPoint: apiEndPoint,
17+
path: path,
18+
headers: headers,
19+
args: args,
20+
body: dictToJsonData(body))
1321
}
1422

15-
static func createPostRequest<T: Encodable>(forApiEndPoint apiEndPoint: String, path: String, headers: [String: String]? = nil, args: [String: String]? = nil, body: T) -> URLRequest? {
16-
createPostRequest(forApiEndPoint: apiEndPoint, path: path, headers: headers, args: args, body: try? JSONEncoder().encode(body))
23+
static func createPostRequest<T: Encodable>(forApiEndPoint apiEndPoint: String,
24+
path: String,
25+
headers: [String: String]? = nil,
26+
args: [String: String]? = nil,
27+
body: T) -> URLRequest? {
28+
createPostRequest(forApiEndPoint: apiEndPoint,
29+
path: path,
30+
headers: headers,
31+
args: args,
32+
body: try? JSONEncoder().encode(body))
1733
}
1834

19-
static func createPostRequest(forApiEndPoint apiEndPoint: String, path: String, headers: [String: String]? = nil, args: [String: String]? = nil, body: Data? = nil) -> URLRequest? {
35+
static func createPostRequest(forApiEndPoint apiEndPoint: String,
36+
path: String,
37+
headers: [String: String]? = nil,
38+
args: [String: String]? = nil,
39+
body: Data? = nil) -> URLRequest? {
2040
guard let url = getUrlComponents(forApiEndPoint: apiEndPoint, path: path, args: args)?.url else {
2141
return nil
2242
}
@@ -29,7 +49,10 @@ struct IterableRequestUtil {
2949
return request
3050
}
3151

32-
static func createGetRequest(forApiEndPoint apiEndPoint: String, path: String, headers: [String: String]? = nil, args: [String: String]? = nil) -> URLRequest? {
52+
static func createGetRequest(forApiEndPoint apiEndPoint: String,
53+
path: String,
54+
headers: [String: String]? = nil,
55+
args: [String: String]? = nil) -> URLRequest? {
3356
guard let url = getUrlComponents(forApiEndPoint: apiEndPoint, path: path, args: args)?.url else {
3457
return nil
3558
}

swift-sdk/Internal/IterableTask.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ struct IterableTask {
1111
let id: String
1212
let name: String?
1313
let version: Int
14-
let createdAt: Date?
15-
let modifiedAt: Date?
14+
let createdAt: Date? // Time at which this task record was created
15+
let modifiedAt: Date? // Time when this record was modified
1616
let type: IterableTaskType
1717
let attempts: Int
1818
let lastAttemptedAt: Date?
1919
let processing: Bool
20-
let scheduledAt: Date
20+
let scheduledAt: Date // Time after which this task can be scheduled
2121
let data: Data?
2222
let failed: Bool
2323
let blocking: Bool
24-
let requestedAt: Date
24+
let requestedAt: Date // Time when the request was made by SDK
2525
let taskFailureData: Data?
2626

2727
init(id: String,

swift-sdk/Internal/IterableTaskRunner.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ class IterableTaskRunner: NSObject {
1313
persistenceContextProvider: IterablePersistenceContextProvider,
1414
notificationCenter: NotificationCenterProtocol = NotificationCenter.default,
1515
timeInterval: TimeInterval = 1.0 * 60,
16-
connectivityManager: NetworkConnectivityManager = NetworkConnectivityManager()) {
16+
connectivityManager: NetworkConnectivityManager = NetworkConnectivityManager(),
17+
dateProvider: DateProviderProtocol = SystemDateProvider()) {
1718
ITBInfo()
1819
self.networkSession = networkSession
1920
self.persistenceContextProvider = persistenceContextProvider
2021
self.notificationCenter = notificationCenter
2122
self.timeInterval = timeInterval
23+
self.dateProvider = dateProvider
2224
self.connectivityManager = connectivityManager
2325

2426
super.init()
@@ -174,7 +176,7 @@ class IterableTaskRunner: NSObject {
174176

175177
switch task.type {
176178
case .apiCall:
177-
let processor = IterableAPICallTaskProcessor(networkSession: networkSession)
179+
let processor = IterableAPICallTaskProcessor(networkSession: networkSession, dateProvider: dateProvider)
178180
return processAPICallTask(processor: processor, task: task)
179181
}
180182
}
@@ -183,7 +185,6 @@ class IterableTaskRunner: NSObject {
183185
task: IterableTask) -> Future<TaskExecutionResult, Never> {
184186
ITBInfo()
185187
let result = Promise<TaskExecutionResult, Never>()
186-
let processor = IterableAPICallTaskProcessor(networkSession: networkSession)
187188
do {
188189
try processor.process(task: task).onSuccess { taskResult in
189190
switch taskResult {
@@ -254,6 +255,7 @@ class IterableTaskRunner: NSObject {
254255
private let persistenceContextProvider: IterablePersistenceContextProvider
255256
private let notificationCenter: NotificationCenterProtocol
256257
private let timeInterval: TimeInterval
258+
private let dateProvider: DateProviderProtocol
257259
private let connectivityManager: NetworkConnectivityManager
258260
private weak var timer: Timer?
259261
private var running = false

tests/offline-events-tests/RequestProcessorTests.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,9 @@ class RequestProcessorTests: XCTestCase {
684684
let networkSession = MockNetworkSession(statusCode: 400)
685685
networkSession.requestCallback = { request in
686686
TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path)
687-
XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict))
687+
var requestBody = request.bodyDict
688+
requestBody.removeValue(forKey: "createdAt")
689+
XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: requestBody))
688690
}
689691
let requestProcessor = createRequestProcessor(networkSession: networkSession,
690692
notificationCenter: notificationCenter,
@@ -737,7 +739,9 @@ class RequestProcessorTests: XCTestCase {
737739
expectation: XCTestExpectation) {
738740
networkSession.requestCallback = { request in
739741
TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path)
740-
XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict))
742+
var requestBody = request.bodyDict
743+
requestBody.removeValue(forKey: "createdAt")
744+
XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: requestBody))
741745
}
742746

743747
request().onSuccess { json in
@@ -754,7 +758,9 @@ class RequestProcessorTests: XCTestCase {
754758
expectation: XCTestExpectation) {
755759
networkSession.requestCallback = { request in
756760
TestUtils.validate(request: request, apiEndPoint: Endpoint.api, path: path)
757-
XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: request.bodyDict))
761+
var requestBody = request.bodyDict
762+
requestBody.removeValue(forKey: "createdAt")
763+
XCTAssertTrue(TestUtils.areEqual(dict1: bodyDict, dict2: requestBody))
758764
}
759765
request().onSuccess { json in
760766
XCTFail()

tests/offline-events-tests/TaskProcessorTests.swift

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,80 @@ class TaskProcessorTests: XCTestCase {
142142
try persistenceProvider.mainQueueContext().save()
143143
wait(for: [expectation1], timeout: 15.0)
144144
}
145+
146+
func testSentAtInHeader() throws {
147+
let expectation1 = expectation(description: #function)
148+
let task = try createSampleTask()!
149+
let date = Date()
150+
let sentAtTime = "\(Int(date.timeIntervalSince1970 * 1000))"
151+
let dateProvider = MockDateProvider()
152+
dateProvider.currentDate = date
153+
154+
let networkSession = MockNetworkSession()
155+
networkSession.requestCallback = { request in
156+
if request.allHTTPHeaderFields!.contains(where: { $0.key == "sentAt" && $0.value == sentAtTime }) {
157+
expectation1.fulfill()
158+
}
159+
}
160+
161+
// process data
162+
let processor = IterableAPICallTaskProcessor(networkSession: networkSession, dateProvider: dateProvider)
163+
try processor.process(task: task)
164+
.onSuccess { taskResult in
165+
switch taskResult {
166+
case .success(detail: _):
167+
break
168+
case .failureWithNoRetry(detail: _):
169+
XCTFail("not expecting failure with no retry")
170+
case .failureWithRetry(retryAfter: _, detail: _):
171+
XCTFail("not expecting failure with retry")
172+
}
173+
}
174+
.onError { _ in
175+
XCTFail()
176+
}
145177

146-
private func createSampleTask() throws -> IterableTask? {
178+
try persistenceProvider.mainQueueContext().delete(task: task)
179+
try persistenceProvider.mainQueueContext().save()
180+
wait(for: [expectation1], timeout: 5.0)
181+
}
182+
183+
func testCreatedAtInBody() throws {
184+
let expectation1 = expectation(description: #function)
185+
let date = Date()
186+
let createdAtTime = Int(date.timeIntervalSince1970 * 1000)
187+
let task = try createSampleTask(scheduledAt: date)!
188+
189+
let networkSession = MockNetworkSession()
190+
networkSession.requestCallback = { request in
191+
if request.bodyDict.contains(where: { $0.key == "createdAt" && ($0.value as! Int) == createdAtTime }) {
192+
expectation1.fulfill()
193+
}
194+
}
195+
196+
// process data
197+
let processor = IterableAPICallTaskProcessor(networkSession: networkSession)
198+
try processor.process(task: task)
199+
.onSuccess { taskResult in
200+
switch taskResult {
201+
case .success(detail: _):
202+
break
203+
case .failureWithNoRetry(detail: _):
204+
XCTFail("not expecting failure with no retry")
205+
case .failureWithRetry(retryAfter: _, detail: _):
206+
XCTFail("not expecting failure with retry")
207+
}
208+
}
209+
.onError { _ in
210+
XCTFail()
211+
}
212+
213+
try persistenceProvider.mainQueueContext().delete(task: task)
214+
try persistenceProvider.mainQueueContext().save()
215+
wait(for: [expectation1], timeout: 5.0)
216+
}
217+
218+
private func createSampleTask(scheduledAt: Date = Date(), requestedAt: Date = Date()) throws -> IterableTask? {
147219
let apiKey = "test-api-key"
148220
let email = "[email protected]"
149221
let eventName = "CustomEvent1"
@@ -169,9 +241,9 @@ class TaskProcessorTests: XCTestCase {
169241
let taskId = IterableUtil.generateUUID()
170242
try persistenceProvider.mainQueueContext().create(task: IterableTask(id: taskId,
171243
type: .apiCall,
172-
scheduledAt: Date(),
244+
scheduledAt: scheduledAt,
173245
data: data,
174-
requestedAt: Date()))
246+
requestedAt: requestedAt))
175247
try persistenceProvider.mainQueueContext().save()
176248

177249
return try persistenceProvider.mainQueueContext().findTask(withId: taskId)

0 commit comments

Comments
 (0)