Compare commits

..
9 Commits
Author SHA1 Message Date
Paolo D'AmicoandGitHub 888457387b Merge pull request #17 from PostHog/v1.1.3 2020-11-23 09:33:50 -06:00
Paolo D'Amico fe45ff2ab0 version bump 2020-11-23 10:08:04 -05:00
Marius AndraandGitHub 221d7f09f3 Do not start Feature Flag polling if no API Key (#15)
* jetbrains idea .gitignore

* do not start the poller if no personal api key
2020-11-23 15:37:52 +01:00
Tim GlaserandGitHub 98d2d4cc05 Merge pull request #12 from PostHog/investigate
Send distinctID to the decide endpoint to determin if user should have features enabled
2020-09-30 14:34:43 +01:00
James Greenhill b7c1572c32 bump version to 1.1.2 2020-09-30 14:25:49 +01:00
James Greenhill 16acf2e278 better testing and org 2020-09-30 14:20:50 +01:00
James Greenhill df9ae05202 fix tests 2020-09-30 14:10:34 +01:00
James Greenhill b05ee3884a Send distinctID to the decide endpoint to determin if user should have features enabled 2020-09-30 13:53:47 +01:00
James GreenhillandGitHub 144a7744e4 Merge pull request #11 from PostHog/none
Default to False if the feature flag key does not exist. None is confusing
2020-09-29 17:06:53 +01:00
8 changed files with 58 additions and 28 deletions
+2
View File
@@ -12,3 +12,5 @@ env
flake8.out
pylint.out
posthog-analytics
.idea
+14 -12
View File
@@ -5,12 +5,13 @@ 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 post, get, APIError
from posthog.request import batch_post, decide, get, APIError
from posthog.version import VERSION
from posthog.poller import Poller
@@ -188,7 +189,7 @@ class Client(object):
if self.sync_mode:
self.log.debug('enqueued with blocking %s.', msg['event'])
post(self.api_key, self.host, gzip=self.gzip,
batch_post(self.api_key, self.host, gzip=self.gzip,
timeout=self.timeout, batch=[msg])
return True, msg
@@ -227,11 +228,6 @@ class Client(object):
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:
@@ -249,6 +245,11 @@ class Client(object):
self._last_feature_flag_poll = datetime.utcnow().replace(tzinfo=tzutc())
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
self._load_feature_flags()
poller = Poller(interval=timedelta(seconds=self.poll_interval), execute=self._load_feature_flags)
poller.start()
@@ -256,7 +257,6 @@ class Client(object):
def feature_enabled(self, key, distinct_id, default=False):
require('key', key, string_types)
require('distinct_id', distinct_id, ID_TYPES)
error = False
if not self.personal_api_key:
self.log.warning('[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags.')
@@ -266,7 +266,6 @@ class Client(object):
# If loading in previous line failed
if not self.feature_flags:
response = default
error = True
else:
try:
feature_flag = [flag for flag in self.feature_flags if flag['key'] == key][0]
@@ -277,13 +276,16 @@ class Client(object):
response = _hash(key, distinct_id) <= (feature_flag['rollout_percentage'] / 100)
else:
try:
request = get(self.api_key, '/decide/', self.host, timeout=1)
response = key in request['featureFlags']
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)
error = True
self.capture(distinct_id, '$feature_flag_called', {'$feature_flag': key, '$feature_flag_response': response})
return response
+2 -2
View File
@@ -4,7 +4,7 @@ import monotonic
import backoff
import json
from posthog.request import post, APIError, DatetimeSerializer
from posthog.request import batch_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():
post(self.api_key, self.host, gzip=self.gzip,
batch_post(self.api_key, self.host, gzip=self.gzip,
timeout=self.timeout, batch=batch)
send_request()
+28 -2
View File
@@ -15,12 +15,13 @@ _session = requests.sessions.Session()
DEFAULT_HOST = 'https://app.posthog.com'
USER_AGENT = 'posthog-python/' + VERSION
def post(api_key, host=None, gzip=False, timeout=15, **kwargs):
def post(api_key, host=None, path=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) + '/batch/'
url = remove_trailing_slash(host or DEFAULT_HOST) + path
body['api_key'] = api_key
data = json.dumps(body, cls=DatetimeSerializer)
log.debug('making request: %s', data)
@@ -44,6 +45,14 @@ def post(api_key, host=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)
@@ -51,6 +60,23 @@ def post(api_key, host=None, gzip=False, timeout=15, **kwargs):
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
+2 -2
View File
@@ -208,7 +208,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.post', side_effect=mock_post_fn) \
with mock.patch('posthog.consumer.batch_post', side_effect=mock_post_fn) \
as mock_post:
for _ in range(20):
client.identify('distinct_id', {'trait': 'value'})
@@ -259,7 +259,7 @@ class TestClient(unittest.TestCase):
}]
self.assertTrue(client.feature_enabled('beta-feature', 'distinct_id'))
@mock.patch('posthog.client.get')
@mock.patch('posthog.client.decide')
def test_feature_enabled_request(self, patch_get):
patch_get.return_value = {
'featureFlags': ['beta-feature']
+3 -3
View File
@@ -60,7 +60,7 @@ class TestConsumer(unittest.TestCase):
flush_interval = 0.3
consumer = Consumer(q, TEST_API_KEY, flush_at=10,
flush_interval=flush_interval)
with mock.patch('posthog.consumer.post') as mock_post:
with mock.patch('posthog.consumer.batch_post') as mock_post:
consumer.start()
for i in range(0, 3):
track = {
@@ -80,7 +80,7 @@ class TestConsumer(unittest.TestCase):
flush_at = 10
consumer = Consumer(q, TEST_API_KEY, flush_at=flush_at,
flush_interval=flush_interval)
with mock.patch('posthog.consumer.post') as mock_post:
with mock.patch('posthog.consumer.batch_post') as mock_post:
consumer.start()
for i in range(0, flush_at * 2):
track = {
@@ -110,7 +110,7 @@ class TestConsumer(unittest.TestCase):
raise expected_exception
mock_post.call_count = 0
with mock.patch('posthog.consumer.post',
with mock.patch('posthog.consumer.batch_post',
mock.Mock(side_effect=mock_post)):
track = {
'type': 'track',
+6 -6
View File
@@ -3,14 +3,14 @@ import unittest
import json
import requests
from posthog.request import post, DatetimeSerializer
from posthog.request import batch_post, DatetimeSerializer
from posthog.test.utils import TEST_API_KEY
class TestRequests(unittest.TestCase):
def test_valid_request(self):
res = post(TEST_API_KEY, batch=[{
res = batch_post(TEST_API_KEY, batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
@@ -18,11 +18,11 @@ class TestRequests(unittest.TestCase):
self.assertEqual(res.status_code, 200)
def test_invalid_request_error(self):
self.assertRaises(Exception, post, 'testsecret',
self.assertRaises(Exception, batch_post, 'testsecret',
'https://t.posthog.com', False, '[{]')
def test_invalid_host(self):
self.assertRaises(Exception, post, 'testsecret',
self.assertRaises(Exception, batch_post, 'testsecret',
't.posthog.com/', batch=[])
def test_datetime_serialization(self):
@@ -38,7 +38,7 @@ class TestRequests(unittest.TestCase):
self.assertEqual(result, expected)
def test_should_not_timeout(self):
res = post(TEST_API_KEY, batch=[{
res = batch_post(TEST_API_KEY, batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
@@ -47,7 +47,7 @@ class TestRequests(unittest.TestCase):
def test_should_timeout(self):
with self.assertRaises(requests.ReadTimeout):
post('key', batch=[{
batch_post('key', batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
+1 -1
View File
@@ -1 +1 @@
VERSION = '1.0.11'
VERSION = '1.1.3'