Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add in-app event listener #211

Merged
merged 17 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ on: [pull_request]

jobs:
SwiftLint:
runs-on: ubuntu-latest
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Lint via SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
- name: Install swiftlint
run: brew install swiftlint || brew upgrade swiftlint
- name: Run swiftlint. Fail if any errors.
run: make lint
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"repositoryURL": "https://gitlab.com/bourbonltd/gist-apple.git",
"state": {
"branch": null,
"revision": "2b1f647d96038027c3018e9bfff6cb27e944bf09",
"version": "2.2.1"
"revision": "408077c633807dc00fbce263cf513f69ab40a75b",
"version": "2.2.2"
}
}
]
Expand Down
63 changes: 41 additions & 22 deletions Sources/Common/Util/Log.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public enum CioLogLevel: String, CaseIterable {
case debug

#if canImport(os)
func shouldLog(_ level: OSLogType) -> Bool {
func shouldLog(_ level: CioLogLevel) -> Bool {
switch self {
case .none: return false
case .error:
Expand All @@ -39,15 +39,24 @@ public enum CioLogLevel: String, CaseIterable {
return true
}
}

var osLogLevel: OSLogType {
switch self {
case .none: return .info
case .error: return .error
case .info: return .info
case .debug: return .debug
}
}
#endif
}

// log messages to console.
// sourcery: InjectRegister = "Logger"
public class ConsoleLogger: Logger {
// allows filtering in Console mac app
private let logSubsystem = "io.customer.sdk"
private let logCategory = "CIO"
public static let logSubsystem = "io.customer.sdk"
public static let logCategory = "CIO"

private let siteId: SiteId
private let sdkConfig: SdkConfig
Expand All @@ -61,21 +70,12 @@ public class ConsoleLogger: Logger {
self.sdkConfig = sdkConfig
}

#if canImport(os)
// Unified logging for Swift. https://www.avanderlee.com/workflow/oslog-unified-logging/
// This means we can view logs in xcode console + Console app.
private func printMessage(_ message: String, _ level: OSLogType) {
private func printMessage(_ message: String, _ level: CioLogLevel) {
if !minLogLevel.shouldLog(level) { return }

let messageToPrint = "(siteid:\(siteId.abbreviatedSiteId)) \(message)"

if #available(iOS 14, *) {
let logger = os.Logger(subsystem: self.logSubsystem, category: self.logCategory)
logger.log(level: level, "\(messageToPrint, privacy: .public)")
} else {
let logger = OSLog(subsystem: logSubsystem, category: logCategory)
os_log("%{public}@", log: logger, type: level, messageToPrint)
}
ConsoleLogger.logMessageToConsole(messageToPrint, level: level)
}

public func debug(_ message: String) {
Expand All @@ -89,14 +89,33 @@ public class ConsoleLogger: Logger {
public func error(_ message: String) {
printMessage("🛑 \(message)", .error)
}
#else
// At this time, Linux cannot use `os.log` or `OSLog`. Instead, use: https://github.com/apple/swift-log/
// As we don't officially support Linux at this time, no need to add a dependency to the project.
// therefore, we are not logging if can't import os.log
public func debug(_ message: String) {}
public func info(_ message: String) {}
public func error(_ message: String) {}
#endif

public static func logMessageToConsole(_ message: String, level: CioLogLevel) {
#if canImport(os)
// Unified logging for Swift. https://www.avanderlee.com/workflow/oslog-unified-logging/
// This means we can view logs in xcode console + Console app.
if #available(iOS 14, *) {
let logger = os.Logger(subsystem: self.logSubsystem, category: self.logCategory)
logger.log(level: level.osLogLevel, "\(message, privacy: .public)")
} else {
let logger = OSLog(subsystem: logSubsystem, category: logCategory)
os_log("%{public}@", log: logger, type: level.osLogLevel, message)
}
#else
// At this time, Linux cannot use `os.log` or `OSLog`. Instead, use: https://github.com/apple/swift-log/
// As we don't officially support Linux at this time, no need to add a dependency to the project.
// therefore, we are not logging if can't import os.log
#endif
}
}

// Log messages to customers when the SDK is not initialized.
// Great for alerts when the SDK may not be setup correctly.
// Since the SDK is not initialized, dependencies graph is not created.
// Therefore, this is just a function that's available at the top-level available to all
// of the SDK without the use of dependencies.
public func sdkNotInitializedAlert(_ message: String) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea but if all its doing is logging a message why are we calling it, sdkNotInitializedAlert because maybe we might have more alerts that could utilize this method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about making the naming more generic. However, I found some cons with that approach.

In the function body, you see "⚠️ \(message)", level: .error. So, we are prefixing all log messages with ⚠️ and we are using the log level of error.

If we were to rename this function to something more generic:

public func logMessage(_ message: String, level: CioLogLevel) {
	ConsoleLogger.logMessageToConsole(message, level: level)
}
  • Everywhere in the code that current calls sdkNotInitializedAlert() will now have copy/pasted code in it.
// before
sdkNotInitializedAlert("foo")

// after
logMessage("⚠️ foo", level: .error)
  • There is a small number of edge cases where we need to log a message and the dependency injection graph is not yet constructed. Therefore, I don't believe at this time we will encounter a problem where the number of functions similar to sdkNotInitializedAlert grows. Currently, the only edge case I can consider needing logging without a DI graph is when the SDK is not initialized.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, not a blocker to PR, but maybe an idea.

maybe, have an icon enum type for where we will use ⚠️ message

// before 
sdkNotInitializedAlert("foo")


// after
 logMessage(message: "foo", level: .error, icon: .error)
similarly,
 logMessage(message: "foo", level: .error, icon: .warning)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am leaning towards the current implementation of sdkNotInitializedAlert because

  • A function name that's more generic such as logMessage doesn't tell the developer when to use that function to log a message and when to use the Logger that's inside of the DI graph. A function name such as sdkNotInitializedAlert suggests that this only be used when the SDK is not initialized. Otherwise, use the logger in the DI graph.
  • I like how DRY sdkNotInitializedAlert's implementation is where the icon and log level are encapsulated into 1 place in the code. If we were to replace it with: logMessage(message: "foo", level: .error, icon: .warning), it would no longer be DRY.

ConsoleLogger.logMessageToConsole("⚠️ \(message)", level: .error)
}

extension CioLogLevel {
Expand Down
56 changes: 49 additions & 7 deletions Sources/MessagingInApp/MessagingInApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import Gist

public protocol MessagingInAppInstance: AutoMockable {
func initialize(organizationId: String)
// sourcery:Name=initializeEventListener
func initialize(organizationId: String, eventListener: InAppEventListener)
}

public class MessagingInApp: ModuleTopLevelObject<MessagingInAppInstance>, MessagingInAppInstance {
@Atomic public private(set) static var shared = MessagingInApp()

// testing constructor
override internal init(implementation: MessagingInAppInstance, sdkInitializedUtil: SdkInitializedUtil) {
override internal init(implementation: MessagingInAppInstance?, sdkInitializedUtil: SdkInitializedUtil) {
super.init(implementation: implementation, sdkInitializedUtil: sdkInitializedUtil)
}

Expand All @@ -25,27 +26,68 @@ public class MessagingInApp: ModuleTopLevelObject<MessagingInAppInstance>, Messa
Self.shared = MessagingInApp()
}

// Initialize SDK module
// testing constructor
internal static func initialize(organizationId: String, eventListener: InAppEventListener?, implementation: MessagingInAppInstance?, sdkInitializedUtil: SdkInitializedUtil) {
Self.shared = MessagingInApp(implementation: implementation, sdkInitializedUtil: sdkInitializedUtil)

if let eventListener = eventListener {
Self.initialize(organizationId: organizationId, eventListener: eventListener)
} else {
Self.initialize(organizationId: organizationId)
}
}

// MARK: static initialized functions for customers.

// static functions are identical to initialize functions in InAppInstance protocol to try and make a more convenient
// API for customers. Customers can use `MessagingInApp.initialize(...)` instead of `MessagingInApp.shared.initialize(...)`.
// Trying to follow the same API as `CustomerIO` class with `initialize()`.

public static func initialize(organizationId: String) {
Self.shared.initialize(organizationId: organizationId)
}

public static func initialize(organizationId: String, eventListener: InAppEventListener) {
Self.shared.initialize(organizationId: organizationId, eventListener: eventListener)
}

// MARK: initialize functions to initialize module.

// Multiple initialize functions to inherit the InAppInstance protocol which contains multiple initialize functions.

public func initialize(organizationId: String) {
initialize() // enables features such as setting up hooks
commonInitialize(organizationId: organizationId, eventListener: nil)
}

public func initialize(organizationId: String, eventListener: InAppEventListener) {
levibostian marked this conversation as resolved.
Show resolved Hide resolved
commonInitialize(organizationId: organizationId, eventListener: eventListener)
}

private func commonInitialize(organizationId: String, eventListener: InAppEventListener?) {
guard let implementation = implementation else {
sdkNotInitializedAlert("CustomerIO class has not yet been initialized. Request to initialize the in-app module has been ignored.")
return
}

initialize()

implementation?.initialize(organizationId: organizationId)
if let eventListener = eventListener {
implementation.initialize(organizationId: organizationId, eventListener: eventListener)
} else {
implementation.initialize(organizationId: organizationId)
}
}

override public func inititlize(diGraph: DIGraph) {
let logger = diGraph.logger
logger.debug("Setting up MessagingInApp module...")
logger.debug("Setting up in-app module...")

// Register MessagingPush module hooks now that the module is being initialized.
let hooks = diGraph.hooksManager
let moduleHookProvider = MessagingInAppModuleHookProvider()
hooks.add(key: .messagingInApp, provider: moduleHookProvider)

logger.info("MessagingInApp module setup with SDK")
logger.info("In-app module setup with SDK")
}

override public func getImplementationInstance(diGraph: DIGraph) -> MessagingInAppInstance {
Expand Down
29 changes: 27 additions & 2 deletions Sources/MessagingInApp/MessagingInAppImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,31 @@ internal class MessagingInAppImplementation: MessagingInAppInstance {
private var queue: Queue
private var jsonAdapter: JsonAdapter
private var inAppProvider: InAppProvider
private var profileStore: ProfileStore

private var eventListener: InAppEventListener?

init(diGraph: DIGraph) {
self.logger = diGraph.logger
self.queue = diGraph.queue
self.jsonAdapter = diGraph.jsonAdapter
self.inAppProvider = diGraph.inAppProvider
self.profileStore = diGraph.profileStore
}

func initialize(organizationId: String) {
logger.debug("gist SDK being setup \(organizationId)")

inAppProvider.initialize(organizationId: organizationId, delegate: self)

// if identifier is already present, set the userToken again so in case if the customer was already identified and
// module was added later on, we can notify gist about it.
if let identifier = profileStore.identifier {
inAppProvider.setProfileIdentifier(identifier)
}
}

func initialize(organizationId: String, eventListener: InAppEventListener) {
self.eventListener = eventListener
initialize(organizationId: organizationId)
}
}

Expand Down Expand Up @@ -54,6 +67,8 @@ extension MessagingInAppImplementation: GistDelegate {
public func messageShown(message: Message) {
logger.debug("in-app message opened. \(message.describeForLogs)")

eventListener?.messageShown(message: InAppMessage(gistMessage: message))

if let deliveryId = getDeliveryId(from: message) {
// the state of the SDK does not change if adding this queue task isn't successful so ignore result
_ = queue.addTrackInAppDeliveryTask(deliveryId: deliveryId, event: .opened)
Expand All @@ -62,10 +77,14 @@ extension MessagingInAppImplementation: GistDelegate {

public func messageDismissed(message: Message) {
logger.debug("in-app message dismissed. \(message.describeForLogs)")

eventListener?.messageDismissed(message: InAppMessage(gistMessage: message))
}

public func messageError(message: Message) {
logger.error("error with in-app message. \(message.describeForLogs)")

eventListener?.errorWithMessage(message: InAppMessage(gistMessage: message))
}

public func action(message: Message, currentRoute: String, action: String, name: String) {
Expand All @@ -80,6 +99,12 @@ extension MessagingInAppImplementation: GistDelegate {
// the state of the SDK does not change if adding this queue task isn't successful so ignore result
_ = queue.addTrackInAppDeliveryTask(deliveryId: deliveryId, event: .clicked)
}

eventListener?.messageActionTaken(
message: InAppMessage(gistMessage: message),
action: action,
name: name
)
}

private func getDeliveryId(from message: Message) -> String? {
Expand Down
10 changes: 10 additions & 0 deletions Sources/MessagingInApp/Type/InAppEventListener.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Common
import Foundation
import Gist

public protocol InAppEventListener: AutoMockable {
func messageShown(message: InAppMessage)
func messageDismissed(message: InAppMessage)
func errorWithMessage(message: InAppMessage)
func messageActionTaken(message: InAppMessage, action: String, name: String)
}
15 changes: 15 additions & 0 deletions Sources/MessagingInApp/Type/InAppMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation
import Gist

typealias GistMessage = Message

public struct InAppMessage: Equatable {
public let messageId: String
public let deliveryId: String? // (Currently taken from Gist's campaignId property). Can be nil when sending test
// in-app messages

internal init(gistMessage: GistMessage) {
self.messageId = gistMessage.messageId
self.deliveryId = gistMessage.gistProperties.campaignId
}
}