diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index 357619413..5c4d66b28 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -74,7 +74,6 @@ 6992AA8E2AFE51CE00087600 /* PostHog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; }; 6992AA8F2AFE51CE00087600 /* PostHog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6992AA942AFE529E00087600 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6992AA932AFE529E00087600 /* AppDelegate.swift */; }; - 6993C40B2B9F35E300075A72 /* PostHogSessionReplayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6993C40A2B9F35E300075A72 /* PostHogSessionReplayConfig.swift */; }; 699991882AFE1B37000DCB78 /* PostHogExampleMacOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699991872AFE1B37000DCB78 /* PostHogExampleMacOSApp.swift */; }; 6999918A2AFE1B37000DCB78 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699991892AFE1B37000DCB78 /* ContentView.swift */; }; 6999918C2AFE1B39000DCB78 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6999918B2AFE1B39000DCB78 /* Assets.xcassets */; }; @@ -83,6 +82,9 @@ 699991952AFE1B56000DCB78 /* PostHog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6999919A2AFE1BAB000DCB78 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699991992AFE1BAB000DCB78 /* AppDelegate.swift */; }; 69BA38D72B888E8500AA69D6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 69BA38D62B888E8500AA69D6 /* PrivacyInfo.xcprivacy */; }; + 69EE82BA2BA9C50400EB9542 /* PostHogReplayIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */; }; + 69EE82BC2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */; }; + 69EE82BE2BA9C8AA00EB9542 /* ViewLayoutTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EE82BD2BA9C8AA00EB9542 /* ViewLayoutTracker.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -275,7 +277,6 @@ 6992AA862AFE51A100087600 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6992AA892AFE51A100087600 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 6992AA932AFE529E00087600 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 6993C40A2B9F35E300075A72 /* PostHogSessionReplayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionReplayConfig.swift; sourceTree = ""; }; 699991562AFE0E9F000DCB78 /* PostHogExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PostHogExample.entitlements; sourceTree = ""; }; 699991852AFE1B37000DCB78 /* PostHogExampleMacOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PostHogExampleMacOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 699991872AFE1B37000DCB78 /* PostHogExampleMacOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogExampleMacOSApp.swift; sourceTree = ""; }; @@ -285,6 +286,9 @@ 699991902AFE1B39000DCB78 /* PostHogExampleMacOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PostHogExampleMacOS.entitlements; sourceTree = ""; }; 699991992AFE1BAB000DCB78 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 69BA38D62B888E8500AA69D6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogReplayIntegration.swift; sourceTree = ""; }; + 69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionReplayConfig.swift; sourceTree = ""; }; + 69EE82BD2BA9C8AA00EB9542 /* ViewLayoutTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewLayoutTracker.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -432,6 +436,7 @@ 3AC745B7296D6FE60025C109 /* PostHog */ = { isa = PBXGroup; children = ( + 69EE82B82BA9C4DA00EB9542 /* Replay */, 69BA38E62B893F2200AA69D6 /* Resources */, 69779BED2AE6B29E00D7A48E /* Models */, 3AC745B8296D6FE60025C109 /* PostHog.h */, @@ -451,7 +456,6 @@ 6926DA8D2ADD2876005760D2 /* PostHogContext.swift */, 69779BEB2AE68E6900D7A48E /* UIViewController.swift */, 690FF0C42AEFAE8200A0B06B /* PostHogLegacyQueue.swift */, - 6993C40A2B9F35E300075A72 /* PostHogSessionReplayConfig.swift */, ); path = PostHog; sourceTree = ""; @@ -580,6 +584,16 @@ path = Resources; sourceTree = ""; }; + 69EE82B82BA9C4DA00EB9542 /* Replay */ = { + isa = PBXGroup; + children = ( + 69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */, + 69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */, + 69EE82BD2BA9C8AA00EB9542 /* ViewLayoutTracker.swift */, + ); + path = Replay; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -723,7 +737,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1500; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1530; TargetAttributes = { 3AA34CF6296D951A003398F4 = { CreatedOnToolsVersion = 14.2; @@ -893,9 +907,9 @@ buildActionMask = 2147483647; files = ( 690FF05F2AE7E2D400A0B06B /* Data+Gzip.swift in Sources */, + 69EE82BE2BA9C8AA00EB9542 /* ViewLayoutTracker.swift in Sources */, 69261D1F2AD9681300232EC7 /* PostHogConsumerPayload.swift in Sources */, 690FF0BF2AEFA97F00A0B06B /* FileUtils.swift in Sources */, - 6993C40B2B9F35E300075A72 /* PostHogSessionReplayConfig.swift in Sources */, 69261D252AD9787A00232EC7 /* PostHogExtensions.swift in Sources */, 3AE3FB4E2993D1D600AFFC18 /* PostHogSessionManager.swift in Sources */, 3AE3FB49299391DF00AFFC18 /* PostHogStorage.swift in Sources */, @@ -913,6 +927,8 @@ 3AE3FB332991388500AFFC18 /* PostHogQueue.swift in Sources */, 690FF0B52AEBBD3C00A0B06B /* DictUtils.swift in Sources */, 69261D1B2AD9678C00232EC7 /* PostHogEvent.swift in Sources */, + 69EE82BC2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift in Sources */, + 69EE82BA2BA9C50400EB9542 /* PostHogReplayIntegration.swift in Sources */, 3AE3FB472992AB0000AFFC18 /* Hedgelog.swift in Sources */, 69261D132AD5685B00232EC7 /* PostHogFeatureFlags.swift in Sources */, 69261D1D2AD967CD00232EC7 /* PostHogFileBackedQueue.swift in Sources */, @@ -1031,7 +1047,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = PostHogExample/PostHogExample.entitlements; - CODE_SIGN_IDENTITY = ""; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"PostHogExample/Preview Content\""; @@ -1046,10 +1062,12 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + "LD_RUNPATH_SEARCH_PATHS[arch=*]" = "@executable_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.posthog.PostHogExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -1066,7 +1084,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = PostHogExample/PostHogExample.entitlements; - CODE_SIGN_IDENTITY = ""; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"PostHogExample/Preview Content\""; @@ -1085,6 +1103,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.posthog.PostHogExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -1230,10 +1249,11 @@ buildSettings = { BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = PNC2XCH2XP; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; diff --git a/PostHog.xcodeproj/xcshareddata/xcschemes/PostHog.xcscheme b/PostHog.xcodeproj/xcshareddata/xcschemes/PostHog.xcscheme index f4679242a..1ef3a959e 100644 --- a/PostHog.xcodeproj/xcshareddata/xcschemes/PostHog.xcscheme +++ b/PostHog.xcodeproj/xcshareddata/xcschemes/PostHog.xcscheme @@ -1,6 +1,6 @@ + + + + Bool { + var active = false + sessionLock.withLock { + active = sessionId != nil + } + return active + } } diff --git a/PostHog/PostHogSessionReplayConfig.swift b/PostHog/PostHogSessionReplayConfig.swift deleted file mode 100644 index 22df8c81e..000000000 --- a/PostHog/PostHogSessionReplayConfig.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// PostHogSessionReplayConfig.swift -// PostHog -// - -import Foundation - -@objc(PostHogSessionReplayConfig) public class PostHogSessionReplayConfig: NSObject { - /// Enable masking of all text input fields - /// Experimental support - /// Default: true - @objc public var maskAllTextInputs: Bool = true - - /// Enable masking of all images to a placeholder - /// Experimental support - /// Default: true - @objc public var maskAllImages: Bool = true - - // TODO: sessionRecording config such as consoleLogRecordingEnabled, networkPayloadCapture, sampleRate, etc -} diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift new file mode 100644 index 000000000..0f26705e0 --- /dev/null +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -0,0 +1,72 @@ +// +// PostHogReplayIntegration.swift +// PostHog +// +// Created by Manoel Aranda Neto on 19.03.24. +// +#if os(iOS) || os(tvOS) + import Foundation + import UIKit + + class PostHogReplayIntegration { + private let config: PostHogConfig + + private let timeInterval = 1.0 / 2.0 + private var timer: Timer? + + init(_ config: PostHogConfig) { + self.config = config + } + + func start() { + stopTimer() + timer = Timer.scheduledTimer(timeInterval: timeInterval, + target: self, + selector: #selector(snapshot), + userInfo: nil, + repeats: true) + ViewLayoutTracker.swizzleLayoutSubviews() + } + + func stop() { + stopTimer() + ViewLayoutTracker.unSwizzleLayoutSubviews() + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + + private func generateSnapshot(_ view: UIView) { + let size = view.bounds.size + } + + private func isSessionActive() -> Bool { + config.sessionReplay && PostHogSDK.shared.isSessionActive() + } + + @objc private func snapshot() { + if !isSessionActive() { + return + } + + if !ViewLayoutTracker.hasChanges { + return + } + // TODO: thread safe + ViewLayoutTracker.clear() + + guard let activeScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) else { + return + } + + guard let window = (activeScene as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow }) else { + return + } + + // TODO: offload conversion to off main thread + generateSnapshot(window) + } + } +#endif diff --git a/PostHog/Replay/PostHogSessionReplayConfig.swift b/PostHog/Replay/PostHogSessionReplayConfig.swift new file mode 100644 index 000000000..2b72d8966 --- /dev/null +++ b/PostHog/Replay/PostHogSessionReplayConfig.swift @@ -0,0 +1,24 @@ +// +// PostHogSessionReplayConfig.swift +// PostHog +// +// Created by Manoel Aranda Neto on 19.03.24. +// +#if os(iOS) || os(tvOS) + import Foundation + + @objc(PostHogSessionReplayConfig) public class PostHogSessionReplayConfig: NSObject { + @objc var captureConsoleLogs: Bool = true + /// Enable masking of all text input fields + /// Experimental support + /// Default: true + @objc public var maskAllTextInputs: Bool = true + + /// Enable masking of all images to a placeholder + /// Experimental support + /// Default: true + @objc public var maskAllImages: Bool = true + + // TODO: sessionRecording config such as networkPayloadCapture, sampleRate, etc + } +#endif diff --git a/PostHog/Replay/ViewLayoutTracker.swift b/PostHog/Replay/ViewLayoutTracker.swift new file mode 100644 index 000000000..9d6a15db5 --- /dev/null +++ b/PostHog/Replay/ViewLayoutTracker.swift @@ -0,0 +1,48 @@ +#if os(iOS) || os(tvOS) + import Foundation + import UIKit + + enum ViewLayoutTracker { + static var hasChanges = false + static var hasSwizzled = false + + static func viewDidLayout(view _: UIView) { + ViewLayoutTracker.hasChanges = true + } + + static func clear() { + ViewLayoutTracker.hasChanges = false + } + + static func swizzleLayoutSubviews() { + if ViewLayoutTracker.hasSwizzled { + return + } + hasSwizzled = true + UIViewController.swizzle(forClass: UIView.self, + original: #selector(UIView.layoutSubviews), + new: #selector(UIView.ph_layoutSubviews)) + } + + static func unSwizzleLayoutSubviews() { + if !ViewLayoutTracker.hasSwizzled { + return + } + hasSwizzled = false + UIViewController.swizzle(forClass: UIView.self, + original: #selector(UIView.ph_layoutSubviews), + new: #selector(UIView.layoutSubviews)) + } + } + + extension UIView { + @objc func ph_layoutSubviews() { + guard Thread.isMainThread else { + return + } + ph_layoutSubviews() + ViewLayoutTracker.viewDidLayout(view: self) + } + } + +#endif diff --git a/PostHog/UIViewController.swift b/PostHog/UIViewController.swift index 087401937..d259e4442 100644 --- a/PostHog/UIViewController.swift +++ b/PostHog/UIViewController.swift @@ -25,6 +25,12 @@ import Foundation new: #selector(UIViewController.viewDidApperOverride)) } + static func unswizzleScreenView() { + UIViewController.swizzle(forClass: UIViewController.self, + original: #selector(UIViewController.viewDidApperOverride), + new: #selector(UIViewController.viewDidAppear(_:))) + } + private func activeController() -> UIViewController? { // if a view is being dismissed, this will return nil if let root = viewIfLoaded?.window?.rootViewController { diff --git a/PostHogExample/Api.swift b/PostHogExample/Api.swift index b5070cee4..5f59f4d83 100644 --- a/PostHogExample/Api.swift +++ b/PostHogExample/Api.swift @@ -19,18 +19,18 @@ class Api: ObservableObject { var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "main") - func listBeers(completion: @escaping ([PostHogBeerInfo]) -> Void) { + func listBeers(completion _: @escaping ([PostHogBeerInfo]) -> Void) { guard let url = URL(string: "https://api.punkapi.com/v2/beers") else { return } logger.info("Requesting beers list...") - URLSession.shared.dataTask(with: url) { data, _, _ in - let beers = try! JSONDecoder().decode([PostHogBeerInfo].self, from: data!) - - DispatchQueue.main.async { - completion(beers) - } + URLSession.shared.dataTask(with: url) { _, _, _ in +// let beers = try! JSONDecoder().decode([PostHogBeerInfo].self, from: data!) +// +// DispatchQueue.main.async { +// completion(beers) +// } }.resume() } diff --git a/PostHogExample/AppDelegate.swift b/PostHogExample/AppDelegate.swift index 15e5e8af9..97be5a52a 100644 --- a/PostHogExample/AppDelegate.swift +++ b/PostHogExample/AppDelegate.swift @@ -18,7 +18,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { config.captureScreenViews = false config.captureApplicationLifecycleEvents = false config.flushAt = 1 - config.flushIntervalSeconds = 10 +// config.flushIntervalSeconds = 10 config.debug = true config.sendFeatureFlagEvent = false config.sessionReplay = true @@ -26,22 +26,22 @@ class AppDelegate: NSObject, UIApplicationDelegate { PostHogSDK.shared.setup(config) PostHogSDK.shared.debug() - var width: Float = 0 - var height: Float = 0 - #if os(iOS) || os(tvOS) - width = Float(UIScreen.main.bounds.width) - height = Float(UIScreen.main.bounds.height) - #elseif os(macOS) - if let mainScreen = NSScreen.main { - width = Float(screenFrame.size.width) - height = Float(screenFrame.size.height) - } - #endif - - let timestamp = Int(Date().timeIntervalSince1970.rounded()) - let data: [String: Any] = ["href": "AppDelegate", "width": width, "height": height] - let snapshotData: [String: Any] = ["type": 4, "data": data, "timestamp": 1_710_173_534_407] - PostHogSDK.shared.capture("$snapshot", properties: ["$snapshot_source": "mobile", "$snapshot_data": snapshotData]) +// var width: Float = 0 +// var height: Float = 0 +// #if os(iOS) || os(tvOS) +// width = Float(UIScreen.main.bounds.width) +// height = Float(UIScreen.main.bounds.height) +// #elseif os(macOS) +// if let mainScreen = NSScreen.main { +// width = Float(screenFrame.size.width) +// height = Float(screenFrame.size.height) +// } +// #endif + +// let timestamp = Int(Date().timeIntervalSince1970.rounded()) +// let data: [String: Any] = ["href": "AppDelegate", "width": width, "height": height] +// let snapshotData: [String: Any] = ["type": 4, "data": data, "timestamp": 1_710_173_534_407] +// PostHogSDK.shared.capture("$snapshot", properties: ["$snapshot_source": "mobile", "$snapshot_data": snapshotData]) let defaultCenter = NotificationCenter.default