Exemplo n.º 1
0
class CustomerIOBackend(BaseBackend):
    template_name = 'customerevents/customerio.html'

    def __init__(self, SITE_ID, API_KEY, **kwargs):
        self.site_id = SITE_ID
        self.connection = CustomerIO(SITE_ID, API_KEY)
        super(CustomerIOBackend, self).__init__(**kwargs)

    def get_context(self, identity, properties, events, **kwargs):
        if identity.startswith('session:'):
            raise NotImplementedError #anonymous sessions are not implemented
        context = {
            'identify': {
                'id': identity
            },
            'site_id': self.site_id,
            'tracks': list()
        }
        context['identify'].update(properties)
        context['identify'] = json.dumps(context['identify'])
        context.update(kwargs)
        for event_name, event_properties in events:
            context['tracks'].append((event_name, json.dumps(event_properties)))
        return context

    def send(self, identity, properties, aliases, events, request_meta):
        if identity.startswith('session:'): #ignore anonymous sessions
            return
        self.connection.identify(id=identity, **properties)
        for event_name, event_properties in events:
            ep = dict(event_properties)
            ep.update({'customer_id': identity,
                       'name': event_name})
            self.connection.track(**ep)
Exemplo n.º 2
0
class CustomerIoIdentifyOperator(BaseOperator):
    """
    Send Track Event to Segment for a specified user_id and event

    :param csv_file: a CSV (plain or zipped) path with data to be used as event properties.
        One of them *must* be called 'userId' (templated)
    :type csv_file: str
    :param customerio_site_id: The 'site_id' property from Customer.IO account we want to update
    :type customerio_site_id: str
    :param customerio_api_key: The 'api_key' property from Customer.IO account we want to update
    :type customerio_api_key: str
    """
    template_fields = ('csv_file', 'event')
    ui_color = '#ffd700'

    @apply_defaults
    def __init__(self, csv_file, customerio_site_id, customerio_api_key, *args,
                 **kwargs):
        super().__init__(*args, **kwargs)
        self.csv_file = csv_file
        self.cio = CustomerIO(site_id=customerio_site_id,
                              api_key=customerio_api_key)

    def execute(self, context):

        if self.csv_file.endswith('.gz'):
            file_reader = gzip.open(self.csv_file, "rt", newline="")
        else:
            file_reader = open(self.csv_file, 'r')

        csv_reader = csv.DictReader(file_reader)
        for row in csv_reader:
            # converts a csv row into a map: header1 -> value, header2 -> value...
            props = dict(row)
            user_id = props.pop('userId', None)

            if user_id is None:
                self.log.info('No userId set in CSV row: %s >>> Skipping.',
                              props)
                continue

            # fixing numerics types set as strings from csv
            clean_props = dict()
            for key in props:
                clean_props[key] = retype(props.get(key))

            self.log.info('Sending identify for userId %s with properties: %s',
                          user_id, clean_props)

            self.cio.identify(id=user_id, **clean_props)
Exemplo n.º 3
0
    def test_client_connection_handling(self):
        retries = 5
        cio = CustomerIO(site_id="siteid",
                         api_key="apikey",
                         host=self.server.server_address[0],
                         port=self.server.server_port,
                         retries=retries)

        # should not raise exception as i should be less than retries and
        # therefore the last request should return a valid response
        for i in xrange(retries):
            cio.identify(i, fail_count=i)

        # should raise expection as we get invalid responses for all retries
        with self.assertRaises(CustomerIOException):
            cio.identify(retries, fail_count=retries)
    def test_client_connection_handling(self):
        retries = 5
        cio = CustomerIO(
            site_id="siteid",
            api_key="apikey",
            host=self.server.server_address[0],
            port=self.server.server_port,
            retries=retries)

        # should not raise exception as i should be less than retries and 
        # therefore the last request should return a valid response
        for i in xrange(retries):
            cio.identify(i, fail_count=i)

        # should raise expection as we get invalid responses for all retries
        with self.assertRaises(CustomerIOException):
            cio.identify(retries, fail_count=retries)
