diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index 17ddcc8b89..403aab5a7e 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -90,6 +90,9 @@ EADEB8F31DECD335005322DA /* PostHogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EADEB8E71DECD335005322DA /* PostHogTests.swift */; }; EADEB8F41DECD335005322DA /* ContextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EADEB8E81DECD335005322DA /* ContextTest.swift */; }; EADEB8F51DECD335005322DA /* CryptoTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EADEB8E91DECD335005322DA /* CryptoTest.swift */; }; + F250ADE0288B401B0045FA7E /* PHGSession.m in Sources */ = {isa = PBXBuildFile; fileRef = F250ADDF288B401B0045FA7E /* PHGSession.m */; }; + F250ADE2288ED2E20045FA7E /* SessionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F250ADE1288ED2E20045FA7E /* SessionTest.swift */; }; + F250ADE3288ED4910045FA7E /* PHGSession.h in Headers */ = {isa = PBXBuildFile; fileRef = F250ADDE288B3ED80045FA7E /* PHGSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; F2A54FB028772F97006C53FF /* FeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2A54FAF28772F97006C53FF /* FeatureFlagTests.swift */; }; F2A54FB128772F97006C53FF /* FeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2A54FAF28772F97006C53FF /* FeatureFlagTests.swift */; }; F2A54FB32877582E006C53FF /* PHGGroupPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = F2A54FB22877582E006C53FF /* PHGGroupPayload.m */; }; @@ -197,6 +200,9 @@ EADEB8FF1DED3711005322DA /* circle.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = circle.yml; sourceTree = ""; }; EADEB9001DED3711005322DA /* PostHog.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = PostHog.podspec; sourceTree = ""; }; EADEB9011DED3711005322DA /* Podfile.lock */ = {isa = PBXFileReference; lastKnownFileType = text; path = Podfile.lock; sourceTree = ""; }; + F250ADDE288B3ED80045FA7E /* PHGSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PHGSession.h; sourceTree = ""; }; + F250ADDF288B401B0045FA7E /* PHGSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PHGSession.m; sourceTree = ""; }; + F250ADE1288ED2E20045FA7E /* SessionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTest.swift; sourceTree = ""; }; F2A54FAF28772F97006C53FF /* FeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagTests.swift; sourceTree = ""; }; F2A54FB22877582E006C53FF /* PHGGroupPayload.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PHGGroupPayload.m; sourceTree = ""; }; F2A54FB42877583C006C53FF /* PHGGroupPayload.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PHGGroupPayload.h; sourceTree = ""; }; @@ -341,6 +347,7 @@ 6EEC1C702017EA370089C478 /* EndToEndTests.swift */, A31958EE2385AC3A00A47EFA /* SerializationTests.m */, F2A54FAF28772F97006C53FF /* FeatureFlagTests.swift */, + F250ADE1288ED2E20045FA7E /* SessionTest.swift */, ); indentWidth = 2; path = PostHogTests; @@ -376,6 +383,8 @@ BF8C89BCDF12D1B64E9475F1 /* PHGCrypto.h */, F2A54FB22877582E006C53FF /* PHGGroupPayload.m */, F2A54FB42877583C006C53FF /* PHGGroupPayload.h */, + F250ADDE288B3ED80045FA7E /* PHGSession.h */, + F250ADDF288B401B0045FA7E /* PHGSession.m */, ); path = Classes; sourceTree = ""; @@ -423,6 +432,7 @@ BF8C8AD1FBFBB6941DE29D3B /* PHGFileStorage.h in Headers */, BF8C845A65AB5AD06E1EE26B /* PHGStorage.h in Headers */, BF8C890FADEC88022E7A3588 /* NSData+PHGGZIP.h in Headers */, + F250ADE3288ED4910045FA7E /* PHGSession.h in Headers */, BF8C8181F96E1C8CB86D783D /* PHGPostHogUtils.h in Headers */, BF8C8FCEC80B47BF2236236E /* PHGUtils.h in Headers */, BF8C8DCC18E3AA39CCB330BC /* PHGStoreKitCapturer.h in Headers */, @@ -713,6 +723,7 @@ BF8C8A634BCECED539A18D57 /* PHGPostHogIntegration.m in Sources */, BF8C8735F5CF80C193F5AEDF /* PHGAliasPayload.m in Sources */, BF8C8374E0D8F97469715282 /* PHGPayload.m in Sources */, + F250ADE0288B401B0045FA7E /* PHGSession.m in Sources */, BF8C86ED787477703D8BCD6C /* PHGScreenPayload.m in Sources */, BF8C8334DB2564A7CFA7A87A /* PHGPayloadManager.m in Sources */, BF8C87E4B70CA478721EB11D /* PHGCapturePayload.m in Sources */, @@ -726,6 +737,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F250ADE2288ED2E20045FA7E /* SessionTest.swift in Sources */, EADEB8F51DECD335005322DA /* CryptoTest.swift in Sources */, EADEB8F21DECD335005322DA /* UserDefaultsStorageTest.swift in Sources */, EAA542771EB4035400945DA7 /* CapturingTests.swift in Sources */, diff --git a/PostHog/Classes/PHGSession.h b/PostHog/Classes/PHGSession.h new file mode 100644 index 0000000000..2e39356164 --- /dev/null +++ b/PostHog/Classes/PHGSession.h @@ -0,0 +1,16 @@ +#import +#import "PHGStorage.h" + +@class PHGSession; + + +@interface PHGSession : NSObject + +- (instancetype _Nonnull)initWithStorage:(id _Nonnull)fileStorage userDefaultsStorage:(id _Nonnull)userDefaultsStorage; +- (NSString *_Nullable)getId; + +- (void)checkAndSetSessionId:(NSTimeInterval)timestamp; +- (void)checkAndSetSessionId; +- (void)resetSession; + +@end diff --git a/PostHog/Classes/PHGSession.m b/PostHog/Classes/PHGSession.m new file mode 100644 index 0000000000..588c584fe0 --- /dev/null +++ b/PostHog/Classes/PHGSession.m @@ -0,0 +1,111 @@ +// +// PHGSession.m +// PostHog +// +// Created by Eric Duong on 7/22/22. +// Copyright © 2022 PostHog. All rights reserved. +// + +#import +#import "PHGSession.h" +#import "PHGPostHogUtils.h" +#import "PHGStorage.h" + +static int const SESSION_CHANGE_THRESHOLD = 1800; + +static NSString *const PHGSessionId = @"PHGSessionId"; +static NSString *const kPHGSessionId = @"posthog.sessionId"; + +static NSString *const PHGSessionLastTimestamp = @"PHGSessionLastTimestamp"; +static NSString *const kPHGSessionLastTimestamp = @"posthog.sessionlastTimestamp"; + +@interface PHGSession () + +@property (nonatomic, strong) id fileStorage; +@property (nonatomic, strong) id userDefaultsStorage; + +@end + +@implementation PHGSession + +- (instancetype _Nonnull)initWithStorage:(id)fileStorage userDefaultsStorage:(id)userDefaultsStorage; +{ + if (self = [super init]) { + self.fileStorage = fileStorage; + self.userDefaultsStorage = userDefaultsStorage; + + } + return self; +} + +- (void)checkAndSetSessionId +{ + NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + [self checkAndSetSessionId:now]; +} + +- (void)checkAndSetSessionId:(NSTimeInterval)timestamp +{ + NSString *sessionId = [self getSessionId]; + NSTimeInterval sessionLastTimestamp = [self getSessionLastTimestamp]; + if (sessionId == nil || sessionLastTimestamp == 0 || fabs(timestamp - sessionLastTimestamp) > SESSION_CHANGE_THRESHOLD) { + NSString *newSessionId = createUUIDString(); + [self saveSessionId:newSessionId sessionLastTimestamp:timestamp]; + } +} + +- (void)saveSessionId:(NSString *)sessionId sessionLastTimestamp:(NSTimeInterval)sessionLastTimestamp +{ + +#if TARGET_OS_TV + [self.userDefaultsStorage setString:sessionId forKey:PHGSessionId]; +#else + [self.fileStorage setString:sessionId forKey:kPHGSessionId]; +#endif + + NSNumber* nsSessionLastTimestamp = [NSNumber numberWithDouble:sessionLastTimestamp]; +#if TARGET_OS_TV + [self.userDefaultsStorage setString:distinctId forKey:PHGSessionLastTimestamp]; +#else + [self.fileStorage setNumber:nsSessionLastTimestamp forKey:kPHGSessionLastTimestamp]; +#endif +} + +- (NSString *)getSessionId +{ +#if TARGET_OS_TV + return [[NSUserDefaults standardUserDefaults] valueForKey:PHGSessionId]; +#else + return [self.fileStorage stringForKey:kPHGSessionId]; +#endif +} + +- (NSTimeInterval)getSessionLastTimestamp +{ + NSNumber *timestamp; +#if TARGET_OS_TV + timestamp = [[NSUserDefaults standardUserDefaults] numberForKey:PHGSessionLastTimestamp]; +#else + timestamp = [self.fileStorage numberForKey:kPHGSessionLastTimestamp]; +#endif + return [timestamp doubleValue]; +} + +- (NSString *)getId +{ + [self checkAndSetSessionId]; + return [self getSessionId]; +} + +- (void)resetSession +{ +#if TARGET_OS_TV + [self.userDefaultsStorage removeKey:PHGSessionId]; + [self.userDefaultsStorage removeKey:PHGSessionLastTimestamp]; +#else + [self.fileStorage removeKey:kPHGSessionId]; + [self.fileStorage removeKey:kPHGSessionLastTimestamp]; +#endif +} + +@end diff --git a/PostHog/Internal/PHGFileStorage.m b/PostHog/Internal/PHGFileStorage.m index 3e4cb91d7b..a99421b9fa 100644 --- a/PostHog/Internal/PHGFileStorage.m +++ b/PostHog/Internal/PHGFileStorage.m @@ -114,6 +114,21 @@ - (void)setString:(nullable NSString *)string forKey:(NSString *)key [self setJSON:string forKey:key]; } +- (nullable NSNumber *)numberForKey:(NSString *)key +{ + NSDictionary *data = [self jsonForKey:key]; + if (data) { + return data[key]; + } + return nil; +} + + +- (void)setNumber:(nullable NSNumber *)number forKey:(NSString *)key +{ + [self setJSON:number forKey:key]; +} + + (NSURL *)applicationSupportDirectoryURL { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); diff --git a/PostHog/Internal/PHGPostHogIntegration.m b/PostHog/Internal/PHGPostHogIntegration.m index c4e9217b93..1d167a6282 100644 --- a/PostHog/Internal/PHGPostHogIntegration.m +++ b/PostHog/Internal/PHGPostHogIntegration.m @@ -13,6 +13,7 @@ #import "PHGScreenPayload.h" #import "PHGAliasPayload.h" #import "PHGGroupPayload.h" +#import "PHGSession.h" #if TARGET_OS_IOS #import @@ -54,6 +55,7 @@ @interface PHGPostHogIntegration () @property (nonatomic, strong) NSURLSessionUploadTask *batchRequest; @property (nonatomic, assign) UIBackgroundTaskIdentifier flushTaskID; @property (nonatomic, strong) PHGReachability *reachability; +@property (nonatomic, strong) PHGSession *sessionManager; @property (nonatomic, strong) NSTimer *flushTimer; @property (nonatomic, strong) dispatch_queue_t serialQueue; @property (nonatomic, strong) dispatch_queue_t backgroundTaskQueue; @@ -79,6 +81,7 @@ - (id)initWithPostHog:(PHGPostHog *)posthog httpClient:(PHGHTTPClient *)httpClie self.fileStorage = fileStorage; self.userDefaultsStorage = userDefaultsStorage; self.distinctId = [self getDistinctId]; + self.sessionManager = [[PHGSession alloc] initWithStorage:fileStorage userDefaultsStorage:userDefaultsStorage]; self.reachability = [PHGReachability reachabilityWithHostname:@"google.com"]; [self.reachability startNotifier]; self.cachedStaticContext = [self staticContext]; @@ -226,6 +229,7 @@ - (NSDictionary *)liveContext context[@"$groups"] = [self getGroups]; context[@"$active_feature_flags"] = [self getFeatureFlags]; + context[@"$session_id"] = [self.sessionManager getId]; NSDictionary *flagsAndValues = [self getFeatureFlagsAndValues]; @@ -506,6 +510,7 @@ - (void)reset #else [self.fileStorage removeKey:kPHGDistinctIdFilename]; #endif + [self.sessionManager resetSession]; self.distinctId = nil; }]; } diff --git a/PostHog/Internal/PHGStorage.h b/PostHog/Internal/PHGStorage.h index 7874db4e6d..d651b0362a 100644 --- a/PostHog/Internal/PHGStorage.h +++ b/PostHog/Internal/PHGStorage.h @@ -20,6 +20,9 @@ - (void)setString:(NSString *_Nullable)string forKey:(NSString *_Nonnull)key; - (NSString *_Nullable)stringForKey:(NSString *_Nonnull)key; -// Number and Booleans are intentionally omitted at the moment because they are not needed +- (void)setNumber:(NSNumber *_Nullable)number forKey:(NSString *_Nonnull)key; +- (nullable NSNumber *)numberForKey:(NSString *_Nonnull)key; + +// Booleans are intentionally omitted at the moment because they are not needed @end diff --git a/PostHog/Internal/PHGUserDefaultsStorage.m b/PostHog/Internal/PHGUserDefaultsStorage.m index f6afc4eb73..8f0be8b51c 100644 --- a/PostHog/Internal/PHGUserDefaultsStorage.m +++ b/PostHog/Internal/PHGUserDefaultsStorage.m @@ -130,6 +130,27 @@ - (void)setString:(NSString *)string forKey:(NSString *)key [self setPlist:string forKey:key]; } +- (NSNumber *)numberForKey:(NSString *)key +{ + if (!self.crypto) { + key = [self namespacedKey:key]; + + double value = [self.defaults doubleForKey:key]; + return [NSNumber numberWithDouble:value]; + } + return [self plistForKey:key]; +} + +- (void)setNumber:(NSNumber *)number forKey:(NSString *)key +{ + if (!self.crypto) { + key = [self namespacedKey:key]; + [self setObject:number forKey:key]; + return; + } + [self setPlist:number forKey:key]; +} + #pragma mark - Helpers - (id _Nullable)plistForKey:(NSString *)key diff --git a/PostHog/PostHog.h b/PostHog/PostHog.h index 40f03dc8cf..29a48eb6de 100644 --- a/PostHog/PostHog.h +++ b/PostHog/PostHog.h @@ -10,3 +10,4 @@ FOUNDATION_EXPORT const unsigned char PostHogVersionString[]; #import "PHGPostHogIntegration.h" #import "PHGContext.h" #import "PHGMiddleware.h" +#import "PHGSession.h" diff --git a/PostHogTests/SessionTest.swift b/PostHogTests/SessionTest.swift new file mode 100644 index 0000000000..c063d7d928 --- /dev/null +++ b/PostHogTests/SessionTest.swift @@ -0,0 +1,45 @@ +import Quick +import Nimble +import PostHog + +class SessionTest: QuickSpec { + override func spec() { + + var sessionHandler: PHGSession! + var storage : PHGFileStorage! + var userDefaultsStorage : PHGUserDefaultsStorage! + + beforeEach { + let url = PHGFileStorage.applicationSupportDirectoryURL() + expect(url).toNot(beNil()) + expect(url?.pathComponents[url!.pathComponents.count - 2]) == "Application Support" + expect(url?.lastPathComponent) == Bundle.main.bundleIdentifier + storage = PHGFileStorage(folder: url!, crypto: nil) + userDefaultsStorage = PHGUserDefaultsStorage(defaults: UserDefaults.standard, namespacePrefix: nil, crypto: nil) + sessionHandler = PHGSession(storage: storage, userDefaultsStorage: userDefaultsStorage) + } + + it("Generates a session id") { + let sessionId = sessionHandler.getId() + expect(sessionId).toNot(beNil()) + + let anotherSessionId = sessionHandler.getId() + expect(anotherSessionId).to(equal(sessionId)) + + } + + it("Generates a new session id after refresh") { + let sessionId = sessionHandler.getId() + expect(sessionId).toNot(beNil()) + + let timeInterval = Date().addingTimeInterval(TimeInterval(40.0 * 60.0)).timeIntervalSince1970 + sessionHandler.checkAndSetSessionId(timeInterval) + + let anotherSessionId = sessionHandler.getId() + expect(anotherSessionId).toNot(equal(sessionId)) + + } + + } + +}