Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
372fb74637 | ||
|
|
ba11548089 | ||
|
|
49d0821e27 | ||
|
|
b4489f1dca | ||
|
|
8040964761 | ||
|
|
4f853403b9 | ||
|
|
ac61fb0e01 | ||
|
|
7196dc6048 | ||
|
|
45303b899e | ||
|
|
7e463ccad6 | ||
|
|
41dec34929 | ||
|
|
0bf9db0108 | ||
|
|
e8308360bb | ||
|
|
15ebe85a78 | ||
|
|
dbc22d2f9f | ||
|
|
f3ee238823 | ||
|
|
71d81b2da9 | ||
|
|
5493029577 | ||
|
|
d66f944571 | ||
|
|
9d620967f8 | ||
|
|
563404f914 | ||
|
|
d15aac41a9 | ||
|
|
8a3e28b949 | ||
|
|
1aa0d6335c | ||
|
|
3b46c60cf1 | ||
|
|
4ad8cbfa58 | ||
|
|
984a679b19 | ||
|
|
dd1bad6175 | ||
|
|
e6f71e4cc3 | ||
|
|
17874cb131 | ||
|
|
8366e09df9 | ||
|
|
e28b237ff1 | ||
|
|
05fde2a51e | ||
|
|
2be04f3b8b | ||
|
|
d52b605742 | ||
|
|
7ee3002c6f | ||
|
|
d1bc9135c7 | ||
|
|
111813296c | ||
|
|
31acda73a3 | ||
|
|
888457387b | ||
|
|
fe45ff2ab0 | ||
|
|
221d7f09f3 | ||
|
|
a5f2e030b5 | ||
|
|
98d2d4cc05 | ||
|
|
b7c1572c32 | ||
|
|
16acf2e278 | ||
|
|
df9ae05202 | ||
|
|
b05ee3884a | ||
|
|
144a7744e4 | ||
|
|
ca979f06c8 | ||
|
|
caa64fb6fe | ||
|
|
e007dee07e | ||
|
|
1de9553d61 | ||
|
|
d447d170fa | ||
|
|
d92c398c0f | ||
|
|
b331c4aae3 | ||
|
|
5e70ca84bb | ||
|
|
adef8d4928 | ||
|
|
8682091eec | ||
|
|
721a6aacf7 | ||
|
|
72e7e4ad72 | ||
|
|
95d4375663 | ||
|
|
6e39aa0ceb | ||
|
|
c72ab9a3fd |
@@ -0,0 +1,61 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
code-quality:
|
||||
name: Code quality checks
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install dev dependencies
|
||||
run: |
|
||||
python -m pip install -e .[dev]
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Check formatting with black
|
||||
run: |
|
||||
black --check .
|
||||
|
||||
- name: Check import order with isort
|
||||
run: |
|
||||
isort --check-only .
|
||||
|
||||
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 .[test]
|
||||
|
||||
- name: Run posthog tests
|
||||
run: |
|
||||
python setup.py test
|
||||
@@ -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
|
||||
+8
-5
@@ -1,14 +1,17 @@
|
||||
**sublime**
|
||||
*.pyc
|
||||
dist
|
||||
dist/
|
||||
*.egg-info
|
||||
dist
|
||||
MANIFEST
|
||||
build
|
||||
.eggs
|
||||
build/
|
||||
.eggs/
|
||||
.coverage
|
||||
.vscode/
|
||||
env
|
||||
env/
|
||||
venv/
|
||||
flake8.out
|
||||
pylint.out
|
||||
posthog-analytics
|
||||
.idea
|
||||
.python-version
|
||||
.coverage
|
||||
@@ -0,0 +1,9 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: stable
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.7.0
|
||||
hooks:
|
||||
- id: isort
|
||||
@@ -1,7 +1,10 @@
|
||||
test:
|
||||
lint:
|
||||
pylint --rcfile=.pylintrc --reports=y --exit-zero analytics | tee pylint.out
|
||||
flake8 --max-complexity=10 --statistics analytics > flake8.out || true
|
||||
coverage run --branch --include=analytics/\* --omit=*/test* setup.py test
|
||||
|
||||
test:
|
||||
coverage run -m pytest
|
||||
coverage report
|
||||
|
||||
release:
|
||||
rm -rf dist/*
|
||||
@@ -26,4 +29,4 @@ release_analytics:
|
||||
e2e_test:
|
||||
.buildscripts/e2e.sh
|
||||
|
||||
.PHONY: test release e2e_test
|
||||
.PHONY: test lint release e2e_test
|
||||
|
||||
@@ -3,3 +3,20 @@
|
||||
Please see the main [PostHog docs](https://posthog.com/docs).
|
||||
|
||||
Specifically, the [Python integration](https://posthog.com/docs/integrations/python-integration) details.
|
||||
|
||||
## Questions?
|
||||
|
||||
### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
|
||||
|
||||
# Local Development
|
||||
|
||||
## Testing Locally
|
||||
|
||||
1. Run `python3 -m venv env` (creates virtual environment called "env")
|
||||
2. Run `source env/bin/activate` (activates the virtual environment)
|
||||
3. Run `python3 -m pip install -e ".[test]"` (installs the package in develop mode, along with test dependencies)
|
||||
4. Run `make test`
|
||||
|
||||
## Running Locally
|
||||
|
||||
Assuming you have a [local version of PostHog](https://posthog.com/docs/developing-locally) running, you can run `python3 example.py` to see the library in action.
|
||||
+32
-5
@@ -1,20 +1,47 @@
|
||||
# PostHog Python library example
|
||||
|
||||
# Import the library
|
||||
import time
|
||||
|
||||
import posthog
|
||||
|
||||
# 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'})
|
||||
posthog.capture("distinct_id", "event", {"property1": "value", "property2": "value"})
|
||||
|
||||
print(posthog.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
print("sleeping")
|
||||
time.sleep(5)
|
||||
|
||||
print(posthog.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
# # Alias a previous distinct id with a new one
|
||||
# posthog.alias('distinct_id', 'new_distinct_id')
|
||||
|
||||
posthog.alias("distinct_id", "new_distinct_id")
|
||||
|
||||
posthog.capture("new_distinct_id", "event2", {"property1": "value", "property2": "value"})
|
||||
|
||||
# # Add properties to the person
|
||||
# posthog.identify('distinct_id', {'email': 'something@something.com'})
|
||||
posthog.identify("new_distinct_id", {"email": "something@something.com"})
|
||||
|
||||
# properties set only once to the person
|
||||
posthog.set_once("new_distinct_id", {"self_serve_signup": True})
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
posthog.set_once(
|
||||
"new_distinct_id", {"self_serve_signup": False}
|
||||
) # this will not change the property (because it was already set)
|
||||
|
||||
posthog.set("new_distinct_id", {"current_browser": "Chrome"})
|
||||
posthog.set("new_distinct_id", {"current_browser": "Firefox"})
|
||||
|
||||
posthog.shutdown()
|
||||
|
||||
+161
-44
@@ -1,37 +1,38 @@
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
from posthog.version import VERSION
|
||||
from posthog.client import Client
|
||||
from typing import Optional, Dict, Callable
|
||||
from posthog.version import VERSION
|
||||
|
||||
__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
|
||||
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, # 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]
|
||||
):
|
||||
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.
|
||||
|
||||
A `capture` call requires
|
||||
- `distinct id` which uniquely identifies your user
|
||||
- `event name` to make sure
|
||||
- `event name` to specify the event
|
||||
- We recommend using [verb] [noun], like `movie played` or `movie updated` to easily identify what your events mean later on.
|
||||
|
||||
Optionally you can submit
|
||||
@@ -42,22 +43,31 @@ def capture(
|
||||
posthog.capture('distinct id', 'movie played', {'movie_id': '123', 'category': 'romcom'})
|
||||
```
|
||||
"""
|
||||
_proxy('capture', distinct_id=distinct_id, event=event, properties=properties, context=context, timestamp=timestamp, message_id=message_id)
|
||||
_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]
|
||||
):
|
||||
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.
|
||||
|
||||
An `identify` call requires
|
||||
- `distinct id` which uniquely identifies your user
|
||||
- `properties` with a dict with any key: value pairs
|
||||
- `properties` with a dict with any key: value pairs
|
||||
|
||||
For example:
|
||||
```python
|
||||
@@ -67,20 +77,94 @@ def identify(
|
||||
})
|
||||
```
|
||||
"""
|
||||
_proxy('identify', distinct_id=distinct_id, properties=properties, context=context, timestamp=timestamp, message_id=message_id)
|
||||
_proxy(
|
||||
"identify",
|
||||
distinct_id=distinct_id,
|
||||
properties=properties,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
message_id=message_id,
|
||||
)
|
||||
|
||||
|
||||
def set(
|
||||
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
|
||||
"""
|
||||
Set properties on a user record.
|
||||
This will overwrite previous people property values, just like `identify`.
|
||||
|
||||
A `set` call requires
|
||||
- `distinct id` which uniquely identifies your user
|
||||
- `properties` with a dict with any key: value pairs
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.set('distinct id', {
|
||||
'current_browser': 'Chrome',
|
||||
})
|
||||
```
|
||||
"""
|
||||
_proxy(
|
||||
"set",
|
||||
distinct_id=distinct_id,
|
||||
properties=properties,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
message_id=message_id,
|
||||
)
|
||||
|
||||
|
||||
def set_once(
|
||||
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
|
||||
"""
|
||||
Set properties on a user record, only if they do not yet exist.
|
||||
This will not overwrite previous people property values, unlike `identify`.
|
||||
|
||||
A `set_once` call requires
|
||||
- `distinct id` which uniquely identifies your user
|
||||
- `properties` with a dict with any key: value pairs
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.set_once('distinct id', {
|
||||
'referred_by': 'friend',
|
||||
})
|
||||
```
|
||||
"""
|
||||
_proxy(
|
||||
"set_once",
|
||||
distinct_id=distinct_id,
|
||||
properties=properties,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
message_id=message_id,
|
||||
)
|
||||
|
||||
|
||||
def group(*args, **kwargs):
|
||||
"""Send a group call."""
|
||||
_proxy('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]
|
||||
):
|
||||
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?"
|
||||
@@ -98,33 +182,60 @@ def alias(
|
||||
posthog.alias('anonymous session id', 'distinct id')
|
||||
```
|
||||
"""
|
||||
_proxy('alias', previous_id=previous_id, distinct_id=distinct_id, context=context, timestamp=timestamp, message_id=message_id)
|
||||
_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."""
|
||||
_proxy('page', *args, **kwargs)
|
||||
_proxy("page", *args, **kwargs)
|
||||
|
||||
|
||||
def screen(*args, **kwargs):
|
||||
"""Send a screen call."""
|
||||
_proxy('screen', *args, **kwargs)
|
||||
_proxy("screen", *args, **kwargs)
|
||||
|
||||
|
||||
def flush():
|
||||
"""Tell the client to flush."""
|
||||
_proxy('flush')
|
||||
_proxy("flush")
|
||||
|
||||
|
||||
def join():
|
||||
"""Block program until the client clears the queue"""
|
||||
_proxy('join')
|
||||
_proxy("join")
|
||||
|
||||
|
||||
def shutdown():
|
||||
"""Flush all messages and cleanly shutdown the client"""
|
||||
_proxy('flush')
|
||||
_proxy('join')
|
||||
_proxy("flush")
|
||||
_proxy("join")
|
||||
|
||||
|
||||
def _proxy(method, *args, **kwargs):
|
||||
@@ -133,9 +244,15 @@ def _proxy(method, *args, **kwargs):
|
||||
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)
|
||||
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,
|
||||
)
|
||||
|
||||
fn = getattr(default_client, method)
|
||||
fn(*args, **kwargs)
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
+223
-79
@@ -1,15 +1,17 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
import atexit
|
||||
import hashlib
|
||||
import logging
|
||||
import numbers
|
||||
import atexit
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
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.poller import Poller
|
||||
from posthog.request import APIError, batch_post, decide, get
|
||||
from posthog.utils import clean, guess_timezone
|
||||
from posthog.version import VERSION
|
||||
|
||||
try:
|
||||
@@ -18,21 +20,42 @@ except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
|
||||
ID_TYPES = (numbers.Number, string_types)
|
||||
ID_TYPES = (numbers.Number, string_types, UUID)
|
||||
__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Create a new PostHog client."""
|
||||
log = logging.getLogger('posthog')
|
||||
|
||||
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):
|
||||
require('api_key', api_key, string_types)
|
||||
log = logging.getLogger("posthog")
|
||||
|
||||
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,
|
||||
project_api_key=None,
|
||||
):
|
||||
|
||||
self.queue = queue.Queue(max_queue_size)
|
||||
self.api_key = api_key
|
||||
|
||||
# api_key: This should be the Team API Key (token), public
|
||||
self.api_key = api_key or project_api_key
|
||||
|
||||
require("api_key", self.api_key, string_types)
|
||||
|
||||
self.on_error = on_error
|
||||
self.debug = debug
|
||||
self.send = send
|
||||
@@ -40,6 +63,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)
|
||||
@@ -58,9 +86,15 @@ class Client(object):
|
||||
for n in range(thread):
|
||||
self.consumers = []
|
||||
consumer = Consumer(
|
||||
self.queue, api_key, host=host, on_error=on_error,
|
||||
flush_at=flush_at, flush_interval=flush_interval,
|
||||
gzip=gzip, retries=max_retries, timeout=timeout,
|
||||
self.queue,
|
||||
api_key,
|
||||
host=host,
|
||||
on_error=on_error,
|
||||
flush_at=flush_at,
|
||||
flush_interval=flush_interval,
|
||||
gzip=gzip,
|
||||
retries=max_retries,
|
||||
timeout=timeout,
|
||||
)
|
||||
self.consumers.append(consumer)
|
||||
|
||||
@@ -68,127 +102,157 @@ class Client(object):
|
||||
if send:
|
||||
consumer.start()
|
||||
|
||||
def identify(self, distinct_id=None, properties=None, context=None, timestamp=None,
|
||||
message_id=None):
|
||||
def identify(self, distinct_id=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)
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
require("properties", properties, dict)
|
||||
|
||||
msg = {
|
||||
'timestamp': timestamp,
|
||||
'context': context,
|
||||
'distinct_id': distinct_id,
|
||||
'$set': properties,
|
||||
'event': '$identify',
|
||||
'messageId': message_id,
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"distinct_id": distinct_id,
|
||||
"$set": properties,
|
||||
"event": "$identify",
|
||||
"messageId": message_id,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def capture(self, distinct_id=None, event=None, properties=None, context=None,
|
||||
timestamp=None, message_id=None):
|
||||
def capture(self, distinct_id=None, event=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)
|
||||
require('event', event, string_types)
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
require("properties", properties, dict)
|
||||
require("event", event, string_types)
|
||||
|
||||
msg = {
|
||||
'properties': properties,
|
||||
'timestamp': timestamp,
|
||||
'context': context,
|
||||
'distinct_id': distinct_id,
|
||||
'event': event,
|
||||
'messageId': message_id,
|
||||
"properties": properties,
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"distinct_id": distinct_id,
|
||||
"event": event,
|
||||
"messageId": message_id,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def alias(self, previous_id=None, distinct_id=None, context=None,
|
||||
timestamp=None, message_id=None):
|
||||
def set(self, distinct_id=None, properties=None, context=None, timestamp=None, message_id=None):
|
||||
properties = properties or {}
|
||||
context = context or {}
|
||||
|
||||
require('previous_id', previous_id, ID_TYPES)
|
||||
require('distinct_id', distinct_id, ID_TYPES)
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
require("properties", properties, dict)
|
||||
|
||||
msg = {
|
||||
'properties': {
|
||||
'distinct_id': previous_id,
|
||||
'alias': distinct_id,
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"distinct_id": distinct_id,
|
||||
"$set": properties,
|
||||
"event": "$set",
|
||||
"messageId": message_id,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def set_once(self, distinct_id=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)
|
||||
|
||||
msg = {
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"distinct_id": distinct_id,
|
||||
"$set_once": properties,
|
||||
"event": "$set_once",
|
||||
"messageId": message_id,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def alias(self, previous_id=None, distinct_id=None, context=None, timestamp=None, message_id=None):
|
||||
context = context or {}
|
||||
|
||||
require("previous_id", previous_id, ID_TYPES)
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
|
||||
msg = {
|
||||
"properties": {
|
||||
"distinct_id": previous_id,
|
||||
"alias": distinct_id,
|
||||
},
|
||||
'timestamp': timestamp,
|
||||
'context': context,
|
||||
'event': '$create_alias'
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"event": "$create_alias",
|
||||
"distinct_id": previous_id,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def page(self, distinct_id=None, url=None, properties=None,
|
||||
context=None, timestamp=None, message_id=None):
|
||||
def page(self, distinct_id=None, url=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)
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
require("properties", properties, dict)
|
||||
|
||||
require('url', url, string_types)
|
||||
properties['$current_url'] = url
|
||||
require("url", url, string_types)
|
||||
properties["$current_url"] = url
|
||||
|
||||
msg = {
|
||||
'event': '$pageview',
|
||||
'properties': properties,
|
||||
'timestamp': timestamp,
|
||||
'context': context,
|
||||
'distinct_id': distinct_id,
|
||||
'messageId': message_id,
|
||||
"event": "$pageview",
|
||||
"properties": properties,
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"distinct_id": distinct_id,
|
||||
"messageId": message_id,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def _enqueue(self, msg):
|
||||
"""Push a new `msg` onto the queue, return `(success, msg)`"""
|
||||
timestamp = msg['timestamp']
|
||||
timestamp = msg["timestamp"]
|
||||
if timestamp is None:
|
||||
timestamp = datetime.utcnow().replace(tzinfo=tzutc())
|
||||
message_id = msg.get('messageId')
|
||||
message_id = msg.get("messageId")
|
||||
if message_id is None:
|
||||
message_id = uuid4()
|
||||
|
||||
require('timestamp', timestamp, datetime)
|
||||
require('context', msg['context'], dict)
|
||||
require("timestamp", timestamp, datetime)
|
||||
require("context", msg["context"], dict)
|
||||
|
||||
# add common
|
||||
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
|
||||
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
|
||||
|
||||
msg['distinct_id'] = stringify_id(msg.get('distinct_id', None))
|
||||
msg["distinct_id"] = stringify_id(msg.get("distinct_id", None))
|
||||
|
||||
msg = clean(msg)
|
||||
self.log.debug('queueing: %s', msg)
|
||||
self.log.debug("queueing: %s", msg)
|
||||
|
||||
# if send is False, return msg as if it was successfully queued
|
||||
if not self.send:
|
||||
return True, msg
|
||||
|
||||
if self.sync_mode:
|
||||
self.log.debug('enqueued with blocking %s.', msg['event'])
|
||||
post(self.api_key, self.host, gzip=self.gzip,
|
||||
timeout=self.timeout, batch=[msg])
|
||||
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['event'])
|
||||
self.log.debug("enqueued %s.", msg["event"])
|
||||
return True, msg
|
||||
except queue.Full:
|
||||
self.log.warning('analytics-python queue is full')
|
||||
self.log.warning("analytics-python queue is full")
|
||||
return False, msg
|
||||
|
||||
def flush(self):
|
||||
@@ -197,7 +261,7 @@ class Client(object):
|
||||
size = queue.qsize()
|
||||
queue.join()
|
||||
# Note that this message may not be precise, because of threading.
|
||||
self.log.debug('successfully flushed about %s items.', size)
|
||||
self.log.debug("successfully flushed about %s items.", size)
|
||||
|
||||
def join(self):
|
||||
"""Ends the consumer thread once the queue is empty.
|
||||
@@ -216,11 +280,91 @@ class Client(object):
|
||||
self.flush()
|
||||
self.join()
|
||||
|
||||
def _load_feature_flags(self):
|
||||
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",
|
||||
)
|
||||
else:
|
||||
raise APIError(status=e.status, message=e.message)
|
||||
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):
|
||||
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()
|
||||
|
||||
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") and feature_flag.get("rollout_percentage"):
|
||||
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`"""
|
||||
if not isinstance(field, data_type):
|
||||
msg = '{0} must have {1}, got: {2}'.format(name, data_type, field)
|
||||
msg = "{0} must have {1}, got: {2}".format(name, data_type, field)
|
||||
raise AssertionError(msg)
|
||||
|
||||
|
||||
|
||||
+29
-27
@@ -1,10 +1,11 @@
|
||||
import json
|
||||
import logging
|
||||
from threading import Thread
|
||||
import monotonic
|
||||
import backoff
|
||||
import json
|
||||
|
||||
from posthog.request import post, APIError, DatetimeSerializer
|
||||
import backoff
|
||||
import monotonic
|
||||
|
||||
from posthog.request import APIError, DatetimeSerializer, batch_post
|
||||
|
||||
try:
|
||||
from queue import Empty
|
||||
@@ -20,11 +21,21 @@ BATCH_SIZE_LIMIT = 475000
|
||||
|
||||
class Consumer(Thread):
|
||||
"""Consumes the messages from the client's queue."""
|
||||
log = logging.getLogger('posthog')
|
||||
|
||||
def __init__(self, queue, api_key, flush_at=100, host=None,
|
||||
on_error=None, flush_interval=0.5, gzip=False, retries=10,
|
||||
timeout=15):
|
||||
log = logging.getLogger("posthog")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
queue,
|
||||
api_key,
|
||||
flush_at=100,
|
||||
host=None,
|
||||
on_error=None,
|
||||
flush_interval=0.5,
|
||||
gzip=False,
|
||||
retries=10,
|
||||
timeout=15,
|
||||
):
|
||||
"""Create a consumer thread."""
|
||||
Thread.__init__(self)
|
||||
# Make consumer a daemon thread so that it doesn't block program exit
|
||||
@@ -46,11 +57,11 @@ class Consumer(Thread):
|
||||
|
||||
def run(self):
|
||||
"""Runs the consumer."""
|
||||
self.log.debug('consumer is running...')
|
||||
self.log.debug("consumer is running...")
|
||||
while self.running:
|
||||
self.upload()
|
||||
|
||||
self.log.debug('consumer exited.')
|
||||
self.log.debug("consumer exited.")
|
||||
|
||||
def pause(self):
|
||||
"""Pause the consumer."""
|
||||
@@ -67,7 +78,7 @@ class Consumer(Thread):
|
||||
self.request(batch)
|
||||
success = True
|
||||
except Exception as e:
|
||||
self.log.error('error uploading: %s', e)
|
||||
self.log.error("error uploading: %s", e)
|
||||
success = False
|
||||
if self.on_error:
|
||||
self.on_error(e, batch)
|
||||
@@ -90,19 +101,15 @@ class Consumer(Thread):
|
||||
if elapsed >= self.flush_interval:
|
||||
break
|
||||
try:
|
||||
item = queue.get(
|
||||
block=True, timeout=self.flush_interval - elapsed)
|
||||
item_size = len(json.dumps(
|
||||
item, cls=DatetimeSerializer).encode())
|
||||
item = queue.get(block=True, timeout=self.flush_interval - elapsed)
|
||||
item_size = len(json.dumps(item, cls=DatetimeSerializer).encode())
|
||||
if item_size > MAX_MSG_SIZE:
|
||||
self.log.error(
|
||||
'Item exceeds 32kb limit, dropping. (%s)', str(item))
|
||||
self.log.error("Item exceeds 32kb limit, dropping. (%s)", str(item))
|
||||
continue
|
||||
items.append(item)
|
||||
total_size += item_size
|
||||
if total_size >= BATCH_SIZE_LIMIT:
|
||||
self.log.debug(
|
||||
'hit batch size limit (size: %d)', total_size)
|
||||
self.log.debug("hit batch size limit (size: %d)", total_size)
|
||||
break
|
||||
except Empty:
|
||||
break
|
||||
@@ -110,7 +117,7 @@ class Consumer(Thread):
|
||||
return items
|
||||
|
||||
def request(self, batch):
|
||||
"""Attempt to upload the batch and retry before raising an error """
|
||||
"""Attempt to upload the batch and retry before raising an error"""
|
||||
|
||||
def fatal_exception(exc):
|
||||
if isinstance(exc, APIError):
|
||||
@@ -122,13 +129,8 @@ class Consumer(Thread):
|
||||
# retry on all other errors (eg. network)
|
||||
return False
|
||||
|
||||
@backoff.on_exception(
|
||||
backoff.expo,
|
||||
Exception,
|
||||
max_tries=self.retries + 1,
|
||||
giveup=fatal_exception)
|
||||
@backoff.on_exception(backoff.expo, Exception, max_tries=self.retries + 1, giveup=fatal_exception)
|
||||
def send_request():
|
||||
post(self.api_key, self.host, gzip=self.gzip,
|
||||
timeout=self.timeout, batch=batch)
|
||||
batch_post(self.api_key, self.host, gzip=self.gzip, timeout=self.timeout, batch=batch)
|
||||
|
||||
send_request()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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)
|
||||
+66
-32
@@ -1,69 +1,103 @@
|
||||
from datetime import date, datetime
|
||||
from dateutil.tz import tzutc
|
||||
import logging
|
||||
import json
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from gzip import GzipFile
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from requests import sessions
|
||||
from io import BytesIO
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import requests
|
||||
from dateutil.tz import tzutc
|
||||
|
||||
from posthog.version import VERSION
|
||||
from posthog.utils import remove_trailing_slash
|
||||
from posthog.version import VERSION
|
||||
|
||||
_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: str, host: Optional[str] = None, path=None, gzip: bool = False, timeout: int = 15, **kwargs
|
||||
) -> requests.Response:
|
||||
"""Post the `kwargs` to the API"""
|
||||
log = logging.getLogger('posthog')
|
||||
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/'
|
||||
body['api_key'] = api_key
|
||||
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
|
||||
}
|
||||
log.debug("making request: %s", data)
|
||||
headers = {"Content-Type": "application/json", "User-Agent": USER_AGENT}
|
||||
if gzip:
|
||||
headers['Content-Encoding'] = 'gzip'
|
||||
headers["Content-Encoding"] = "gzip"
|
||||
buf = BytesIO()
|
||||
with GzipFile(fileobj=buf, mode='w') as gz:
|
||||
with GzipFile(fileobj=buf, mode="w") as gz:
|
||||
# 'data' was produced by json.dumps(),
|
||||
# whose default encoding is utf-8.
|
||||
gz.write(data.encode('utf-8'))
|
||||
gz.write(data.encode("utf-8"))
|
||||
data = buf.getvalue()
|
||||
|
||||
res = _session.post(url, data=data,
|
||||
headers=headers, timeout=timeout)
|
||||
res = _session.post(url, data=data, headers=headers, timeout=timeout)
|
||||
|
||||
if res.status_code == 200:
|
||||
log.debug('data uploaded successfully')
|
||||
return res
|
||||
log.debug("data uploaded successfully")
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def _process_response(
|
||||
res: requests.Response, success_message: str, *, return_json: bool = True
|
||||
) -> Union[requests.Response, Any]:
|
||||
log = logging.getLogger("posthog")
|
||||
if not res:
|
||||
raise APIError(
|
||||
"N/A",
|
||||
"Error when fetching PostHog API, please make sure you are using your public project token/key and not a private API key.",
|
||||
)
|
||||
if res.status_code == 200:
|
||||
log.debug(success_message)
|
||||
return res.json() if return_json else res
|
||||
try:
|
||||
payload = res.json()
|
||||
log.debug('received response: %s', payload)
|
||||
raise APIError(res.status_code, payload['code'], payload['message'])
|
||||
log.debug("received response: %s", payload)
|
||||
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 decide(api_key: str, host: Optional[str] = None, gzip: bool = False, timeout: int = 15, **kwargs) -> Any:
|
||||
"""Post the `kwargs to the decide API endpoint"""
|
||||
res = post(api_key, host, "/decide/", gzip, timeout, **kwargs)
|
||||
return _process_response(res, success_message="Feature flags decided successfully")
|
||||
|
||||
|
||||
def batch_post(
|
||||
api_key: str, host: Optional[str] = None, gzip: bool = False, timeout: int = 15, **kwargs
|
||||
) -> requests.Response:
|
||||
"""Post the `kwargs` to the batch API endpoint for events"""
|
||||
res = post(api_key, host, "/batch/", gzip, timeout, **kwargs)
|
||||
return _process_response(res, success_message="data uploaded successfully", return_json=False)
|
||||
|
||||
|
||||
def get(api_key: str, url: str, host: Optional[str] = None, timeout: Optional[int] = None) -> requests.Response:
|
||||
url = remove_trailing_slash(host or DEFAULT_HOST) + url
|
||||
res = requests.get(url, headers={"Authorization": "Bearer %s" % api_key, "User-Agent": USER_AGENT}, timeout=timeout)
|
||||
return _process_response(res, success_message=f"GET {url} completed successfully")
|
||||
|
||||
|
||||
class APIError(Exception):
|
||||
|
||||
def __init__(self, status, code, message):
|
||||
def __init__(self, status: Union[int, str], message: str):
|
||||
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):
|
||||
def default(self, obj):
|
||||
def default(self, obj: Any):
|
||||
if isinstance(obj, (date, datetime)):
|
||||
return obj.isoformat()
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import unittest
|
||||
import pkgutil
|
||||
import logging
|
||||
import pkgutil
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
def all_names():
|
||||
for _, modname, _ in pkgutil.iter_modules(__path__):
|
||||
yield 'posthog.test.' + modname
|
||||
yield "posthog.test." + modname
|
||||
|
||||
|
||||
def all():
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
from datetime import date, datetime
|
||||
import unittest
|
||||
import six
|
||||
import mock
|
||||
import time
|
||||
|
||||
from posthog.version import VERSION
|
||||
from posthog.client import Client
|
||||
from posthog.test.utils import TEST_API_KEY
|
||||
|
||||
class TestClient(unittest.TestCase):
|
||||
|
||||
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.fail)
|
||||
|
||||
def test_requires_api_key(self):
|
||||
self.assertRaises(AssertionError, Client)
|
||||
|
||||
def test_empty_flush(self):
|
||||
self.client.flush()
|
||||
|
||||
def test_basic_capture(self):
|
||||
client = self.client
|
||||
success, msg = client.capture('distinct_id', 'python test event')
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg['event'], 'python test event')
|
||||
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)
|
||||
|
||||
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(
|
||||
distinct_id=157963456373623802, event='python test event')
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg['distinct_id'], '157963456373623802')
|
||||
|
||||
def test_advanced_capture(self):
|
||||
client = self.client
|
||||
success, msg = client.capture(
|
||||
'distinct_id', 'python test event', {'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['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')
|
||||
|
||||
def test_basic_identify(self):
|
||||
client = self.client
|
||||
success, msg = client.identify('distinct_id', {'trait': 'value'})
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
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')
|
||||
|
||||
def test_advanced_identify(self):
|
||||
client = self.client
|
||||
success, msg = client.identify(
|
||||
'distinct_id', {'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['$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')
|
||||
|
||||
def test_basic_alias(self):
|
||||
client = self.client
|
||||
success, msg = client.alias('previousId', 'distinct_id')
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
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', url='https://posthog.com/contact')
|
||||
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')
|
||||
|
||||
def test_advanced_page(self):
|
||||
client = self.client
|
||||
success, msg = client.page(
|
||||
'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']['$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')
|
||||
|
||||
def test_flush(self):
|
||||
client = self.client
|
||||
# set up the consumer with more requests than a single batch will allow
|
||||
for i in range(1000):
|
||||
success, msg = client.identify('distinct_id', {'trait': 'value'})
|
||||
# We can't reliably assert that the queue is non-empty here; that's
|
||||
# a race condition. We do our best to load it up though.
|
||||
client.flush()
|
||||
# Make sure that the client queue is empty after flushing
|
||||
self.assertTrue(client.queue.empty())
|
||||
|
||||
def test_shutdown(self):
|
||||
client = self.client
|
||||
# set up the consumer with more requests than a single batch will allow
|
||||
for i in range(1000):
|
||||
success, msg = client.identify('distinct_id', {'trait': 'value'})
|
||||
client.shutdown()
|
||||
# we expect two things after shutdown:
|
||||
# 1. client queue is empty
|
||||
# 2. consumer thread has stopped
|
||||
self.assertTrue(client.queue.empty())
|
||||
for consumer in client.consumers:
|
||||
self.assertFalse(consumer.is_alive())
|
||||
|
||||
def test_synchronous(self):
|
||||
client = Client(TEST_API_KEY, sync_mode=True)
|
||||
|
||||
success, message = client.identify('distinct_id')
|
||||
self.assertFalse(client.consumers)
|
||||
self.assertTrue(client.queue.empty())
|
||||
self.assertTrue(success)
|
||||
|
||||
def test_overflow(self):
|
||||
client = Client(TEST_API_KEY, max_queue_size=1)
|
||||
# Ensure consumer thread is no longer uploading
|
||||
client.join()
|
||||
|
||||
for i in range(10):
|
||||
client.identify('distinct_id')
|
||||
|
||||
success, msg = client.identify('distinct_id')
|
||||
# Make sure we are informed that the queue is at capacity
|
||||
self.assertFalse(success)
|
||||
|
||||
def test_unicode(self):
|
||||
Client(six.u('unicode_key'))
|
||||
|
||||
def test_numeric_distinct_id(self):
|
||||
self.client.capture(1234, 'python event')
|
||||
self.client.flush()
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
def test_debug(self):
|
||||
Client('bad_key', debug=True)
|
||||
|
||||
def test_gzip(self):
|
||||
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(TEST_API_KEY, on_error=self.fail,
|
||||
flush_at=10, flush_interval=3)
|
||||
|
||||
def mock_post_fn(*args, **kwargs):
|
||||
self.assertEquals(len(kwargs['batch']), 10)
|
||||
|
||||
# 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) \
|
||||
as mock_post:
|
||||
for _ in range(20):
|
||||
client.identify('distinct_id', {'trait': 'value'})
|
||||
time.sleep(1)
|
||||
self.assertEquals(mock_post.call_count, 2)
|
||||
|
||||
def test_user_defined_timeout(self):
|
||||
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(TEST_API_KEY)
|
||||
for consumer in client.consumers:
|
||||
self.assertEquals(consumer.timeout, 15)
|
||||
@@ -1,54 +0,0 @@
|
||||
from datetime import datetime, date
|
||||
import unittest
|
||||
import json
|
||||
import requests
|
||||
|
||||
from posthog.request import 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=[{
|
||||
'distinct_id': 'distinct_id',
|
||||
'event': 'python event',
|
||||
'type': 'track'
|
||||
}])
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_invalid_request_error(self):
|
||||
self.assertRaises(Exception, post, 'testsecret',
|
||||
'https://t.posthog.com', False, '[{]')
|
||||
|
||||
def test_invalid_host(self):
|
||||
self.assertRaises(Exception, post, 'testsecret',
|
||||
't.posthog.com/', batch=[])
|
||||
|
||||
def test_datetime_serialization(self):
|
||||
data = {'created': datetime(2012, 3, 4, 5, 6, 7, 891011)}
|
||||
result = json.dumps(data, cls=DatetimeSerializer)
|
||||
self.assertEqual(result, '{"created": "2012-03-04T05:06:07.891011"}')
|
||||
|
||||
def test_date_serialization(self):
|
||||
today = date.today()
|
||||
data = {'created': today}
|
||||
result = json.dumps(data, cls=DatetimeSerializer)
|
||||
expected = '{"created": "%s"}' % today.isoformat()
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_should_not_timeout(self):
|
||||
res = post(TEST_API_KEY, batch=[{
|
||||
'distinct_id': 'distinct_id',
|
||||
'event': 'python event',
|
||||
'type': 'track'
|
||||
}], timeout=15)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_should_timeout(self):
|
||||
with self.assertRaises(requests.ReadTimeout):
|
||||
post('key', batch=[{
|
||||
'distinct_id': 'distinct_id',
|
||||
'event': 'python event',
|
||||
'type': 'track'
|
||||
}], timeout=0.0001)
|
||||
@@ -0,0 +1,376 @@
|
||||
import time
|
||||
import unittest
|
||||
from datetime import date, datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import mock
|
||||
import six
|
||||
from freezegun import freeze_time
|
||||
|
||||
from posthog.client import Client
|
||||
from posthog.request import APIError
|
||||
from posthog.test.test_utils import TEST_API_KEY
|
||||
from posthog.version import VERSION
|
||||
|
||||
|
||||
class TestClient(unittest.TestCase):
|
||||
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(TEST_API_KEY, on_error=self.set_fail)
|
||||
|
||||
def test_requires_api_key(self):
|
||||
self.assertRaises(AssertionError, Client)
|
||||
|
||||
def test_empty_flush(self):
|
||||
self.client.flush()
|
||||
|
||||
def test_basic_capture(self):
|
||||
client = self.client
|
||||
success, msg = client.capture("distinct_id", "python test event")
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg["event"], "python test event")
|
||||
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)
|
||||
|
||||
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(distinct_id=157963456373623802, event="python test event")
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg["distinct_id"], "157963456373623802")
|
||||
|
||||
def test_advanced_capture(self):
|
||||
client = self.client
|
||||
success, msg = client.capture(
|
||||
"distinct_id",
|
||||
"python test event",
|
||||
{"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["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")
|
||||
|
||||
def test_basic_identify(self):
|
||||
client = self.client
|
||||
success, msg = client.identify("distinct_id", {"trait": "value"})
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
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")
|
||||
|
||||
def test_advanced_identify(self):
|
||||
client = self.client
|
||||
success, msg = client.identify(
|
||||
"distinct_id", {"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["$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")
|
||||
|
||||
def test_basic_set(self):
|
||||
client = self.client
|
||||
success, msg = client.set("distinct_id", {"trait": "value"})
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
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")
|
||||
|
||||
def test_advanced_set(self):
|
||||
client = self.client
|
||||
success, msg = client.set(
|
||||
"distinct_id", {"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["$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")
|
||||
|
||||
def test_basic_set_once(self):
|
||||
client = self.client
|
||||
success, msg = client.set_once("distinct_id", {"trait": "value"})
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg["$set_once"]["trait"], "value")
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertTrue(isinstance(msg["messageId"], str))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_advanced_set_once(self):
|
||||
client = self.client
|
||||
success, msg = client.set_once(
|
||||
"distinct_id", {"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["$set_once"]["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")
|
||||
|
||||
def test_basic_alias(self):
|
||||
client = self.client
|
||||
success, msg = client.alias("previousId", "distinct_id")
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
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", url="https://posthog.com/contact")
|
||||
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")
|
||||
|
||||
def test_basic_page_distinct_uuid(self):
|
||||
client = self.client
|
||||
distinct_id = uuid4()
|
||||
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"], str(distinct_id))
|
||||
self.assertEqual(msg["properties"]["$current_url"], "https://posthog.com/contact")
|
||||
|
||||
def test_advanced_page(self):
|
||||
client = self.client
|
||||
success, msg = client.page(
|
||||
"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"]["$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")
|
||||
|
||||
def test_flush(self):
|
||||
client = self.client
|
||||
# set up the consumer with more requests than a single batch will allow
|
||||
for i in range(1000):
|
||||
success, msg = client.identify("distinct_id", {"trait": "value"})
|
||||
# We can't reliably assert that the queue is non-empty here; that's
|
||||
# a race condition. We do our best to load it up though.
|
||||
client.flush()
|
||||
# Make sure that the client queue is empty after flushing
|
||||
self.assertTrue(client.queue.empty())
|
||||
|
||||
def test_shutdown(self):
|
||||
client = self.client
|
||||
# set up the consumer with more requests than a single batch will allow
|
||||
for i in range(1000):
|
||||
success, msg = client.identify("distinct_id", {"trait": "value"})
|
||||
client.shutdown()
|
||||
# we expect two things after shutdown:
|
||||
# 1. client queue is empty
|
||||
# 2. consumer thread has stopped
|
||||
self.assertTrue(client.queue.empty())
|
||||
for consumer in client.consumers:
|
||||
self.assertFalse(consumer.is_alive())
|
||||
|
||||
def test_synchronous(self):
|
||||
client = Client(TEST_API_KEY, sync_mode=True)
|
||||
|
||||
success, message = client.identify("distinct_id")
|
||||
self.assertFalse(client.consumers)
|
||||
self.assertTrue(client.queue.empty())
|
||||
self.assertTrue(success)
|
||||
|
||||
def test_overflow(self):
|
||||
client = Client(TEST_API_KEY, max_queue_size=1)
|
||||
# Ensure consumer thread is no longer uploading
|
||||
client.join()
|
||||
|
||||
for i in range(10):
|
||||
client.identify("distinct_id")
|
||||
|
||||
success, msg = client.identify("distinct_id")
|
||||
# Make sure we are informed that the queue is at capacity
|
||||
self.assertFalse(success)
|
||||
|
||||
def test_unicode(self):
|
||||
Client(six.u("unicode_key"))
|
||||
|
||||
def test_numeric_distinct_id(self):
|
||||
self.client.capture(1234, "python event")
|
||||
self.client.flush()
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
def test_debug(self):
|
||||
Client("bad_key", debug=True)
|
||||
|
||||
def test_gzip(self):
|
||||
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(TEST_API_KEY, on_error=self.fail, flush_at=10, flush_interval=3)
|
||||
|
||||
def mock_post_fn(*args, **kwargs):
|
||||
self.assertEquals(len(kwargs["batch"]), 10)
|
||||
|
||||
# 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) as mock_post:
|
||||
for _ in range(20):
|
||||
client.identify("distinct_id", {"trait": "value"})
|
||||
time.sleep(1)
|
||||
self.assertEquals(mock_post.call_count, 2)
|
||||
|
||||
def test_user_defined_timeout(self):
|
||||
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(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"))
|
||||
@@ -1,23 +1,23 @@
|
||||
import unittest
|
||||
import mock
|
||||
import time
|
||||
import json
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
try:
|
||||
from queue import Queue
|
||||
except ImportError:
|
||||
from Queue import Queue
|
||||
|
||||
from posthog.consumer import Consumer, MAX_MSG_SIZE
|
||||
from posthog.consumer import MAX_MSG_SIZE, Consumer
|
||||
from posthog.request import APIError
|
||||
from posthog.test.utils import TEST_API_KEY
|
||||
from posthog.test.test_utils import TEST_API_KEY
|
||||
|
||||
|
||||
class TestConsumer(unittest.TestCase):
|
||||
|
||||
def test_next(self):
|
||||
q = Queue()
|
||||
consumer = Consumer(q, '')
|
||||
consumer = Consumer(q, "")
|
||||
q.put(1)
|
||||
next = consumer.next()
|
||||
self.assertEqual(next, [1])
|
||||
@@ -25,7 +25,7 @@ class TestConsumer(unittest.TestCase):
|
||||
def test_next_limit(self):
|
||||
q = Queue()
|
||||
flush_at = 50
|
||||
consumer = Consumer(q, '', flush_at)
|
||||
consumer = Consumer(q, "", flush_at)
|
||||
for i in range(10000):
|
||||
q.put(i)
|
||||
next = consumer.next()
|
||||
@@ -33,8 +33,8 @@ class TestConsumer(unittest.TestCase):
|
||||
|
||||
def test_dropping_oversize_msg(self):
|
||||
q = Queue()
|
||||
consumer = Consumer(q, '')
|
||||
oversize_msg = {'m': 'x' * MAX_MSG_SIZE}
|
||||
consumer = Consumer(q, "")
|
||||
oversize_msg = {"m": "x" * MAX_MSG_SIZE}
|
||||
q.put(oversize_msg)
|
||||
next = consumer.next()
|
||||
self.assertEqual(next, [])
|
||||
@@ -43,11 +43,7 @@ class TestConsumer(unittest.TestCase):
|
||||
def test_upload(self):
|
||||
q = Queue()
|
||||
consumer = Consumer(q, TEST_API_KEY)
|
||||
track = {
|
||||
'type': 'track',
|
||||
'event': 'python event',
|
||||
'distinct_id': 'distinct_id'
|
||||
}
|
||||
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
|
||||
q.put(track)
|
||||
success = consumer.upload()
|
||||
self.assertTrue(success)
|
||||
@@ -58,16 +54,11 @@ 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,
|
||||
flush_interval=flush_interval)
|
||||
with mock.patch('posthog.consumer.post') as mock_post:
|
||||
consumer = Consumer(q, TEST_API_KEY, flush_at=10, flush_interval=flush_interval)
|
||||
with mock.patch("posthog.consumer.batch_post") as mock_post:
|
||||
consumer.start()
|
||||
for i in range(0, 3):
|
||||
track = {
|
||||
'type': 'track',
|
||||
'event': 'python event %d' % i,
|
||||
'distinct_id': 'distinct_id'
|
||||
}
|
||||
track = {"type": "track", "event": "python event %d" % i, "distinct_id": "distinct_id"}
|
||||
q.put(track)
|
||||
time.sleep(flush_interval * 1.1)
|
||||
self.assertEqual(mock_post.call_count, 3)
|
||||
@@ -78,45 +69,30 @@ class TestConsumer(unittest.TestCase):
|
||||
q = Queue()
|
||||
flush_interval = 0.5
|
||||
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:
|
||||
consumer = Consumer(q, TEST_API_KEY, flush_at=flush_at, flush_interval=flush_interval)
|
||||
with mock.patch("posthog.consumer.batch_post") as mock_post:
|
||||
consumer.start()
|
||||
for i in range(0, flush_at * 2):
|
||||
track = {
|
||||
'type': 'track',
|
||||
'event': 'python event %d' % i,
|
||||
'distinct_id': 'distinct_id'
|
||||
}
|
||||
track = {"type": "track", "event": "python event %d" % i, "distinct_id": "distinct_id"}
|
||||
q.put(track)
|
||||
time.sleep(flush_interval * 1.1)
|
||||
self.assertEqual(mock_post.call_count, 2)
|
||||
|
||||
def test_request(self):
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
track = {
|
||||
'type': 'track',
|
||||
'event': 'python event',
|
||||
'distinct_id': 'distinct_id'
|
||||
}
|
||||
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
|
||||
consumer.request([track])
|
||||
|
||||
def _test_request_retry(self, consumer,
|
||||
expected_exception, exception_count):
|
||||
|
||||
def _test_request_retry(self, consumer, expected_exception, exception_count):
|
||||
def mock_post(*args, **kwargs):
|
||||
mock_post.call_count += 1
|
||||
if mock_post.call_count <= exception_count:
|
||||
raise expected_exception
|
||||
|
||||
mock_post.call_count = 0
|
||||
|
||||
with mock.patch('posthog.consumer.post',
|
||||
mock.Mock(side_effect=mock_post)):
|
||||
track = {
|
||||
'type': 'track',
|
||||
'event': 'python event',
|
||||
'distinct_id': 'distinct_id'
|
||||
}
|
||||
with mock.patch("posthog.consumer.batch_post", mock.Mock(side_effect=mock_post)):
|
||||
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
|
||||
# request() should succeed if the number of exceptions raised is
|
||||
# less than the retries paramater.
|
||||
if exception_count <= consumer.retries:
|
||||
@@ -131,38 +107,35 @@ class TestConsumer(unittest.TestCase):
|
||||
self.assertEqual(exc, expected_exception)
|
||||
else:
|
||||
self.fail(
|
||||
"request() should raise an exception if still failing "
|
||||
"after %d retries" % consumer.retries)
|
||||
"request() should raise an exception if still failing after %d retries" % consumer.retries
|
||||
)
|
||||
|
||||
def test_request_retry(self):
|
||||
# we should retry on general errors
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
self._test_request_retry(consumer, Exception('generic exception'), 2)
|
||||
self._test_request_retry(consumer, Exception("generic exception"), 2)
|
||||
|
||||
# we should retry on server errors
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
self._test_request_retry(consumer, APIError(
|
||||
500, 'code', 'Internal Server Error'), 2)
|
||||
self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 2)
|
||||
|
||||
# we should retry on HTTP 429 errors
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
self._test_request_retry(consumer, APIError(
|
||||
429, 'code', 'Too Many Requests'), 2)
|
||||
self._test_request_retry(consumer, APIError(429, "Too Many Requests"), 2)
|
||||
|
||||
# we should NOT retry on other client errors
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
api_error = APIError(400, 'code', 'Client Errors')
|
||||
api_error = APIError(400, "Client Errors")
|
||||
try:
|
||||
self._test_request_retry(consumer, api_error, 1)
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
self.fail('request() should not retry on client errors')
|
||||
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)
|
||||
self._test_request_retry(consumer, APIError(
|
||||
500, 'code', 'Internal Server Error'), 3)
|
||||
self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 3)
|
||||
|
||||
def test_pause(self):
|
||||
consumer = Consumer(None, TEST_API_KEY)
|
||||
@@ -171,13 +144,8 @@ class TestConsumer(unittest.TestCase):
|
||||
|
||||
def test_max_batch_size(self):
|
||||
q = Queue()
|
||||
consumer = Consumer(
|
||||
q, TEST_API_KEY, flush_at=100000, flush_interval=3)
|
||||
track = {
|
||||
'type': 'track',
|
||||
'event': 'python event',
|
||||
'distinct_id': 'distinct_id'
|
||||
}
|
||||
consumer = Consumer(q, TEST_API_KEY, flush_at=100000, flush_interval=3)
|
||||
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
|
||||
msg_size = len(json.dumps(track).encode())
|
||||
# number of messages in a maximum-size batch
|
||||
n_msgs = int(475000 / msg_size)
|
||||
@@ -185,13 +153,10 @@ class TestConsumer(unittest.TestCase):
|
||||
def mock_post_fn(_, data, **kwargs):
|
||||
res = mock.Mock()
|
||||
res.status_code = 200
|
||||
self.assertTrue(len(data.encode()) < 500000,
|
||||
'batch size (%d) exceeds 500KB limit'
|
||||
% len(data.encode()))
|
||||
self.assertTrue(len(data.encode()) < 500000, "batch size (%d) exceeds 500KB limit" % len(data.encode()))
|
||||
return res
|
||||
|
||||
with mock.patch('posthog.request._session.post',
|
||||
side_effect=mock_post_fn) as mock_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):
|
||||
q.put(track)
|
||||
@@ -4,13 +4,12 @@ import posthog
|
||||
|
||||
|
||||
class TestModule(unittest.TestCase):
|
||||
|
||||
def failed(self):
|
||||
self.failed = True
|
||||
|
||||
def setUp(self):
|
||||
self.failed = False
|
||||
posthog.api_key = 'testsecret'
|
||||
posthog.api_key = "testsecret"
|
||||
posthog.on_error = self.failed
|
||||
|
||||
def test_no_api_key(self):
|
||||
@@ -22,19 +21,19 @@ class TestModule(unittest.TestCase):
|
||||
self.assertRaises(Exception, posthog.capture)
|
||||
|
||||
def test_track(self):
|
||||
posthog.capture('distinct_id', 'python module event')
|
||||
posthog.capture("distinct_id", "python module event")
|
||||
posthog.flush()
|
||||
|
||||
def test_identify(self):
|
||||
posthog.identify('distinct_id', {'email': 'user@email.com'})
|
||||
posthog.identify("distinct_id", {"email": "user@email.com"})
|
||||
posthog.flush()
|
||||
|
||||
def test_alias(self):
|
||||
posthog.alias('previousId', 'distinct_id')
|
||||
posthog.alias("previousId", "distinct_id")
|
||||
posthog.flush()
|
||||
|
||||
def test_page(self):
|
||||
posthog.page('distinct_id', 'https://posthog.com/contact')
|
||||
posthog.page("distinct_id", "https://posthog.com/contact")
|
||||
posthog.flush()
|
||||
|
||||
def test_flush(self):
|
||||
@@ -0,0 +1,44 @@
|
||||
import json
|
||||
import unittest
|
||||
from datetime import date, datetime
|
||||
|
||||
import requests
|
||||
|
||||
from posthog.request import DatetimeSerializer, batch_post
|
||||
from posthog.test.test_utils import TEST_API_KEY
|
||||
|
||||
|
||||
class TestRequests(unittest.TestCase):
|
||||
def test_valid_request(self):
|
||||
res = batch_post(TEST_API_KEY, batch=[{"distinct_id": "distinct_id", "event": "python event", "type": "track"}])
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_invalid_request_error(self):
|
||||
self.assertRaises(Exception, batch_post, "testsecret", "https://t.posthog.com", False, "[{]")
|
||||
|
||||
def test_invalid_host(self):
|
||||
self.assertRaises(Exception, batch_post, "testsecret", "t.posthog.com/", batch=[])
|
||||
|
||||
def test_datetime_serialization(self):
|
||||
data = {"created": datetime(2012, 3, 4, 5, 6, 7, 891011)}
|
||||
result = json.dumps(data, cls=DatetimeSerializer)
|
||||
self.assertEqual(result, '{"created": "2012-03-04T05:06:07.891011"}')
|
||||
|
||||
def test_date_serialization(self):
|
||||
today = date.today()
|
||||
data = {"created": today}
|
||||
result = json.dumps(data, cls=DatetimeSerializer)
|
||||
expected = '{"created": "%s"}' % today.isoformat()
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_should_not_timeout(self):
|
||||
res = batch_post(
|
||||
TEST_API_KEY, batch=[{"distinct_id": "distinct_id", "event": "python event", "type": "track"}], timeout=15
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_should_timeout(self):
|
||||
with self.assertRaises(requests.ReadTimeout):
|
||||
batch_post(
|
||||
"key", batch=[{"distinct_id": "distinct_id", "event": "python event", "type": "track"}], timeout=0.0001
|
||||
)
|
||||
@@ -1,16 +1,17 @@
|
||||
import unittest
|
||||
from datetime import date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
import unittest
|
||||
from uuid import UUID
|
||||
|
||||
from dateutil.tz import tzutc
|
||||
import six
|
||||
from dateutil.tz import tzutc
|
||||
|
||||
from posthog import utils
|
||||
|
||||
TEST_API_KEY = 'kOOlRy2QlMY9jHZQv0bKz0FZyazBUoY8Arj0lFVNjs4'
|
||||
TEST_API_KEY = "kOOlRy2QlMY9jHZQv0bKz0FZyazBUoY8Arj0lFVNjs4"
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
|
||||
def test_timezone_utils(self):
|
||||
now = datetime.now()
|
||||
utcnow = datetime.now(tz=tzutc())
|
||||
@@ -25,21 +26,21 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
def test_clean(self):
|
||||
simple = {
|
||||
'decimal': Decimal('0.142857'),
|
||||
'unicode': six.u('woo'),
|
||||
'date': datetime.now(),
|
||||
'long': 200000000,
|
||||
'integer': 1,
|
||||
'float': 2.0,
|
||||
'bool': True,
|
||||
'str': 'woo',
|
||||
'none': None
|
||||
"decimal": Decimal("0.142857"),
|
||||
"unicode": six.u("woo"),
|
||||
"date": datetime.now(),
|
||||
"long": 200000000,
|
||||
"integer": 1,
|
||||
"float": 2.0,
|
||||
"bool": True,
|
||||
"str": "woo",
|
||||
"none": None,
|
||||
}
|
||||
|
||||
complicated = {
|
||||
'exception': Exception('This should show up'),
|
||||
'timedelta': timedelta(microseconds=20),
|
||||
'list': [1, 2, 3]
|
||||
"exception": Exception("This should show up"),
|
||||
"timedelta": timedelta(microseconds=20),
|
||||
"list": [1, 2, 3],
|
||||
}
|
||||
|
||||
combined = dict(simple.items())
|
||||
@@ -50,10 +51,13 @@ 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),
|
||||
'registration': datetime.utcnow(),
|
||||
"birthdate": date(1980, 1, 1),
|
||||
"registration": datetime.utcnow(),
|
||||
}
|
||||
self.assertEqual(dict_with_dates, utils.clean(dict_with_dates))
|
||||
|
||||
@@ -66,14 +70,12 @@ class TestUtils(unittest.TestCase):
|
||||
utils.clean(item)
|
||||
|
||||
def test_clean_fn(self):
|
||||
cleaned = utils.clean({'fn': lambda x: x, 'number': 4})
|
||||
self.assertEqual(cleaned['number'], 4)
|
||||
cleaned = utils.clean({"fn": lambda x: x, "number": 4})
|
||||
self.assertEqual(cleaned["number"], 4)
|
||||
# TODO: fixme, different behavior on python 2 and 3
|
||||
if 'fn' in cleaned:
|
||||
self.assertEqual(cleaned['fn'], None)
|
||||
if "fn" in cleaned:
|
||||
self.assertEqual(cleaned["fn"], None)
|
||||
|
||||
def test_remove_slash(self):
|
||||
self.assertEqual('http://posthog.io',
|
||||
utils.remove_trailing_slash('http://posthog.io/'))
|
||||
self.assertEqual('http://posthog.io',
|
||||
utils.remove_trailing_slash('http://posthog.io'))
|
||||
self.assertEqual("http://posthog.io", utils.remove_trailing_slash("http://posthog.io/"))
|
||||
self.assertEqual("http://posthog.io", utils.remove_trailing_slash("http://posthog.io"))
|
||||
+15
-13
@@ -1,12 +1,13 @@
|
||||
from dateutil.tz import tzlocal, tzutc
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
import numbers
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
import six
|
||||
from dateutil.tz import tzlocal, tzutc
|
||||
|
||||
log = logging.getLogger('posthog')
|
||||
log = logging.getLogger("posthog")
|
||||
|
||||
|
||||
def is_naive(dt):
|
||||
@@ -17,8 +18,7 @@ def is_naive(dt):
|
||||
def total_seconds(delta):
|
||||
"""Determines total seconds with python < 2.7 compat."""
|
||||
# http://stackoverflow.com/questions/3694835/python-2-6-5-divide-timedelta-with-timedelta
|
||||
return (delta.microseconds
|
||||
+ (delta.seconds + delta.days * 24 * 3600) * 1e6) / 1e6
|
||||
return (delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 1e6) / 1e6
|
||||
|
||||
|
||||
def guess_timezone(dt):
|
||||
@@ -39,7 +39,7 @@ def guess_timezone(dt):
|
||||
|
||||
|
||||
def remove_trailing_slash(host):
|
||||
if host.endswith('/'):
|
||||
if host.endswith("/"):
|
||||
return host[:-1]
|
||||
return host
|
||||
|
||||
@@ -47,8 +47,9 @@ def remove_trailing_slash(host):
|
||||
def clean(item):
|
||||
if isinstance(item, Decimal):
|
||||
return float(item)
|
||||
elif isinstance(item, (six.string_types, bool, numbers.Number, datetime,
|
||||
date, type(None))):
|
||||
if isinstance(item, UUID):
|
||||
return str(item)
|
||||
elif isinstance(item, (six.string_types, bool, numbers.Number, datetime, date, type(None))):
|
||||
return item
|
||||
elif isinstance(item, (set, list, tuple)):
|
||||
return _clean_list(item)
|
||||
@@ -69,9 +70,10 @@ def _clean_dict(dict_):
|
||||
data[k] = clean(v)
|
||||
except TypeError:
|
||||
log.warning(
|
||||
'Dictionary values must be serializeable to '
|
||||
'JSON "%s" value %s of type %s is unsupported.',
|
||||
k, v, type(v),
|
||||
'Dictionary values must be serializeable to JSON "%s" value %s of type %s is unsupported.',
|
||||
k,
|
||||
v,
|
||||
type(v),
|
||||
)
|
||||
return data
|
||||
|
||||
@@ -82,6 +84,6 @@ def _coerce_unicode(cmplx):
|
||||
except AttributeError as exception:
|
||||
item = ":".join(exception)
|
||||
item.decode("utf-8", "strict")
|
||||
log.warning('Error decoding: %s', item)
|
||||
log.warning("Error decoding: %s", item)
|
||||
return None
|
||||
return item
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
VERSION = '1.0.11'
|
||||
VERSION = "1.3.1"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = true
|
||||
force_grid_wrap = 8
|
||||
ensure_newline_before_comments = true
|
||||
line_length = 120
|
||||
virtual_env = "env"
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -8,39 +7,38 @@ except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
# Don't import analytics-python module here, since deps may not be installed
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'posthog'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "posthog"))
|
||||
from version import VERSION
|
||||
|
||||
long_description = '''
|
||||
long_description = """
|
||||
PostHog is developer-friendly, self-hosted product analytics. posthog-python is the python package.
|
||||
'''
|
||||
"""
|
||||
|
||||
install_requires = [
|
||||
"requests>=2.7,<3.0",
|
||||
"six>=1.5",
|
||||
"monotonic>=1.5",
|
||||
"backoff==1.6.0",
|
||||
"python-dateutil>2.1"
|
||||
]
|
||||
install_requires = ["requests>=2.7,<3.0", "six>=1.5", "monotonic>=1.5", "backoff==1.6.0", "python-dateutil>2.1"]
|
||||
|
||||
tests_require = [
|
||||
"mock>=2.0.0"
|
||||
]
|
||||
extras_require = {
|
||||
"dev": [
|
||||
"black",
|
||||
"isort",
|
||||
"pre-commit",
|
||||
],
|
||||
"test": ["mock>=2.0.0", "freezegun==0.3.15", "pylint", "flake8", "coverage"],
|
||||
}
|
||||
|
||||
setup(
|
||||
name='posthog',
|
||||
name="posthog",
|
||||
version=VERSION,
|
||||
url='https://github.com/posthog/posthog-python',
|
||||
author='Posthog',
|
||||
author_email='hey@posthog.com',
|
||||
maintainer='PostHog',
|
||||
maintainer_email='hey@posthog.com',
|
||||
test_suite='posthog.test.all',
|
||||
packages=['posthog', 'posthog.test'],
|
||||
license='MIT License',
|
||||
url="https://github.com/posthog/posthog-python",
|
||||
author="Posthog",
|
||||
author_email="hey@posthog.com",
|
||||
maintainer="PostHog",
|
||||
maintainer_email="hey@posthog.com",
|
||||
test_suite="posthog.test.all",
|
||||
packages=["posthog", "posthog.test"],
|
||||
license="MIT License",
|
||||
install_requires=install_requires,
|
||||
tests_require=tests_require,
|
||||
description='Integrate PostHog into any python application.',
|
||||
extras_require=extras_require,
|
||||
description="Integrate PostHog into any python application.",
|
||||
long_description=long_description,
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
+15
-24
@@ -1,4 +1,3 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -8,39 +7,31 @@ except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
# Don't import module here, since deps may not be installed
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'posthoganalytics'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "posthoganalytics"))
|
||||
from version import VERSION
|
||||
|
||||
long_description = '''
|
||||
long_description = """
|
||||
PostHog is developer-friendly, self-hosted product analytics. posthog-python is the python package.
|
||||
'''
|
||||
"""
|
||||
|
||||
install_requires = [
|
||||
"requests>=2.7,<3.0",
|
||||
"six>=1.5",
|
||||
"monotonic>=1.5",
|
||||
"backoff==1.6.0",
|
||||
"python-dateutil>2.1"
|
||||
]
|
||||
install_requires = ["requests>=2.7,<3.0", "six>=1.5", "monotonic>=1.5", "backoff==1.6.0", "python-dateutil>2.1"]
|
||||
|
||||
tests_require = [
|
||||
"mock>=2.0.0"
|
||||
]
|
||||
tests_require = ["mock>=2.0.0"]
|
||||
|
||||
setup(
|
||||
name='posthoganalytics',
|
||||
name="posthoganalytics",
|
||||
version=VERSION,
|
||||
url='https://github.com/posthog/posthog-python',
|
||||
author='Posthog',
|
||||
author_email='hey@posthog.com',
|
||||
maintainer='PostHog',
|
||||
maintainer_email='hey@posthog.com',
|
||||
test_suite='posthoganalytics.test.all',
|
||||
packages=['posthoganalytics', 'posthoganalytics.test'],
|
||||
license='MIT License',
|
||||
url="https://github.com/posthog/posthog-python",
|
||||
author="Posthog",
|
||||
author_email="hey@posthog.com",
|
||||
maintainer="PostHog",
|
||||
maintainer_email="hey@posthog.com",
|
||||
test_suite="posthoganalytics.test.all",
|
||||
packages=["posthoganalytics", "posthoganalytics.test"],
|
||||
license="MIT License",
|
||||
install_requires=install_requires,
|
||||
tests_require=tests_require,
|
||||
description='Integrate PostHog into any python application.',
|
||||
description="Integrate PostHog into any python application.",
|
||||
long_description=long_description,
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
+55
-32
@@ -1,42 +1,39 @@
|
||||
import posthog
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
__name__ = 'simulator.py'
|
||||
__version__ = '0.0.1'
|
||||
__description__ = 'scripting simulator'
|
||||
import posthog
|
||||
|
||||
__name__ = "simulator.py"
|
||||
__version__ = "0.0.1"
|
||||
__description__ = "scripting simulator"
|
||||
|
||||
|
||||
def json_hash(str):
|
||||
if str:
|
||||
return json.loads(str)
|
||||
|
||||
|
||||
# posthog -method=<method> -posthog-write-key=<posthogWriteKey> [options]
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='send a posthog message')
|
||||
parser = argparse.ArgumentParser(description="send a posthog message")
|
||||
|
||||
parser.add_argument('--writeKey', help='the posthog writeKey')
|
||||
parser.add_argument('--type', help='The posthog message type')
|
||||
parser.add_argument("--writeKey", help="the posthog writeKey")
|
||||
parser.add_argument("--type", help="The posthog message type")
|
||||
|
||||
parser.add_argument('--distinct_id', help='the user id to send the event as')
|
||||
parser.add_argument(
|
||||
'--anonymousId', help='the anonymous user id to send the event as')
|
||||
parser.add_argument(
|
||||
'--context', help='additional context for the event (JSON-encoded)')
|
||||
parser.add_argument("--distinct_id", help="the user id to send the event as")
|
||||
parser.add_argument("--anonymousId", help="the anonymous user id to send the event as")
|
||||
parser.add_argument("--context", help="additional context for the event (JSON-encoded)")
|
||||
|
||||
parser.add_argument('--event', help='the event name to send with the event')
|
||||
parser.add_argument(
|
||||
'--properties', help='the event properties to send (JSON-encoded)')
|
||||
parser.add_argument("--event", help="the event name to send with the event")
|
||||
parser.add_argument("--properties", help="the event properties to send (JSON-encoded)")
|
||||
|
||||
parser.add_argument(
|
||||
'--name', help='name of the screen or page to send with the message')
|
||||
parser.add_argument("--name", help="name of the screen or page to send with the message")
|
||||
|
||||
parser.add_argument(
|
||||
'--traits', help='the identify/group traits to send (JSON-encoded)')
|
||||
parser.add_argument("--traits", help="the identify/group traits to send (JSON-encoded)")
|
||||
|
||||
parser.add_argument('--groupId', help='the group id')
|
||||
parser.add_argument("--groupId", help="the group id")
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
@@ -46,18 +43,48 @@ def failed(status, msg):
|
||||
|
||||
|
||||
def capture():
|
||||
posthog.capture(options.distinct_id, options.event, anonymous_id=options.anonymousId,
|
||||
properties=json_hash(options.properties), context=json_hash(options.context))
|
||||
posthog.capture(
|
||||
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,
|
||||
properties=json_hash(options.properties), context=json_hash(options.context))
|
||||
posthog.page(
|
||||
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,
|
||||
traits=json_hash(options.traits), context=json_hash(options.context))
|
||||
posthog.identify(
|
||||
options.distinct_id,
|
||||
anonymous_id=options.anonymousId,
|
||||
traits=json_hash(options.traits),
|
||||
context=json_hash(options.context),
|
||||
)
|
||||
|
||||
|
||||
def set_once():
|
||||
posthog.set_once(
|
||||
options.distinct_id,
|
||||
properties=json_hash(options.traits),
|
||||
context=json_hash(options.context),
|
||||
)
|
||||
|
||||
|
||||
def set():
|
||||
posthog.set(
|
||||
options.distinct_id,
|
||||
properties=json_hash(options.traits),
|
||||
context=json_hash(options.context),
|
||||
)
|
||||
|
||||
|
||||
def unknown():
|
||||
@@ -68,16 +95,12 @@ posthog.api_key = options.writeKey
|
||||
posthog.on_error = failed
|
||||
posthog.debug = True
|
||||
|
||||
log = logging.getLogger('posthog')
|
||||
log = logging.getLogger("posthog")
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.DEBUG)
|
||||
log.addHandler(ch)
|
||||
|
||||
switcher = {
|
||||
"capture": capture,
|
||||
"page": page,
|
||||
"identify": identify
|
||||
}
|
||||
switcher = {"capture": capture, "page": page, "identify": identify, "set_once": set_once, "set": set}
|
||||
|
||||
func = switcher.get(options.type)
|
||||
if func:
|
||||
|
||||
Reference in New Issue
Block a user