3 /// The Matomo Tracker is a Swift framework to send analytics to the Matomo server.
6 /// * Use the track methods to track your views, events and more.
7 final public class MatomoTracker: NSObject {
9 /// Defines if the user opted out of tracking. When set to true, every event
10 /// will be discarded immediately. This property is persisted between app launches.
11 @objc public var isOptedOut: Bool {
13 return matomoUserDefaults.optOut
16 matomoUserDefaults.optOut = newValue
20 /// Will be used to associate all future events with a given userID. This property
21 /// is persisted between app launches.
22 @objc public var visitorId: String? {
24 return matomoUserDefaults.visitorUserId
27 matomoUserDefaults.visitorUserId = newValue
28 visitor = Visitor.current(in: matomoUserDefaults)
32 internal var matomoUserDefaults: MatomoUserDefaults
33 private let dispatcher: Dispatcher
34 private var queue: Queue
35 internal let siteId: String
37 internal var dimensions: [CustomDimension] = []
39 internal var customVariables: [CustomVariable] = []
41 /// This logger is used to perform logging of all sorts of Matomo related information.
42 /// Per default it is a `DefaultLogger` with a `minLevel` of `LogLevel.warning`. You can
43 /// set your own Logger with a custom `minLevel` or a complete custom logging mechanism.
44 @objc public var logger: Logger = DefaultLogger(minLevel: .warning)
46 /// The `contentBase` is used to build the url of an Event, if the Event hasn't got a url set.
47 /// This autogenerated url will then have the format <contentBase>/<actions>.
48 /// Per default the `contentBase` is http://<Application Bundle Name>.
49 /// Set the `contentBase` to nil, if you don't want to auto generate a url.
50 @objc public var contentBase: URL?
52 internal static var _sharedInstance: MatomoTracker?
54 /// Create and Configure a new Tracker
57 /// - siteId: The unique site id generated by the server when a new site was created.
58 /// - queue: The queue to use to store all analytics until it is dispatched to the server.
59 /// - dispatcher: The dispatcher to use to transmit all analytics to the server.
60 required public init(siteId: String, queue: Queue, dispatcher: Dispatcher) {
63 self.dispatcher = dispatcher
64 self.contentBase = URL(string: "http://\(Application.makeCurrentApplication().bundleIdentifier ?? "unknown")")
65 self.matomoUserDefaults = MatomoUserDefaults(suiteName: "\(siteId)\(dispatcher.baseURL.absoluteString)")
66 self.visitor = Visitor.current(in: matomoUserDefaults)
67 self.session = Session.current(in: matomoUserDefaults)
73 /// Create and Configure a new Tracker
75 /// A volatile memory queue will be used to store the analytics data. All not transmitted data will be lost when the application gets terminated.
76 /// The URLSessionDispatcher will be used to transmit the data to the server.
79 /// - siteId: The unique site id generated by the server when a new site was created.
80 /// - baseURL: The url of the Matomo server. This url has to end in `piwik.php`.
81 /// - userAgent: An optional parameter for custom user agent.
82 @objc convenience public init(siteId: String, baseURL: URL, userAgent: String? = nil) {
83 assert(baseURL.absoluteString.hasSuffix("piwik.php"), "The baseURL is expected to end in piwik.php")
85 let queue = MemoryQueue()
86 let dispatcher = URLSessionDispatcher(baseURL: baseURL, userAgent: userAgent)
87 self.init(siteId: siteId, queue: queue, dispatcher: dispatcher)
90 internal func queue(event: Event) {
91 guard Thread.isMainThread else {
92 DispatchQueue.main.sync {
93 self.queue(event: event)
97 guard !isOptedOut else { return }
98 logger.verbose("Queued event: \(event)")
99 queue.enqueue(event: event)
100 nextEventStartsANewSession = false
105 private let numberOfEventsDispatchedAtOnce = 20
106 private(set) var isDispatching = false
109 /// Manually start the dispatching process. You might want to call this method in AppDelegates `applicationDidEnterBackground` to transmit all data
110 /// whenever the user leaves the application.
111 @objc public func dispatch() {
112 guard !isDispatching else {
113 logger.verbose("MatomoTracker is already dispatching.")
116 guard queue.eventCount > 0 else {
117 logger.info("No need to dispatch. Dispatch queue is empty.")
121 logger.info("Start dispatching events")
126 private func dispatchBatch() {
127 guard Thread.isMainThread else {
128 DispatchQueue.main.sync {
133 queue.first(limit: numberOfEventsDispatchedAtOnce) { events in
134 guard events.count > 0 else {
135 // there are no more events queued, finish dispatching
136 self.isDispatching = false
137 self.startDispatchTimer()
138 self.logger.info("Finished dispatching events")
141 self.dispatcher.send(events: events, success: {
142 DispatchQueue.main.async {
143 self.queue.remove(events: events, completion: {
144 self.logger.info("Dispatched batch of \(events.count) events.")
145 DispatchQueue.main.async {
150 }, failure: { error in
151 self.isDispatching = false
152 self.startDispatchTimer()
153 self.logger.warning("Failed dispatching events with error \(error)")
158 // MARK: dispatch timer
160 @objc public var dispatchInterval: TimeInterval = 30.0 {
165 private var dispatchTimer: Timer?
167 private func startDispatchTimer() {
168 guard Thread.isMainThread else {
169 DispatchQueue.main.sync {
170 self.startDispatchTimer()
174 guard dispatchInterval > 0 else { return } // Discussion: Do we want the possibility to dispatch synchronous? That than would be dispatchInterval = 0
175 if let dispatchTimer = dispatchTimer {
176 dispatchTimer.invalidate()
177 self.dispatchTimer = nil
179 self.dispatchTimer = Timer.scheduledTimer(timeInterval: dispatchInterval, target: self, selector: #selector(dispatch), userInfo: nil, repeats: false)
182 internal var visitor: Visitor
183 internal var session: Session
184 internal var nextEventStartsANewSession = true
186 internal var campaignName: String? = nil
187 internal var campaignKeyword: String? = nil
189 /// Adds the name and keyword for the current campaign.
190 /// This is usually very helpfull if you use deeplinks into your app.
192 /// More information on campaigns: [https://matomo.org/docs/tracking-campaigns/](https://matomo.org/docs/tracking-campaigns/)
195 /// - name: The name of the campaign.
196 /// - keyword: The keyword of the campaign.
197 @objc public func trackCampaign(name: String?, keyword: String?) {
199 campaignKeyword = keyword
202 /// 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.
203 /// More information on content: [https://matomo.org/docs/content-tracking/](https://matomo.org/docs/content-tracking/)
206 /// - name: The name of the content. For instance 'Ad Foo Bar'
207 /// - piece: The actual content piece. For instance the path to an image, video, audio, any text
208 /// - target: The target of the content. For instance the URL of a landing page
209 /// - interaction: The name of the interaction with the content. For instance a 'click'
210 @objc public func trackContentImpression(name: String, piece: String?, target: String?) {
211 track(Event(tracker: self, action: [], contentName: name, contentPiece: piece, contentTarget: target))
213 @objc public func trackContentInteraction(name: String, interaction: String, piece: String?, target: String?) {
214 track(Event(tracker: self, action: [], contentName: name, contentInteraction: interaction, contentPiece: piece, contentTarget: target))
218 extension MatomoTracker {
219 /// Starts a new Session
221 /// Use this function to manually start a new Session. A new Session will be automatically created only on app start.
222 /// You can use the AppDelegates `applicationWillEnterForeground` to start a new visit whenever the app enters foreground.
223 public func startNewSession() {
224 matomoUserDefaults.previousVisit = matomoUserDefaults.currentVisit
225 matomoUserDefaults.currentVisit = Date()
226 matomoUserDefaults.totalNumberOfVisits += 1
227 self.session = Session.current(in: matomoUserDefaults)
231 extension MatomoTracker {
233 /// Tracks a custom Event
235 /// - Parameter event: The event that should be tracked.
236 public func track(_ event: Event) {
239 if (event.campaignName == campaignName && event.campaignKeyword == campaignKeyword) {
241 campaignKeyword = nil
245 /// Tracks a screenview.
247 /// 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.
249 /// - Parameter view: An array of hierarchical screen names.
250 /// - Parameter url: The optional url of the page that was viewed.
251 /// - Parameter dimensions: An optional array of dimensions, that will be set only in the scope of this view.
252 public func track(view: [String], url: URL? = nil, dimensions: [CustomDimension] = []) {
253 let event = Event(tracker: self, action: view, url: url, dimensions: dimensions)
257 /// Tracks an event as described here: https://matomo.org/docs/event-tracking/
260 /// - category: The Category of the Event
261 /// - action: The Action of the Event
262 /// - name: The optional name of the Event
263 /// - value: The optional value of the Event
264 /// - dimensions: An optional array of dimensions, that will be set only in the scope of this event.
265 /// - url: The optional url of the page that was viewed.
266 public func track(eventWithCategory category: String, action: String, name: String? = nil, value: Float? = nil, dimensions: [CustomDimension] = [], url: URL? = nil) {
267 let event = Event(tracker: self, action: [], url: url, eventCategory: category, eventAction: action, eventName: name, eventValue: value, dimensions: dimensions)
272 extension MatomoTracker {
274 /// Tracks a search result page as described here: https://matomo.org/docs/site-search/
277 /// - query: The string the user was searching for
278 /// - category: An optional category which the user was searching in
279 /// - resultCount: The number of results that were displayed for that search
280 /// - dimensions: An optional array of dimensions, that will be set only in the scope of this event.
281 /// - url: The optional url of the page that was viewed.
282 public func trackSearch(query: String, category: String?, resultCount: Int?, dimensions: [CustomDimension] = [], url: URL? = nil) {
283 let event = Event(tracker: self, action: [], url: url, searchQuery: query, searchCategory: category, searchResultsCount: resultCount, dimensions: dimensions)
288 extension MatomoTracker {
289 /// Set a permanent custom dimension.
291 /// 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.
293 /// For more information on custom dimensions visit https://matomo.org/docs/custom-dimensions/
295 /// - Parameter value: The value you want to set for this dimension.
296 /// - Parameter index: The index of the dimension. A dimension with this index must be setup in the Matomo backend.
297 @available(*, deprecated, message: "use setDimension: instead")
298 public func set(value: String, forIndex index: Int) {
299 let dimension = CustomDimension(index: index, value: value)
300 remove(dimensionAtIndex: dimension.index)
301 dimensions.append(dimension)
304 /// Set a permanent custom dimension.
306 /// 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.
308 /// For more information on custom dimensions visit https://matomo.org/docs/custom-dimensions/
310 /// - Parameter dimension: The Dimension to set
311 public func set(dimension: CustomDimension) {
312 remove(dimensionAtIndex: dimension.index)
313 dimensions.append(dimension)
316 /// Set a permanent custom dimension by value and index.
318 /// This is a convenience alternative to set(dimension:) and calls the exact same functionality. Also, it is accessible from Objective-C.
320 /// - Parameter value: The value for the new Custom Dimension
321 /// - Parameter forIndex: The index of the new Custom Dimension
322 @objc public func setDimension(_ value: String, forIndex index: Int) {
323 set(dimension: CustomDimension( index: index, value: value ));
326 /// Removes a previously set custom dimension.
328 /// Use this method to remove a dimension that was set using the `set(value: String, forDimension index: Int)` method.
330 /// - Parameter index: The index of the dimension.
331 @objc public func remove(dimensionAtIndex index: Int) {
332 dimensions = dimensions.filter({ dimension in
333 dimension.index != index
339 extension MatomoTracker {
341 /// Set a permanent new Custom Variable.
343 /// - Parameter dimension: The Custom Variable to set
344 public func set(customVariable: CustomVariable) {
345 removeCustomVariable(withIndex: customVariable.index)
346 customVariables.append(customVariable)
349 /// Set a permanent new Custom Variable.
351 /// - Parameter name: The index of the new Custom Variable
352 /// - Parameter name: The name of the new Custom Variable
353 /// - Parameter value: The value of the new Custom Variable
354 @objc public func setCustomVariable(withIndex index: UInt, name: String, value: String) {
355 set(customVariable: CustomVariable(index: index, name: name, value: value))
358 /// Remove a previously set Custom Variable.
360 /// - Parameter index: The index of the Custom Variable.
361 @objc public func removeCustomVariable(withIndex index: UInt) {
362 customVariables = customVariables.filter { $0.index != index }
366 // Objective-c compatibility extension
367 extension MatomoTracker {
368 @objc public func track(view: [String], url: URL? = nil) {
369 track(view: view, url: url, dimensions: [])
372 @objc public func track(eventWithCategory category: String, action: String, name: String? = nil, number: NSNumber? = nil, url: URL? = nil) {
373 let value = number == nil ? nil : number!.floatValue
374 track(eventWithCategory: category, action: action, name: name, value: value, url: url)
377 @available(*, deprecated, message: "use trackEventWithCategory:action:name:number:url instead")
378 @objc public func track(eventWithCategory category: String, action: String, name: String? = nil, number: NSNumber? = nil) {
379 track(eventWithCategory: category, action: action, name: name, number: number, url: nil)
382 @objc public func trackSearch(query: String, category: String?, resultCount: Int, url: URL? = nil) {
383 trackSearch(query: query, category: category, resultCount: resultCount, url: url)
386 extension MatomoTracker {
387 public func copyFromOldSharedInstance() {
388 matomoUserDefaults.copy(from: UserDefaults.standard)