1- from datetime import datetime
1+ from datetime import datetime , timedelta
22from uuid import uuid4
33import logging
44import numbers
55import atexit
6+ import hashlib
67
78from dateutil .tz import tzutc
89from six import string_types
910
1011from posthog .utils import guess_timezone , clean
1112from posthog .consumer import Consumer
12- from posthog .request import post
13+ from posthog .request import post , get , APIError
1314from posthog .version import VERSION
15+ from posthog .poller import Poller
1416
1517try :
1618 import queue
1921
2022
2123ID_TYPES = (numbers .Number , string_types )
24+ __LONG_SCALE__ = float (0xFFFFFFFFFFFFFFF )
2225
2326
2427class Client (object ):
@@ -28,10 +31,12 @@ class Client(object):
2831 def __init__ (self , api_key = None , host = None , debug = False ,
2932 max_queue_size = 10000 , send = True , on_error = None , flush_at = 100 ,
3033 flush_interval = 0.5 , gzip = False , max_retries = 3 ,
31- sync_mode = False , timeout = 15 , thread = 1 ):
34+ sync_mode = False , timeout = 15 , thread = 1 , poll_interval = 30 , personal_api_key = None ):
3235 require ('api_key' , api_key , string_types )
3336
3437 self .queue = queue .Queue (max_queue_size )
38+
39+ # api_key: This should be the Team API Key (token), public
3540 self .api_key = api_key
3641 self .on_error = on_error
3742 self .debug = debug
@@ -40,6 +45,11 @@ def __init__(self, api_key=None, host=None, debug=False,
4045 self .host = host
4146 self .gzip = gzip
4247 self .timeout = timeout
48+ self .feature_flags = None
49+ self .poll_interval = poll_interval
50+
51+ # personal_api_key: This should be a generated Personal API Key, private
52+ self .personal_api_key = personal_api_key
4353
4454 if debug :
4555 self .log .setLevel (logging .DEBUG )
@@ -216,6 +226,72 @@ def shutdown(self):
216226 self .flush ()
217227 self .join ()
218228
229+ def _load_feature_flags (self ):
230+ if not self .personal_api_key :
231+ raise ValueError ('You have to specify a personal_api_key to use feature flags.' )
232+
233+ try :
234+ self .feature_flags = get (self .personal_api_key , '/api/feature_flag/' , self .host )['results' ]
235+ except APIError as e :
236+ if e .status == 401 :
237+ raise APIError (
238+ status = 401 ,
239+ message = 'You are using a write-only key with feature flags. ' \
240+ 'To use feature flags, please set a personal_api_key ' \
241+ 'More information: https://posthog.com/docs/api/overview'
242+ )
243+ except Exception as e :
244+ self .log .warning ('[FEATURE FLAGS] Fetching feature flags failed with following error. We will retry in %s seconds.' % self .poll_interval )
245+ self .log .warning (e )
246+
247+ self ._last_feature_flag_poll = datetime .utcnow ().replace (tzinfo = tzutc ())
248+
249+ def load_feature_flags (self ):
250+ self ._load_feature_flags ()
251+ poller = Poller (interval = timedelta (seconds = self .poll_interval ), execute = self ._load_feature_flags )
252+ poller .start ()
253+
254+ def feature_enabled (self , key , distinct_id , default = False ):
255+ require ('key' , key , string_types )
256+ require ('distinct_id' , distinct_id , ID_TYPES )
257+ error = False
258+
259+ if not self .feature_flags :
260+ self .load_feature_flags ()
261+
262+ # If loading in previous line failed
263+ if not self .feature_flags :
264+ response = default
265+ error = True
266+ else :
267+ try :
268+ feature_flag = [flag for flag in self .feature_flags if flag ['key' ] == key ][0 ]
269+ except IndexError :
270+ return default
271+
272+ if feature_flag .get ('is_simple_flag' ):
273+ response = _hash (key , distinct_id ) <= (feature_flag ['rollout_percentage' ] / 100 )
274+ else :
275+ try :
276+ request = get (self .api_key , '/decide/' , self .host , timeout = 1 )
277+ response = key in request ['featureFlags' ]
278+ except Exception as e :
279+ response = default
280+ self .log .warning ('[FEATURE FLAGS] Unable to get data for flag %s, because of the following error:' % key )
281+ self .log .warning (e )
282+ error = True
283+
284+ self .capture (distinct_id , '$feature_flag_called' , {'$feature_flag' : key , '$feature_flag_response' : response })
285+ return response
286+
287+ # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
288+ # Given the same distinct_id and key, it'll always return the same float. These floats are
289+ # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
290+ # we can do _hash(key, distinct_id) < 0.2
291+ def _hash (key , distinct_id ):
292+ hash_key = "%s.%s" % (key , distinct_id )
293+ hash_val = int (hashlib .sha1 (hash_key .encode ("utf-8" )).hexdigest ()[:15 ], 16 )
294+ return hash_val / __LONG_SCALE__
219295
220296def require (name , field , data_type ):
221297 """Require that the named `field` has the right `data_type`"""
0 commit comments