Adds tracking
authorJohannes Trum <johannes@die-trums.de>
Fri, 3 Mar 2017 22:47:41 +0000 (23:47 +0100)
committerJohannes Trum <johannes@die-trums.de>
Fri, 3 Mar 2017 22:47:55 +0000 (23:47 +0100)
resources/language/English/strings.po
resources/language/German/strings.po
resources/lib/KodiHelper.py
resources/lib/MSLHttpRequestHandler.py
resources/lib/UniversalAnalytics/HTTPLog.py [new file with mode: 0644]
resources/lib/UniversalAnalytics/LICENSE [new file with mode: 0644]
resources/lib/UniversalAnalytics/Tracker.py [new file with mode: 0644]
resources/lib/UniversalAnalytics/__init__.py [new file with mode: 0644]
resources/settings.xml

index f1f0ab238dbf5b96d67c2815b2da8061e826b1ea..cd4d8d28191171b24c60dcfe667d43126c051c6c 100644 (file)
@@ -144,3 +144,8 @@ msgstr ""
 msgctxt "#30031"
 msgid "Change library title"
 msgstr ""
+
+msgctxt "#30032"
+msgid "Tracking"
+msgstr ""
+
index 344f0283a19049507f1e1b4b59ebdf7cf7617280..6986359118b433cdd0dad7bef0ac6a1c28642950 100644 (file)
@@ -144,3 +144,7 @@ msgstr "Aus Bibliothek entfernen"
 msgctxt "#30031"
 msgid "Change library title"
 msgstr "Export titel ändern"
+
+msgctxt "#30032"
+msgid "Tracking"
+msgstr "Tracking"
index 04d268e1c861a94b60e3108217d2e5877b7db76b..9fdb8bff9584a28e238807365c5e8c31465dbe4c 100644 (file)
@@ -10,6 +10,8 @@ import xbmcgui
 import xbmcaddon
 import xbmc
 import json
+import uuid
+from UniversalAnalytics import Tracker
 try:
    import cPickle as pickle
 except:
@@ -638,6 +640,9 @@ class KodiHelper:
             self.log(msg='Inputstream addon not found')
             return False
 
+        # track play event
+        self.track_event('playVideo')
+
         # inputstream addon properties
         msl_service_url = 'http://localhost:' + str(self.addon.getSetting('msl_service_port'))
         play_item = xbmcgui.ListItem(path=msl_service_url + '/manifest?id=' + video_id)
