Compare commits

..
Author SHA1 Message Date
James HawkinsandGitHub df0dc10f36 Update README.md 2020-02-18 18:20:20 -08:00
18 changed files with 298 additions and 461 deletions
-27
View File
@@ -1,27 +0,0 @@
name: Backend CI
on:
- pull_request
jobs:
tests:
name: Python tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install requirements.txt dependencies with pip
run: |
python -m pip install -e .
- name: Run posthog tests
run: |
python setup.py test
+2 -2
View File
@@ -1,5 +1,5 @@
# PostHog Python
Please see the main [PostHog docs](https://posthog.com/docs).
Please see the main [PostHog docs](https://github.com/PostHog/posthog/wiki).
Specifically, the [Python integration](https://posthog.com/docs/integrations/python-integration) details.
Specifically, the Python integration details are [here](https://github.com/PostHog/posthog/wiki/Python-integration).
+4 -13
View File
@@ -2,28 +2,19 @@
# Import the library
import posthog
import time
# You can find this key on the /setup page in PostHog
posthog.api_key = ''
posthog.personal_api_key = ''
posthog.api_key = '<your key>'
# Where you host PostHog, with no trailing /.
# You can remove this line if you're using posthog.com
posthog.host = 'http://127.0.0.1:8000'
posthog.host = 'http://127.0.0.1:8000'
# Capture an event
posthog.capture('distinct_id', 'event', {'property1': 'value', 'property2': 'value'})
print(posthog.feature_enabled('beta-feature', 'distinct_id'))
print('sleeping')
time.sleep(45)
print(posthog.feature_enabled('beta-feature', 'distinct_id'))
# # Alias a previous distinct id with a new one
# Alias a previous distinct id with a new one
posthog.alias('distinct_id', 'new_distinct_id')
# # Add properties to the person
# Add properties to the person
posthog.identify('distinct_id', {'email': 'something@something.com'})
+14 -57
View File
@@ -6,27 +6,18 @@ from typing import Optional, Dict, Callable
__version__ = VERSION
"""Settings."""
api_key = None # type: str
host = None # type: str
on_error = None # type: Callable
debug = False # type: bool
send = True # type: bool
sync_mode = False # type: bool
disabled = False # type: bool
personal_api_key = None # type: str
api_key: str = None
host: str = None
on_error: Callable = None
debug: bool = False
send: bool = True
sync_mode:bool = False
default_client = None
def capture(
distinct_id, # type: str,
event, # type: str,
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
):
# type: (...) -> None
def capture(distinct_id: str, event: str, properties: Optional[Dict]=None, context: Optional[Dict]=None,
timestamp: Optional[str]=None, message_id: Optional[str]=None) -> None:
"""
Capture allows you to capture anything a user does within your system, which you can later use in PostHog to find patterns in usage, work out which features to improve or where people are giving up.
@@ -45,14 +36,8 @@ def capture(
"""
_proxy('capture', distinct_id=distinct_id, event=event, properties=properties, context=context, timestamp=timestamp, message_id=message_id)
def identify(
distinct_id, # type: str,
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
):
# type: (...) -> None
def identify(distinct_id: str, properties: Optional[Dict]=None, context: Optional[Dict]=None, timestamp: Optional[str]=None,
message_id=None) -> None:
"""
Identify lets you add metadata on your users so you can more easily identify who they are in PostHog, and even do things like segment users by these properties.
@@ -62,7 +47,7 @@ def identify(
For example:
```python
posthog.identify('distinct id', {
posthog.capture('distinct id', {
'email': 'dwayne@gmail.com',
'name': 'Dwayne Johnson'
})
@@ -75,14 +60,7 @@ def group(*args, **kwargs):
_proxy('group', *args, **kwargs)
def alias(
previous_id, # type: str,
distinct_id, # type: str,
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
):
# type: (...) -> None
def alias(previous_id: str, distinct_id: str, context: Optional[Dict]=None, timestamp: Optional[str]=None, message_id: Optional[str]=None) -> None:
"""
To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call. This will allow you to answer questions like "Which marketing channels leads to users churning after a month?" or "What do users do on our website before signing up?"
@@ -101,25 +79,6 @@ def alias(
"""
_proxy('alias', previous_id=previous_id, distinct_id=distinct_id, context=context, timestamp=timestamp, message_id=message_id)
def feature_enabled(
key, # type: str,
distinct_id, # type: str,
default=False, # type: bool
):
# type: (...) -> bool
"""
Use feature flags to enable or disable features for users.
For example:
```python
if posthog.feature_enabled('beta feature', 'distinct id'):
# do something
```
You can call `posthog.load_feature_flags()` before to make sure you're not doing unexpected requests.
"""
return _proxy('feature_enabled', key=key, distinct_id=distinct_id, default=default)
def page(*args, **kwargs):
"""Send a page call."""
@@ -150,12 +109,10 @@ def shutdown():
def _proxy(method, *args, **kwargs):
"""Create an analytics client if one doesn't exist and send to it."""
global default_client
if disabled:
return None
if not default_client:
default_client = Client(api_key, host=host, debug=debug,
on_error=on_error, send=send,
sync_mode=sync_mode, personal_api_key=personal_api_key)
sync_mode=sync_mode)
fn = getattr(default_client, method)
return fn(*args, **kwargs)
fn(*args, **kwargs)
+66 -92
View File
@@ -1,19 +1,16 @@
from datetime import datetime, timedelta
from datetime import datetime
from uuid import uuid4
import logging
import numbers
import atexit
import hashlib
import requests
from dateutil.tz import tzutc
from six import string_types
from posthog.utils import guess_timezone, clean
from posthog.consumer import Consumer
from posthog.request import batch_post, decide, get, APIError
from posthog.request import post
from posthog.version import VERSION
from posthog.poller import Poller
try:
import queue
@@ -22,7 +19,6 @@ except ImportError:
ID_TYPES = (numbers.Number, string_types)
__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)
class Client(object):
@@ -32,12 +28,10 @@ class Client(object):
def __init__(self, api_key=None, host=None, debug=False,
max_queue_size=10000, send=True, on_error=None, flush_at=100,
flush_interval=0.5, gzip=False, max_retries=3,
sync_mode=False, timeout=15, thread=1, poll_interval=30, personal_api_key=None):
sync_mode=False, timeout=15, thread=1):
require('api_key', api_key, string_types)
self.queue = queue.Queue(max_queue_size)
# api_key: This should be the Team API Key (token), public
self.api_key = api_key
self.on_error = on_error
self.debug = debug
@@ -46,11 +40,6 @@ class Client(object):
self.host = host
self.gzip = gzip
self.timeout = timeout
self.feature_flags = None
self.poll_interval = poll_interval
# personal_api_key: This should be a generated Personal API Key, private
self.personal_api_key = personal_api_key
if debug:
self.log.setLevel(logging.DEBUG)
@@ -89,6 +78,7 @@ class Client(object):
msg = {
'timestamp': timestamp,
'context': context,
'type': 'identify',
'distinct_id': distinct_id,
'$set': properties,
'event': '$identify',
@@ -110,6 +100,7 @@ class Client(object):
'timestamp': timestamp,
'context': context,
'distinct_id': distinct_id,
'type': 'capture',
'event': event,
'messageId': message_id,
}
@@ -130,12 +121,34 @@ class Client(object):
},
'timestamp': timestamp,
'context': context,
'type': 'alias',
'event': '$create_alias'
}
return self._enqueue(msg)
def page(self, distinct_id=None, url=None, properties=None,
def group(self, distinct_id=None, group_id=None, traits=None, context=None,
timestamp=None, message_id=None):
traits = traits or {}
context = context or {}
require('distinct_id', distinct_id, ID_TYPES)
require('group_id', group_id, ID_TYPES)
require('traits', traits, dict)
msg = {
'timestamp': timestamp,
'groupId': group_id,
'context': context,
'distinct_id': distinct_id,
'traits': traits,
'type': 'group',
'messageId': message_id,
}
return self._enqueue(msg)
def page(self, distinct_id=None, category=None, name=None, properties=None,
context=None, timestamp=None, message_id=None):
properties = properties or {}
context = context or {}
@@ -143,15 +156,46 @@ class Client(object):
require('distinct_id', distinct_id, ID_TYPES)
require('properties', properties, dict)
require('url', url, string_types)
properties['$current_url'] = url
if name:
require('name', name, string_types)
if category:
require('category', category, string_types)
msg = {
'event': '$pageview',
'properties': properties,
'timestamp': timestamp,
'category': category,
'context': context,
'distinct_id': distinct_id,
'type': 'page',
'name': name,
'messageId': message_id,
}
return self._enqueue(msg)
def screen(self, distinct_id=None, category=None, name=None, properties=None,
context=None, timestamp=None, message_id=None):
properties = properties or {}
context = context or {}
require('distinct_id', distinct_id, ID_TYPES)
require('properties', properties, dict)
if name:
require('name', name, string_types)
if category:
require('category', category, string_types)
msg = {
'properties': properties,
'timestamp': timestamp,
'category': category,
'context': context,
'distinct_id': distinct_id,
'type': 'screen',
'name': name,
'messageId': message_id,
}
@@ -166,6 +210,7 @@ class Client(object):
if message_id is None:
message_id = uuid4()
require('type', msg['type'], string_types)
require('timestamp', timestamp, datetime)
require('context', msg['context'], dict)
@@ -188,15 +233,15 @@ class Client(object):
return True, msg
if self.sync_mode:
self.log.debug('enqueued with blocking %s.', msg['event'])
batch_post(self.api_key, self.host, gzip=self.gzip,
self.log.debug('enqueued with blocking %s.', msg['type'])
post(self.api_key, self.host, gzip=self.gzip,
timeout=self.timeout, batch=[msg])
return True, msg
try:
self.queue.put(msg, block=False)
self.log.debug('enqueued %s.', msg['event'])
self.log.debug('enqueued %s.', msg['type'])
return True, msg
except queue.Full:
self.log.warning('analytics-python queue is full')
@@ -227,77 +272,6 @@ class Client(object):
self.flush()
self.join()
def _load_feature_flags(self):
if not self.personal_api_key:
self.log.warning('[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags.')
self.feature_flags = []
return
try:
self.feature_flags = get(self.personal_api_key, '/api/feature_flag/', self.host)['results']
except APIError as e:
if e.status == 401:
raise APIError(
status=401,
message='You are using a write-only key with feature flags. ' \
'To use feature flags, please set a personal_api_key ' \
'More information: https://posthog.com/docs/api/overview'
)
except Exception as e:
self.log.warning('[FEATURE FLAGS] Fetching feature flags failed with following error. We will retry in %s seconds.' % self.poll_interval)
self.log.warning(e)
self._last_feature_flag_poll = datetime.utcnow().replace(tzinfo=tzutc())
def load_feature_flags(self):
self._load_feature_flags()
poller = Poller(interval=timedelta(seconds=self.poll_interval), execute=self._load_feature_flags)
poller.start()
def feature_enabled(self, key, distinct_id, default=False):
require('key', key, string_types)
require('distinct_id', distinct_id, ID_TYPES)
if not self.personal_api_key:
self.log.warning('[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags.')
if not self.feature_flags:
self.load_feature_flags()
# If loading in previous line failed
if not self.feature_flags:
response = default
else:
try:
feature_flag = [flag for flag in self.feature_flags if flag['key'] == key][0]
except IndexError:
return default
if feature_flag.get('is_simple_flag'):
response = _hash(key, distinct_id) <= (feature_flag['rollout_percentage'] / 100)
else:
try:
request_data = {
"distinct_id": distinct_id,
"personal_api_key": self.personal_api_key,
}
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
response = key in resp_data['featureFlags']
except Exception as e:
response = default
self.log.warning('[FEATURE FLAGS] Unable to get data for flag %s, because of the following error:' % key)
self.log.warning(e)
self.capture(distinct_id, '$feature_flag_called', {'$feature_flag': key, '$feature_flag_response': response})
return response
# This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
# Given the same distinct_id and key, it'll always return the same float. These floats are
# uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
# we can do _hash(key, distinct_id) < 0.2
def _hash(key, distinct_id):
hash_key = "%s.%s" % (key, distinct_id)
hash_val = int(hashlib.sha1(hash_key.encode("utf-8")).hexdigest()[:15], 16)
return hash_val / __LONG_SCALE__
def require(name, field, data_type):
"""Require that the named `field` has the right `data_type`"""
+2 -2
View File
@@ -4,7 +4,7 @@ import monotonic
import backoff
import json
from posthog.request import batch_post, APIError, DatetimeSerializer
from posthog.request import post, APIError, DatetimeSerializer
try:
from queue import Empty
@@ -128,7 +128,7 @@ class Consumer(Thread):
max_tries=self.retries + 1,
giveup=fatal_exception)
def send_request():
batch_post(self.api_key, self.host, gzip=self.gzip,
post(self.api_key, self.host, gzip=self.gzip,
timeout=self.timeout, batch=batch)
send_request()
-19
View File
@@ -1,19 +0,0 @@
import threading
class Poller(threading.Thread):
def __init__(self, interval, execute, *args, **kwargs):
threading.Thread.__init__(self)
self.daemon = False
self.stopped = threading.Event()
self.interval = interval
self.execute = execute
self.args = args
self.kwargs = kwargs
def stop(self):
self.stopped.set()
self.join()
def run(self):
while not self.stopped.wait(self.interval.total_seconds()):
self.execute(*self.args, **self.kwargs)
+11 -58
View File
@@ -4,30 +4,27 @@ import logging
import json
from gzip import GzipFile
from requests.auth import HTTPBasicAuth
import requests
from requests import sessions
from io import BytesIO
from posthog.version import VERSION
from posthog.utils import remove_trailing_slash
_session = requests.sessions.Session()
DEFAULT_HOST = 'https://app.posthog.com'
USER_AGENT = 'posthog-python/' + VERSION
_session = sessions.Session()
def post(api_key, host=None, path=None, gzip=False, timeout=15, **kwargs):
def post(api_key, host=None, gzip=False, timeout=15, **kwargs):
"""Post the `kwargs` to the API"""
log = logging.getLogger('posthog')
body = kwargs
body["sentAt"] = datetime.utcnow().replace(tzinfo=tzutc()).isoformat()
url = remove_trailing_slash(host or DEFAULT_HOST) + path
url = remove_trailing_slash(host or 'https://t.posthog.com') + '/batch/'
body['api_key'] = api_key
data = json.dumps(body, cls=DatetimeSerializer)
log.debug('making request: %s', data)
headers = {
'Content-Type': 'application/json',
'User-Agent': USER_AGENT
'User-Agent': 'analytics-python/' + VERSION
}
if gzip:
headers['Content-Encoding'] = 'gzip'
@@ -45,68 +42,24 @@ def post(api_key, host=None, path=None, gzip=False, timeout=15, **kwargs):
log.debug('data uploaded successfully')
return res
def decide(api_key, host=None, gzip=False, timeout=15, **kwargs):
"""Post the `kwargs to the decide API endpoint"""
log = logging.getLogger('posthog')
res = post(api_key, host, '/decide/', gzip, timeout, **kwargs)
if res.status_code == 200:
log.debug('Feature flags decided successfully')
return res.json()
try:
payload = res.json()
log.debug('received response: %s', payload)
raise APIError(res.status_code, payload['detail'])
raise APIError(res.status_code, payload['code'], payload['message'])
except ValueError:
raise APIError(res.status_code, res.text)
def batch_post(api_key, host=None, gzip=False, timeout=15, **kwargs):
"""Post the `kwargs` to the batch API endpoint for events"""
log = logging.getLogger('posthog')
res = post(api_key, host, '/batch/', gzip, timeout, **kwargs)
if res.status_code == 200:
log.debug('data uploaded successfully')
return res
try:
payload = res.json()
log.debug('received response: %s', payload)
raise APIError(res.status_code, payload['detail'])
except ValueError:
raise APIError(res.status_code, res.text)
def get(api_key, url, host=None, timeout=None):
log = logging.getLogger('posthog')
url = remove_trailing_slash(host or DEFAULT_HOST) + url
response = requests.get(
url,
headers={
'Authorization': 'Bearer %s' % api_key,
'User-Agent': USER_AGENT
},
timeout=timeout
)
if response.status_code == 200:
return response.json()
try:
payload = response.json()
log.debug('received response: %s', payload)
raise APIError(response.status_code, payload['detail'])
except ValueError:
raise APIError(response.status_code, response.text)
raise APIError(res.status_code, 'unknown', res.text)
class APIError(Exception):
def __init__(self, status, message):
def __init__(self, status, code, message):
self.message = message
self.status = status
self.code = code
def __str__(self):
msg = "[PostHog] {0} ({1})"
return msg.format(self.message, self.status)
msg = "[PostHog] {0}: {1} ({2})"
return msg.format(self.code, self.message, self.status)
class DatetimeSerializer(json.JSONEncoder):
+1 -1
View File
@@ -6,7 +6,7 @@ import sys
def all_names():
for _, modname, _ in pkgutil.iter_modules(__path__):
yield 'posthog.test.' + modname
yield 'analytics.test.' + modname
def all():
+123 -124
View File
@@ -3,24 +3,20 @@ import unittest
import six
import mock
import time
from freezegun import freeze_time
from posthog.version import VERSION
from posthog.client import Client
from posthog.test.utils import TEST_API_KEY
from posthog.request import APIError
class TestClient(unittest.TestCase):
def set_fail(self, e, batch):
def fail(self, e, batch):
"""Mark the failure handler"""
print('FAIL', e, batch)
self.failed = True
def setUp(self):
self.failed = False
self.client = Client(TEST_API_KEY, on_error=self.set_fail)
self.client = Client('testsecret', on_error=self.fail)
def test_requires_api_key(self):
self.assertRaises(AssertionError, Client)
@@ -28,9 +24,9 @@ class TestClient(unittest.TestCase):
def test_empty_flush(self):
self.client.flush()
def test_basic_capture(self):
def test_basic_track(self):
client = self.client
success, msg = client.capture('distinct_id', 'python test event')
success, msg = client.track('distinct_id', 'python test event')
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
@@ -39,14 +35,14 @@ class TestClient(unittest.TestCase):
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertTrue(isinstance(msg['messageId'], str))
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
self.assertEqual(msg['properties']['$lib_version'], VERSION)
self.assertEqual(msg['properties'], {})
self.assertEqual(msg['type'], 'track')
def test_stringifies_distinct_id(self):
# A large number that loses precision in node:
# node -e "console.log(157963456373623802 + 1)" > 157963456373623800
client = self.client
success, msg = client.capture(
success, msg = client.track(
distinct_id=157963456373623802, event='python test event')
client.flush()
self.assertTrue(success)
@@ -54,9 +50,9 @@ class TestClient(unittest.TestCase):
self.assertEqual(msg['distinct_id'], '157963456373623802')
def test_advanced_capture(self):
def test_advanced_track(self):
client = self.client
success, msg = client.capture(
success, msg = client.track(
'distinct_id', 'python test event', {'property': 'value'},
{'ip': '192.168.0.1'}, datetime(2014, 9, 3),
'messageId')
@@ -64,13 +60,14 @@ class TestClient(unittest.TestCase):
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['properties']['property'], 'value')
self.assertEqual(msg['properties'], {'property': 'value'})
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['event'], 'python test event')
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
self.assertEqual(msg['properties']['$lib_version'], VERSION)
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'track')
def test_basic_identify(self):
client = self.client
@@ -79,10 +76,11 @@ class TestClient(unittest.TestCase):
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['$set']['trait'], 'value')
self.assertEqual(msg['traits'], {'trait': 'value'})
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertTrue(isinstance(msg['messageId'], str))
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'identify')
def test_advanced_identify(self):
client = self.client
@@ -94,12 +92,46 @@ class TestClient(unittest.TestCase):
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['$set']['trait'], 'value')
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
self.assertEqual(msg['properties']['$lib_version'], VERSION)
self.assertEqual(msg['traits'], {'trait': 'value'})
self.assertEqual(msg['context']['library'], {
'name': 'analytics-python',
'version': VERSION
})
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'identify')
def test_basic_group(self):
client = self.client
success, msg = client.group('distinct_id', 'groupId')
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['groupId'], 'groupId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'group')
def test_advanced_group(self):
client = self.client
success, msg = client.group(
'distinct_id', 'groupId', {'trait': 'value'}, {'ip': '192.168.0.1'},
datetime(2014, 9, 3), 'messageId')
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['traits'], {'trait': 'value'})
self.assertEqual(msg['context']['library'], {
'name': 'analytics-python',
'version': VERSION
})
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'group')
def test_basic_alias(self):
client = self.client
@@ -107,35 +139,71 @@ class TestClient(unittest.TestCase):
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['properties']['distinct_id'], 'previousId')
self.assertEqual(msg['properties']['alias'], 'distinct_id')
self.assertEqual(msg['previousId'], 'previousId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
def test_basic_page(self):
client = self.client
success, msg = client.page('distinct_id', url='https://posthog.com/contact')
success, msg = client.page('distinct_id', name='name')
self.assertFalse(self.failed)
client.flush()
self.assertTrue(success)
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['properties']['$current_url'], 'https://posthog.com/contact')
self.assertEqual(msg['type'], 'page')
self.assertEqual(msg['name'], 'name')
def test_advanced_page(self):
client = self.client
success, msg = client.page(
'distinct_id', 'https://posthog.com/contact', {'property': 'value'},
'distinct_id', 'category', 'name', {'property': 'value'},
{'ip': '192.168.0.1'}, datetime(2014, 9, 3), 'messageId')
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['properties']['$current_url'], 'https://posthog.com/contact')
self.assertEqual(msg['properties']['property'], 'value')
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
self.assertEqual(msg['properties']['$lib_version'], VERSION)
self.assertEqual(msg['properties'], {'property': 'value'})
self.assertEqual(msg['context']['library'], {
'name': 'analytics-python',
'version': VERSION
})
self.assertEqual(msg['category'], 'category')
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'page')
self.assertEqual(msg['name'], 'name')
def test_basic_screen(self):
client = self.client
success, msg = client.screen('distinct_id', name='name')
client.flush()
self.assertTrue(success)
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'screen')
self.assertEqual(msg['name'], 'name')
def test_advanced_screen(self):
client = self.client
success, msg = client.screen(
'distinct_id', 'category', 'name', {'property': 'value'},
{'ip': '192.168.0.1'}, datetime(2014, 9, 3), 'messageId')
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['properties'], {'property': 'value'})
self.assertEqual(msg['context']['library'], {
'name': 'analytics-python',
'version': VERSION
})
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['category'], 'category')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'screen')
self.assertEqual(msg['name'], 'name')
def test_flush(self):
client = self.client
@@ -162,7 +230,7 @@ class TestClient(unittest.TestCase):
self.assertFalse(consumer.is_alive())
def test_synchronous(self):
client = Client(TEST_API_KEY, sync_mode=True)
client = Client('testsecret', sync_mode=True)
success, message = client.identify('distinct_id')
self.assertFalse(client.consumers)
@@ -170,7 +238,7 @@ class TestClient(unittest.TestCase):
self.assertTrue(success)
def test_overflow(self):
client = Client(TEST_API_KEY, max_queue_size=1)
client = Client('testsecret', max_queue_size=1)
# Ensure consumer thread is no longer uploading
client.join()
@@ -181,26 +249,46 @@ class TestClient(unittest.TestCase):
# Make sure we are informed that the queue is at capacity
self.assertFalse(success)
def test_success_on_invalid_api_key(self):
client = Client('bad_key', on_error=self.fail)
client.track('distinct_id', 'event')
client.flush()
self.assertFalse(self.failed)
def test_unicode(self):
Client(six.u('unicode_key'))
def test_numeric_distinct_id(self):
self.client.capture(1234, 'python event')
self.client.track(1234, 'python event')
self.client.flush()
self.assertFalse(self.failed)
def test_debug(self):
Client('bad_key', debug=True)
def test_identify_with_date_object(self):
client = self.client
success, msg = client.identify(
'distinct_id',
{
'birthdate': date(1981, 2, 2),
},
)
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['traits'], {'birthdate': date(1981, 2, 2)})
def test_gzip(self):
client = Client(TEST_API_KEY, on_error=self.fail, gzip=True)
client = Client('testsecret', on_error=self.fail, gzip=True)
for _ in range(10):
client.identify('distinct_id', {'trait': 'value'})
client.flush()
self.assertFalse(self.failed)
def test_user_defined_flush_at(self):
client = Client(TEST_API_KEY, on_error=self.fail,
client = Client('testsecret', on_error=self.fail,
flush_at=10, flush_interval=3)
def mock_post_fn(*args, **kwargs):
@@ -208,7 +296,7 @@ class TestClient(unittest.TestCase):
# the post function should be called 2 times, with a batch size of 10
# each time.
with mock.patch('posthog.consumer.batch_post', side_effect=mock_post_fn) \
with mock.patch('analytics.consumer.post', side_effect=mock_post_fn) \
as mock_post:
for _ in range(20):
client.identify('distinct_id', {'trait': 'value'})
@@ -216,100 +304,11 @@ class TestClient(unittest.TestCase):
self.assertEquals(mock_post.call_count, 2)
def test_user_defined_timeout(self):
client = Client(TEST_API_KEY, timeout=10)
client = Client('testsecret', timeout=10)
for consumer in client.consumers:
self.assertEquals(consumer.timeout, 10)
def test_default_timeout_15(self):
client = Client(TEST_API_KEY)
client = Client('testsecret')
for consumer in client.consumers:
self.assertEquals(consumer.timeout, 15)
@mock.patch('posthog.client.Poller')
@mock.patch('posthog.client.get')
def test_load_feature_flags(self, patch_get, patch_poll):
patch_get.return_value = {
'results': [{
'id': 1,
'name': 'Beta Feature',
'key': 'beta-feature'
}]
}
client = Client(TEST_API_KEY, personal_api_key='test')
with freeze_time('2020-01-01T12:01:00.0000Z'):
client.load_feature_flags()
self.assertEqual(client.feature_flags[0]['key'], 'beta-feature')
self.assertEqual(client._last_feature_flag_poll.isoformat(), "2020-01-01T12:01:00+00:00")
self.assertEqual(patch_poll.call_count, 1)
def test_load_feature_flags_wrong_key(self):
client = Client(TEST_API_KEY, personal_api_key=TEST_API_KEY)
with freeze_time('2020-01-01T12:01:00.0000Z'):
self.assertRaises(APIError, client.load_feature_flags)
@mock.patch('posthog.client.get')
def test_feature_enabled_simple(self, patch_get):
client = Client(TEST_API_KEY)
client.feature_flags = [{
'id': 1,
'name': 'Beta Feature',
'key': 'beta-feature',
'is_simple_flag': True,
'rollout_percentage': 100
}]
self.assertTrue(client.feature_enabled('beta-feature', 'distinct_id'))
@mock.patch('posthog.client.decide')
def test_feature_enabled_request(self, patch_get):
patch_get.return_value = {
'featureFlags': ['beta-feature']
}
client = Client(TEST_API_KEY)
client.feature_flags = [{
'id': 1,
'name': 'Beta Feature',
'key': 'beta-feature',
'is_simple_flag': False,
'rollout_percentage': 100
}]
self.assertTrue(client.feature_enabled('beta-feature', 'distinct_id'))
@mock.patch('posthog.client.Poller')
@mock.patch('posthog.client.get')
def test_feature_enabled_doesnt_exist(self, patch_get, patch_poll):
client = Client(TEST_API_KEY, personal_api_key='test')
client.feature_flags = []
self.assertFalse(client.feature_enabled('doesnt-exist', 'distinct_id'))
self.assertTrue(client.feature_enabled('doesnt-exist', 'distinct_id', True))
@mock.patch('posthog.client.Poller')
@mock.patch('posthog.client.get')
def test_personal_api_key_doesnt_exist(self, patch_get, patch_poll):
client = Client(TEST_API_KEY)
client.feature_flags = []
self.assertFalse(client.feature_enabled('doesnt-exist', 'distinct_id'))
self.assertTrue(client.feature_enabled('doesnt-exist', 'distinct_id', True))
@mock.patch('posthog.client.Poller')
@mock.patch('posthog.client.get')
def test_load_feature_flags_error(self, patch_get, patch_poll):
def raise_effect():
raise Exception('http exception')
patch_get.return_value.raiseError.side_effect = raise_effect
client = Client(TEST_API_KEY, personal_api_key='test')
client.feature_flags = []
self.assertFalse(client.feature_enabled('doesnt-exist', 'distinct_id'))
@mock.patch('posthog.client.Poller')
@mock.patch('posthog.client.get')
def test_call_identify_fails(self, patch_get, patch_poll):
def raise_effect():
raise Exception('http exception')
patch_get.return_value.raiseError.side_effect = raise_effect
client = Client(TEST_API_KEY, personal_api_key='test')
client.feature_flags = [{'key': 'example', 'is_simple_flag': False}]
self.assertFalse(client.feature_enabled('example', 'distinct_id'))
+19 -20
View File
@@ -10,7 +10,6 @@ except ImportError:
from posthog.consumer import Consumer, MAX_MSG_SIZE
from posthog.request import APIError
from posthog.test.utils import TEST_API_KEY
class TestConsumer(unittest.TestCase):
@@ -42,7 +41,7 @@ class TestConsumer(unittest.TestCase):
def test_upload(self):
q = Queue()
consumer = Consumer(q, TEST_API_KEY)
consumer = Consumer(q, 'testsecret')
track = {
'type': 'track',
'event': 'python event',
@@ -58,9 +57,9 @@ class TestConsumer(unittest.TestCase):
# The consumer should upload _n_ times.
q = Queue()
flush_interval = 0.3
consumer = Consumer(q, TEST_API_KEY, flush_at=10,
consumer = Consumer(q, 'testsecret', flush_at=10,
flush_interval=flush_interval)
with mock.patch('posthog.consumer.batch_post') as mock_post:
with mock.patch('analytics.consumer.post') as mock_post:
consumer.start()
for i in range(0, 3):
track = {
@@ -78,9 +77,9 @@ class TestConsumer(unittest.TestCase):
q = Queue()
flush_interval = 0.5
flush_at = 10
consumer = Consumer(q, TEST_API_KEY, flush_at=flush_at,
consumer = Consumer(q, 'testsecret', flush_at=flush_at,
flush_interval=flush_interval)
with mock.patch('posthog.consumer.batch_post') as mock_post:
with mock.patch('analytics.consumer.post') as mock_post:
consumer.start()
for i in range(0, flush_at * 2):
track = {
@@ -93,7 +92,7 @@ class TestConsumer(unittest.TestCase):
self.assertEqual(mock_post.call_count, 2)
def test_request(self):
consumer = Consumer(None, TEST_API_KEY)
consumer = Consumer(None, 'testsecret')
track = {
'type': 'track',
'event': 'python event',
@@ -110,7 +109,7 @@ class TestConsumer(unittest.TestCase):
raise expected_exception
mock_post.call_count = 0
with mock.patch('posthog.consumer.batch_post',
with mock.patch('analytics.consumer.post',
mock.Mock(side_effect=mock_post)):
track = {
'type': 'track',
@@ -136,22 +135,22 @@ class TestConsumer(unittest.TestCase):
def test_request_retry(self):
# we should retry on general errors
consumer = Consumer(None, TEST_API_KEY)
consumer = Consumer(None, 'testsecret')
self._test_request_retry(consumer, Exception('generic exception'), 2)
# we should retry on server errors
consumer = Consumer(None, TEST_API_KEY)
consumer = Consumer(None, 'testsecret')
self._test_request_retry(consumer, APIError(
500, 'Internal Server Error'), 2)
500, 'code', 'Internal Server Error'), 2)
# we should retry on HTTP 429 errors
consumer = Consumer(None, TEST_API_KEY)
consumer = Consumer(None, 'testsecret')
self._test_request_retry(consumer, APIError(
429, 'Too Many Requests'), 2)
429, 'code', 'Too Many Requests'), 2)
# we should NOT retry on other client errors
consumer = Consumer(None, TEST_API_KEY)
api_error = APIError(400, 'Client Errors')
consumer = Consumer(None, 'testsecret')
api_error = APIError(400, 'code', 'Client Errors')
try:
self._test_request_retry(consumer, api_error, 1)
except APIError:
@@ -160,19 +159,19 @@ class TestConsumer(unittest.TestCase):
self.fail('request() should not retry on client errors')
# test for number of exceptions raise > retries value
consumer = Consumer(None, TEST_API_KEY, retries=3)
consumer = Consumer(None, 'testsecret', retries=3)
self._test_request_retry(consumer, APIError(
500, 'Internal Server Error'), 3)
500, 'code', 'Internal Server Error'), 3)
def test_pause(self):
consumer = Consumer(None, TEST_API_KEY)
consumer = Consumer(None, 'testsecret')
consumer.pause()
self.assertFalse(consumer.running)
def test_max_batch_size(self):
q = Queue()
consumer = Consumer(
q, TEST_API_KEY, flush_at=100000, flush_interval=3)
q, 'testsecret', flush_at=100000, flush_interval=3)
track = {
'type': 'track',
'event': 'python event',
@@ -190,7 +189,7 @@ class TestConsumer(unittest.TestCase):
% len(data.encode()))
return res
with mock.patch('posthog.request._session.post',
with mock.patch('analytics.request._session.post',
side_effect=mock_post_fn) as mock_post:
consumer.start()
for _ in range(0, n_msgs + 2):
+24 -16
View File
@@ -1,6 +1,6 @@
import unittest
import posthog
import analytics
class TestModule(unittest.TestCase):
@@ -10,32 +10,40 @@ class TestModule(unittest.TestCase):
def setUp(self):
self.failed = False
posthog.api_key = 'testsecret'
posthog.on_error = self.failed
analytics.api_key = 'testsecret'
analytics.on_error = self.failed
def test_no_api_key(self):
posthog.api_key = None
self.assertRaises(Exception, posthog.capture)
analytics.api_key = None
self.assertRaises(Exception, analytics.track)
def test_no_host(self):
posthog.host = None
self.assertRaises(Exception, posthog.capture)
analytics.host = None
self.assertRaises(Exception, analytics.track)
def test_track(self):
posthog.capture('distinct_id', 'python module event')
posthog.flush()
analytics.track('distinct_id', 'python module event')
analytics.flush()
def test_identify(self):
posthog.identify('distinct_id', {'email': 'user@email.com'})
posthog.flush()
analytics.identify('distinct_id', {'email': 'user@email.com'})
analytics.flush()
def test_group(self):
analytics.group('distinct_id', 'groupId')
analytics.flush()
def test_alias(self):
posthog.alias('previousId', 'distinct_id')
posthog.flush()
analytics.alias('previousId', 'distinct_id')
analytics.flush()
def test_page(self):
posthog.page('distinct_id', 'https://posthog.com/contact')
posthog.flush()
analytics.page('distinct_id')
analytics.flush()
def test_screen(self):
analytics.screen('distinct_id')
analytics.flush()
def test_flush(self):
posthog.flush()
analytics.flush()
+6 -7
View File
@@ -3,14 +3,13 @@ import unittest
import json
import requests
from posthog.request import batch_post, DatetimeSerializer
from posthog.test.utils import TEST_API_KEY
from posthog.request import post, DatetimeSerializer
class TestRequests(unittest.TestCase):
def test_valid_request(self):
res = batch_post(TEST_API_KEY, batch=[{
res = post(batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
@@ -18,11 +17,11 @@ class TestRequests(unittest.TestCase):
self.assertEqual(res.status_code, 200)
def test_invalid_request_error(self):
self.assertRaises(Exception, batch_post, 'testsecret',
self.assertRaises(Exception, post, 'testsecret',
'https://t.posthog.com', False, '[{]')
def test_invalid_host(self):
self.assertRaises(Exception, batch_post, 'testsecret',
self.assertRaises(Exception, post, 'testsecret',
't.posthog.com/', batch=[])
def test_datetime_serialization(self):
@@ -38,7 +37,7 @@ class TestRequests(unittest.TestCase):
self.assertEqual(result, expected)
def test_should_not_timeout(self):
res = batch_post(TEST_API_KEY, batch=[{
res = post(batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
@@ -47,7 +46,7 @@ class TestRequests(unittest.TestCase):
def test_should_timeout(self):
with self.assertRaises(requests.ReadTimeout):
batch_post('key', batch=[{
post(batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
-5
View File
@@ -1,6 +1,5 @@
from datetime import date, datetime, timedelta
from decimal import Decimal
from uuid import UUID
import unittest
from dateutil.tz import tzutc
@@ -8,7 +7,6 @@ import six
from posthog import utils
TEST_API_KEY = 'kOOlRy2QlMY9jHZQv0bKz0FZyazBUoY8Arj0lFVNjs4'
class TestUtils(unittest.TestCase):
@@ -51,9 +49,6 @@ class TestUtils(unittest.TestCase):
utils.clean(combined)
self.assertEqual(combined.keys(), pre_clean_keys)
# test UUID separately, as the UUID object doesn't equal its string representation according to Python
self.assertEqual(utils.clean(UUID('12345678123456781234567812345678')), '12345678-1234-5678-1234-567812345678')
def test_clean_with_dates(self):
dict_with_dates = {
'birthdate': date(1980, 1, 1),
-3
View File
@@ -1,7 +1,6 @@
from dateutil.tz import tzlocal, tzutc
from datetime import date, datetime
from decimal import Decimal
from uuid import UUID
import logging
import numbers
@@ -48,8 +47,6 @@ def remove_trailing_slash(host):
def clean(item):
if isinstance(item, Decimal):
return float(item)
if isinstance(item, UUID):
return str(item)
elif isinstance(item, (six.string_types, bool, numbers.Number, datetime,
date, type(None))):
return item
+1 -1
View File
@@ -1 +1 @@
VERSION = '1.1.2'
VERSION = '1.0.8'
+1 -2
View File
@@ -24,8 +24,7 @@ install_requires = [
]
tests_require = [
"mock>=2.0.0",
"freezegun==0.3.15"
"mock>=2.0.0"
]
setup(
+24 -12
View File
@@ -1,4 +1,4 @@
import posthog
import analytics
import argparse
import json
import logging
@@ -12,7 +12,7 @@ def json_hash(str):
if str:
return json.loads(str)
# posthog -method=<method> -posthog-write-key=<posthogWriteKey> [options]
# analytics -method=<method> -posthog-write-key=<posthogWriteKey> [options]
parser = argparse.ArgumentParser(description='send a posthog message')
@@ -45,28 +45,38 @@ def failed(status, msg):
raise Exception(msg)
def capture():
posthog.capture(options.distinct_id, options.event, anonymous_id=options.anonymousId,
def track():
analytics.track(options.distinct_id, options.event, anonymous_id=options.anonymousId,
properties=json_hash(options.properties), context=json_hash(options.context))
def page():
posthog.page(options.distinct_id, name=options.name, anonymous_id=options.anonymousId,
analytics.page(options.distinct_id, name=options.name, anonymous_id=options.anonymousId,
properties=json_hash(options.properties), context=json_hash(options.context))
def screen():
analytics.screen(options.distinct_id, name=options.name, anonymous_id=options.anonymousId,
properties=json_hash(options.properties), context=json_hash(options.context))
def identify():
posthog.identify(options.distinct_id, anonymous_id=options.anonymousId,
analytics.identify(options.distinct_id, anonymous_id=options.anonymousId,
traits=json_hash(options.traits), context=json_hash(options.context))
def group():
analytics.group(options.distinct_id, options.groupId, json_hash(options.traits),
json_hash(options.context), anonymous_id=options.anonymousId)
def unknown():
print()
posthog.api_key = options.writeKey
posthog.on_error = failed
posthog.debug = True
analytics.api_key = options.writeKey
analytics.on_error = failed
analytics.debug = True
log = logging.getLogger('posthog')
ch = logging.StreamHandler()
@@ -74,14 +84,16 @@ ch.setLevel(logging.DEBUG)
log.addHandler(ch)
switcher = {
"capture": capture,
"track": track,
"page": page,
"identify": identify
"screen": screen,
"identify": identify,
"group": group
}
func = switcher.get(options.type)
if func:
func()
posthog.shutdown()
analytics.shutdown()
else:
print("Invalid Message Type " + options.type)