Exemplo n.º 5
0
class CIOApi(object):
    """Wrapper for CustomerIO python library."""
    def __init__(self):
        super(CIOApi, self).__init__()
        self.cio = CustomerIO(settings.CUSTOMERIO_SITE_ID, settings.CUSTOMERIO_API_KEY)

    def api_call(func):
        """Wrapper for API cals."""
        def wrapped_func(*args, **kwargs):
            args_str = u', '.join(unicode(a) for a in args[1:])
            args_str += u', '.join(unicode(v) for v in kwargs.values())
            logger.debug(u'CustomerIO api call: %s %s' % (func.__name__, args_str))
            if not settings.CUSTOMERIO_ENABLED:
                return None
            try:
                return func(*args, **kwargs)
            except Exception as e:
                logger.exception(e)
        return wrapped_func

    @api_call
    def create_or_update(self, membership):
        self.cio.identify(
            id=membership.id,
            email=membership.user.email,
            created_at=membership.user.date_joined,
            last_login=membership.last_login,
            short_name=membership.get_short_name(),
            full_name=membership.get_full_name(),
            role=membership.get_role_display(),
            account_name=membership.account.name,
            account_plan=membership.account.plan.get_name_display(),
            account_is_trial=membership.account.is_trial(),
            account_is_active=membership.account.is_active,
            account_date_cancel=membership.account.date_cancel,
            account_date_created=membership.account.date_created,
        )

    @api_call
    def track_event(self, membership, name, **kwargs):
        self.cio.track(customer_id=membership.id, name=name, **kwargs)

    @api_call
    def delete(self, membership):
        self.cio.delete(customer_id=membership.id)
class TestCustomerIO(HTTPSTestCase):
    '''Starts server which the client connects to in the following tests'''
    def setUp(self):
        self.cio = CustomerIO(
            site_id='siteid',
            api_key='apikey',
            host=self.server.server_address[0],
            port=self.server.server_port,
            retries=5,
            backoff_factor=0)

        # do not verify the ssl certificate as it is self signed
        # should only be done for tests
        self.cio.http.verify = False

    def _check_request(self, resp, rq, *args, **kwargs):
        request = resp.request
        self.assertEqual(request.method, rq['method'])
        self.assertEqual(json.loads(request.body.decode('utf-8')), rq['body'])
        self.assertEqual(request.headers['Authorization'], rq['authorization'])
        self.assertEqual(request.headers['Content-Type'], rq['content_type'])
        self.assertEqual(int(request.headers['Content-Length']), len(json.dumps(rq['body'])))
        self.assertTrue(request.url.endswith(rq['url_suffix']),
            'url: {} expected suffix: {}'.format(request.url, rq['url_suffix']))


    def test_client_connection_handling(self):
        retries = self.cio.retries
        # should not raise exception as i should be less than retries and 
        # therefore the last request should return a valid response
        for i in range(retries):
            self.cio.identify(i, fail_count=i)

        # should raise expection as we get invalid responses for all retries
        with self.assertRaises(CustomerIOException):
            self.cio.identify(retries, fail_count=retries)


    def test_identify_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1',
            'body': {"name": "john", "email": "*****@*****.**"},
        }))

        self.cio.identify(id=1, name='john', email='*****@*****.**')

        with self.assertRaises(TypeError):
            self.cio.identify(random_attr="some_value")


    def test_track_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/events',
            'body': {"data": {"email": "*****@*****.**"}, "name": "sign_up"},
        }))

        self.cio.track(customer_id=1, name='sign_up', email='*****@*****.**')

        with self.assertRaises(TypeError):
            self.cio.track(random_attr="some_value")


    def test_pageview_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/events',
            'body': {"data": {"referer": "category_1"}, "type": "page", "name": "product_1"},
        }))

        self.cio.pageview(customer_id=1, page='product_1', referer='category_1')

        with self.assertRaises(TypeError):
            self.cio.pageview(random_attr="some_value")


    def test_delete_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'DELETE',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1',
            'body': {},
        }))

        self.cio.delete(customer_id=1)

        with self.assertRaises(TypeError):
            self.cio.delete(random_attr="some_value")


    def test_backfill_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/events',
            'body': {"timestamp": 1234567890, "data": {"email": "*****@*****.**"}, "name": "signup"},
        }))

        self.cio.backfill(customer_id=1, name='signup', timestamp=1234567890, email='*****@*****.**')

        with self.assertRaises(TypeError):
            self.cio.backfill(random_attr="some_value")

    def test_base_url(self):
        test_cases = [
            # host, port, prefix, result
            (None, None, None, 'https://track.customer.io/api/v1'),
            (None, None, 'v2', 'https://track.customer.io/v2'),
            (None, None, '/v2/', 'https://track.customer.io/v2'),
            ('sub.domain.com', 1337, '/v2/', 'https://sub.domain.com:1337/v2'),
            ('/sub.domain.com/', 1337, '/v2/', 'https://sub.domain.com:1337/v2'),
            ('http://sub.domain.com/', 1337, '/v2/', 'https://sub.domain.com:1337/v2'),
        ]

        for host, port, prefix, result in test_cases:
            cio = CustomerIO(host=host, port=port, url_prefix=prefix)
            self.assertEqual(cio.base_url, result)


    def test_device_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_1", "platform":"ios"}}
        }))

        self.cio.add_device(customer_id=1, device_id="device_1", platform="ios")
        with self.assertRaises(TypeError):
            self.cio.add_device(random_attr="some_value")

    def test_device_call_last_used(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_2", "platform": "android", "last_used": 1234567890}}
        }))

        self.cio.add_device(customer_id=1, device_id="device_2", platform="android", last_used=1234567890)

    def test_device_call_valid_platform(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_3", "platform": "notsupported"}}
        }))

        with self.assertRaises(CustomerIOException):
            self.cio.add_device(customer_id=1, device_id="device_3", platform=None)
   
    def test_device_call_has_customer_id(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_4", "platform": "ios"}}
        }))

        with self.assertRaises(CustomerIOException):
            self.cio.add_device(customer_id="", device_id="device_4", platform="ios")
    
    def test_device_call_has_device_id(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_5", "platform": "ios"}}
        }))

        with self.assertRaises(CustomerIOException):
            self.cio.add_device(customer_id=1, device_id="", platform="ios")

    def test_device_delete_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'DELETE',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices/device_1',
            'body': {}
        }))

        self.cio.delete_device(customer_id=1, device_id="device_1")
        with self.assertRaises(TypeError):
            self.cio.delete_device(random_attr="some_value")
    
    def test_suppress_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/suppress',
            'body': {},
        }))

        self.cio.suppress(customer_id=1)

        with self.assertRaises(CustomerIOException):
            self.cio.suppress(None)

    def test_unsuppress_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/unsuppress',
            'body': {},
        }))

        self.cio.unsuppress(customer_id=1)

        with self.assertRaises(CustomerIOException):
            self.cio.unsuppress(None)


    @unittest.skipIf(sys.version_info.major > 2, "python2 specific test case")
    def test_sanitize_py2(self):
        data_in = dict(dt=datetime.fromtimestamp(1234567890))
        data_out = self.cio._sanitize(data_in)
        self.assertEqual(data_out, dict(dt=1234567890))


    @unittest.skipIf(sys.version_info.major < 3, "python3 specific test case")
    def test_sanitize_py3(self):
        from datetime import timezone
        data_in = dict(dt=datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc))
        data_out = self.cio._sanitize(data_in)
        self.assertEqual(data_out, dict(dt=1234567890))
