diff --git a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved index e40be5404..4d0dfb64b 100644 --- a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,15 +9,6 @@ "version" : "0.20220203.1" } }, - { - "identity" : "analytics-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/segmentio/analytics-swift.git", - "state" : { - "revision" : "92cc824211160ab98c28c7d40c1e6d27645c2bf1", - "version" : "1.2.3" - } - }, { "identity" : "appauth-ios", "kind" : "remoteSourceControl", @@ -153,6 +144,15 @@ "version" : "2.6.0" } }, + { + "identity" : "posthog-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PostHog/posthog-ios.git", + "state" : { + "revision" : "3a6afc24d6bde730a19470d4e6b713f44d076ad9", + "version" : "2.0.3" + } + }, { "identity" : "promises", "kind" : "remoteSourceControl", @@ -171,15 +171,6 @@ "version" : "12.0.1" } }, - { - "identity" : "sovran-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/segmentio/Sovran-Swift.git", - "state" : { - "revision" : "944c17d7c46bd95fc37f09136cabd172be5b413b", - "version" : "1.0.3" - } - }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", diff --git a/apple/OmnivoreKit/Package.swift b/apple/OmnivoreKit/Package.swift index 864f2212c..cc04187a2 100644 --- a/apple/OmnivoreKit/Package.swift +++ b/apple/OmnivoreKit/Package.swift @@ -47,7 +47,7 @@ let package = Package( .target( name: "Utils", dependencies: [ - .product(name: "Segment", package: "analytics-swift") + .product(name: "PostHog", package: "posthog-ios") ], resources: [.process("Resources")] ), @@ -67,10 +67,10 @@ var dependencies: [Package.Dependency] { .package(url: "https://github.com/Square/Valet", from: "4.1.2"), .package(url: "https://github.com/maticzav/swift-graphql", from: "2.3.1"), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), - .package(url: "https://github.com/segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2"), .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"), - .package(url: "https://github.com/exyte/PopupView.git", from: "2.6.0") + .package(url: "https://github.com/exyte/PopupView.git", from: "2.6.0"), + .package(url: "https://github.com/PostHog/posthog-ios.git", from: "2.0.0") ] // Comment out following line for macOS build deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", from: "12.0.1")) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 5a8cb711f..0e9533313 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -41,6 +41,7 @@ import Views do { try await dataService.deleteAccount(userID: currentViewer.unwrappedUserID) authenticator.logout(dataService: dataService, isAccountDeletion: true) + EventTracker.reset() } catch { deleteAccountErrorMessage = "We were unable to delete your account." } diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index 292810f96..185685255 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -49,22 +49,10 @@ struct InnerRootView: View { @ViewBuilder private var innerBody: some View { if authenticator.isLoggedIn { - // GeometryReader { geo in PrimaryContentView() -// #if os(iOS) -// .miniPlayer() -// .formSheet(isPresented: $viewModel.showNewFeaturePrimer, -// modalSize: CGSize(width: geo.size.width * 0.66, height: geo.size.width * 0.66)) { -// FeaturePrimer.recommendationsPrimer -// } -// .onAppear { -// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { -// viewModel.showNewFeaturePrimer = viewModel.shouldShowNewFeaturePrimer -// viewModel.shouldShowNewFeaturePrimer = false -// } -// } -// #endif -// } + #if os(iOS) + .miniPlayer() + #endif } else { WelcomeView() .accessibilityElement() diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift index bc3121b7e..18f63fe1c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift @@ -17,8 +17,6 @@ public final class RootViewModel: ObservableObject { @Published public var showNewFeaturePrimer = false @AppStorage(UserDefaultKey.shouldShowNewFeaturePrimer.rawValue) var shouldShowNewFeaturePrimer = false - @Published var showMiniPlayer = false - public init() { registerFonts() diff --git a/apple/OmnivoreKit/Sources/App/Views/SlideAnimatedTransitioning.swift b/apple/OmnivoreKit/Sources/App/Views/SlideAnimatedTransitioning.swift index 1236c513c..164cae639 100644 --- a/apple/OmnivoreKit/Sources/App/Views/SlideAnimatedTransitioning.swift +++ b/apple/OmnivoreKit/Sources/App/Views/SlideAnimatedTransitioning.swift @@ -45,12 +45,14 @@ extension SlideAnimatedTransitioning: UIViewControllerAnimatedTransitioning { fromVC.view?.layer.shadowOpacity = 0.1 }, completion: { _ in - toVC.view?.layer.opacity = 1.0 - toVC.view?.layer.shadowOpacity = 0 - fromVC.view?.layer.opacity = 1.0 - fromVC.view?.layer.shadowOpacity = 0 + if !transitionContext.transitionWasCancelled { + toVC.view?.layer.opacity = 1.0 + toVC.view?.layer.shadowOpacity = 0 + fromVC.view?.layer.opacity = 1.0 + fromVC.view?.layer.shadowOpacity = 0 - fromVC.view.removeFromSuperview() + fromVC.view.removeFromSuperview() + } // when cancelling or completing the animation, ios simulator seems to sometimes flash black backgrounds during the animation. on devices, this doesn't seem to happen though. // containerView.backgroundColor = [UIColor whiteColor]; transitionContext.completeTransition(!transitionContext.transitionWasCancelled) diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift index ac54c7491..7ac953c79 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift @@ -43,6 +43,7 @@ public final class Authenticator: ObservableObject { Authenticator.unregisterIntercomUser?() isLoggedIn = false showAppleRevokeTokenAlert = isAccountDeletion + EventTracker.reset() } public func clearCreds() { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/LeaveGroup.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/LeaveGroup.swift index 47c2c90b4..144a149cb 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/LeaveGroup.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/LeaveGroup.swift @@ -12,7 +12,7 @@ public extension DataService { let selection = Selection { try $0.on( - leaveGroupError: .init { .error(errorMessage: try $0.errorCodes().first.toString()) }, + leaveGroupError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") }, leaveGroupSuccess: .init { .saved(success: try $0.success()) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift index 041dcca0f..d17cdd272 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RecommendPage.swift @@ -12,7 +12,7 @@ public extension DataService { let selection = Selection { try $0.on( - recommendError: .init { .error(errorMessage: try $0.errorCodes().first.toString()) }, + recommendError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") }, recommendSuccess: .init { .saved(success: try $0.success()) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift index 50920d45f..b75c73933 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift @@ -35,7 +35,7 @@ public extension DataService { let selection = Selection { try $0.on( - updatePageError: .init { .error(errorMessage: try $0.errorCodes().first.toString()) }, + updatePageError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") }, updatePageSuccess: .init { .saved(title: try $0.updatedPage(selection: Selection.Article { try $0.title() })) } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateLinkedItemTitle.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateLinkedItemTitle.swift index 9c2689094..e4acbf144 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateLinkedItemTitle.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateLinkedItemTitle.swift @@ -39,7 +39,7 @@ extension DataService { let selection = Selection { try $0.on( - updatePageError: .init { .error(errorMessage: try $0.errorCodes().first.toString()) }, + updatePageError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") }, updatePageSuccess: .init { .saved(title: try $0.updatedPage(selection: Selection.Article { try $0.title() })) } diff --git a/apple/OmnivoreKit/Sources/Utils/EventTracking/EventTracker.swift b/apple/OmnivoreKit/Sources/Utils/EventTracking/EventTracker.swift index fa63eb75b..0dc18838f 100644 --- a/apple/OmnivoreKit/Sources/Utils/EventTracking/EventTracker.swift +++ b/apple/OmnivoreKit/Sources/Utils/EventTracking/EventTracker.swift @@ -1,11 +1,24 @@ import Foundation -import Segment +import PostHog public enum EventTracker { - public static func start() { - // invoke the closure that creates the segment instance - _ = segment?.version() - } + public static var posthog: PHGPostHog? = { + guard let writeKey = AppKeys.sharedInstance?.posthogClientKey else { + return nil + } + + guard let posthogInstanceAddress = AppKeys.sharedInstance?.posthogInstanceAddress else { + return nil + } + + let configuration = PHGPostHogConfiguration(apiKey: writeKey, host: posthogInstanceAddress) + + configuration.recordScreenViews = false + configuration.captureApplicationLifecycleEvents = true + + PHGPostHog.setup(with: configuration) + return PHGPostHog.shared() + }() public static func trackForDebugging(_ message: String) { #if DEBUG @@ -14,29 +27,14 @@ public enum EventTracker { } public static func track(_ event: TrackableEvent) { - segment?.track(name: event.name, properties: event.properties) + posthog?.capture(event.name, properties: event.properties) } public static func registerUser(userID: String) { - segment?.identify(userId: userID) + posthog?.identify(userID) } - public static func recordUserTraits(userID: String, traits: [String: String]) { - segment?.identify(userId: userID, traits: traits) + public static func reset() { + posthog?.reset() } } - -private let segment: Analytics? = { - guard let writeKey = AppKeys.sharedInstance?.segmentClientKey else { - return nil - } - - let config = Configuration(writeKey: writeKey) - .flushAt(20) // default is 20 - .trackApplicationLifecycleEvents(true) // default is true - .autoAddSegmentDestination(true) // default is true - .flushInterval(30) // default is 30 seconds - .trackDeeplinks(true) // default is true - - return Analytics(configuration: config) -}() diff --git a/apple/OmnivoreKit/Sources/Utils/Secrets.swift b/apple/OmnivoreKit/Sources/Utils/Secrets.swift index 094d24400..61897b19f 100644 --- a/apple/OmnivoreKit/Sources/Utils/Secrets.swift +++ b/apple/OmnivoreKit/Sources/Utils/Secrets.swift @@ -6,7 +6,8 @@ public struct AppKeys: Decodable { public let firebaseDemoKeys: FirebaseKeys? public let firebaseProdKeys: FirebaseKeys? public let iosClientGoogleId: String? - public let segmentClientKey: String? + public let posthogClientKey: String? + public let posthogInstanceAddress: String? public static let sharedInstance = AppKeys.make() private init() { @@ -15,7 +16,8 @@ public struct AppKeys: Decodable { self.firebaseDemoKeys = nil self.firebaseProdKeys = nil self.iosClientGoogleId = nil - self.segmentClientKey = nil + self.posthogClientKey = nil + self.posthogInstanceAddress = nil } } diff --git a/apple/Sources/AppDelegate.swift b/apple/Sources/AppDelegate.swift index 85210dc7f..a7c5d83c1 100644 --- a/apple/Sources/AppDelegate.swift +++ b/apple/Sources/AppDelegate.swift @@ -42,8 +42,6 @@ private let logger = Logger(subsystem: "app.omnivore", category: "app-delegate") } #endif - EventTracker.start() - if let intercomKeys = AppKeys.sharedInstance?.intercom { Intercom.setApiKey(intercomKeys.apiKey, forAppId: intercomKeys.appID) diff --git a/packages/api/src/routers/auth/apple_auth.ts b/packages/api/src/routers/auth/apple_auth.ts index c852e9cfd..73cbf56a0 100644 --- a/packages/api/src/routers/auth/apple_auth.ts +++ b/packages/api/src/routers/auth/apple_auth.ts @@ -14,6 +14,7 @@ import { createWebAuthToken, suggestedUsername, } from './jwt_helpers' +import { analytics } from '../../utils/analytics' const appleBaseURL = 'https://appleid.apple.com' const audienceName = 'app.omnivore.app' @@ -145,6 +146,17 @@ export async function handleAppleWebAuth( ? ssoRedirectURL(ssoToken) : `${baseURL()}/home` + analytics.track({ + userId: user.id, + event: 'login', + properties: { + method: 'apple', + email: user.email, + username: user.profile.username, + env: env.server.apiEnv, + }, + }) + return { authToken, redirectURL, diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 730b6bd7e..381d974f0 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -51,6 +51,7 @@ import { } from './google_auth' import { createWebAuthToken } from './jwt_helpers' import { createMobileAccountCreationResponse } from './mobile/account_creation' +import { analytics } from '../../utils/analytics' export interface SignupRequest { email: string @@ -323,6 +324,17 @@ export function authRouter() { ) } + analytics.track({ + userId: user.id, + event: 'login', + properties: { + method: 'google', + email: user.email, + username: user.profile.username, + env: env.server.apiEnv, + }, + }) + res.setHeader('set-cookie', result.headers['set-cookie']) await handleSuccessfulLogin(req, res, user, data.googleLogin.newUser) @@ -447,6 +459,17 @@ export function authRouter() { ) } + analytics.track({ + userId: user.id, + event: 'login', + properties: { + method: 'email', + email: user.email, + username: user.profile.username, + env: env.server.apiEnv, + }, + }) + await handleSuccessfulLogin(req, res, user, false) } catch (e) { logger.info('email-login exception:', e) @@ -548,6 +571,17 @@ export function authRouter() { } } + analytics.track({ + userId: user.id, + event: 'login', + properties: { + method: 'email_verification', + email: user.email, + username: user.profile.username, + env: env.server.apiEnv, + }, + }) + res.set('Message', 'EMAIL_CONFIRMED') await handleSuccessfulLogin(req, res, user, false) } catch (e) { @@ -664,6 +698,17 @@ export function authRouter() { ) } + analytics.track({ + userId: user.id, + event: 'login', + properties: { + method: 'password_reset', + email: user.email, + username: user.profile.username, + env: env.server.apiEnv, + }, + }) + await handleSuccessfulLogin(req, res, user, false) } catch (e) { logger.info('reset-password exception:', e)