Compare commits
47
Commits
1.0.7
...
investigate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7c1572c32 | ||
|
|
16acf2e278 | ||
|
|
df9ae05202 | ||
|
|
b05ee3884a | ||
|
|
144a7744e4 | ||
|
|
ca979f06c8 | ||
|
|
caa64fb6fe | ||
|
|
e007dee07e | ||
|
|
1de9553d61 | ||
|
|
d447d170fa | ||
|
|
d92c398c0f | ||
|
|
b331c4aae3 | ||
|
|
5e70ca84bb | ||
|
|
adef8d4928 | ||
|
|
8682091eec | ||
|
|
8977c4e3ab | ||
|
|
710ac05862 | ||
|
|
721a6aacf7 | ||
|
|
678e4ac97b | ||
|
|
72e7e4ad72 | ||
|
|
95d4375663 | ||
|
|
6e39aa0ceb | ||
|
|
c72ab9a3fd | ||
|
|
6508aa6994 | ||
|
|
3b7f37aa39 | ||
|
|
ac74ee9a5c | ||
|
|
b9e323bf47 | ||
|
|
8fee12f004 | ||
|
|
3c23f0159f | ||
|
|
c944d7df2c | ||
|
|
611e01f9eb | ||
|
|
1840bb8f57 | ||
|
|
cb06d2fd5d | ||
|
|
610fc816f1 | ||
|
|
992ab2b9a7 | ||
|
|
fd68a10e63 | ||
|
|
48c1cf3c02 | ||
|
|
b923116a9e | ||
|
|
0eed53222c | ||
|
|
1670901156 | ||
|
|
ab65f8f0f8 | ||
|
|
133e1a991c | ||
|
|
34e92615c2 | ||
|
|
cf09644d4f | ||
|
|
0cfce57d0e | ||
|
|
e9862cf671 | ||
|
|
7a93a99541 |
@@ -0,0 +1,27 @@
|
||||
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
|
||||
@@ -1,5 +1,7 @@
|
||||
Copyright (c) 2020 PostHog (part of Hiberly Inc)
|
||||
|
||||
Copyright (c) 2013 Segment Inc. friends@segment.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
|
||||
@@ -1,118 +1,5 @@
|
||||
# PostHog Python
|
||||
|
||||
Official PostHog Python library to capture and send events to any PostHog instance (including PostHog.com).
|
||||
Please see the main [PostHog docs](https://posthog.com/docs).
|
||||
|
||||
This library uses an internal queue to make calls non-blocking and fast. It also batches requests and flushes asynchronously, making it perfect to use in any part of your web app or other server side application that needs performance.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install posthog
|
||||
```
|
||||
|
||||
In your app, import the posthog library and set your api key **before** making any calls.
|
||||
|
||||
```python
|
||||
import posthog
|
||||
|
||||
posthog.api_key = 'YOUR API KEY'
|
||||
```
|
||||
|
||||
You can find your key in the /setup page in PostHog.
|
||||
|
||||
To debug, you can set debug mode.
|
||||
```python
|
||||
posthog.debug = True
|
||||
```
|
||||
|
||||
## Making calls
|
||||
|
||||
### Capture
|
||||
|
||||
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.
|
||||
|
||||
A `capture` call requires
|
||||
- `distinct id` which uniquely identifies your user
|
||||
- `event name` to make sure
|
||||
- We recommend using [verb] [noun], like `movie played` or `movie updated` to easily identify what your events mean later on.
|
||||
|
||||
Optionally you can submit
|
||||
- `properties`, which can be a dict with any information you'd like to add
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.capture('distinct id', 'movie played', {'movie_id': '123', 'category': 'romcom'})
|
||||
```
|
||||
|
||||
### Identify
|
||||
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.
|
||||
|
||||
An `identify` call requires
|
||||
- `distinct id` which uniquely identifies your user
|
||||
- `properties` with a dict with any key: value pairs
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.capture('distinct id', {
|
||||
'email': 'dwayne@gmail.com',
|
||||
'name': 'Dwayne Johnson'
|
||||
})
|
||||
```
|
||||
|
||||
The most obvious place to make this call is whenever a user signs up, or when they update their information.
|
||||
|
||||
### Alias
|
||||
|
||||
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?"
|
||||
|
||||
In a purely back-end implementation, this means whenever an anonymous user does something, you'll want to send a session ID ([Django](https://stackoverflow.com/questions/526179/in-django-how-can-i-find-out-the-request-session-sessionid-and-use-it-as-a-vari), [Flask](https://stackoverflow.com/questions/15156132/flask-login-how-to-get-session-id)) with the capture call. Then, when that users signs up, you want to do an alias call with the session ID and the newly created user ID.
|
||||
|
||||
The same concept applies for when a user logs in.
|
||||
|
||||
An `alias` call requires
|
||||
- `previous distinct id` the unique ID of the user before
|
||||
- `distinct id` the current unique id
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.alias('anonymous session id', 'distinct id')
|
||||
```
|
||||
|
||||
## Django
|
||||
|
||||
For Django, you can do the initialisation of the key in the AppConfig, so that it's available everywhere.
|
||||
|
||||
in `yourapp/apps.py`
|
||||
```python
|
||||
from django.apps import AppConfig
|
||||
import posthog
|
||||
|
||||
class YourAppConfig(AppConfig):
|
||||
def ready(self):
|
||||
posthog.api_key = 'your key'
|
||||
```
|
||||
|
||||
Then, anywhere else in your app you can do
|
||||
```python
|
||||
import posthog
|
||||
|
||||
def homepage(request):
|
||||
# example capture
|
||||
posthog.capture(request.session.session_key, 'page view', ....)
|
||||
```
|
||||
|
||||
# Development
|
||||
|
||||
## Naming confusion
|
||||
|
||||
As our open source project [PostHog](https://github.com/PostHog/posthog) shares the same module name, we create a special `posthog-analytics` package, mostly for internal use to avoid module collision. It is the exact same.
|
||||
|
||||
## How to release
|
||||
1. Increase `VERSION` in `posthog/version.py`
|
||||
2. run `make release` and `make release_analytics`
|
||||
3. `git commit -am "Release X.Y.Z."` (where X.Y.Z is the new version)
|
||||
4. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version).
|
||||
|
||||
## Thank you
|
||||
|
||||
This library is largely based on the `analytics-python` package.
|
||||
Specifically, the [Python integration](https://posthog.com/docs/integrations/python-integration) details.
|
||||
|
||||
+13
-4
@@ -2,19 +2,28 @@
|
||||
|
||||
# Import the library
|
||||
import posthog
|
||||
import time
|
||||
|
||||
# You can find this key on the /setup page in PostHog
|
||||
posthog.api_key = '<your key>'
|
||||
posthog.api_key = ''
|
||||
posthog.personal_api_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'})
|
||||
|
||||
# Alias a previous distinct id with a new one
|
||||
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
|
||||
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'})
|
||||
+57
-14
@@ -6,18 +6,27 @@ from typing import Optional, Dict, Callable
|
||||
__version__ = VERSION
|
||||
|
||||
"""Settings."""
|
||||
api_key: str = None
|
||||
host: str = None
|
||||
on_error: Callable = None
|
||||
debug: bool = False
|
||||
send: bool = True
|
||||
sync_mode:bool = False
|
||||
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
|
||||
|
||||
default_client = 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:
|
||||
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
|
||||
"""
|
||||
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.
|
||||
|
||||
@@ -36,8 +45,14 @@ def capture(distinct_id: str, event: str, properties: Optional[Dict]=None, conte
|
||||
"""
|
||||
_proxy('capture', distinct_id=distinct_id, event=event, properties=properties, context=context, timestamp=timestamp, message_id=message_id)
|
||||
|
||||
def identify(distinct_id: str, properties: Optional[Dict]=None, context: Optional[Dict]=None, timestamp: Optional[str]=None,
|
||||
message_id=None) -> None:
|
||||
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
|
||||
"""
|
||||
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.
|
||||
|
||||
@@ -47,7 +62,7 @@ def identify(distinct_id: str, properties: Optional[Dict]=None, context: Optiona
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.capture('distinct id', {
|
||||
posthog.identify('distinct id', {
|
||||
'email': 'dwayne@gmail.com',
|
||||
'name': 'Dwayne Johnson'
|
||||
})
|
||||
@@ -60,7 +75,14 @@ def group(*args, **kwargs):
|
||||
_proxy('group', *args, **kwargs)
|
||||
|
||||
|
||||
def alias(previous_id: str, distinct_id: str, context: Optional[Dict]=None, timestamp: Optional[str]=None, message_id: Optional[str]=None) -> None:
|
||||
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
|
||||
"""
|
||||
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?"
|
||||
|
||||
@@ -79,6 +101,25 @@ def alias(previous_id: str, distinct_id: str, context: Optional[Dict]=None, time
|
||||
"""
|
||||
_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."""
|
||||
@@ -109,10 +150,12 @@ 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)
|
||||
sync_mode=sync_mode, personal_api_key=personal_api_key)
|
||||
|
||||
fn = getattr(default_client, method)
|
||||
fn(*args, **kwargs)
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
+94
-66
@@ -1,16 +1,19 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
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 post
|
||||
from posthog.request import batch_post, decide, get, APIError
|
||||
from posthog.version import VERSION
|
||||
from posthog.poller import Poller
|
||||
|
||||
try:
|
||||
import queue
|
||||
@@ -19,6 +22,7 @@ except ImportError:
|
||||
|
||||
|
||||
ID_TYPES = (numbers.Number, string_types)
|
||||
__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)
|
||||
|
||||
|
||||
class Client(object):
|
||||
@@ -28,10 +32,12 @@ 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):
|
||||
sync_mode=False, timeout=15, thread=1, poll_interval=30, personal_api_key=None):
|
||||
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
|
||||
@@ -40,6 +46,11 @@ 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)
|
||||
@@ -78,7 +89,6 @@ class Client(object):
|
||||
msg = {
|
||||
'timestamp': timestamp,
|
||||
'context': context,
|
||||
'type': 'identify',
|
||||
'distinct_id': distinct_id,
|
||||
'$set': properties,
|
||||
'event': '$identify',
|
||||
@@ -100,7 +110,6 @@ class Client(object):
|
||||
'timestamp': timestamp,
|
||||
'context': context,
|
||||
'distinct_id': distinct_id,
|
||||
'type': 'capture',
|
||||
'event': event,
|
||||
'messageId': message_id,
|
||||
}
|
||||
@@ -121,34 +130,12 @@ class Client(object):
|
||||
},
|
||||
'timestamp': timestamp,
|
||||
'context': context,
|
||||
'type': 'alias',
|
||||
'event': '$create_alias'
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
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,
|
||||
def page(self, distinct_id=None, url=None, properties=None,
|
||||
context=None, timestamp=None, message_id=None):
|
||||
properties = properties or {}
|
||||
context = context or {}
|
||||
@@ -156,46 +143,15 @@ class Client(object):
|
||||
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)
|
||||
require('url', url, string_types)
|
||||
properties['$current_url'] = url
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -210,7 +166,6 @@ 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)
|
||||
|
||||
@@ -218,6 +173,8 @@ class Client(object):
|
||||
timestamp = guess_timezone(timestamp)
|
||||
msg['timestamp'] = timestamp.isoformat()
|
||||
msg['messageId'] = stringify_id(message_id)
|
||||
if not msg.get('properties'):
|
||||
msg['properties'] = {}
|
||||
msg['properties']['$lib'] = 'posthog-python'
|
||||
msg['properties']['$lib_version'] = VERSION
|
||||
|
||||
@@ -231,15 +188,15 @@ class Client(object):
|
||||
return True, msg
|
||||
|
||||
if self.sync_mode:
|
||||
self.log.debug('enqueued with blocking %s.', msg['type'])
|
||||
post(self.api_key, self.host, gzip=self.gzip,
|
||||
self.log.debug('enqueued with blocking %s.', msg['event'])
|
||||
batch_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['type'])
|
||||
self.log.debug('enqueued %s.', msg['event'])
|
||||
return True, msg
|
||||
except queue.Full:
|
||||
self.log.warning('analytics-python queue is full')
|
||||
@@ -270,6 +227,77 @@ 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
@@ -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()
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
+58
-11
@@ -4,27 +4,30 @@ import logging
|
||||
import json
|
||||
from gzip import GzipFile
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from requests import sessions
|
||||
import requests
|
||||
from io import BytesIO
|
||||
|
||||
from posthog.version import VERSION
|
||||
from posthog.utils import remove_trailing_slash
|
||||
|
||||
_session = sessions.Session()
|
||||
_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 'https://t.posthog.com') + '/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)
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'analytics-python/' + VERSION
|
||||
'User-Agent': USER_AGENT
|
||||
}
|
||||
if gzip:
|
||||
headers['Content-Encoding'] = 'gzip'
|
||||
@@ -42,24 +45,68 @@ 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)
|
||||
raise APIError(res.status_code, payload['code'], payload['message'])
|
||||
raise APIError(res.status_code, payload['detail'])
|
||||
except ValueError:
|
||||
raise APIError(res.status_code, 'unknown', res.text)
|
||||
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)
|
||||
|
||||
|
||||
class APIError(Exception):
|
||||
|
||||
def __init__(self, status, code, message):
|
||||
def __init__(self, status, message):
|
||||
self.message = message
|
||||
self.status = status
|
||||
self.code = code
|
||||
|
||||
def __str__(self):
|
||||
msg = "[PostHog] {0}: {1} ({2})"
|
||||
return msg.format(self.code, self.message, self.status)
|
||||
msg = "[PostHog] {0} ({1})"
|
||||
return msg.format(self.message, self.status)
|
||||
|
||||
|
||||
class DatetimeSerializer(json.JSONEncoder):
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
|
||||
def all_names():
|
||||
for _, modname, _ in pkgutil.iter_modules(__path__):
|
||||
yield 'analytics.test.' + modname
|
||||
yield 'posthog.test.' + modname
|
||||
|
||||
|
||||
def all():
|
||||
|
||||
+124
-123
@@ -3,20 +3,24 @@ 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 fail(self, e, batch):
|
||||
def set_fail(self, e, batch):
|
||||
"""Mark the failure handler"""
|
||||
print('FAIL', e, batch)
|
||||
self.failed = True
|
||||
|
||||
def setUp(self):
|
||||
self.failed = False
|
||||
self.client = Client('testsecret', on_error=self.fail)
|
||||
self.client = Client(TEST_API_KEY, on_error=self.set_fail)
|
||||
|
||||
def test_requires_api_key(self):
|
||||
self.assertRaises(AssertionError, Client)
|
||||
@@ -24,9 +28,9 @@ class TestClient(unittest.TestCase):
|
||||
def test_empty_flush(self):
|
||||
self.client.flush()
|
||||
|
||||
def test_basic_track(self):
|
||||
def test_basic_capture(self):
|
||||
client = self.client
|
||||
success, msg = client.track('distinct_id', 'python test event')
|
||||
success, msg = client.capture('distinct_id', 'python test event')
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
@@ -35,14 +39,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'], {})
|
||||
self.assertEqual(msg['type'], 'track')
|
||||
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
|
||||
self.assertEqual(msg['properties']['$lib_version'], VERSION)
|
||||
|
||||
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.track(
|
||||
success, msg = client.capture(
|
||||
distinct_id=157963456373623802, event='python test event')
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
@@ -50,9 +54,9 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertEqual(msg['distinct_id'], '157963456373623802')
|
||||
|
||||
def test_advanced_track(self):
|
||||
def test_advanced_capture(self):
|
||||
client = self.client
|
||||
success, msg = client.track(
|
||||
success, msg = client.capture(
|
||||
'distinct_id', 'python test event', {'property': 'value'},
|
||||
{'ip': '192.168.0.1'}, datetime(2014, 9, 3),
|
||||
'messageId')
|
||||
@@ -60,14 +64,13 @@ 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
|
||||
@@ -76,11 +79,10 @@ class TestClient(unittest.TestCase):
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg['traits'], {'trait': 'value'})
|
||||
self.assertEqual(msg['$set']['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
|
||||
@@ -92,46 +94,12 @@ 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['traits'], {'trait': 'value'})
|
||||
self.assertEqual(msg['context']['library'], {
|
||||
'name': 'analytics-python',
|
||||
'version': VERSION
|
||||
})
|
||||
self.assertEqual(msg['$set']['trait'], 'value')
|
||||
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
|
||||
self.assertEqual(msg['properties']['$lib_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
|
||||
@@ -139,71 +107,35 @@ class TestClient(unittest.TestCase):
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
self.assertEqual(msg['previousId'], 'previousId')
|
||||
self.assertEqual(msg['distinct_id'], 'distinct_id')
|
||||
self.assertEqual(msg['properties']['distinct_id'], 'previousId')
|
||||
self.assertEqual(msg['properties']['alias'], 'distinct_id')
|
||||
|
||||
def test_basic_page(self):
|
||||
client = self.client
|
||||
success, msg = client.page('distinct_id', name='name')
|
||||
success, msg = client.page('distinct_id', url='https://posthog.com/contact')
|
||||
self.assertFalse(self.failed)
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(msg['distinct_id'], 'distinct_id')
|
||||
self.assertEqual(msg['type'], 'page')
|
||||
self.assertEqual(msg['name'], 'name')
|
||||
self.assertEqual(msg['properties']['$current_url'], 'https://posthog.com/contact')
|
||||
|
||||
def test_advanced_page(self):
|
||||
client = self.client
|
||||
success, msg = client.page(
|
||||
'distinct_id', 'category', 'name', {'property': 'value'},
|
||||
'distinct_id', 'https://posthog.com/contact', {'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.assertEqual(msg['category'], 'category')
|
||||
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.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
|
||||
@@ -230,7 +162,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertFalse(consumer.is_alive())
|
||||
|
||||
def test_synchronous(self):
|
||||
client = Client('testsecret', sync_mode=True)
|
||||
client = Client(TEST_API_KEY, sync_mode=True)
|
||||
|
||||
success, message = client.identify('distinct_id')
|
||||
self.assertFalse(client.consumers)
|
||||
@@ -238,7 +170,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertTrue(success)
|
||||
|
||||
def test_overflow(self):
|
||||
client = Client('testsecret', max_queue_size=1)
|
||||
client = Client(TEST_API_KEY, max_queue_size=1)
|
||||
# Ensure consumer thread is no longer uploading
|
||||
client.join()
|
||||
|
||||
@@ -249,46 +181,26 @@ 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.track(1234, 'python event')
|
||||
self.client.capture(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('testsecret', on_error=self.fail, gzip=True)
|
||||
client = Client(TEST_API_KEY, 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('testsecret', on_error=self.fail,
|
||||
client = Client(TEST_API_KEY, on_error=self.fail,
|
||||
flush_at=10, flush_interval=3)
|
||||
|
||||
def mock_post_fn(*args, **kwargs):
|
||||
@@ -296,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('analytics.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'})
|
||||
@@ -304,11 +216,100 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEquals(mock_post.call_count, 2)
|
||||
|
||||
def test_user_defined_timeout(self):
|
||||
client = Client('testsecret', timeout=10)
|
||||
client = Client(TEST_API_KEY, timeout=10)
|
||||
for consumer in client.consumers:
|
||||
self.assertEquals(consumer.timeout, 10)
|
||||
|
||||
def test_default_timeout_15(self):
|
||||
client = Client('testsecret')
|
||||
client = Client(TEST_API_KEY)
|
||||
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'))
|
||||
+20
-19
@@ -10,6 +10,7 @@ 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):
|
||||
@@ -41,7 +42,7 @@ class TestConsumer(unittest.TestCase):
|
||||
|
||||
def test_upload(self):
|
||||
q = Queue()
|
||||
consumer = Consumer(q, 'testsecret')
|
||||
consumer = Consumer(q, TEST_API_KEY)
|
||||
track = {
|
||||
'type': 'track',
|
||||
'event': 'python event',
|
||||
@@ -57,9 +58,9 @@ class TestConsumer(unittest.TestCase):
|
||||
# The consumer should upload _n_ times.
|
||||
q = Queue()
|
||||
flush_interval = 0.3
|
||||
consumer = Consumer(q, 'testsecret', flush_at=10,
|
||||
consumer = Consumer(q, TEST_API_KEY, flush_at=10,
|
||||
flush_interval=flush_interval)
|
||||
with mock.patch('analytics.consumer.post') as mock_post:
|
||||
with mock.patch('posthog.consumer.batch_post') as mock_post:
|
||||
consumer.start()
|
||||
for i in range(0, 3):
|
||||
track = {
|
||||
@@ -77,9 +78,9 @@ class TestConsumer(unittest.TestCase):
|
||||
q = Queue()
|
||||
flush_interval = 0.5
|
||||
flush_at = 10
|
||||
consumer = Consumer(q, 'testsecret', flush_at=flush_at,
|
||||
consumer = Consumer(q, TEST_API_KEY, flush_at=flush_at,
|
||||
flush_interval=flush_interval)
|
||||
with mock.patch('analytics.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 = {
|
||||
@@ -92,7 +93,7 @@ class TestConsumer(unittest.TestCase):
|
||||
self.assertEqual(mock_post.call_count, 2)
|
||||
|
||||
def test_request(self):
|
||||
consumer = Consumer(None, 'testsecret')
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
track = {
|
||||
'type': 'track',
|
||||
'event': 'python event',
|
||||
@@ -109,7 +110,7 @@ class TestConsumer(unittest.TestCase):
|
||||
raise expected_exception
|
||||
mock_post.call_count = 0
|
||||
|
||||
with mock.patch('analytics.consumer.post',
|
||||
with mock.patch('posthog.consumer.batch_post',
|
||||
mock.Mock(side_effect=mock_post)):
|
||||
track = {
|
||||
'type': 'track',
|
||||
@@ -135,22 +136,22 @@ class TestConsumer(unittest.TestCase):
|
||||
|
||||
def test_request_retry(self):
|
||||
# we should retry on general errors
|
||||
consumer = Consumer(None, 'testsecret')
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
self._test_request_retry(consumer, Exception('generic exception'), 2)
|
||||
|
||||
# we should retry on server errors
|
||||
consumer = Consumer(None, 'testsecret')
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
self._test_request_retry(consumer, APIError(
|
||||
500, 'code', 'Internal Server Error'), 2)
|
||||
500, 'Internal Server Error'), 2)
|
||||
|
||||
# we should retry on HTTP 429 errors
|
||||
consumer = Consumer(None, 'testsecret')
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
self._test_request_retry(consumer, APIError(
|
||||
429, 'code', 'Too Many Requests'), 2)
|
||||
429, 'Too Many Requests'), 2)
|
||||
|
||||
# we should NOT retry on other client errors
|
||||
consumer = Consumer(None, 'testsecret')
|
||||
api_error = APIError(400, 'code', 'Client Errors')
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
api_error = APIError(400, 'Client Errors')
|
||||
try:
|
||||
self._test_request_retry(consumer, api_error, 1)
|
||||
except APIError:
|
||||
@@ -159,19 +160,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, 'testsecret', retries=3)
|
||||
consumer = Consumer(None, TEST_API_KEY, retries=3)
|
||||
self._test_request_retry(consumer, APIError(
|
||||
500, 'code', 'Internal Server Error'), 3)
|
||||
500, 'Internal Server Error'), 3)
|
||||
|
||||
def test_pause(self):
|
||||
consumer = Consumer(None, 'testsecret')
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
consumer.pause()
|
||||
self.assertFalse(consumer.running)
|
||||
|
||||
def test_max_batch_size(self):
|
||||
q = Queue()
|
||||
consumer = Consumer(
|
||||
q, 'testsecret', flush_at=100000, flush_interval=3)
|
||||
q, TEST_API_KEY, flush_at=100000, flush_interval=3)
|
||||
track = {
|
||||
'type': 'track',
|
||||
'event': 'python event',
|
||||
@@ -189,7 +190,7 @@ class TestConsumer(unittest.TestCase):
|
||||
% len(data.encode()))
|
||||
return res
|
||||
|
||||
with mock.patch('analytics.request._session.post',
|
||||
with mock.patch('posthog.request._session.post',
|
||||
side_effect=mock_post_fn) as mock_post:
|
||||
consumer.start()
|
||||
for _ in range(0, n_msgs + 2):
|
||||
|
||||
+16
-24
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
import analytics
|
||||
import posthog
|
||||
|
||||
|
||||
class TestModule(unittest.TestCase):
|
||||
@@ -10,40 +10,32 @@ class TestModule(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.failed = False
|
||||
analytics.api_key = 'testsecret'
|
||||
analytics.on_error = self.failed
|
||||
posthog.api_key = 'testsecret'
|
||||
posthog.on_error = self.failed
|
||||
|
||||
def test_no_api_key(self):
|
||||
analytics.api_key = None
|
||||
self.assertRaises(Exception, analytics.track)
|
||||
posthog.api_key = None
|
||||
self.assertRaises(Exception, posthog.capture)
|
||||
|
||||
def test_no_host(self):
|
||||
analytics.host = None
|
||||
self.assertRaises(Exception, analytics.track)
|
||||
posthog.host = None
|
||||
self.assertRaises(Exception, posthog.capture)
|
||||
|
||||
def test_track(self):
|
||||
analytics.track('distinct_id', 'python module event')
|
||||
analytics.flush()
|
||||
posthog.capture('distinct_id', 'python module event')
|
||||
posthog.flush()
|
||||
|
||||
def test_identify(self):
|
||||
analytics.identify('distinct_id', {'email': 'user@email.com'})
|
||||
analytics.flush()
|
||||
|
||||
def test_group(self):
|
||||
analytics.group('distinct_id', 'groupId')
|
||||
analytics.flush()
|
||||
posthog.identify('distinct_id', {'email': 'user@email.com'})
|
||||
posthog.flush()
|
||||
|
||||
def test_alias(self):
|
||||
analytics.alias('previousId', 'distinct_id')
|
||||
analytics.flush()
|
||||
posthog.alias('previousId', 'distinct_id')
|
||||
posthog.flush()
|
||||
|
||||
def test_page(self):
|
||||
analytics.page('distinct_id')
|
||||
analytics.flush()
|
||||
|
||||
def test_screen(self):
|
||||
analytics.screen('distinct_id')
|
||||
analytics.flush()
|
||||
posthog.page('distinct_id', 'https://posthog.com/contact')
|
||||
posthog.flush()
|
||||
|
||||
def test_flush(self):
|
||||
analytics.flush()
|
||||
posthog.flush()
|
||||
|
||||
@@ -3,13 +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(batch=[{
|
||||
res = batch_post(TEST_API_KEY, batch=[{
|
||||
'distinct_id': 'distinct_id',
|
||||
'event': 'python event',
|
||||
'type': 'track'
|
||||
@@ -17,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):
|
||||
@@ -37,7 +38,7 @@ class TestRequests(unittest.TestCase):
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_should_not_timeout(self):
|
||||
res = post(batch=[{
|
||||
res = batch_post(TEST_API_KEY, batch=[{
|
||||
'distinct_id': 'distinct_id',
|
||||
'event': 'python event',
|
||||
'type': 'track'
|
||||
@@ -46,7 +47,7 @@ class TestRequests(unittest.TestCase):
|
||||
|
||||
def test_should_timeout(self):
|
||||
with self.assertRaises(requests.ReadTimeout):
|
||||
post(batch=[{
|
||||
batch_post('key', batch=[{
|
||||
'distinct_id': 'distinct_id',
|
||||
'event': 'python event',
|
||||
'type': 'track'
|
||||
|
||||
@@ -7,6 +7,7 @@ import six
|
||||
|
||||
from posthog import utils
|
||||
|
||||
TEST_API_KEY = 'kOOlRy2QlMY9jHZQv0bKz0FZyazBUoY8Arj0lFVNjs4'
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
VERSION = '1.0.7'
|
||||
VERSION = '1.1.2'
|
||||
|
||||
@@ -24,7 +24,8 @@ install_requires = [
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
"mock>=2.0.0"
|
||||
"mock>=2.0.0",
|
||||
"freezegun==0.3.15"
|
||||
]
|
||||
|
||||
setup(
|
||||
|
||||
+12
-24
@@ -1,4 +1,4 @@
|
||||
import analytics
|
||||
import posthog
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
@@ -12,7 +12,7 @@ def json_hash(str):
|
||||
if str:
|
||||
return json.loads(str)
|
||||
|
||||
# analytics -method=<method> -posthog-write-key=<posthogWriteKey> [options]
|
||||
# posthog -method=<method> -posthog-write-key=<posthogWriteKey> [options]
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='send a posthog message')
|
||||
@@ -45,38 +45,28 @@ def failed(status, msg):
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
def track():
|
||||
analytics.track(options.distinct_id, options.event, anonymous_id=options.anonymousId,
|
||||
def capture():
|
||||
posthog.capture(options.distinct_id, options.event, anonymous_id=options.anonymousId,
|
||||
properties=json_hash(options.properties), context=json_hash(options.context))
|
||||
|
||||
|
||||
def page():
|
||||
analytics.page(options.distinct_id, name=options.name, anonymous_id=options.anonymousId,
|
||||
posthog.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():
|
||||
analytics.identify(options.distinct_id, anonymous_id=options.anonymousId,
|
||||
posthog.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()
|
||||
|
||||
|
||||
analytics.api_key = options.writeKey
|
||||
analytics.on_error = failed
|
||||
analytics.debug = True
|
||||
posthog.api_key = options.writeKey
|
||||
posthog.on_error = failed
|
||||
posthog.debug = True
|
||||
|
||||
log = logging.getLogger('posthog')
|
||||
ch = logging.StreamHandler()
|
||||
@@ -84,16 +74,14 @@ ch.setLevel(logging.DEBUG)
|
||||
log.addHandler(ch)
|
||||
|
||||
switcher = {
|
||||
"track": track,
|
||||
"capture": capture,
|
||||
"page": page,
|
||||
"screen": screen,
|
||||
"identify": identify,
|
||||
"group": group
|
||||
"identify": identify
|
||||
}
|
||||
|
||||
func = switcher.get(options.type)
|
||||
if func:
|
||||
func()
|
||||
analytics.shutdown()
|
||||
posthog.shutdown()
|
||||
else:
|
||||
print("Invalid Message Type " + options.type)
|
||||
|
||||
Reference in New Issue
Block a user