Exemplo n.º 7
0
    def deploy(self):
        instance = self._get_pusher_instance()
        cio = CustomerIO(settings.CUSTOMERIO_SITE_ID, settings.CUSTOMERIO_API_KEY)
        cio.identify(id=self.email, email=self.email)
        instance[self.deploy_id].trigger('info_update', {
            'message': "Creating a new container...",
            'percent': 30
        })
        headers = {
            'content-type': 'application/json',
        }
        # run the container
        ports = self.project.ports.split(' ')
        hostnames = self.project.hostnames.split(' ')
        payload = {
            "image": self.project.image_name,
            "hosts": ["/api/v1/hosts/1/"],
            "ports": ports,
            "command": "",
            "links": "",
            "memory": "",
            "environment": self.project.env_vars,
        }
        if "edx" in self.project.name.lower():
            edx_env = []
            edx_env.append("EDX_LMS_BASE=lms-{0}.demo.appsembler.com".format(self.deploy_id))
            edx_env.append("EDX_PREVIEW_LMS_BASE=lms-{0}.demo.appsembler.com".format(self.deploy_id))
            edx_env.append("EDX_CMS_BASE=cms-{0}.demo.appsembler.com".format(self.deploy_id))
            edx_env.append("INTERCOM_APP_ID={0}".format(settings.INTERCOM_EDX_APP_ID))
            edx_env.append("INTERCOM_APP_SECRET={0}".format(settings.INTERCOM_EDX_APP_SECRET))
            edx_env.append("INTERCOM_USER_EMAIL={0}".format(self.email))
            env_string = " ".join(edx_env)
            env_string = " " + env_string
            payload['environment'] += env_string

        r = requests.post(
            "{0}/api/v1/containers/?username={1}&api_key={2}".format(settings.SHIPYARD_HOST, settings.SHIPYARD_USER, settings.SHIPYARD_KEY),
            data=json.dumps(payload),
            headers=headers
        )
        if r.status_code == 201:
            # This sleep is needed to avoid problems with the API
            time.sleep(3)
            container_uri = urlparse(r.headers['location']).path
            self.remote_container_id = container_uri.split('/')[-2]

            # create the app (for dynamic routing)
            instance[self.deploy_id].trigger('info_update', {
                'message': "Assigning an URL to the app...",
                'percent': 60
            })
            time.sleep(2)
            app_ids = []
            domains = []
            for port, hostname in zip(ports, hostnames):
                domain_name = "{0}.demo.appsembler.com".format(self.deploy_id)
                if hostname:
                    domain_name = "{0}-{1}".format(hostname, domain_name)
                domains.append(domain_name)
                payload = {
                    "name": self.deploy_id,
                    "description": "{0} for {1}".format(self.project.name, self.email),
                    "domain_name": domain_name,
                    "backend_port": port,
                    "protocol": "https" if "443" in self.project.ports else "http",
                    "containers": [container_uri]
                }
                r = requests.post(
                    "{0}/api/v1/applications/?username={1}&api_key={2}".format(settings.SHIPYARD_HOST, settings.SHIPYARD_USER, settings.SHIPYARD_KEY),
                    data=json.dumps(payload),
                    headers=headers
                )
                if r.status_code == 201:
                    app_uri = urlparse(r.headers['location']).path
                    app_ids.append(app_uri.split('/')[-2])
                time.sleep(2)
            self.remote_app_id = " ".join(app_ids)
        status = r.status_code
        time.sleep(1)
        instance[self.deploy_id].trigger('info_update', {
            'message': "Getting information...",
            'percent': 90
        })
        time.sleep(1)
        if status == 201:
            scheme = "https" if "443" in self.project.ports else "http"
            app_urls = []
            for domain in domains:
                app_url = "{0}://{1}".format(scheme, domain)
                app_urls.append(app_url)
            self.url = " ".join(app_urls)
            self.status = 'Completed'
            self.launch_time = timezone.now()
            self.expiration_time = self.expiration_datetime()
            instance[self.deploy_id].trigger('deployment_complete', {
                'app_name': self.project.name,
                'message': "Deployment complete!",
                'app_url': self.url,
                'username': self.project.default_username,
                'password': self.project.default_password
            })
            Event.create(
                event_name="deployed_app",
                email=self.email,
                metadata={
                    'app_name': self.project.name,
                    'app_url': self.url,
                    'deploy_id': self.deploy_id,
                }
            )
            if self.email:
                cio.track(customer_id=self.email,
                          name='app_deploy_complete',
                          app_url=self.url.replace(" ", "\n"),
                          app_name=self.project.name,
                          status_url="http://launcher.appsembler.com" + reverse('deployment_detail', kwargs={'deploy_id': self.deploy_id}),
                          trial_duration=self.project.trial_duration,
                          username=self.project.default_username,
                          password=self.project.default_password
                )
        else:
            self.status = 'Failed'
            error_log = DeploymentErrorLog(deployment=self, http_status=status, error_log=r.text)
            error_log.save()
            send_mail(
                "Deployment failed: {0}".format(self.deploy_id),
                "Error log link: {0}".format(reverse('admin:deployment_deploymenterrorlog_change', args=(error_log.id,))),
                '*****@*****.**',
                ['*****@*****.**', '*****@*****.**']

            )
            instance[self.deploy_id].trigger('deployment_failed', {
                'message': "Deployment failed!",
            })
        self.save()
