X-Git-Url: https://git.mdrn.pl/wl-app.git/blobdiff_plain/53b27422d140022594fc241cca91c3183be57bca..48b2fe9f7c2dc3d9aeaaa6dbfb27c7da4f3235ff:/iOS/Pods/MatomoTracker/MatomoTracker/MatomoTracker.swift diff --git a/iOS/Pods/MatomoTracker/MatomoTracker/MatomoTracker.swift b/iOS/Pods/MatomoTracker/MatomoTracker/MatomoTracker.swift new file mode 100644 index 0000000..60e8e42 --- /dev/null +++ b/iOS/Pods/MatomoTracker/MatomoTracker/MatomoTracker.swift @@ -0,0 +1,390 @@ +import Foundation + +/// The Matomo Tracker is a Swift framework to send analytics to the Matomo server. +/// +/// ## Basic Usage +/// * Use the track methods to track your views, events and more. +final public class MatomoTracker: NSObject { + + /// Defines if the user opted out of tracking. When set to true, every event + /// will be discarded immediately. This property is persisted between app launches. + @objc public var isOptedOut: Bool { + get { + return matomoUserDefaults.optOut + } + set { + matomoUserDefaults.optOut = newValue + } + } + + /// Will be used to associate all future events with a given userID. This property + /// is persisted between app launches. + @objc public var visitorId: String? { + get { + return matomoUserDefaults.visitorUserId + } + set { + matomoUserDefaults.visitorUserId = newValue + visitor = Visitor.current(in: matomoUserDefaults) + } + } + + internal var matomoUserDefaults: MatomoUserDefaults + private let dispatcher: Dispatcher + private var queue: Queue + internal let siteId: String + + internal var dimensions: [CustomDimension] = [] + + internal var customVariables: [CustomVariable] = [] + + /// This logger is used to perform logging of all sorts of Matomo related information. + /// Per default it is a `DefaultLogger` with a `minLevel` of `LogLevel.warning`. You can + /// set your own Logger with a custom `minLevel` or a complete custom logging mechanism. + @objc public var logger: Logger = DefaultLogger(minLevel: .warning) + + /// The `contentBase` is used to build the url of an Event, if the Event hasn't got a url set. + /// This autogenerated url will then have the format /. + /// Per default the `contentBase` is http://. + /// Set the `contentBase` to nil, if you don't want to auto generate a url. + @objc public var contentBase: URL? + + internal static var _sharedInstance: MatomoTracker? + + /// Create and Configure a new Tracker + /// + /// - Parameters: + /// - siteId: The unique site id generated by the server when a new site was created. + /// - queue: The queue to use to store all analytics until it is dispatched to the server. + /// - dispatcher: The dispatcher to use to transmit all analytics to the server. + required public init(siteId: String, queue: Queue, dispatcher: Dispatcher) { + self.siteId = siteId + self.queue = queue + self.dispatcher = dispatcher + self.contentBase = URL(string: "http://\(Application.makeCurrentApplication().bundleIdentifier ?? "unknown")") + self.matomoUserDefaults = MatomoUserDefaults(suiteName: "\(siteId)\(dispatcher.baseURL.absoluteString)") + self.visitor = Visitor.current(in: matomoUserDefaults) + self.session = Session.current(in: matomoUserDefaults) + super.init() + startNewSession() + startDispatchTimer() + } + + /// Create and Configure a new Tracker + /// + /// A volatile memory queue will be used to store the analytics data. All not transmitted data will be lost when the application gets terminated. + /// The URLSessionDispatcher will be used to transmit the data to the server. + /// + /// - Parameters: + /// - siteId: The unique site id generated by the server when a new site was created. + /// - baseURL: The url of the Matomo server. This url has to end in `piwik.php`. + /// - userAgent: An optional parameter for custom user agent. + @objc convenience public init(siteId: String, baseURL: URL, userAgent: String? = nil) { + assert(baseURL.absoluteString.hasSuffix("piwik.php"), "The baseURL is expected to end in piwik.php") + + let queue = MemoryQueue() + let dispatcher = URLSessionDispatcher(baseURL: baseURL, userAgent: userAgent) + self.init(siteId: siteId, queue: queue, dispatcher: dispatcher) + } + + internal func queue(event: Event) { + guard Thread.isMainThread else { + DispatchQueue.main.sync { + self.queue(event: event) + } + return + } + guard !isOptedOut else { return } + logger.verbose("Queued event: \(event)") + queue.enqueue(event: event) + nextEventStartsANewSession = false + } + + // MARK: dispatching + + private let numberOfEventsDispatchedAtOnce = 20 + private(set) var isDispatching = false + + + /// Manually start the dispatching process. You might want to call this method in AppDelegates `applicationDidEnterBackground` to transmit all data + /// whenever the user leaves the application. + @objc public func dispatch() { + guard !isDispatching else { + logger.verbose("MatomoTracker is already dispatching.") + return + } + guard queue.eventCount > 0 else { + logger.info("No need to dispatch. Dispatch queue is empty.") + startDispatchTimer() + return + } + logger.info("Start dispatching events") + isDispatching = true + dispatchBatch() + } + + private func dispatchBatch() { + guard Thread.isMainThread else { + DispatchQueue.main.sync { + self.dispatchBatch() + } + return + } + queue.first(limit: numberOfEventsDispatchedAtOnce) { events in + guard events.count > 0 else { + // there are no more events queued, finish dispatching + self.isDispatching = false + self.startDispatchTimer() + self.logger.info("Finished dispatching events") + return + } + self.dispatcher.send(events: events, success: { + DispatchQueue.main.async { + self.queue.remove(events: events, completion: { + self.logger.info("Dispatched batch of \(events.count) events.") + DispatchQueue.main.async { + self.dispatchBatch() + } + }) + } + }, failure: { error in + self.isDispatching = false + self.startDispatchTimer() + self.logger.warning("Failed dispatching events with error \(error)") + }) + } + } + + // MARK: dispatch timer + + @objc public var dispatchInterval: TimeInterval = 30.0 { + didSet { + startDispatchTimer() + } + } + private var dispatchTimer: Timer? + + private func startDispatchTimer() { + guard Thread.isMainThread else { + DispatchQueue.main.sync { + self.startDispatchTimer() + } + return + } + guard dispatchInterval > 0 else { return } // Discussion: Do we want the possibility to dispatch synchronous? That than would be dispatchInterval = 0 + if let dispatchTimer = dispatchTimer { + dispatchTimer.invalidate() + self.dispatchTimer = nil + } + self.dispatchTimer = Timer.scheduledTimer(timeInterval: dispatchInterval, target: self, selector: #selector(dispatch), userInfo: nil, repeats: false) + } + + internal var visitor: Visitor + internal var session: Session + internal var nextEventStartsANewSession = true + + internal var campaignName: String? = nil + internal var campaignKeyword: String? = nil + + /// Adds the name and keyword for the current campaign. + /// This is usually very helpfull if you use deeplinks into your app. + /// + /// More information on campaigns: [https://matomo.org/docs/tracking-campaigns/](https://matomo.org/docs/tracking-campaigns/) + /// + /// - Parameters: + /// - name: The name of the campaign. + /// - keyword: The keyword of the campaign. + @objc public func trackCampaign(name: String?, keyword: String?) { + campaignName = name + campaignKeyword = keyword + } + + /// There are several ways to track content impressions and interactions manually, semi-automatically and automatically. Please be aware that content impressions will be tracked using bulk tracking which will always send a POST request, even if GET is configured which is the default. For more details have a look at the in-depth guide to Content Tracking. + /// More information on content: [https://matomo.org/docs/content-tracking/](https://matomo.org/docs/content-tracking/) + /// + /// - Parameters: + /// - name: The name of the content. For instance 'Ad Foo Bar' + /// - piece: The actual content piece. For instance the path to an image, video, audio, any text + /// - target: The target of the content. For instance the URL of a landing page + /// - interaction: The name of the interaction with the content. For instance a 'click' + @objc public func trackContentImpression(name: String, piece: String?, target: String?) { + track(Event(tracker: self, action: [], contentName: name, contentPiece: piece, contentTarget: target)) + } + @objc public func trackContentInteraction(name: String, interaction: String, piece: String?, target: String?) { + track(Event(tracker: self, action: [], contentName: name, contentInteraction: interaction, contentPiece: piece, contentTarget: target)) + } +} + +extension MatomoTracker { + /// Starts a new Session + /// + /// Use this function to manually start a new Session. A new Session will be automatically created only on app start. + /// You can use the AppDelegates `applicationWillEnterForeground` to start a new visit whenever the app enters foreground. + public func startNewSession() { + matomoUserDefaults.previousVisit = matomoUserDefaults.currentVisit + matomoUserDefaults.currentVisit = Date() + matomoUserDefaults.totalNumberOfVisits += 1 + self.session = Session.current(in: matomoUserDefaults) + } +} + +extension MatomoTracker { + + /// Tracks a custom Event + /// + /// - Parameter event: The event that should be tracked. + public func track(_ event: Event) { + queue(event: event) + + if (event.campaignName == campaignName && event.campaignKeyword == campaignKeyword) { + campaignName = nil + campaignKeyword = nil + } + } + + /// Tracks a screenview. + /// + /// This method can be used to track hierarchical screen names, e.g. screen/settings/register. Use this to create a hierarchical and logical grouping of screen views in the Matomo web interface. + /// + /// - Parameter view: An array of hierarchical screen names. + /// - Parameter url: The optional url of the page that was viewed. + /// - Parameter dimensions: An optional array of dimensions, that will be set only in the scope of this view. + public func track(view: [String], url: URL? = nil, dimensions: [CustomDimension] = []) { + let event = Event(tracker: self, action: view, url: url, dimensions: dimensions) + queue(event: event) + } + + /// Tracks an event as described here: https://matomo.org/docs/event-tracking/ + /// + /// - Parameters: + /// - category: The Category of the Event + /// - action: The Action of the Event + /// - name: The optional name of the Event + /// - value: The optional value of the Event + /// - dimensions: An optional array of dimensions, that will be set only in the scope of this event. + /// - url: The optional url of the page that was viewed. + public func track(eventWithCategory category: String, action: String, name: String? = nil, value: Float? = nil, dimensions: [CustomDimension] = [], url: URL? = nil) { + let event = Event(tracker: self, action: [], url: url, eventCategory: category, eventAction: action, eventName: name, eventValue: value, dimensions: dimensions) + queue(event: event) + } +} + +extension MatomoTracker { + + /// Tracks a search result page as described here: https://matomo.org/docs/site-search/ + /// + /// - Parameters: + /// - query: The string the user was searching for + /// - category: An optional category which the user was searching in + /// - resultCount: The number of results that were displayed for that search + /// - dimensions: An optional array of dimensions, that will be set only in the scope of this event. + /// - url: The optional url of the page that was viewed. + public func trackSearch(query: String, category: String?, resultCount: Int?, dimensions: [CustomDimension] = [], url: URL? = nil) { + let event = Event(tracker: self, action: [], url: url, searchQuery: query, searchCategory: category, searchResultsCount: resultCount, dimensions: dimensions) + queue(event: event) + } +} + +extension MatomoTracker { + /// Set a permanent custom dimension. + /// + /// Use this method to set a dimension that will be send with every event. This is best for Custom Dimensions in scope "Visit". A typical example could be any device information or the version of the app the visitor is using. + /// + /// For more information on custom dimensions visit https://matomo.org/docs/custom-dimensions/ + /// + /// - Parameter value: The value you want to set for this dimension. + /// - Parameter index: The index of the dimension. A dimension with this index must be setup in the Matomo backend. + @available(*, deprecated, message: "use setDimension: instead") + public func set(value: String, forIndex index: Int) { + let dimension = CustomDimension(index: index, value: value) + remove(dimensionAtIndex: dimension.index) + dimensions.append(dimension) + } + + /// Set a permanent custom dimension. + /// + /// Use this method to set a dimension that will be send with every event. This is best for Custom Dimensions in scope "Visit". A typical example could be any device information or the version of the app the visitor is using. + /// + /// For more information on custom dimensions visit https://matomo.org/docs/custom-dimensions/ + /// + /// - Parameter dimension: The Dimension to set + public func set(dimension: CustomDimension) { + remove(dimensionAtIndex: dimension.index) + dimensions.append(dimension) + } + + /// Set a permanent custom dimension by value and index. + /// + /// This is a convenience alternative to set(dimension:) and calls the exact same functionality. Also, it is accessible from Objective-C. + /// + /// - Parameter value: The value for the new Custom Dimension + /// - Parameter forIndex: The index of the new Custom Dimension + @objc public func setDimension(_ value: String, forIndex index: Int) { + set(dimension: CustomDimension( index: index, value: value )); + } + + /// Removes a previously set custom dimension. + /// + /// Use this method to remove a dimension that was set using the `set(value: String, forDimension index: Int)` method. + /// + /// - Parameter index: The index of the dimension. + @objc public func remove(dimensionAtIndex index: Int) { + dimensions = dimensions.filter({ dimension in + dimension.index != index + }) + } +} + + +extension MatomoTracker { + + /// Set a permanent new Custom Variable. + /// + /// - Parameter dimension: The Custom Variable to set + public func set(customVariable: CustomVariable) { + removeCustomVariable(withIndex: customVariable.index) + customVariables.append(customVariable) + } + + /// Set a permanent new Custom Variable. + /// + /// - Parameter name: The index of the new Custom Variable + /// - Parameter name: The name of the new Custom Variable + /// - Parameter value: The value of the new Custom Variable + @objc public func setCustomVariable(withIndex index: UInt, name: String, value: String) { + set(customVariable: CustomVariable(index: index, name: name, value: value)) + } + + /// Remove a previously set Custom Variable. + /// + /// - Parameter index: The index of the Custom Variable. + @objc public func removeCustomVariable(withIndex index: UInt) { + customVariables = customVariables.filter { $0.index != index } + } +} + +// Objective-c compatibility extension +extension MatomoTracker { + @objc public func track(view: [String], url: URL? = nil) { + track(view: view, url: url, dimensions: []) + } + + @objc public func track(eventWithCategory category: String, action: String, name: String? = nil, number: NSNumber? = nil, url: URL? = nil) { + let value = number == nil ? nil : number!.floatValue + track(eventWithCategory: category, action: action, name: name, value: value, url: url) + } + + @available(*, deprecated, message: "use trackEventWithCategory:action:name:number:url instead") + @objc public func track(eventWithCategory category: String, action: String, name: String? = nil, number: NSNumber? = nil) { + track(eventWithCategory: category, action: action, name: name, number: number, url: nil) + } + + @objc public func trackSearch(query: String, category: String?, resultCount: Int, url: URL? = nil) { + trackSearch(query: query, category: category, resultCount: resultCount, url: url) + }} + +extension MatomoTracker { + public func copyFromOldSharedInstance() { + matomoUserDefaults.copy(from: UserDefaults.standard) + } +}