diff --git a/Snowplow iOSTests/TestDataPersistence.m b/Snowplow iOSTests/TestDataPersistence.m new file mode 100644 index 000000000..586cc4755 --- /dev/null +++ b/Snowplow iOSTests/TestDataPersistence.m @@ -0,0 +1,87 @@ +// +// TestDataPersistence.m +// Snowplow-iOSTests +// +// Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. +// +// Authors: Alex Benini +// Copyright: Copyright (c) 2013-2021 Snowplow Analytics Ltd +// License: Apache License Version 2.0 +// + +#import +#import "SPDataPersistence.h" + +@interface TestDataPersistence : XCTestCase + +@end + +@implementation TestDataPersistence + +- (void)setUp { + [SPDataPersistence removeDataPersistenceWithNamespace:@"namespace"]; + [SPDataPersistence removeDataPersistenceWithNamespace:@"namespace1"]; + [SPDataPersistence removeDataPersistenceWithNamespace:@"namespace2"]; +} + +- (void)testStringFromNamespace { + XCTAssertEqualObjects(@"abc-1_2_3", [SPDataPersistence stringFromNamespace:@"abc 1_2_3"]); +} + +- (void)testDataPersistenceForNamespaceWithDifferentNamespaces { + SPDataPersistence *dp1 = [SPDataPersistence dataPersistenceForNamespace:@"namespace1"]; + SPDataPersistence *dp2 = [SPDataPersistence dataPersistenceForNamespace:@"namespace2"]; + XCTAssertNotEqual(dp1, dp2); +} + +- (void)testDataPersistenceForNamespaceWithSameNamespaces { + SPDataPersistence *dp1 = [SPDataPersistence dataPersistenceForNamespace:@"namespace"]; + SPDataPersistence *dp2 = [SPDataPersistence dataPersistenceForNamespace:@"namespace"]; + XCTAssertEqual(dp1, dp2); +} + +- (void)testRemoveDataPersistenceForNamespace { + SPDataPersistence *dp1 = [SPDataPersistence dataPersistenceForNamespace:@"namespace"]; + [SPDataPersistence removeDataPersistenceWithNamespace:@"namespace"]; + SPDataPersistence *dp2 = [SPDataPersistence dataPersistenceForNamespace:@"namespace"]; + XCTAssertNotEqual(dp1, dp2); +} + +- (void)testDataIsCorrectlyStored { + SPDataPersistence *dp = [SPDataPersistence dataPersistenceForNamespace:@"namespace"]; + NSDictionary *session = @{@"key": @"value"}; + dp.session = session; + XCTAssertEqualObjects(session, dp.session); + XCTAssertEqualObjects(session, dp.data[@"session"]); + // Override session + session = @{@"key2": @"value2"}; + dp.session = session; + XCTAssertEqualObjects(session, dp.session); + XCTAssertEqualObjects(session, dp.data[@"session"]); +} + +- (void)testDataIsStoredWithoutInterference { + SPDataPersistence *dp1 = [SPDataPersistence dataPersistenceForNamespace:@"namespace1"]; + SPDataPersistence *dp2 = [SPDataPersistence dataPersistenceForNamespace:@"namespace2"]; + NSDictionary *session = @{@"key": @"value"}; + dp1.session = session; + // Check dp1 + XCTAssertEqualObjects(session, dp1.session); + XCTAssertEqualObjects(session, dp1.data[@"session"]); + // Check dp2 + XCTAssertNotEqualObjects(session, dp2.session); + XCTAssertNotEqualObjects(session, dp2.data[@"session"]); +} + +@end diff --git a/Snowplow iOSTests/TestMemoryEventStore.m b/Snowplow iOSTests/TestMemoryEventStore.m new file mode 100644 index 000000000..22c10672c --- /dev/null +++ b/Snowplow iOSTests/TestMemoryEventStore.m @@ -0,0 +1,82 @@ +// +// TestMemoryEventStore.m +// Snowplow +// +// Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. +// +// Authors: Jonathan Almeida +// Copyright: Copyright (c) 2013-2021 Snowplow Analytics Ltd +// License: Apache License Version 2.0 +// + +#import +#import "SPMemoryEventStore.h" +#import "SPPayload.h" + +@interface TestMemoryEventStore : XCTestCase +@end + +@implementation TestMemoryEventStore + +- (void)testInit { + SPMemoryEventStore * eventStore = [[SPMemoryEventStore alloc] init]; + XCTAssertNotNil(eventStore); +} + +- (void)testInsertPayload { + SPMemoryEventStore * eventStore = [[SPMemoryEventStore alloc] init]; + [eventStore removeAllEvents]; + + // Build an event + SPPayload * payload = [[SPPayload alloc] init]; + [payload addValueToPayload:@"pv" forKey:@"e"]; + [payload addValueToPayload:@"www.foobar.com" forKey:@"url"]; + [payload addValueToPayload:@"Welcome to foobar!" forKey:@"page"]; + [payload addValueToPayload:@"MEEEE" forKey:@"refr"]; + + // Insert an event + [eventStore addEvent:payload]; + + XCTAssertEqual([eventStore count], 1); + NSArray *events = [eventStore emittableEventsWithQueryLimit:1]; + XCTAssertEqualObjects([events[0].payload getAsDictionary], [payload getAsDictionary]); + [eventStore removeEventWithId:0]; + + XCTAssertEqual([eventStore count], 0); +} + +- (void)testInsertManyPayloads { + SPMemoryEventStore * eventStore = [[SPMemoryEventStore alloc] init]; + [eventStore removeAllEvents]; + + // Build an event + SPPayload * payload = [[SPPayload alloc] init]; + [payload addValueToPayload:@"pv" forKey:@"e"]; + [payload addValueToPayload:@"www.foobar.com" forKey:@"url"]; + [payload addValueToPayload:@"Welcome to foobar!" forKey:@"page"]; + [payload addValueToPayload:@"MEEEE" forKey:@"refr"]; + + for (int i = 0; i < 250; i++) { + [eventStore addEvent:payload]; + } + + XCTAssertEqual([eventStore count], 250); + XCTAssertEqual([eventStore emittableEventsWithQueryLimit:600].count, 250); + XCTAssertEqual([eventStore emittableEventsWithQueryLimit:150].count, 150); + + [eventStore removeAllEvents]; + XCTAssertEqual([eventStore count], 0); +} + +@end diff --git a/Snowplow iOSTests/TestEventStore.m b/Snowplow iOSTests/TestSQLiteEventStore.m similarity index 98% rename from Snowplow iOSTests/TestEventStore.m rename to Snowplow iOSTests/TestSQLiteEventStore.m index aee5a2cdf..2ba220cb3 100644 --- a/Snowplow iOSTests/TestEventStore.m +++ b/Snowplow iOSTests/TestSQLiteEventStore.m @@ -1,5 +1,5 @@ // -// TestEventStore.m +// TestSQLiteEventStore.m // Snowplow // // Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. @@ -24,10 +24,10 @@ #import "SPSQLiteEventStore.h" #import "SPPayload.h" -@interface TestEventStore : XCTestCase +@interface TestSQLiteEventStore : XCTestCase @end -@implementation TestEventStore +@implementation TestSQLiteEventStore - (void)setUp { [SPSQLiteEventStore removeUnsentEventsExceptForNamespaces:@[]]; diff --git a/Snowplow iOSTests/TestSession.m b/Snowplow iOSTests/TestSession.m index f0a3cd19e..7612e5b61 100644 --- a/Snowplow iOSTests/TestSession.m +++ b/Snowplow iOSTests/TestSession.m @@ -22,6 +22,7 @@ #import #import "SPSession.h" +#import "SPDataPersistence.h" #import "SPTrackerConstants.h" /// Category needed to make the private methods testable. @@ -40,7 +41,6 @@ @implementation TestSession - (void)setUp { [super setUp]; - [self cleanSessionFileWithNamespace:nil]; [self cleanSessionFileWithNamespace:@"tracker"]; } @@ -368,14 +368,7 @@ - (void)testMultipleTrackersUpdateDifferentSessions { /// Service methods - (void)cleanSessionFileWithNamespace:(NSString *)namespace { - NSString *sessionFilename = @"session.dict"; - if (namespace) { - sessionFilename = [SPSession createSessionFilenameWithNamespace:@"tracker"]; - } - NSError *error = nil; - NSURL *sessionFileUrl = [SPSession createSessionFileUrlWithFilename:sessionFilename]; - [[NSFileManager defaultManager] removeItemAtURL:sessionFileUrl error:&error]; - NSLog(@"%@", error.localizedDescription); + [SPDataPersistence removeDataPersistenceWithNamespace:namespace]; } @end diff --git a/Snowplow.xcodeproj/project.pbxproj b/Snowplow.xcodeproj/project.pbxproj index 339bd71c1..6a07f9a64 100644 --- a/Snowplow.xcodeproj/project.pbxproj +++ b/Snowplow.xcodeproj/project.pbxproj @@ -61,7 +61,7 @@ 754774CD222756470043B814 /* UIViewController+SPScreenView_SWIZZLE.h in Headers */ = {isa = PBXBuildFile; fileRef = 754774CB222756470043B814 /* UIViewController+SPScreenView_SWIZZLE.h */; settings = {ATTRIBUTES = (Private, ); }; }; 754774D0222756470043B814 /* UIViewController+SPScreenView_SWIZZLE.m in Sources */ = {isa = PBXBuildFile; fileRef = 754774CC222756470043B814 /* UIViewController+SPScreenView_SWIZZLE.m */; }; 75CAC40521F2955100271FB3 /* TestSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 75CAC3F121F2955000271FB3 /* TestSession.m */; }; - 75CAC40621F2955100271FB3 /* TestEventStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 75CAC3F221F2955000271FB3 /* TestEventStore.m */; }; + 75CAC40621F2955100271FB3 /* TestSQLiteEventStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 75CAC3F221F2955000271FB3 /* TestSQLiteEventStore.m */; }; 75CAC40721F2955100271FB3 /* TestPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = 75CAC3F321F2955000271FB3 /* TestPayload.m */; }; 75CAC40821F2955100271FB3 /* LegacyTestSubject.m in Sources */ = {isa = PBXBuildFile; fileRef = 75CAC3F421F2955000271FB3 /* LegacyTestSubject.m */; }; 75CAC40921F2955100271FB3 /* TestSelfDescribingJson.m in Sources */ = {isa = PBXBuildFile; fileRef = 75CAC3F521F2955000271FB3 /* TestSelfDescribingJson.m */; }; @@ -564,6 +564,24 @@ ED98972C26287F7A00145157 /* SPConfigurationCache.m in Sources */ = {isa = PBXBuildFile; fileRef = ED98972526287F7900145157 /* SPConfigurationCache.m */; }; ED98972D26287F7A00145157 /* SPConfigurationCache.m in Sources */ = {isa = PBXBuildFile; fileRef = ED98972526287F7900145157 /* SPConfigurationCache.m */; }; EDA06FB62664CD2F007FA773 /* SPMockEventStore.m in Sources */ = {isa = PBXBuildFile; fileRef = EDA06FB52664CD2F007FA773 /* SPMockEventStore.m */; }; + EDB2FD1926C130B80031B872 /* SPDataPersistence.h in Headers */ = {isa = PBXBuildFile; fileRef = EDB2FD1726C130B80031B872 /* SPDataPersistence.h */; }; + EDB2FD1A26C130B80031B872 /* SPDataPersistence.h in Headers */ = {isa = PBXBuildFile; fileRef = EDB2FD1726C130B80031B872 /* SPDataPersistence.h */; }; + EDB2FD1B26C130B80031B872 /* SPDataPersistence.h in Headers */ = {isa = PBXBuildFile; fileRef = EDB2FD1726C130B80031B872 /* SPDataPersistence.h */; }; + EDB2FD1C26C130B80031B872 /* SPDataPersistence.h in Headers */ = {isa = PBXBuildFile; fileRef = EDB2FD1726C130B80031B872 /* SPDataPersistence.h */; }; + EDB2FD1D26C130B80031B872 /* SPDataPersistence.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB2FD1826C130B80031B872 /* SPDataPersistence.m */; }; + EDB2FD1E26C130B80031B872 /* SPDataPersistence.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB2FD1826C130B80031B872 /* SPDataPersistence.m */; }; + EDB2FD1F26C130B80031B872 /* SPDataPersistence.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB2FD1826C130B80031B872 /* SPDataPersistence.m */; }; + EDB2FD2026C130B80031B872 /* SPDataPersistence.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB2FD1826C130B80031B872 /* SPDataPersistence.m */; }; + EDB2FD2226C57F6D0031B872 /* TestDataPersistence.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB2FD2126C57F6C0031B872 /* TestDataPersistence.m */; }; + EDB693FA26B7F61D00B76A79 /* SPMemoryEventStore.h in Headers */ = {isa = PBXBuildFile; fileRef = EDB693F826B7F61D00B76A79 /* SPMemoryEventStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EDB693FB26B7F61D00B76A79 /* SPMemoryEventStore.h in Headers */ = {isa = PBXBuildFile; fileRef = EDB693F826B7F61D00B76A79 /* SPMemoryEventStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EDB693FC26B7F61D00B76A79 /* SPMemoryEventStore.h in Headers */ = {isa = PBXBuildFile; fileRef = EDB693F826B7F61D00B76A79 /* SPMemoryEventStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EDB693FD26B7F61D00B76A79 /* SPMemoryEventStore.h in Headers */ = {isa = PBXBuildFile; fileRef = EDB693F826B7F61D00B76A79 /* SPMemoryEventStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EDB693FE26B7F61D00B76A79 /* SPMemoryEventStore.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB693F926B7F61D00B76A79 /* SPMemoryEventStore.m */; }; + EDB693FF26B7F61D00B76A79 /* SPMemoryEventStore.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB693F926B7F61D00B76A79 /* SPMemoryEventStore.m */; }; + EDB6940026B7F61D00B76A79 /* SPMemoryEventStore.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB693F926B7F61D00B76A79 /* SPMemoryEventStore.m */; }; + EDB6940126B7F61D00B76A79 /* SPMemoryEventStore.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB693F926B7F61D00B76A79 /* SPMemoryEventStore.m */; }; + EDB6940326B83BF300B76A79 /* TestMemoryEventStore.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB6940226B83BF300B76A79 /* TestMemoryEventStore.m */; }; EDCBD0D824F5084900D39DD2 /* TestNetworkConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = EDCBD0D724F5084800D39DD2 /* TestNetworkConnection.m */; }; EDD8540C24EE786900661F6B /* SPEventStore.h in Headers */ = {isa = PBXBuildFile; fileRef = EDD8540A24EE786900661F6B /* SPEventStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; EDD8540D24EE786900661F6B /* SPEventStore.h in Headers */ = {isa = PBXBuildFile; fileRef = EDD8540A24EE786900661F6B /* SPEventStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -776,7 +794,7 @@ 75CAC3B921F28BD600271FB3 /* SnowplowIgluClient.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SnowplowIgluClient.framework; path = Carthage/Build/iOS/SnowplowIgluClient.framework; sourceTree = ""; }; 75CAC3C021F2930100271FB3 /* VVJSONSchemaValidation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VVJSONSchemaValidation.framework; path = Carthage/Build/iOS/VVJSONSchemaValidation.framework; sourceTree = ""; }; 75CAC3F121F2955000271FB3 /* TestSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestSession.m; sourceTree = ""; }; - 75CAC3F221F2955000271FB3 /* TestEventStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestEventStore.m; sourceTree = ""; }; + 75CAC3F221F2955000271FB3 /* TestSQLiteEventStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestSQLiteEventStore.m; sourceTree = ""; }; 75CAC3F321F2955000271FB3 /* TestPayload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestPayload.m; sourceTree = ""; }; 75CAC3F421F2955000271FB3 /* LegacyTestSubject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LegacyTestSubject.m; sourceTree = ""; }; 75CAC3F521F2955000271FB3 /* TestSelfDescribingJson.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestSelfDescribingJson.m; sourceTree = ""; }; @@ -920,6 +938,12 @@ ED98972526287F7900145157 /* SPConfigurationCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPConfigurationCache.m; sourceTree = ""; }; EDA06FB42664CD2F007FA773 /* SPMockEventStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPMockEventStore.h; sourceTree = ""; }; EDA06FB52664CD2F007FA773 /* SPMockEventStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPMockEventStore.m; sourceTree = ""; }; + EDB2FD1726C130B80031B872 /* SPDataPersistence.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPDataPersistence.h; sourceTree = ""; }; + EDB2FD1826C130B80031B872 /* SPDataPersistence.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPDataPersistence.m; sourceTree = ""; }; + EDB2FD2126C57F6C0031B872 /* TestDataPersistence.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestDataPersistence.m; sourceTree = ""; }; + EDB693F826B7F61D00B76A79 /* SPMemoryEventStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPMemoryEventStore.h; sourceTree = ""; }; + EDB693F926B7F61D00B76A79 /* SPMemoryEventStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPMemoryEventStore.m; sourceTree = ""; }; + EDB6940226B83BF300B76A79 /* TestMemoryEventStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestMemoryEventStore.m; sourceTree = ""; }; EDCBD0D724F5084800D39DD2 /* TestNetworkConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestNetworkConnection.m; sourceTree = ""; }; EDD8540A24EE786900661F6B /* SPEventStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPEventStore.h; sourceTree = ""; }; EDD8541424EEC25000661F6B /* SPEmitterEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPEmitterEvent.h; sourceTree = ""; }; @@ -1032,7 +1056,8 @@ 75CAC40321F2955100271FB3 /* SnowplowTests-Info.plist */, EDCBD0D724F5084800D39DD2 /* TestNetworkConnection.m */, ED88B6B2258253F20048FAD1 /* TestEvents.m */, - 75CAC3F221F2955000271FB3 /* TestEventStore.m */, + EDB6940226B83BF300B76A79 /* TestMemoryEventStore.m */, + 75CAC3F221F2955000271FB3 /* TestSQLiteEventStore.m */, 75CAC3F621F2955000271FB3 /* TestGeneratedJsons.m */, 75CAC3F321F2955000271FB3 /* TestPayload.m */, 75CAC40121F2955100271FB3 /* TestRequest.m */, @@ -1042,6 +1067,7 @@ 75CAC40221F2955100271FB3 /* TestUtils.m */, 7534D20722569F3400904EE5 /* TestScreenState.m */, EDEE836224C0C317000B8530 /* TestLogger.m */, + EDB2FD2126C57F6C0031B872 /* TestDataPersistence.m */, ); path = "Snowplow iOSTests"; sourceTree = ""; @@ -1369,6 +1395,8 @@ isa = PBXGroup; children = ( EDD8540A24EE786900661F6B /* SPEventStore.h */, + EDB693F826B7F61D00B76A79 /* SPMemoryEventStore.h */, + EDB693F926B7F61D00B76A79 /* SPMemoryEventStore.m */, ABB767AE194974D3006275D1 /* SPSQLiteEventStore.h */, ABB767AF194974D3006275D1 /* SPSQLiteEventStore.m */, ); @@ -1388,6 +1416,8 @@ ED34672926415C1D0018BA61 /* SPJSONSerialization.m */, ED9897142627006F00145157 /* NSDictionary+SP_TypeMethods.h */, ED9897152627006F00145157 /* NSDictionary+SP_TypeMethods.m */, + EDB2FD1726C130B80031B872 /* SPDataPersistence.h */, + EDB2FD1826C130B80031B872 /* SPDataPersistence.m */, ); path = Utils; sourceTree = ""; @@ -1476,6 +1506,7 @@ ED88B7342587777B0048FAD1 /* SPEmitterEventProcessing.h in Headers */, EDF2A1B426402D53009032AB /* SPSubjectController.h in Headers */, ED88B64A257A57F80048FAD1 /* SPGlobalContextsController.h in Headers */, + EDB693FA26B7F61D00B76A79 /* SPMemoryEventStore.h in Headers */, CE4F9D02244B066500968CFC /* SPEcommerceItem.h in Headers */, EDD8541624EEC25100661F6B /* SPEmitterEvent.h in Headers */, 752DAC3221CC43C60065F874 /* SPTrackerConstants.h in Headers */, @@ -1538,6 +1569,7 @@ CE4F9C9A244B066500968CFC /* SPEvent.h in Headers */, ED88B76D2587B4EF0048FAD1 /* SPNetworkController.h in Headers */, ED7F0840261924BF005D377E /* SPConfigurationFetcher.h in Headers */, + EDB2FD1926C130B80031B872 /* SPDataPersistence.h in Headers */, ED88B5DF257950210048FAD1 /* SPGDPRConfiguration.h in Headers */, ED88B58F257922490048FAD1 /* SPEmitterController.h in Headers */, 752DAC3621CC43C70065F874 /* SPSession.h in Headers */, @@ -1566,6 +1598,7 @@ CE4F9CA3244B066500968CFC /* SPPageView.h in Headers */, CE4F9D03244B066500968CFC /* SPEcommerceItem.h in Headers */, EDEE834C24BDB326000B8530 /* SPTrackerError.h in Headers */, + EDB2FD1A26C130B80031B872 /* SPDataPersistence.h in Headers */, 75CAC45821F2A21B00271FB3 /* Snowplow-umbrella-header.h in Headers */, 75CAC45921F2A21B00271FB3 /* SPTrackerConstants.h in Headers */, ED8BF8B425700B40001DFDD9 /* SPTrackerConfiguration.h in Headers */, @@ -1641,6 +1674,7 @@ ED87A3DA25765DAE000C54EB /* SPSessionController.h in Headers */, ED88672C2573C1F100DB53BB /* SPSessionConfiguration.h in Headers */, EDF2A1B526402D53009032AB /* SPSubjectController.h in Headers */, + EDB693FB26B7F61D00B76A79 /* SPMemoryEventStore.h in Headers */, ED277BE32625F5C5002C7B6D /* SPFetchedConfigurationBundle.h in Headers */, 75CAC46221F2A21B00271FB3 /* SPRequestResult.h in Headers */, EDDD6FFE264E873B00259404 /* SPController.h in Headers */, @@ -1712,6 +1746,7 @@ ED87A41E2577AC5B000C54EB /* SPTrackerController.h in Headers */, ED7F081726190E00005D377E /* SPConfigurationProvider.h in Headers */, CE4F9D08244B066500968CFC /* SPSchemaRuleset.h in Headers */, + EDB2FD1B26C130B80031B872 /* SPDataPersistence.h in Headers */, 75CAC43221F2A0CC00271FB3 /* SPSQLiteEventStore.h in Headers */, EDEE835C24BE0944000B8530 /* SPLogger.h in Headers */, ED88B7362587777B0048FAD1 /* SPEmitterEventProcessing.h in Headers */, @@ -1725,6 +1760,7 @@ EDD8541824EEC25100661F6B /* SPEmitterEvent.h in Headers */, CE4F9CA0244B066500968CFC /* SPSchemaRule.h in Headers */, ED87A4302577ADFF000C54EB /* SPTrackerControllerImpl.h in Headers */, + EDB693FC26B7F61D00B76A79 /* SPMemoryEventStore.h in Headers */, CE4F9CC0244B066500968CFC /* SPForeground.h in Headers */, EDDD7045264F2A8800259404 /* SPEmitterConfigurationUpdate.h in Headers */, ED88B56B2578F8820048FAD1 /* SPEmitterConfiguration.h in Headers */, @@ -1758,6 +1794,7 @@ ED88B7372587777B0048FAD1 /* SPEmitterEventProcessing.h in Headers */, EDF2A1B726402D53009032AB /* SPSubjectController.h in Headers */, ED88B64D257A57F80048FAD1 /* SPGlobalContextsController.h in Headers */, + EDB693FD26B7F61D00B76A79 /* SPMemoryEventStore.h in Headers */, CE4F9D05244B066500968CFC /* SPEcommerceItem.h in Headers */, EDD8541924EEC25100661F6B /* SPEmitterEvent.h in Headers */, 75F9C5E521FA35BC00A5B8FC /* Snowplow-umbrella-header.h in Headers */, @@ -1820,6 +1857,7 @@ CE4F9C9D244B066500968CFC /* SPEvent.h in Headers */, ED88B7702587B4F00048FAD1 /* SPNetworkController.h in Headers */, ED7F0843261924BF005D377E /* SPConfigurationFetcher.h in Headers */, + EDB2FD1C26C130B80031B872 /* SPDataPersistence.h in Headers */, ED88B5E2257950210048FAD1 /* SPGDPRConfiguration.h in Headers */, ED88B592257922490048FAD1 /* SPEmitterController.h in Headers */, 75F9C5ED21FA35BC00A5B8FC /* SPSQLiteEventStore.h in Headers */, @@ -2160,6 +2198,7 @@ EDF2A1BE264032F9009032AB /* SPSubjectControllerImpl.m in Sources */, ED88670225715DD600DB53BB /* SPConfiguration.m in Sources */, ED88B6DF2583DFC90048FAD1 /* SPServiceProvider.m in Sources */, + EDB2FD1D26C130B80031B872 /* SPDataPersistence.m in Sources */, ED7F081826190E00005D377E /* SPConfigurationProvider.m in Sources */, CE4F9C96244B066500968CFC /* SPEcommerceItem.m in Sources */, ED88B611257956490048FAD1 /* SPGDPRControllerImpl.m in Sources */, @@ -2189,6 +2228,7 @@ 752DAC1B21CC42BC0065F874 /* SPEmitter.m in Sources */, 75264A32224E5DD2000E0E9B /* SPInstallTracker.m in Sources */, EDDD7001264E873B00259404 /* SPController.m in Sources */, + EDB693FE26B7F61D00B76A79 /* SPMemoryEventStore.m in Sources */, ED88B66C257A5A520048FAD1 /* SPGlobalContextsControllerImpl.m in Sources */, ED8122AE25E9578600AE7FE8 /* SPSnowplow.m in Sources */, ED8866E62571445300DB53BB /* SPSubjectConfiguration.m in Sources */, @@ -2232,7 +2272,7 @@ 75CAC40821F2955100271FB3 /* LegacyTestSubject.m in Sources */, ED8BF8ED2570FB2F001DFDD9 /* TestTrackerConfiguration.m in Sources */, 75CAC41221F2955100271FB3 /* TestRequest.m in Sources */, - 75CAC40621F2955100271FB3 /* TestEventStore.m in Sources */, + 75CAC40621F2955100271FB3 /* TestSQLiteEventStore.m in Sources */, ED914EB72432081F0068DA0A /* TestSchemaRuleset.m in Sources */, 75CAC41121F2955100271FB3 /* LegacyTestTracker.m in Sources */, 75CAC40D21F2955100271FB3 /* LegacyTestEmitter.m in Sources */, @@ -2246,8 +2286,10 @@ 75CAC40921F2955100271FB3 /* TestSelfDescribingJson.m in Sources */, ED87A43D2577E441000C54EB /* TestTrackerController.m in Sources */, EDEE836324C0C318000B8530 /* TestLogger.m in Sources */, + EDB6940326B83BF300B76A79 /* TestMemoryEventStore.m in Sources */, 75CAC41321F2955100271FB3 /* TestUtils.m in Sources */, 75CAC40521F2955100271FB3 /* TestSession.m in Sources */, + EDB2FD2226C57F6D0031B872 /* TestDataPersistence.m in Sources */, ED7F080626190B5F005D377E /* TestRemoteConfiguration.m in Sources */, 75CAC40E21F2955100271FB3 /* TestRequestResult.m in Sources */, 75CAC40C21F2955100271FB3 /* LegacyTestEvent.m in Sources */, @@ -2303,6 +2345,7 @@ EDDD7048264F2A8800259404 /* SPEmitterConfigurationUpdate.m in Sources */, CE4F9CC7244B066500968CFC /* SPSchemaRuleset.m in Sources */, CE4F9C8B244B066500968CFC /* SPConsentDocument.m in Sources */, + EDB2FD1E26C130B80031B872 /* SPDataPersistence.m in Sources */, EDD8541B24EEC25100661F6B /* SPEmitterEvent.m in Sources */, EDDD7016264F1D2100259404 /* SPTrackerConfigurationUpdate.m in Sources */, ED34672F26415C1D0018BA61 /* SPJSONSerialization.m in Sources */, @@ -2325,6 +2368,7 @@ EDFEEAC923A7CB3C001E6D03 /* SPInstallTracker.m in Sources */, 75CAC44421F2A17500271FB3 /* SPWeakTimerTarget.m in Sources */, ED98971B2627006F00145157 /* NSDictionary+SP_TypeMethods.m in Sources */, + EDB693FF26B7F61D00B76A79 /* SPMemoryEventStore.m in Sources */, CE4F9CD3244B066500968CFC /* SPTrackerEvent.m in Sources */, EDF2A1BF264032F9009032AB /* SPSubjectControllerImpl.m in Sources */, ED91CB7223AA8AD50078E75F /* SPDevicePlatform.m in Sources */, @@ -2382,6 +2426,7 @@ EDDD7049264F2A8800259404 /* SPEmitterConfigurationUpdate.m in Sources */, CE4F9CC8244B066500968CFC /* SPSchemaRuleset.m in Sources */, CE4F9C8C244B066500968CFC /* SPConsentDocument.m in Sources */, + EDB2FD1F26C130B80031B872 /* SPDataPersistence.m in Sources */, EDD8541C24EEC25100661F6B /* SPEmitterEvent.m in Sources */, EDDD7017264F1D2100259404 /* SPTrackerConfigurationUpdate.m in Sources */, ED34673026415C1D0018BA61 /* SPJSONSerialization.m in Sources */, @@ -2404,6 +2449,7 @@ 75CAC45021F2A19500271FB3 /* SPUtilities.m in Sources */, 75CAC45121F2A19500271FB3 /* SPRequestResult.m in Sources */, ED98971C2627006F00145157 /* NSDictionary+SP_TypeMethods.m in Sources */, + EDB6940026B7F61D00B76A79 /* SPMemoryEventStore.m in Sources */, CE4F9CD4244B066500968CFC /* SPTrackerEvent.m in Sources */, EDF2A1C0264032F9009032AB /* SPSubjectControllerImpl.m in Sources */, ED852B3023A0E90E00F2DF6B /* SNOWReachability.m in Sources */, @@ -2469,6 +2515,7 @@ EDDD704A264F2A8800259404 /* SPEmitterConfigurationUpdate.m in Sources */, CE4F9D0D244B066500968CFC /* SPScreenView.m in Sources */, 75F9C5D821FA357100A5B8FC /* SPEmitter.m in Sources */, + EDB2FD2026C130B80031B872 /* SPDataPersistence.m in Sources */, 75F9C5D921FA357100A5B8FC /* SPSubject.m in Sources */, EDDD7018264F1D2100259404 /* SPTrackerConfigurationUpdate.m in Sources */, CE4F9D21244B066500968CFC /* SPEcommerce.m in Sources */, @@ -2491,6 +2538,7 @@ 75F9C5DF21FA357100A5B8FC /* SPRequestResult.m in Sources */, ED87A4352577ADFF000C54EB /* SPTrackerControllerImpl.m in Sources */, CE4F9C8D244B066500968CFC /* SPConsentDocument.m in Sources */, + EDB6940126B7F61D00B76A79 /* SPMemoryEventStore.m in Sources */, ED98971D2627006F00145157 /* NSDictionary+SP_TypeMethods.m in Sources */, CE4F9D01244B066500968CFC /* SPBackground.m in Sources */, ED8867322573C1F200DB53BB /* SPSessionConfiguration.m in Sources */, diff --git a/Snowplow/Internal/Emitter/SPEmitter.m b/Snowplow/Internal/Emitter/SPEmitter.m index e2d069f70..901eb9761 100644 --- a/Snowplow/Internal/Emitter/SPEmitter.m +++ b/Snowplow/Internal/Emitter/SPEmitter.m @@ -23,6 +23,7 @@ #import "SPTrackerConstants.h" #import "SPEmitter.h" #import "SPSQLiteEventStore.h" +#import "SPMemoryEventStore.h" #import "SPDefaultNetworkConnection.h" #import "SPEventStore.h" #import "SPUtilities.h" @@ -117,8 +118,12 @@ - (void)setupNetworkConnection { - (void)setNamespace:(NSString *)namespace { _namespace = namespace; - if (_builderFinished) { - _eventStore = _eventStore ?: [[SPSQLiteEventStore alloc] initWithNamespace:_namespace]; + if (_builderFinished && !_eventStore) { +#if TARGET_OS_TV || TARGET_OS_WATCH + _eventStore = [[SPMemoryEventStore alloc] init]; +#else + _eventStore = [[SPSQLiteEventStore alloc] initWithNamespace:_namespace]; +#endif } } diff --git a/Snowplow/Internal/RemoteConfiguration/SPConfigurationCache.m b/Snowplow/Internal/RemoteConfiguration/SPConfigurationCache.m index 20577a9ac..e6d1e1cfe 100644 --- a/Snowplow/Internal/RemoteConfiguration/SPConfigurationCache.m +++ b/Snowplow/Internal/RemoteConfiguration/SPConfigurationCache.m @@ -34,17 +34,21 @@ @implementation SPConfigurationCache - (instancetype)init { if (self = [super init]) { +#if !(TARGET_OS_TV) && !(TARGET_OS_WATCH) [self createCachePath]; +#endif } return self; } - (nullable SPFetchedConfigurationBundle *)readCache { @synchronized (self) { +#if !(TARGET_OS_TV) && !(TARGET_OS_WATCH) if (self.configuration) { return self.configuration; } [self loadCache]; +#endif return self.configuration; } } @@ -52,15 +56,23 @@ - (nullable SPFetchedConfigurationBundle *)readCache { - (void)writeCache:(SPFetchedConfigurationBundle *)configuration { @synchronized (self) { self.configuration = configuration; +#if !(TARGET_OS_TV) && !(TARGET_OS_WATCH) [self storeCache]; +#endif } } - (void)clearCache { + NSError *error = nil; @synchronized (self) { + self.configuration = nil; +#if !(TARGET_OS_TV) && !(TARGET_OS_WATCH) if (!self.cacheFileUrl) return; - NSError *error = nil; [[NSFileManager defaultManager] removeItemAtURL:self.cacheFileUrl error:&error]; +#endif + } + if (error) { + SPLogError(@"Error on clearing configuration from cache: %@", error.localizedDescription); } } diff --git a/Snowplow/Internal/SPTrackerConstants.h b/Snowplow/Internal/SPTrackerConstants.h index 1fc88d345..d5e46022a 100644 --- a/Snowplow/Internal/SPTrackerConstants.h +++ b/Snowplow/Internal/SPTrackerConstants.h @@ -53,6 +53,10 @@ extern NSString * const kSPVersion; +// --- Dictionary keys + +extern NSString * const kSPInstallationUserId; + // --- Emitter extern NSString * const kSPContentTypeHeader; @@ -148,8 +152,6 @@ extern NSString * const kSPApplicationBuild; // --- Session Context -extern NSString * const kSPInstallationUserId; - extern NSString * const kSPSessionUserId; extern NSString * const kSPSessionId; extern NSString * const kSPSessionPreviousId; diff --git a/Snowplow/Internal/SPTrackerConstants.m b/Snowplow/Internal/SPTrackerConstants.m index 436283667..1ed8604a6 100644 --- a/Snowplow/Internal/SPTrackerConstants.m +++ b/Snowplow/Internal/SPTrackerConstants.m @@ -36,6 +36,10 @@ @implementation SPTrackerConstants NSString * const kSPVersion = @"osx-2.2.1"; #endif +// --- Session Dictionary keys + +NSString * const kSPInstallationUserId = @"SPInstallationUserId"; + // --- Emitter NSString * const kSPContentTypeHeader = @"application/json; charset=utf-8"; @@ -132,8 +136,6 @@ @implementation SPTrackerConstants // --- Session Context -NSString * const kSPInstallationUserId = @"SPInstallationUserId"; - NSString * const kSPSessionUserId = @"userId"; NSString * const kSPSessionId = @"sessionId"; NSString * const kSPSessionPreviousId = @"previousSessionId"; diff --git a/Snowplow/Internal/Session/SPSession.h b/Snowplow/Internal/Session/SPSession.h index 5af0a4a34..a7cfee874 100644 --- a/Snowplow/Internal/Session/SPSession.h +++ b/Snowplow/Internal/Session/SPSession.h @@ -64,12 +64,6 @@ NS_SWIFT_NAME(Session) andBackgroundTimeout:(NSInteger)backgroundTimeout andTracker:(SPTracker *)tracker; -/// Only internal use. -+ (NSString *)createSessionFilenameWithNamespace:(NSString *)namespace; - -/// Only internal use. -+ (NSURL *)createSessionFileUrlWithFilename:(NSString *)filename; - /** * Starts the recurring timer check for sessions */ diff --git a/Snowplow/Internal/Session/SPSession.m b/Snowplow/Internal/Session/SPSession.m index c9f8c19ad..7c79ed819 100644 --- a/Snowplow/Internal/Session/SPSession.m +++ b/Snowplow/Internal/Session/SPSession.m @@ -20,6 +20,7 @@ // License: Apache License Version 2.0 // +#import "SPDataPersistence.h" #import "SPTrackerConstants.h" #import "SPSession.h" #import "SPUtilities.h" @@ -38,8 +39,7 @@ @interface SPSession () @property (atomic) NSNumber *lastSessionCheck; @property (weak) SPTracker *tracker; -@property (nonatomic) NSString *sessionFilename; -@property (nonatomic) NSURL *sessionFileUrl; +@property (nonatomic) SPDataPersistence *dataPersistence; @end @@ -60,10 +60,6 @@ @implementation SPSession { NSInteger _backgroundIndex; } -NSString * const kLegacyFilename = @"session.dict"; -NSString * const kFilenamePrefix = @"session"; -NSString * const kFilenameExt = @"dict"; - - (id) init { return [self initWithForegroundTimeout:600 andBackgroundTimeout:300 andTracker:nil]; } @@ -83,14 +79,10 @@ - (instancetype)initWithForegroundTimeout:(NSInteger)foregroundTimeout andBackgr _inBackground = NO; _isNewSession = YES; _sessionStorage = @"LOCAL_STORAGE"; - self.sessionFilename = kLegacyFilename; self.tracker = tracker; - if (tracker.trackerNamespace) { - self.sessionFilename = [SPSession createSessionFilenameWithNamespace:tracker.trackerNamespace]; - } - self.sessionFileUrl = [SPSession createSessionFileUrlWithFilename:self.sessionFilename]; + self.dataPersistence = [SPDataPersistence dataPersistenceForNamespace:tracker.trackerNamespace]; + NSDictionary *storedSessionDict = self.dataPersistence.session; - NSDictionary * storedSessionDict = [self getSessionFromFile]; if (storedSessionDict) { _userId = [storedSessionDict valueForKey:kSPSessionUserId] ?: [SPUtilities getUUIDString]; _currentSessionId = [storedSessionDict valueForKey:kSPSessionId]; @@ -129,25 +121,6 @@ - (instancetype)initWithForegroundTimeout:(NSInteger)foregroundTimeout andBackgr return self; } -+ (NSString *)createSessionFilenameWithNamespace:(NSString *)namespace { - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^a-zA-Z0-9_]+" options:0 error:nil]; - NSString *suffix = [regex stringByReplacingMatchesInString:namespace options:0 range:NSMakeRange(0, namespace.length) withTemplate:@"-"]; - return [NSString stringWithFormat:@"%@_%@.%@", kFilenamePrefix, suffix, kFilenameExt]; -} - -+ (NSURL *)createSessionFileUrlWithFilename:(NSString *)filename { - NSFileManager *fm = [NSFileManager defaultManager]; - NSURL *url = [fm URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask].lastObject; - url = [url URLByAppendingPathComponent:@"snowplow"]; - NSError *error = nil; - BOOL result = [fm createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:&error]; - if (!result) { - SPLogError(@"Unable to create file for sessions: %@", error.localizedDescription); - return nil; - } - return [url URLByAppendingPathComponent:filename]; -} - // MARK: - Public - (void) startChecker { @@ -231,42 +204,13 @@ - (SPTracker *) getTracker { // MARK: - Private - (BOOL) writeSessionToFile { - NSError *error = nil; NSMutableDictionary *sessionDict = [_sessionDict mutableCopy]; [sessionDict removeObjectForKey:kSPSessionPreviousId]; [sessionDict removeObjectForKey:kSPSessionStorage]; - - BOOL result = NO; - if (@available(iOS 11.0, macOS 10.13, watchOS 4.0, *)) { - result = [sessionDict writeToURL:self.sessionFileUrl error:&error]; - } else { - result = [sessionDict writeToURL:self.sessionFileUrl atomically:YES]; - } - if (!result) { - SPLogError(@"Unable to write file for sessions: %@", error.localizedDescription ?: @"-"); - return NO; - } + self.dataPersistence.session = sessionDict; return YES; } -- (NSDictionary *) getSessionFromFile { - NSDictionary *sessionDict = nil; - sessionDict = [NSDictionary dictionaryWithContentsOfURL:self.sessionFileUrl]; - if (sessionDict) { - return sessionDict; - } - // Load legacy stored session (tracker v.1.x) - @synchronized (SPSession.class) { - sessionDict = [NSDictionary dictionaryWithContentsOfURL:self.sessionFileUrl]; - if (!sessionDict) { - NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject; - path = [path stringByAppendingPathComponent:kLegacyFilename]; - sessionDict = [NSDictionary dictionaryWithContentsOfFile:path]; - } - } - return sessionDict; -} - - (BOOL)shouldUpdateSession { if (_isNewSession) { return YES; diff --git a/Snowplow/Internal/Storage/SPMemoryEventStore.h b/Snowplow/Internal/Storage/SPMemoryEventStore.h new file mode 100644 index 000000000..0ab0377f2 --- /dev/null +++ b/Snowplow/Internal/Storage/SPMemoryEventStore.h @@ -0,0 +1,19 @@ +// +// SPMemoryEventStore.h +// Snowplow +// +// Created by Alex Benini on 02/08/21. +// Copyright © 2021 Snowplow Analytics. All rights reserved. +// + +#import +#import "SPEventStore.h" + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(MemoryEventStore) +@interface SPMemoryEventStore : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Snowplow/Internal/Storage/SPMemoryEventStore.m b/Snowplow/Internal/Storage/SPMemoryEventStore.m new file mode 100644 index 000000000..a671c9e7c --- /dev/null +++ b/Snowplow/Internal/Storage/SPMemoryEventStore.m @@ -0,0 +1,91 @@ +// +// SPMemoryEventStore.m +// Snowplow +// +// Created by Alex Benini on 02/08/21. +// Copyright © 2021 Snowplow Analytics. All rights reserved. +// + +#import "SPMemoryEventStore.h" + +@interface SPMemoryEventStore () + +@property (nonatomic) NSUInteger sendLimit; +@property (nonatomic) NSUInteger index; +@property (nonatomic) NSMutableOrderedSet *orderedSet; + +@end + +@implementation SPMemoryEventStore + +- (instancetype)init { + return [self initWithLimit:250]; +} + +- (instancetype)initWithLimit:(NSUInteger)limit { + if (self = [super init]) { + self.orderedSet = [[NSMutableOrderedSet alloc] init]; + self.sendLimit = limit; + self.index = 0; + } + return self; +} + +// Interface methods + +- (void)addEvent:(nonnull SPPayload *)payload { + @synchronized (self) { + SPEmitterEvent *item = [[SPEmitterEvent alloc] initWithPayload:payload storeId:self.index++]; + [self.orderedSet addObject:item]; + } +} + +- (NSUInteger)count { + @synchronized (self) { + return [self.orderedSet count]; + } +} + +- (nonnull NSArray *)emittableEventsWithQueryLimit:(NSUInteger)queryLimit { + @synchronized (self) { + NSUInteger setCount = [self.orderedSet count]; + if (setCount <= 0) { + return @[]; + } + NSUInteger len = MIN(queryLimit, setCount); + NSRange range = NSMakeRange(0, len); + SPEmitterEvent * __unsafe_unretained array[len]; + [self.orderedSet getObjects:array range:range]; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:len]; + for (int i = 0; i < len; i++) { + [result addObject:array[i]]; + } + return result; + } +} + +- (BOOL)removeAllEvents { + @synchronized (self) { + [self.orderedSet removeAllObjects]; + return YES; + } +} + +- (BOOL)removeEventWithId:(long long)storeId { + return [self removeEventsWithIds:@[[NSNumber numberWithLongLong:storeId]]]; +} + +- (BOOL)removeEventsWithIds:(nonnull NSArray *)storeIds { + @synchronized (self) { + NSMutableArray *itemsToRemove = [NSMutableArray new]; + for (SPEmitterEvent *item in self.orderedSet) { + if ([storeIds containsObject:[NSNumber numberWithLongLong:item.storeId]]) { + [itemsToRemove addObject:item]; + } + } + [self.orderedSet removeObjectsInArray:itemsToRemove]; + return YES; + } +} + +@end diff --git a/Snowplow/Internal/Utils/SPDataPersistence.h b/Snowplow/Internal/Utils/SPDataPersistence.h new file mode 100644 index 000000000..696dd9d51 --- /dev/null +++ b/Snowplow/Internal/Utils/SPDataPersistence.h @@ -0,0 +1,39 @@ +// +// SPDataPersistence.h +// Snowplow +// +// Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. +// +// Authors: Alex Benini +// Copyright: Copyright (c) 2013-2021 Snowplow Analytics Ltd +// License: Apache License Version 2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SPDataPersistence : NSObject + +@property (nonatomic) NSDictionary *> *data; +@property (nonatomic) NSDictionary *session; + ++ (SPDataPersistence *)dataPersistenceForNamespace:(NSString *)namespace; ++ (BOOL)removeDataPersistenceWithNamespace:(NSString *)namespace; + ++ (NSString *)stringFromNamespace:(NSString *)namespace; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Snowplow/Internal/Utils/SPDataPersistence.m b/Snowplow/Internal/Utils/SPDataPersistence.m new file mode 100644 index 000000000..c5c623694 --- /dev/null +++ b/Snowplow/Internal/Utils/SPDataPersistence.m @@ -0,0 +1,236 @@ +// +// SPDataPersistence.m +// Snowplow +// +// Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. +// +// Authors: Alex Benini +// Copyright: Copyright (c) 2013-2021 Snowplow Analytics Ltd +// License: Apache License Version 2.0 +// + +#import "SPDataPersistence.h" +#import "SPTrackerConstants.h" +#import "SPLogger.h" + +@interface SPDataPersistence () + +@property (nonatomic) NSString *escapedNamespace; + +#if TARGET_OS_TV || TARGET_OS_WATCH +@property (nonatomic) NSString *userDefaultsKey; +#endif + +@property (nonatomic) NSURL *directoryUrl; +@property (nonatomic) NSURL *fileUrl; + +@end + +@implementation SPDataPersistence + +static NSMutableDictionary *instances = nil; + +#if TARGET_OS_TV || TARGET_OS_WATCH +NSString *const kSPSessionDictionaryPrefix = @"SPSessionDictionary"; +#endif + +NSString *const kFilename = @"namespace"; +NSString *const kFilenameExt = @"dict"; +NSString *const kSessionFilenameV1 = @"session.dict"; +NSString *const kSessionFilenamePrefixV2_2 = @"session"; + +NSString *sessionKey = @"session"; + ++ (SPDataPersistence *)dataPersistenceForNamespace:(NSString *)namespace { + NSString *escapedNamespace = [SPDataPersistence stringFromNamespace:namespace]; + if ([escapedNamespace length] <= 0) return nil; + @synchronized (SPDataPersistence.class) { + SPDataPersistence *instance = nil; + if (instances) { + instance = [instances objectForKey:escapedNamespace]; + if (instance) { + return instance; + } + } else { + instances = [NSMutableDictionary new]; + } + instance = [[SPDataPersistence alloc] initWithNamespace:escapedNamespace]; + [instances setValue:instance forKey:escapedNamespace]; + return instance; + } +} + ++ (BOOL)removeDataPersistenceWithNamespace:(NSString *)namespace { + SPDataPersistence *instance = [SPDataPersistence dataPersistenceForNamespace:namespace]; + if (!instance) return NO; + @synchronized (SPDataPersistence.class) { + [instances removeObjectForKey:instance.escapedNamespace]; + } + [instance removeAll]; + return YES; +} + ++ (NSString *)stringFromNamespace:(NSString *)namespace { + if (!namespace) return nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^a-zA-Z0-9_]+" options:0 error:nil]; + return [regex stringByReplacingMatchesInString:namespace options:0 range:NSMakeRange(0, namespace.length) withTemplate:@"-"]; +} + +// MARK: - Property accessor methods + +- (NSDictionary *> *)data { + @synchronized (self) { +#if TARGET_OS_TV || TARGET_OS_WATCH + return [[NSUserDefaults standardUserDefaults] dictionaryForKey:self.userDefaultsKey]; +#else + NSMutableDictionary *> *result = + [NSMutableDictionary dictionaryWithContentsOfURL:self.fileUrl]; + + if (!result) { + // Initialise + result = [NSMutableDictionary new]; + // Migrate legacy session data + NSDictionary *sessionDict = [self sessionDictionaryFromLegacyTrackerV2_2] + ?: [self sessionDictionaryFromLegacyTrackerV1] + ?: [NSDictionary new]; + [result setObject:sessionDict forKey:sessionKey]; + [self storeDictionary:result]; + } + + return result; +#endif + } +} + +- (void)setData:(NSDictionary *> *)data { + @synchronized (self) { +#if TARGET_OS_TV || TARGET_OS_WATCH + [[NSUserDefaults standardUserDefaults] setObject:data forKey:self.userDefaultsKey]; +#else + [self storeDictionary:data]; +#endif + } +} + +- (NSDictionary *)session { + return [self.data objectForKey:sessionKey]; +} + +- (void)setSession:(NSDictionary *)session { + @synchronized (self) { + NSMutableDictionary *data = [self.data mutableCopy]; + [data setValue:session forKey:sessionKey]; + self.data = data; + } +} + +// MARK: - Private instance methods + +- (instancetype)initWithNamespace:(NSString *)escapedNamespace { + if (self = [super init]) { + self.escapedNamespace = escapedNamespace; +#if TARGET_OS_TV || TARGET_OS_WATCH + self.userDefaultsKey = [NSString stringWithFormat:@"%@_%@", kSPSessionDictionaryPrefix, escapedNamespace]; +#else + self.directoryUrl = [self createDirectoryUrl]; + NSString *filename = [NSString stringWithFormat:@"%@_%@.%@", kFilename, escapedNamespace, kFilenameExt]; + self.fileUrl = [self.directoryUrl URLByAppendingPathComponent:filename]; +#endif + } + return self; +} + +- (BOOL)removeAll { + @synchronized (self) { +#if TARGET_OS_TV || TARGET_OS_WATCH + [[NSUserDefaults standardUserDefaults] removeObjectForKey:self.userDefaultsKey]; + return YES; +#else + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtURL:self.fileUrl error:&error]; + if (error) { + SPLogError(@"%@", error.localizedDescription); + return NO; + } + return YES; +#endif + } +} + +- (NSURL *)createDirectoryUrl { + NSFileManager *fm = [NSFileManager defaultManager]; + NSURL *url = [fm URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask].lastObject; + url = [url URLByAppendingPathComponent:@"snowplow"]; + NSError *error = nil; + BOOL result = [fm createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:&error]; + if (!result) { + SPLogError(@"Unable to create directory for tracker data persistence: %@", error.localizedDescription); + return nil; + } + return url; +} + +- (BOOL)storeDictionary:(NSDictionary *)dictionary { + BOOL result = NO; + NSError *error = nil; + if (@available(iOS 11.0, macOS 10.13, watchOS 4.0, *)) { + result = [dictionary writeToURL:self.fileUrl error:&error]; + } else { + result = [dictionary writeToURL:self.fileUrl atomically:YES]; + } + if (!result) { + SPLogError(@"Unable to write file for sessions: %@", error.localizedDescription ?: @"-"); + return NO; + } + return YES; +} + +// Migration methods + +- (NSDictionary *)sessionDictionaryFromLegacyTrackerV2_2 { + @synchronized (self) { + NSString *filename = [NSString stringWithFormat:@"%@_%@.%@", kSessionFilenamePrefixV2_2, self.escapedNamespace, kFilenameExt]; + NSURL *fileUrl = [self.directoryUrl URLByAppendingPathComponent:filename]; + NSDictionary *sessionDict = nil; + sessionDict = [NSDictionary dictionaryWithContentsOfURL:fileUrl]; + if (!sessionDict) { + return nil; + } + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtURL:fileUrl error:&error]; + if (error) { + SPLogError(@"%@", error.localizedDescription); + } + return sessionDict; + } +} + +- (NSDictionary *)sessionDictionaryFromLegacyTrackerV1 { + @synchronized (SPDataPersistence.class) { + NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject; + path = [path stringByAppendingPathComponent:kSessionFilenameV1]; + NSDictionary *sessionDict = [NSDictionary dictionaryWithContentsOfFile:path]; + if (!sessionDict) { + return nil; + } + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; + if (error) { + SPLogError(@"%@", error.localizedDescription); + } + return sessionDict; + } +} + +@end diff --git a/Snowplow/Snowplow-umbrella-header.h b/Snowplow/Snowplow-umbrella-header.h index 734aca5b9..094b01df0 100644 --- a/Snowplow/Snowplow-umbrella-header.h +++ b/Snowplow/Snowplow-umbrella-header.h @@ -58,5 +58,6 @@ #import "SPSelfDescribingJson.h" #import "SPUtilities.h" #import "SPSQLiteEventStore.h" +#import "SPMemoryEventStore.h" #import "SPDefaultNetworkConnection.h" #import "SPGdprContext.h" diff --git a/Snowplow/include/SPMemoryEventStore.h b/Snowplow/include/SPMemoryEventStore.h new file mode 120000 index 000000000..cbb8c0d7b --- /dev/null +++ b/Snowplow/include/SPMemoryEventStore.h @@ -0,0 +1 @@ +../Internal/Storage/SPMemoryEventStore.h \ No newline at end of file