try:
	import sys
	sys.path.insert(0, '/Users/nick/dev/theconversation')
except:
	print "could not import -- must be running on heroku"

from customerio import CustomerIO
import settings
from lib import userdb

cio = CustomerIO(settings.get('customer_io_site_id'), settings.get('customer_io_api_key'))

users = userdb.get_all()
for user_info in users:
	if user_info and 'user' in user_info.keys() and 'email_address' in user_info.keys() and user_info['email_address'] != "":
		cio.identify(id=user_info['user']['username'], email=user_info['email_address'], name=user_info['user']['fullname'])
		print "added @" + user_info['user']['username']
class TestCustomerIO(HTTPSTestCase):
    '''Starts server which the client connects to in the following tests'''
    def setUp(self):
        self.cio = CustomerIO(
            site_id='siteid',
            api_key='apikey',
            host=self.server.server_address[0],
            port=self.server.server_port,
            retries=5,
            backoff_factor=0)

        # do not verify the ssl certificate as it is self signed
        # should only be done for tests
        self.cio.http.verify = False

    def _check_request(self, resp, rq, *args, **kwargs):
        request = resp.request
        body = request.body.decode('utf-8') if isinstance(request.body, bytes) else request.body
        self.assertEqual(request.method, rq['method'])
        self.assertEqual(json.loads(body), rq['body'])
        self.assertEqual(request.headers['Authorization'], rq['authorization'])
        self.assertEqual(request.headers['Content-Type'], rq['content_type'])
        self.assertEqual(int(request.headers['Content-Length']), len(json.dumps(rq['body'])))
        self.assertTrue(request.url.endswith(rq['url_suffix']),
            'url: {} expected suffix: {}'.format(request.url, rq['url_suffix']))


    def test_client_connection_handling(self):
        retries = self.cio.retries
        # should not raise exception as i should be less than retries and 
        # therefore the last request should return a valid response
        for i in range(retries):
            self.cio.identify(i, fail_count=i)

        # should raise expection as we get invalid responses for all retries
        with self.assertRaises(CustomerIOException):
            self.cio.identify(retries, fail_count=retries)


    def test_identify_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1',
            'body': {"name": "john", "email": "*****@*****.**"},
        }))

        self.cio.identify(id=1, name='john', email='*****@*****.**')

        with self.assertRaises(TypeError):
            self.cio.identify(random_attr="some_value")


    def test_track_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/events',
            'body': {"data": {"email": "*****@*****.**"}, "name": "sign_up"},
        }))

        self.cio.track(customer_id=1, name='sign_up', email='*****@*****.**')

        with self.assertRaises(TypeError):
            self.cio.track(random_attr="some_value")


    def test_pageview_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/events',
            'body': {"data": {"referer": "category_1"}, "type": "page", "name": "product_1"},
        }))

        self.cio.pageview(customer_id=1, page='product_1', referer='category_1')

        with self.assertRaises(TypeError):
            self.cio.pageview(random_attr="some_value")


    def test_delete_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'DELETE',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1',
            'body': {},
        }))

        self.cio.delete(customer_id=1)

        with self.assertRaises(TypeError):
            self.cio.delete(random_attr="some_value")


    def test_backfill_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/events',
            'body': {"timestamp": 1234567890, "data": {"email": "*****@*****.**"}, "name": "signup"},
        }))

        self.cio.backfill(customer_id=1, name='signup', timestamp=1234567890, email='*****@*****.**')

        with self.assertRaises(TypeError):
            self.cio.backfill(random_attr="some_value")

    def test_base_url(self):
        test_cases = [
            # host, port, prefix, result
            (None, None, None, 'https://track.customer.io/api/v1'),
            (None, None, 'v2', 'https://track.customer.io/v2'),
            (None, None, '/v2/', 'https://track.customer.io/v2'),
            ('sub.domain.com', 1337, '/v2/', 'https://sub.domain.com:1337/v2'),
            ('/sub.domain.com/', 1337, '/v2/', 'https://sub.domain.com:1337/v2'),
            ('http://sub.domain.com/', 1337, '/v2/', 'https://sub.domain.com:1337/v2'),
        ]

        for host, port, prefix, result in test_cases:
            cio = CustomerIO(host=host, port=port, url_prefix=prefix)
            self.assertEqual(cio.base_url, result)


    def test_device_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_1", "platform":"ios"}}
        }))

        self.cio.add_device(customer_id=1, device_id="device_1", platform="ios")
        with self.assertRaises(TypeError):
            self.cio.add_device(random_attr="some_value")

    def test_device_call_last_used(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_2", "platform": "android", "last_used": 1234567890}}
        }))

        self.cio.add_device(customer_id=1, device_id="device_2", platform="android", last_used=1234567890)

    def test_device_call_valid_platform(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_3", "platform": "notsupported"}}
        }))

        with self.assertRaises(CustomerIOException):
            self.cio.add_device(customer_id=1, device_id="device_3", platform=None)
   
    def test_device_call_has_customer_id(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_4", "platform": "ios"}}
        }))

        with self.assertRaises(CustomerIOException):
            self.cio.add_device(customer_id="", device_id="device_4", platform="ios")
    
    def test_device_call_has_device_id(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'PUT',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices',
            'body': {"device": {"id": "device_5", "platform": "ios"}}
        }))

        with self.assertRaises(CustomerIOException):
            self.cio.add_device(customer_id=1, device_id="", platform="ios")

    def test_device_delete_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'DELETE',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/devices/device_1',
            'body': {}
        }))

        self.cio.delete_device(customer_id=1, device_id="device_1")
        with self.assertRaises(TypeError):
            self.cio.delete_device(random_attr="some_value")
    
    def test_suppress_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/suppress',
            'body': {},
        }))

        self.cio.suppress(customer_id=1)

        with self.assertRaises(CustomerIOException):
            self.cio.suppress(None)

    def test_unsuppress_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/customers/1/unsuppress',
            'body': {},
        }))

        self.cio.unsuppress(customer_id=1)

        with self.assertRaises(CustomerIOException):
            self.cio.unsuppress(None)

    def test_add_to_segment_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/segments/1/add_customers',
            'body': {'ids': ['1','2','3']},
        }))

        self.cio.add_to_segment(segment_id=1, customer_ids=[1,2,3])

        with self.assertRaises(CustomerIOException):
            self.cio.add_to_segment(None, None)

        with self.assertRaises(CustomerIOException):
            self.cio.add_to_segment(segment_id=1, customer_ids=False)

        with self.assertRaises(CustomerIOException):
            self.cio.add_to_segment(segment_id=1, customer_ids=[False,True,False])

    def test_remove_from_segment_call(self):
        self.cio.http.hooks=dict(response=partial(self._check_request, rq={
            'method': 'POST',
            'authorization': _basic_auth_str('siteid', 'apikey'),
            'content_type': 'application/json',
            'url_suffix': '/segments/1/remove_customers',
            'body': {'ids': ['1','2','3']},
        }))

        self.cio.remove_from_segment(segment_id=1, customer_ids=[1,2,3])

        with self.assertRaises(CustomerIOException):
            self.cio.remove_from_segment(None, None)

        with self.assertRaises(CustomerIOException):
            self.cio.add_to_segment(segment_id=1, customer_ids=False)

        with self.assertRaises(CustomerIOException):
            self.cio.add_to_segment(segment_id=1, customer_ids=[False,True,False])


    @unittest.skipIf(sys.version_info.major > 2, "python2 specific test case")
    def test_sanitize_py2(self):
        data_in = dict(dt=datetime.fromtimestamp(1234567890))
        data_out = self.cio._sanitize(data_in)
        self.assertEqual(data_out, dict(dt=1234567890))


    @unittest.skipIf(sys.version_info.major < 3, "python3 specific test case")
    def test_sanitize_py3(self):
        from datetime import timezone
        data_in = dict(dt=datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc))
        data_out = self.cio._sanitize(data_in)
        self.assertEqual(data_out, dict(dt=1234567890))