@@ -895,3 +900,21 @@ class KodiHelper:
             instance of the Library class
         """
         self.library = library
+
+    def track_event(self, event):
+        """
+        Send a tracking event if tracking is enabled
+        :param event: the string idetifier of the event
+        :return: None
+        """
+        # Check if tracking is enabled
+        enable_tracking = (self.addon.getSetting('enable_logging') == 'true')
+        if enable_tracking:
+            #Get or Create Tracking id
+            tracking_id = self.addon.getSetting('tracking_id')
+            if tracking_id is '':
+                tracking_id = str(uuid.uuid4())
+                self.addon.setSetting('tracking_id', tracking_id)
+            # Send the tracking event
+            tracker = Tracker.create('UA-46081640-5', client_id=tracking_id)
+            tracker.send('event', event)
index 25abf54fdef82843409c125e23e4c935259192b1..e7682fca24eea89642b930361417867780539230 100644 (file)
@@ -33,8 +33,10 @@ class MSLHttpRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
                 self.wfile.write(base64.standard_b64decode(b64license))
                 self.finish()
             else:
+                kodi_helper.log(msg='Error getting License')
                 self.send_response(400)
         else:
+            kodi_helper.log(msg='Error in License Request')
             self.send_response(400)
 
     def do_GET(self):
diff --git a/resources/lib/UniversalAnalytics/HTTPLog.py b/resources/lib/UniversalAnalytics/HTTPLog.py
new file mode 100644 (file)
index 0000000..4fd8893
--- /dev/null
@@ -0,0 +1,121 @@
+#!/usr/bin/python
+###############################################################################
+# Formatting filter for urllib2's HTTPHandler(debuglevel=1) output
+# Copyright (c) 2013, Analytics Pros
+# 
+# This project is free software, distributed under the BSD license. 
+# Analytics Pros offers consulting and integration services if your firm needs 
+# assistance in strategy, implementation, or auditing existing work.
+###############################################################################
+
+
+import sys, re, os
+from cStringIO import StringIO
+
+
+
+class BufferTranslator(object):
+    """ Provides a buffer-compatible interface for filtering buffer content.
+    """
+    parsers = []
+
+    def __init__(self, output):
+        self.output = output
+        self.encoding = getattr(output, 'encoding', None)
+
+    def write(self, content):
+        content = self.translate(content)
+        self.output.write(content)
+
+
+    @staticmethod
+    def stripslashes(content):
+        return content.decode('string_escape')
+
+    @staticmethod
+    def addslashes(content):
+        return content.encode('string_escape')
+
+    def translate(self, line):
+        for pattern, method in self.parsers:
+            match = pattern.match(line)
+            if match:
+                return method(match)
+            
+        return line
+            
+
+
+class LineBufferTranslator(BufferTranslator):
+    """ Line buffer implementation supports translation of line-format input
+        even when input is not already line-buffered. Caches input until newlines 
+        occur, and then dispatches translated input to output buffer.
+    """
+    def __init__(self, *a, **kw):
+        self._linepending = []
+        super(LineBufferTranslator, self).__init__(*a, **kw)
+    
+    def write(self, _input):
+        lines = _input.splitlines(True)
+        for i in range(0, len(lines)):
+            last = i
+            if lines[i].endswith('\n'):
+                prefix = len(self._linepending) and ''.join(self._linepending) or ''
+                self.output.write(self.translate(prefix + lines[i]))
+                del self._linepending[0:]
+                last = -1
+                
+        if last >= 0:
+            self._linepending.append(lines[ last ])
+
+
+    def __del__(self):
+        if len(self._linepending):
+            self.output.write(self.translate(''.join(self._linepending)))
+
+
+class HTTPTranslator(LineBufferTranslator):
+    """ Translates output from |urllib2| HTTPHandler(debuglevel = 1) into
+        HTTP-compatible, readible text structures for human analysis.
+    """
+
+    RE_LINE_PARSER = re.compile(r'^(?:([a-z]+):)\s*(\'?)([^\r\n]*)\2(?:[\r\n]*)$')
+    RE_LINE_BREAK = re.compile(r'(\r?\n|(?:\\r)?\\n)')
+    RE_HTTP_METHOD = re.compile(r'^(POST|GET|HEAD|DELETE|PUT|TRACE|OPTIONS)')
+    RE_PARAMETER_SPACER = re.compile(r'&([a-z0-9]+)=')
+
+    @classmethod
+    def spacer(cls, line):
+        return cls.RE_PARAMETER_SPACER.sub(r' &\1= ', line)
+
+    def translate(self, line):
+
+        parsed = self.RE_LINE_PARSER.match(line)
+
+        if parsed:
+            value = parsed.group(3)
+            stage = parsed.group(1)
+
+            if stage == 'send': # query string is rendered here
+                return '\n# HTTP Request:\n' + self.stripslashes(value)
+            elif stage == 'reply':
+                return '\n\n# HTTP Response:\n' + self.stripslashes(value)
+            elif stage == 'header':
+                return value + '\n'
+            else:
+                return value
+
+
+        return line
+
+
+def consume(outbuffer = None): # Capture standard output
+    sys.stdout = HTTPTranslator(outbuffer or sys.stdout)
+    return sys.stdout
+
+
+if __name__ == '__main__':
+    consume(sys.stdout).write(sys.stdin.read())
+    print '\n'
+
+# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4
diff --git a/resources/lib/UniversalAnalytics/LICENSE b/resources/lib/UniversalAnalytics/LICENSE
new file mode 100644 (file)
index 0000000..9e8e80a
--- /dev/null
@@ -0,0 +1,29 @@
+- BSD 3-Clause license -
+
+Copyright (c) 2013, Analytics Pros
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this
+  list of conditions and the following disclaimer in the documentation and/or
+  other materials provided with the distribution.
+
+* Neither the name of Analytics Pros nor the names of its contributors may be
+  used to endorse or promote products derived from this software without
+  specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/resources/lib/UniversalAnalytics/Tracker.py b/resources/lib/UniversalAnalytics/Tracker.py
new file mode 100644 (file)
index 0000000..cd76e44
--- /dev/null
@@ -0,0 +1,454 @@
+###############################################################################
+# Universal Analytics for Python
+# Copyright (c) 2013, Analytics Pros
+#
+# This project is free software, distributed under the BSD license.
+# Analytics Pros offers consulting and integration services if your firm needs
+# assistance in strategy, implementation, or auditing existing work.
+###############################################################################
+
+from urllib2 import urlopen, build_opener, install_opener
+from urllib2 import Request, HTTPSHandler
+from urllib2 import URLError, HTTPError
+from urllib import urlencode
+
+import random
+import datetime
+import time
+import uuid
+import hashlib
+import socket
+
+
+
+def generate_uuid(basedata = None):
+    """ Provides a _random_ UUID with no input, or a UUID4-format MD5 checksum of any input data provided """
+    if basedata is None:
+        return str(uuid.uuid4())
+    elif isinstance(basedata, basestring):
+        checksum = hashlib.md5(basedata).hexdigest()
+        return '%8s-%4s-%4s-%4s-%12s' % (checksum[0:8], checksum[8:12], checksum[12:16], checksum[16:20], checksum[20:32])
+
+
+class Time(datetime.datetime):
+    """ Wrappers and convenience methods for processing various time representations """
+
+    @classmethod
+    def from_unix(cls, seconds, milliseconds = 0):
+        """ Produce a full |datetime.datetime| object from a Unix timestamp """
+        base = list(time.gmtime(seconds))[0:6]
+        base.append(milliseconds * 1000) # microseconds
+        return cls(* base)
+
+    @classmethod
+    def to_unix(cls, timestamp):
+        """ Wrapper over time module to produce Unix epoch time as a float """
+        if not isinstance(timestamp, datetime.datetime):
+            raise TypeError, 'Time.milliseconds expects a datetime object'
+        base = time.mktime(timestamp.timetuple())
+        return base
+
+    @classmethod
+    def milliseconds_offset(cls, timestamp, now = None):
+        """ Offset time (in milliseconds) from a |datetime.datetime| object to now """
+        if isinstance(timestamp, (int, float)):
+            base = timestamp
+        else:
+            base = cls.to_unix(timestamp)
+            base = base + (timestamp.microsecond / 1000000)
+        if now is None:
+            now = time.time()
+        return (now - base) * 1000
+
+
+
+class HTTPRequest(object):
+    """ URL Construction and request handling abstraction.
+        This is not intended to be used outside this module.
+
+        Automates mapping of persistent state (i.e. query parameters)
+        onto transcient datasets for each query.
+    """
+
+    endpoint = 'https://www.google-analytics.com/collect'
+
+
+    @staticmethod
+    def debug():
+        """ Activate debugging on urllib2 """
+        handler = HTTPSHandler(debuglevel = 1)
+        opener = build_opener(handler)
+        install_opener(opener)
+
+    # Store properties for all requests
+    def __init__(self, user_agent = None, *args, **opts):
+        self.user_agent = user_agent or 'Analytics Pros - Universal Analytics (Python)'
+
+
+    @classmethod
+    def fixUTF8(cls, data): # Ensure proper encoding for UA's servers...
+        """ Convert all strings to UTF-8 """
+        for key in data:
+            if isinstance(data[ key ], basestring):
+                data[ key ] = data[ key ].encode('utf-8')
+        return data
+
+
+
+    # Apply stored properties to the given dataset & POST to the configured endpoint
+    def send(self, data):
+        request = Request(
+                self.endpoint + '?' + urlencode(self.fixUTF8(data)),
+                headers = {
+                    'User-Agent': self.user_agent
+                }
+            )
+        self.open(request)
+
+    def open(self, request):
+        try:
+            return urlopen(request)
+        except HTTPError as e:
+            return False
+        except URLError as e:
+            self.cache_request(request)
+            return False
+
+    def cache_request(self, request):
+        # TODO: implement a proper caching mechanism here for re-transmitting hits
+        # record = (Time.now(), request.get_full_url(), request.get_data(), request.headers)
+        pass
+
+
+
+
+class HTTPPost(HTTPRequest):
+
+    # Apply stored properties to the given dataset & POST to the configured endpoint
+    def send(self, data):
+        request = Request(
+                self.endpoint,
+                data = urlencode(self.fixUTF8(data)),
+                headers = {
+                    'User-Agent': self.user_agent
+                }
+            )
+        self.open(request)
+
+
+
+
+
+
+class Tracker(object):
+    """ Primary tracking interface for Universal Analytics """
+    params = None
+    parameter_alias = {}
+    valid_hittypes = ('pageview', 'event', 'social', 'screenview', 'transaction', 'item', 'exception', 'timing')
+
+
+    @classmethod
+    def alias(cls, typemap, base, *names):
+        """ Declare an alternate (humane) name for a measurement protocol parameter """
+        cls.parameter_alias[ base ] = (typemap, base)
+        for i in names:
+            cls.parameter_alias[ i ] = (typemap, base)
+
+    @classmethod
+    def coerceParameter(cls, name, value = None):
+        if isinstance(name, basestring) and name[0] == '&':
+            return name[1:], str(value)
+        elif name in cls.parameter_alias:
+            typecast, param_name = cls.parameter_alias.get(name)
+            return param_name, typecast(value)
+        else:
+            raise KeyError, 'Parameter "{0}" is not recognized'.format(name)
+
+
+    def payload(self, data):
+        for key, value in data.iteritems():
+            try:
+                yield self.coerceParameter(key, value)
+            except KeyError:
+                continue
+
+
+
+    option_sequence = {
+        'pageview': [ (basestring, 'dp') ],
+        'event': [ (basestring, 'ec'), (basestring, 'ea'), (basestring, 'el'), (int, 'ev') ],
+        'social': [ (basestring, 'sn'), (basestring, 'sa'), (basestring, 'st') ],
+        'timing': [ (basestring, 'utc'), (basestring, 'utv'), (basestring, 'utt'), (basestring, 'utl') ]
+    }
+
+    @classmethod
+    def consume_options(cls, data, hittype, args):
+        """ Interpret sequential arguments related to known hittypes based on declared structures """
+        opt_position = 0
+        data[ 't' ] = hittype # integrate hit type parameter
+        if hittype in cls.option_sequence:
+            for expected_type, optname in cls.option_sequence[ hittype ]:
+                if opt_position < len(args) and isinstance(args[opt_position], expected_type):
+                    data[ optname ] = args[ opt_position ]
+                opt_position += 1
+
+
+
+
+    @classmethod
+    def hittime(cls, timestamp = None, age = None, milliseconds = None):
+        """ Returns an integer represeting the milliseconds offset for a given hit (relative to now) """
+        if isinstance(timestamp, (int, float)):
+            return int(Time.milliseconds_offset(Time.from_unix(timestamp, milliseconds = milliseconds)))
+        if isinstance(timestamp, datetime.datetime):
+            return int(Time.milliseconds_offset(timestamp))
+        if isinstance(age, (int, float)):
+            return int(age * 1000) + (milliseconds or 0)
+
+
+
+    @property
+    def account(self):
+        return self.params.get('tid', None)
+
+
+    def __init__(self, account, name = None, client_id = None, hash_client_id = False, user_id = None, user_agent = None, use_post = True):
+
+        if use_post is False:
+            self.http = HTTPRequest(user_agent = user_agent)
+        else:
+            self.http = HTTPPost(user_agent = user_agent)
+
+        self.params = { 'v': 1, 'tid': account }
+
+        if client_id is None:
+            client_id = generate_uuid()
+
+        self.params[ 'cid' ] = client_id
+
+        self.hash_client_id = hash_client_id
+
+        if user_id is not None:
+            self.params[ 'uid' ] = user_id
+
+
+    def set_timestamp(self, data):
+        """ Interpret time-related options, apply queue-time parameter as needed """
+        if 'hittime' in data: # an absolute timestamp
+            data['qt'] = self.hittime(timestamp = data.pop('hittime', None))
+        if 'hitage' in data: # a relative age (in seconds)
+            data['qt'] = self.hittime(age = data.pop('hitage', None))
+
+
+    def send(self, hittype, *args, **data):
+        """ Transmit HTTP requests to Google Analytics using the measurement protocol """
+
+        if hittype not in self.valid_hittypes:
+            raise KeyError('Unsupported Universal Analytics Hit Type: {0}'.format(repr(hittype)))
+
+        self.set_timestamp(data)
+        self.consume_options(data, hittype, args)
+
+        for item in args: # process dictionary-object arguments of transcient data
+            if isinstance(item, dict):
+                for key, val in self.payload(item):
+                    data[ key ] = val
+
+        for k, v in self.params.iteritems(): # update only absent parameters
+            if k not in data:
+                data[ k ] = v
+
+
+        data = dict(self.payload(data))
+
+        if self.hash_client_id:
+            data[ 'cid' ] = generate_uuid(data[ 'cid' ])
+
+        # Transmit the hit to Google...
+        self.http.send(data)
+
+
+
+
+    # Setting persistent attibutes of the session/hit/etc (inc. custom dimensions/metrics)
+    def set(self, name, value = None):
+        if isinstance(name, dict):
+            for key, value in name.iteritems():
+                try:
+                    param, value = self.coerceParameter(key, value)
+                    self.params[param] = value
+                except KeyError:
+                    pass
+        elif isinstance(name, basestring):
+            try:
+                param, value = self.coerceParameter(name, value)
+                self.params[param] = value
+            except KeyError:
+                pass
+
+
+
+    def __getitem__(self, name):
+        param, value = self.coerceParameter(name, None)
+        return self.params.get(param, None)
+
+    def __setitem__(self, name, value):
+        param, value = self.coerceParameter(name, value)
+        self.params[param] = value
+
+    def __delitem__(self, name):
+        param, value = self.coerceParameter(name, None)
+        if param in self.params:
+            del self.params[param]
+
+def safe_unicode(obj):
+    """ Safe convertion to the Unicode string version of the object """
+    try:
+        return unicode(obj)
+    except UnicodeDecodeError:
+        return obj.decode('utf-8')
+
+
+# Declaring name mappings for Measurement Protocol parameters
+MAX_CUSTOM_DEFINITIONS = 200
+MAX_EC_LISTS = 11  # 1-based index
+MAX_EC_PRODUCTS = 11  # 1-based index
+MAX_EC_PROMOTIONS = 11 # 1-based index
+
+Tracker.alias(int, 'v', 'protocol-version')
+Tracker.alias(safe_unicode, 'cid', 'client-id', 'clientId', 'clientid')
+Tracker.alias(safe_unicode, 'tid', 'trackingId', 'account')
+Tracker.alias(safe_unicode, 'uid', 'user-id', 'userId', 'userid')
+Tracker.alias(safe_unicode, 'uip', 'user-ip', 'userIp', 'ipaddr')
+Tracker.alias(safe_unicode, 'ua', 'userAgent', 'userAgentOverride', 'user-agent')
+Tracker.alias(safe_unicode, 'dp', 'page', 'path')
+Tracker.alias(safe_unicode, 'dt', 'title', 'pagetitle', 'pageTitle' 'page-title')
+Tracker.alias(safe_unicode, 'dl', 'location')
+Tracker.alias(safe_unicode, 'dh', 'hostname')
+Tracker.alias(safe_unicode, 'sc', 'sessioncontrol', 'session-control', 'sessionControl')
+Tracker.alias(safe_unicode, 'dr', 'referrer', 'referer')
+Tracker.alias(int, 'qt', 'queueTime', 'queue-time')
+Tracker.alias(safe_unicode, 't', 'hitType', 'hittype')
+Tracker.alias(int, 'aip', 'anonymizeIp', 'anonIp', 'anonymize-ip')
+
+
+# Campaign attribution
+Tracker.alias(safe_unicode, 'cn', 'campaign', 'campaignName', 'campaign-name')
+Tracker.alias(safe_unicode, 'cs', 'source', 'campaignSource', 'campaign-source')
+Tracker.alias(safe_unicode, 'cm', 'medium', 'campaignMedium', 'campaign-medium')
+Tracker.alias(safe_unicode, 'ck', 'keyword', 'campaignKeyword', 'campaign-keyword')
+Tracker.alias(safe_unicode, 'cc', 'content', 'campaignContent', 'campaign-content')
+Tracker.alias(safe_unicode, 'ci', 'campaignId', 'campaignID', 'campaign-id')
+
+# Technical specs
+Tracker.alias(safe_unicode, 'sr', 'screenResolution', 'screen-resolution', 'resolution')
+Tracker.alias(safe_unicode, 'vp', 'viewport', 'viewportSize', 'viewport-size')
+Tracker.alias(safe_unicode, 'de', 'encoding', 'documentEncoding', 'document-encoding')
+Tracker.alias(int, 'sd', 'colors', 'screenColors', 'screen-colors')
+Tracker.alias(safe_unicode, 'ul', 'language', 'user-language', 'userLanguage')
+
+# Mobile app
+Tracker.alias(safe_unicode, 'an', 'appName', 'app-name', 'app')
+Tracker.alias(safe_unicode, 'cd', 'contentDescription', 'screenName', 'screen-name', 'content-description')
+Tracker.alias(safe_unicode, 'av', 'appVersion', 'app-version', 'version')
+Tracker.alias(safe_unicode, 'aid', 'appID', 'appId', 'application-id', 'app-id', 'applicationId')
+Tracker.alias(safe_unicode, 'aiid', 'appInstallerId', 'app-installer-id')
+
+# Ecommerce
+Tracker.alias(safe_unicode, 'ta', 'affiliation', 'transactionAffiliation', 'transaction-affiliation')
+Tracker.alias(safe_unicode, 'ti', 'transaction', 'transactionId', 'transaction-id')
+Tracker.alias(float, 'tr', 'revenue', 'transactionRevenue', 'transaction-revenue')
+Tracker.alias(float, 'ts', 'shipping', 'transactionShipping', 'transaction-shipping')
+Tracker.alias(float, 'tt', 'tax', 'transactionTax', 'transaction-tax')
+Tracker.alias(safe_unicode, 'cu', 'currency', 'transactionCurrency', 'transaction-currency') # Currency code, e.g. USD, EUR
+Tracker.alias(safe_unicode, 'in', 'item-name', 'itemName')
+Tracker.alias(float, 'ip', 'item-price', 'itemPrice')
+Tracker.alias(float, 'iq', 'item-quantity', 'itemQuantity')
+Tracker.alias(safe_unicode, 'ic', 'item-code', 'sku', 'itemCode')
+Tracker.alias(safe_unicode, 'iv', 'item-variation', 'item-category', 'itemCategory', 'itemVariation')
+
+# Events
+Tracker.alias(safe_unicode, 'ec', 'event-category', 'eventCategory', 'category')
+Tracker.alias(safe_unicode, 'ea', 'event-action', 'eventAction', 'action')
+Tracker.alias(safe_unicode, 'el', 'event-label', 'eventLabel', 'label')
+Tracker.alias(int, 'ev', 'event-value', 'eventValue', 'value')
+Tracker.alias(int, 'ni', 'noninteractive', 'nonInteractive', 'noninteraction', 'nonInteraction')
+
+
+# Social
+Tracker.alias(safe_unicode, 'sa', 'social-action', 'socialAction')
+Tracker.alias(safe_unicode, 'sn', 'social-network', 'socialNetwork')
+Tracker.alias(safe_unicode, 'st', 'social-target', 'socialTarget')
+
+# Exceptions
+Tracker.alias(safe_unicode, 'exd', 'exception-description', 'exceptionDescription', 'exDescription')
+Tracker.alias(int, 'exf', 'exception-fatal', 'exceptionFatal', 'exFatal')
+
+# User Timing
+Tracker.alias(safe_unicode, 'utc', 'timingCategory', 'timing-category')
+Tracker.alias(safe_unicode, 'utv', 'timingVariable', 'timing-variable')
+Tracker.alias(int, 'utt', 'time', 'timingTime', 'timing-time')
+Tracker.alias(safe_unicode, 'utl', 'timingLabel', 'timing-label')
+Tracker.alias(float, 'dns', 'timingDNS', 'timing-dns')
+Tracker.alias(float, 'pdt', 'timingPageLoad', 'timing-page-load')
+Tracker.alias(float, 'rrt', 'timingRedirect', 'timing-redirect')
+Tracker.alias(safe_unicode, 'tcp', 'timingTCPConnect', 'timing-tcp-connect')
+Tracker.alias(safe_unicode, 'srt', 'timingServerResponse', 'timing-server-response')
+
+# Custom dimensions and metrics
+for i in range(0,200):
+    Tracker.alias(safe_unicode, 'cd{0}'.format(i), 'dimension{0}'.format(i))
+    Tracker.alias(int, 'cm{0}'.format(i), 'metric{0}'.format(i))
+
+# Enhanced Ecommerce
+Tracker.alias(str, 'pa')  # Product action
+Tracker.alias(str, 'tcc')  # Coupon code
+Tracker.alias(unicode, 'pal')  # Product action list
+Tracker.alias(int, 'cos')  # Checkout step
+Tracker.alias(str, 'col')  # Checkout step option
+
+Tracker.alias(str, 'promoa')  # Promotion action
+
+for product_index in range(1, MAX_EC_PRODUCTS):
+    Tracker.alias(str, 'pr{0}id'.format(product_index))  # Product SKU
+    Tracker.alias(unicode, 'pr{0}nm'.format(product_index))  # Product name
+    Tracker.alias(unicode, 'pr{0}br'.format(product_index))  # Product brand
+    Tracker.alias(unicode, 'pr{0}ca'.format(product_index))  # Product category
+    Tracker.alias(unicode, 'pr{0}va'.format(product_index))  # Product variant
+    Tracker.alias(str, 'pr{0}pr'.format(product_index))  # Product price
+    Tracker.alias(int, 'pr{0}qt'.format(product_index))  # Product quantity
+    Tracker.alias(str, 'pr{0}cc'.format(product_index))  # Product coupon code
+    Tracker.alias(int, 'pr{0}ps'.format(product_index))  # Product position
+
+    for custom_index in range(MAX_CUSTOM_DEFINITIONS):
+        Tracker.alias(str, 'pr{0}cd{1}'.format(product_index, custom_index))  # Product custom dimension
+        Tracker.alias(int, 'pr{0}cm{1}'.format(product_index, custom_index))  # Product custom metric
+
+    for list_index in range(1, MAX_EC_LISTS):
+        Tracker.alias(str, 'il{0}pi{1}id'.format(list_index, product_index))  # Product impression SKU
+        Tracker.alias(unicode, 'il{0}pi{1}nm'.format(list_index, product_index))  # Product impression name
+        Tracker.alias(unicode, 'il{0}pi{1}br'.format(list_index, product_index))  # Product impression brand
+        Tracker.alias(unicode, 'il{0}pi{1}ca'.format(list_index, product_index))  # Product impression category
+        Tracker.alias(unicode, 'il{0}pi{1}va'.format(list_index, product_index))  # Product impression variant
+        Tracker.alias(int, 'il{0}pi{1}ps'.format(list_index, product_index))  # Product impression position
+        Tracker.alias(int, 'il{0}pi{1}pr'.format(list_index, product_index))  # Product impression price
+
+        for custom_index in range(MAX_CUSTOM_DEFINITIONS):
+            Tracker.alias(str, 'il{0}pi{1}cd{2}'.format(list_index, product_index, custom_index))  # Product impression custom dimension
+            Tracker.alias(int, 'il{0}pi{1}cm{2}'.format(list_index, product_index, custom_index))  # Product impression custom metric
+
+for list_index in range(1, MAX_EC_LISTS):
+    Tracker.alias(unicode, 'il{0}nm'.format(list_index))  # Product impression list name
+
+for promotion_index in range(1, MAX_EC_PROMOTIONS):
+    Tracker.alias(str, 'promo{0}id'.format(promotion_index))  # Promotion ID
+    Tracker.alias(unicode, 'promo{0}nm'.format(promotion_index))  # Promotion name
+    Tracker.alias(str, 'promo{0}cr'.format(promotion_index))  # Promotion creative
+    Tracker.alias(str, 'promo{0}ps'.format(promotion_index))  # Promotion position
+
+
+# Shortcut for creating trackers
+def create(account, *args, **kwargs):
+    return Tracker(account, *args, **kwargs)
+
+# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4
diff --git a/resources/lib/UniversalAnalytics/__init__.py b/resources/lib/UniversalAnalytics/__init__.py
new file mode 100644 (file)
index 0000000..9c0c110
--- /dev/null
@@ -0,0 +1 @@
+import Tracker
\ No newline at end of file
index b5048518d3a7f715364b25d4888c2c010d296ebe..8fdc9b450d1ccc506f4899467a5b00501226d093 100644 (file)
@@ -11,6 +11,8 @@
   </category>
   <category label="30023">
     <setting id="ssl_verification" type="bool" label="30024" default="true"/>
+    <setting id="enable_logging" type="bool" label="30032" default="true"/>
+    <setting id="" value="tracking_id" visible="false"/>
     <setting id="msl_service_port" value="8000" visible="false"/>
     <setting id="msl_service_certificate" visible="false" value="Cr0CCAMSEOVEukALwQ8307Y2+LVP+0MYh/HPkwUijgIwggEKAoIBAQDm875btoWUbGqQD8eAGuBlGY+Pxo8YF1LQR+Ex0pDONMet8EHslcZRBKNQ/09RZFTP0vrYimyYiBmk9GG+S0wB3CRITgweNE15cD33MQYyS3zpBd4z+sCJam2+jj1ZA4uijE2dxGC+gRBRnw9WoPyw7D8RuhGSJ95OEtzg3Ho+mEsxuE5xg9LM4+Zuro/9msz2bFgJUjQUVHo5j+k4qLWu4ObugFmc9DLIAohL58UR5k0XnvizulOHbMMxdzna9lwTw/4SALadEV/CZXBmswUtBgATDKNqjXwokohncpdsWSauH6vfS6FXwizQoZJ9TdjSGC60rUB2t+aYDm74cIuxAgMBAAE6EHRlc3QubmV0ZmxpeC5jb20SgAOE0y8yWw2Win6M2/bw7+aqVuQPwzS/YG5ySYvwCGQd0Dltr3hpik98WijUODUr6PxMn1ZYXOLo3eED6xYGM7Riza8XskRdCfF8xjj7L7/THPbixyn4mULsttSmWFhexzXnSeKqQHuoKmerqu0nu39iW3pcxDV/K7E6aaSr5ID0SCi7KRcL9BCUCz1g9c43sNj46BhMCWJSm0mx1XFDcoKZWhpj5FAgU4Q4e6f+S8eX39nf6D6SJRb4ap7Znzn7preIvmS93xWjm75I6UBVQGo6pn4qWNCgLYlGGCQCUm5tg566j+/g5jvYZkTJvbiZFwtjMW5njbSRwB3W4CrKoyxw4qsJNSaZRTKAvSjTKdqVDXV/U5HK7SaBA6iJ981/aforXbd2vZlRXO/2S+Maa2mHULzsD+S5l4/YGpSt7PnkCe25F+nAovtl/ogZgjMeEdFyd/9YMYjOS4krYmwp3yJ7m9ZzYCQ6I8RQN4x/yLlHG5RH/+WNLNUs6JAZ0fFdCmw="/>
   </category>