Exemplo n.º 10
0
class MixpanelTrack:
    """Wrapper around the official `mixpanel` server-side integration for Mixpanel.

    You can track events and/or set people profiles. Uses
    https://pypi.python.org/pypi/mixpanel under the hood.

    Prepared as `request.mixpanel` for easy handling.
    """

    events: Events
    event_properties: EventProperties
    global_event_props: PropertiesType
    profile_properties: ProfileProperties
    profile_meta_properties: ProfileMetaProperties

    @staticmethod
    def _resolve_events(dotted_name: t.Optional[object] = None) -> Events:
        """Resolve a dotted-name into an Events object."""
        if not dotted_name:
            return Events()
        if not isinstance(dotted_name, str):
            raise ValueError(
                f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}"
            )
        else:
            resolved = DottedNameResolver().resolve(dotted_name)
            if not issubclass(resolved, Events):
                raise ValueError(
                    "class in dotted_name needs to be based on pyramid_mixpanel.Events"
                )
            return resolved()

    @staticmethod
    def _resolve_event_properties(
        dotted_name: t.Optional[object] = None, ) -> EventProperties:
        """Resolve a dotted-name into an EventProperties object."""
        if not dotted_name:
            return EventProperties()
        if not isinstance(dotted_name, str):
            raise ValueError(
                f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}"
            )
        else:
            resolved = DottedNameResolver().resolve(dotted_name)
            if not issubclass(resolved, EventProperties):
                raise ValueError(
                    "class in dotted_name needs to be based on pyramid_mixpanel.EventProperties"
                )
            return resolved()

    @staticmethod
    def _resolve_profile_properties(
        dotted_name: t.Optional[object] = None, ) -> ProfileProperties:
        """Resolve a dotted-name into an ProfileProperties object."""
        if not dotted_name:
            return ProfileProperties()
        if not isinstance(dotted_name, str):
            raise ValueError(
                f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}"
            )
        else:
            resolved = DottedNameResolver().resolve(dotted_name)
            if not issubclass(resolved, ProfileProperties):
                raise ValueError(
                    "class in dotted_name needs to be based on pyramid_mixpanel.ProfileProperties"
                )
            return resolved()

    @staticmethod
    def _resolve_profile_meta_properties(
        dotted_name: t.Optional[object] = None, ) -> ProfileMetaProperties:
        """Resolve a dotted-name into an ProfileMetaProperties object."""
        if not dotted_name:
            return ProfileMetaProperties()
        if not isinstance(dotted_name, str):
            raise ValueError(
                f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}"
            )
        else:
            resolved = DottedNameResolver().resolve(dotted_name)
            if not issubclass(resolved, ProfileMetaProperties):
                raise ValueError(
                    "class in dotted_name needs to be based on pyramid_mixpanel.ProfileMetaProperties"
                )
            return resolved()

    @staticmethod
    def _resolve_consumer(dotted_name: t.Optional[object] = None,
                          use_structlog: t.Optional[bool] = False) -> Consumer:
        """Resolve a dotted-name into a Consumer object."""
        if not dotted_name:
            return PoliteBufferedConsumer(use_structlog)
        if not isinstance(dotted_name, str):
            raise ValueError(
                f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}"
            )
        else:
            resolved = DottedNameResolver().resolve(dotted_name)
            if not (issubclass(resolved, Consumer)
                    or issubclass(resolved, BufferedConsumer)):
                raise ValueError(
                    "class in dotted_name needs to be based on mixpanel.(Buffered)Consumer"
                )
            return resolved()

    def __init__(self,
                 settings: SettingsType,
                 distinct_id=None,
                 global_event_props=None) -> None:
        """Initialize API connector."""
        self.distinct_id = distinct_id

        self.events = self._resolve_events(settings.get("mixpanel.events"))
        self.event_properties = self._resolve_event_properties(
            settings.get("mixpanel.event_properties"))
        self.profile_properties = self._resolve_profile_properties(
            settings.get("mixpanel.profile_properties"))
        self.profile_meta_properties = self._resolve_profile_meta_properties(
            settings.get("mixpanel.profile_meta_properties"))

        use_structlog = settings.get("pyramid_heroku.structlog", False) is True
        consumer = self._resolve_consumer(settings.get("mixpanel.consumer"),
                                          use_structlog)
        if settings.get("mixpanel.token"):
            self.api = Mixpanel(token=settings["mixpanel.token"],
                                consumer=consumer)
        else:
            self.api = Mixpanel(token="testing",
                                consumer=MockedConsumer())  # nosec

        if global_event_props:
            self.global_event_props = global_event_props
        else:
            self.global_event_props = {}

        if (settings.get("customerio.tracking.site_id")
                and settings.get("customerio.tracking.api_key")
                and settings.get("customerio.tracking.region")):
            # This is here because customerio support is an install extra,
            # i.e. it is optional
            from customerio import CustomerIO
            from customerio import Regions

            if settings["customerio.tracking.region"] == "eu":
                region = Regions.EU
            elif settings["customerio.tracking.region"] == "us":
                region = Regions.US
            else:
                raise ValueError("Unknown customer.io region")

            self.cio = CustomerIO(
                settings["customerio.tracking.site_id"],
                settings["customerio.tracking.api_key"],
                region=region,
            )
        else:
            self.cio = None

    @distinct_id_is_required
    def track(self,
              event: Event,
              props: t.Optional[PropertiesType] = None) -> None:
        """Track a Mixpanel event."""
        if event not in self.events.__dict__.values():
            raise ValueError(f"Event '{event}' is not a member of self.events")

        if props:
            props = {**self.global_event_props, **props}
        else:
            props = self.global_event_props
        for prop in props:
            if prop not in self.event_properties.__dict__.values():
                raise ValueError(
                    f"Property '{prop}' is not a member of self.event_properties"
                )

        self.api.track(
            self.distinct_id,
            event.name,
            {prop.name: value
             for (prop, value) in props.items()},
        )
        if self.cio:
            msg = {
                "customer_id": self.distinct_id,
                "name": event.name,
                **{
                    prop.name.replace("$", ""): value
                    for (prop, value) in props.items()
                },
            }

            if self.api._consumer.__class__ == MockedConsumer:
                self.api._consumer.mocked_messages.append(
                    MockedMessage(endpoint="customer.io", msg=msg))
            else:
                self.cio.track(**msg)

    @distinct_id_is_required
    def profile_set(self,
                    props: PropertiesType,
                    meta: t.Optional[PropertiesType] = None) -> None:
        """Set properties to a Profile.

        This creates a profile if one does not yet exist.

        Use `meta` to override are Mixpanel special properties, such as $ip.
        """
        if not meta:
            meta = {}

        for prop in props:
            if prop not in self.profile_properties.__dict__.values():
                raise ValueError(
                    f"Property '{prop}' is not a member of self.profile_properties"
                )

        for prop in meta:
            if prop not in self.profile_meta_properties.__dict__.values():
                raise ValueError(
                    f"Property '{prop}' is not a member of self.profile_meta_properties"
                )

        # mixpanel and customerio expect different date formats
        # so we have to save the props here so we can format them
        # differently later on in the `if self.cio:` block
        customerio_props = deepcopy(props)

        for (prop, value) in props.items():
            if isinstance(value, datetime):
                props[prop] = value.isoformat()

        self.api.people_set(
            self.distinct_id,
            {prop.name: value
             for (prop, value) in props.items()},
            {prop.name: value
             for (prop, value) in meta.items()},
        )
        if self.cio:

            # customer.io expects dates in unix/epoch format
            for prop, value in customerio_props.items():
                if isinstance(value, datetime):
                    customerio_props[prop] = round(value.timestamp())

            # customer.io expects created timestamp as `created_at`
            if customerio_props.get(Property("$created")):
                customerio_props[Property("created_at")] = customerio_props[
                    Property("$created")]
                del customerio_props[Property("$created")]

            msg = {
                "id": self.distinct_id,
                **{
                    prop.name.replace("$", ""): value
                    for (prop, value) in customerio_props.items()
                },
                **{
                    prop.name.replace("$", ""): value
                    for (prop, value) in meta.items()
                },
            }

            if self.api._consumer.__class__ == MockedConsumer:
                self.api._consumer.mocked_messages.append(
                    MockedMessage(endpoint="customer.io", msg=msg))
            else:
                self.cio.identify(**msg)

    @distinct_id_is_required
    def people_append(self,
                      props: PropertiesType,
                      meta: t.Optional[PropertiesType] = None) -> None:
        """Wrap around api.people_append to set distinct_id."""
        if not meta:
            meta = {}

        for prop in props:
            if prop not in self.profile_properties.__dict__.values():
                raise ValueError(
                    f"Property '{prop}' is not a member of self.profile_properties"
                )

        for prop in meta:
            if prop not in self.profile_meta_properties.__dict__.values():
                raise ValueError(
                    f"Property '{prop}' is not a member of self.profile_meta_properties"
                )

        self.api.people_append(
            self.distinct_id,
            {prop.name: value
             for (prop, value) in props.items()},
            {prop.name: value
             for (prop, value) in meta.items()},
        )

    @distinct_id_is_required
    def people_union(self,
                     props: PropertiesType,
                     meta: t.Optional[PropertiesType] = None) -> None:
        """Wrap around api.people_union to set properties."""
        if not meta:
            meta = {}

        for prop in props:
            if prop not in self.profile_properties.__dict__.values():
                raise ValueError(
                    f"Property '{prop}' is not a member of self.profile_properties"
                )
            if not isinstance(props[prop], list):
                raise TypeError(f"Property '{prop}' value is not a list")

        for prop in meta:
            if prop not in self.profile_meta_properties.__dict__.values():
                raise ValueError(
                    f"Property '{prop}' is not a member of self.profile_meta_properties"
                )
            if not isinstance(meta[prop], list):
                raise TypeError(f"Property '{prop}' value is not a list")

        self.api.people_union(
            self.distinct_id,
            {prop.name: value
             for (prop, value) in props.items()},
            {prop.name: value
             for (prop, value) in meta.items()},
        )

    @distinct_id_is_required
    def profile_increment(self, props: t.Dict[Property, int]) -> None:
        """Wrap around api.people_increment to set distinct_id."""
        for prop in props:
            if prop not in self.profile_properties.__dict__.values():
                raise ValueError(
                    f"Property '{prop}' is not a member of self.profile_properties"
                )

        self.api.people_increment(
            self.distinct_id,
            {prop.name: value
             for (prop, value) in props.items()})

    @distinct_id_is_required
    def profile_track_charge(
            self,
            amount: int,
            props: t.Optional[t.Dict[Property, str]] = None) -> None:
        """Wrap around api.people_track_charge to set distinct_id."""
        if not props:
            props = {}

        for prop in props:
            if prop not in self.profile_properties.__dict__.values():
                raise ValueError(
                    f"Property '{prop}' is not a member of self.profile_properties"
                )

        self.api.people_track_charge(
            self.distinct_id,
            amount,
            {prop.name: value
             for (prop, value) in props.items()},
        )