Compare commits

...
68 Commits
Author SHA1 Message Date
Michael Matloka d66f944571 Bump version to 1.2.1 2021-02-05 16:27:51 +01:00
Michael MatlokaandGitHub 9d620967f8 Merge pull request #22 from PostHog/fix-possibly-null-percentage
Add condition for rollout-percentage
2021-01-28 15:01:30 +01:00
Eric 563404f914 add condition 2021-01-25 19:57:21 -05:00
Yakko MajuriandGitHub d15aac41a9 Update README.md 2021-01-11 17:19:21 -03:00
Michael MatlokaandGitHub 8a3e28b949 Merge pull request #21 from PostHog/code-style
Black and isort all the things
2021-01-05 12:25:57 +01:00
Michael Matloka 1aa0d6335c Add CI job for this 2021-01-05 12:21:39 +01:00
Michael Matloka 3b46c60cf1 Normalize strings to double quotes 2021-01-05 12:09:44 +01:00
Michael Matloka 4ad8cbfa58 Fix weird concat 2021-01-05 12:07:05 +01:00
Michael Matloka 984a679b19 Black and isort all the things! 2021-01-03 05:37:51 +01:00
Michael Matloka dd1bad6175 Setup black, isort and pre-commit 2021-01-03 05:36:26 +01:00
Michael MatlokaandGitHub e6f71e4cc3 Merge pull request #20 from Hungsiro506/patch-1
Fix post() returning None on non-200 response
2021-01-03 05:07:51 +01:00
Michael Matloka 17874cb131 Update .gitignore 2021-01-03 05:00:41 +01:00
Michael Matloka 8366e09df9 Add typing to request.py 2021-01-03 04:58:55 +01:00
Hưng VũandGitHub e28b237ff1 Update request.py
-- Fix post method return None
2020-12-30 17:43:08 +07:00
Michael MatlokaandGitHub 05fde2a51e Merge pull request #18 from PostHog/better-errors
Add project_api_key, improve errors
2020-12-15 12:01:28 +01:00
Michael MatlokaandGitHub 2be04f3b8b Merge pull request #14 from PostHog/uuid-property
Allow passing in UUID as property
2020-12-15 11:21:31 +01:00
Michael Matloka d52b605742 Rerun CI 2020-12-15 11:14:18 +01:00
yakkomajuri 7ee3002c6f update generic error handler 2020-12-04 11:10:22 +00:00
yakkomajuri d1bc9135c7 specify json return 2020-12-04 10:29:07 +00:00
yakkomajuri 111813296c add project_api_key to config 2020-12-04 09:58:43 +00:00
yakkomajuri 31acda73a3 Better API error handling 2020-12-04 09:34:23 +00:00
Paolo D'AmicoandGitHub 888457387b Merge pull request #17 from PostHog/v1.1.3 2020-11-23 09:33:50 -06:00
Paolo D'Amico fe45ff2ab0 version bump 2020-11-23 10:08:04 -05:00
Marius AndraandGitHub 221d7f09f3 Do not start Feature Flag polling if no API Key (#15)
* jetbrains idea .gitignore

* do not start the poller if no personal api key
2020-11-23 15:37:52 +01:00
Michael Matloka a5f2e030b5 Allow passing in UUID as property 2020-11-03 17:14:00 +01:00
Tim GlaserandGitHub 98d2d4cc05 Merge pull request #12 from PostHog/investigate
Send distinctID to the decide endpoint to determin if user should have features enabled
2020-09-30 14:34:43 +01:00
James Greenhill b7c1572c32 bump version to 1.1.2 2020-09-30 14:25:49 +01:00
James Greenhill 16acf2e278 better testing and org 2020-09-30 14:20:50 +01:00
James Greenhill df9ae05202 fix tests 2020-09-30 14:10:34 +01:00
James Greenhill b05ee3884a Send distinctID to the decide endpoint to determin if user should have features enabled 2020-09-30 13:53:47 +01:00
James GreenhillandGitHub 144a7744e4 Merge pull request #11 from PostHog/none
Default to False if the feature flag key does not exist. None is confusing
2020-09-29 17:06:53 +01:00
James Greenhill ca979f06c8 type annotations 2020-09-29 17:02:39 +01:00
James Greenhill caa64fb6fe Default to False if the feature flag key does not exist. None is confusing 2020-09-29 16:07:43 +01:00
Tim GlaserandGitHub e007dee07e Merge pull request #10 from PostHog/warn-instead-of-error
Warn instead of error
2020-09-29 13:16:06 +02:00
Tim Glaser 1de9553d61 Fix tests 2020-09-29 12:13:28 +01:00
Tim Glaser d447d170fa Warn instead of error 2020-09-29 11:57:44 +01:00
Tim GlaserandGitHub d92c398c0f Merge pull request #9 from PostHog/feature-flags
Feature flags
2020-09-17 10:17:42 +02:00
Tim Glaser b331c4aae3 Add error handling and timeouts 2020-09-17 10:13:03 +02:00
Yakko Majuri 5e70ca84bb minor changes 2020-09-14 09:37:14 +00:00
Yakko Majuri adef8d4928 fix comment 2020-09-14 08:26:40 +00:00
Tim GlaserandGitHub 8682091eec Merge pull request #8 from PostHog/add-tests
Fix tests and clean up integration
2020-09-11 15:39:51 +02:00
Tim Glaser 8977c4e3ab Merge branch 'add-tests' of github.com:PostHog/posthog-python into add-tests 2020-09-11 15:37:31 +02:00
Tim Glaser 710ac05862 Cleaned up simulator 2020-09-11 15:37:06 +02:00
Yakko Majuri 721a6aacf7 fix action 2020-09-11 08:58:14 +00:00
Yakko Majuri 678e4ac97b minor changes 2020-09-11 08:50:50 +00:00
Tim Glaser 72e7e4ad72 typo 2020-09-10 16:52:31 +02:00
Tim Glaser 95d4375663 Fix tests 2020-09-10 16:39:27 +02:00
Tim Glaser 6e39aa0ceb Add poller 2020-09-10 16:35:23 +02:00
Tim Glaser c72ab9a3fd Make feature flags work 2020-09-10 16:30:54 +02:00
Yakko Majuri 6508aa6994 further cleanup of action 2020-09-09 16:48:07 +00:00
Yakko Majuri 3b7f37aa39 updated tests action 2020-09-09 16:44:41 +00:00
Tim Glaser ac74ee9a5c fix test 2020-09-09 16:26:59 +02:00
Tim Glaser b9e323bf47 Fix tests 2020-09-09 16:22:40 +02:00
Tim Glaser 8fee12f004 Move tests into correct folder 2020-09-09 16:16:54 +02:00
Tim Glaser 3c23f0159f Fix tests 2020-09-09 16:15:46 +02:00
Marius AndraandGitHub c944d7df2c Update README.md 2020-05-20 16:43:32 +02:00
Tim Glaser 611e01f9eb Release 1.0.11 2020-04-30 10:10:51 +01:00
Tim GlaserandGitHub 1840bb8f57 Merge pull request #6 from PostHog/5-timestamp-correct-type
Closes #5, add correct timestamp types
2020-04-30 10:08:51 +01:00
Tim Glaser cb06d2fd5d Closes #5, add correct timestamp types 2020-04-30 10:08:28 +01:00
Marius AndraandGitHub 610fc816f1 Merge pull request #3 from casio/patch-1
Fix typo in identify() docstring
2020-04-29 22:12:44 +02:00
Marius AndraandGitHub 992ab2b9a7 Merge pull request #4 from PostHog/links-fix
Fixed links to docs
2020-04-29 22:12:05 +02:00
James HawkinsandGitHub fd68a10e63 Fixed links to docs 2020-04-07 16:26:06 +01:00
Tim Glaser 48c1cf3c02 Update instructions 2020-04-01 17:26:10 +01:00
Tim Glaser b923116a9e Release 1.0.10 2020-04-01 17:25:23 +01:00
Carsten KrausandGitHub 0eed53222c Fix typo in docstring
lil typo, fixed : )
2020-03-25 22:42:05 +01:00
Tim Glaser 1670901156 Merge branch 'master' of github.com:PostHog/posthog-python 2020-02-21 17:09:32 -08:00
Tim GlaserandGitHub 133e1a991c Merge pull request #1 from mariusandra/patch-1
Fix typo with "identify" code example
2020-02-18 18:19:25 -08:00
Marius AndraandGitHub 7a93a99541 Fix typo with "identify" code example 2020-02-18 22:10:02 +01:00
22 changed files with 885 additions and 792 deletions
+61
View File
@@ -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 .
- name: Run posthog tests
run: |
python setup.py test
+7 -6
View File
@@ -1,14 +1,15 @@
**sublime**
*.pyc
dist
dist/
*.egg-info
dist
MANIFEST
build
.eggs
.coverage
build/
.eggs/
.coverage/
.vscode/
env
env/
venv/
flake8.out
pylint.out
posthog-analytics
.idea
+9
View File
@@ -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
+4 -115
View File
@@ -1,120 +1,9 @@
# PostHog Python
Official PostHog Python library to capture and send events to any PostHog instance (including PostHog.com).
Please see the main [PostHog docs](https://posthog.com/docs).
This library uses an internal queue to make calls non-blocking and fast. It also batches requests and flushes asynchronously, making it perfect to use in any part of your web app or other server side application that needs performance.
Specifically, the [Python integration](https://posthog.com/docs/integrations/python-integration) details.
## Installation
## Questions?
```bash
pip install posthog
```
In your app, import the posthog library and set your api key **before** making any calls.
```python
import posthog
posthog.api_key = 'YOUR API KEY'
```
You can find your key in the /setup page in PostHog.
To debug, you can set debug mode.
```python
posthog.debug = True
```
## Making calls
### Capture
Capture allows you to capture anything a user does within your system, which you can later use in PostHog to find patterns in usage, work out which features to improve or where people are giving up.
A `capture` call requires
- `distinct id` which uniquely identifies your user
- `event name` to make sure
- We recommend using [verb] [noun], like `movie played` or `movie updated` to easily identify what your events mean later on.
Optionally you can submit
- `properties`, which can be a dict with any information you'd like to add
For example:
```python
posthog.capture('distinct id', 'movie played', {'movie_id': '123', 'category': 'romcom'})
```
### Identify
Identify lets you add metadata on your users so you can more easily identify who they are in PostHog, and even do things like segment users by these properties.
An `identify` call requires
- `distinct id` which uniquely identifies your user
- `properties` with a dict with any key: value pairs
For example:
```python
posthog.identify('distinct id', {
'email': 'dwayne@gmail.com',
'name': 'Dwayne Johnson'
})
```
The most obvious place to make this call is whenever a user signs up, or when they update their information.
### Alias
To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call. This will allow you to answer questions like "Which marketing channels leads to users churning after a month?" or "What do users do on our website before signing up?"
In a purely back-end implementation, this means whenever an anonymous user does something, you'll want to send a session ID ([Django](https://stackoverflow.com/questions/526179/in-django-how-can-i-find-out-the-request-session-sessionid-and-use-it-as-a-vari), [Flask](https://stackoverflow.com/questions/15156132/flask-login-how-to-get-session-id)) with the capture call. Then, when that users signs up, you want to do an alias call with the session ID and the newly created user ID.
The same concept applies for when a user logs in.
If you're using PostHog in the front-end and back-end, doing the `identify` call in the frontend will be enough.
An `alias` call requires
- `previous distinct id` the unique ID of the user before
- `distinct id` the current unique id
For example:
```python
posthog.alias('anonymous session id', 'distinct id')
```
## Django
For Django, you can do the initialisation of the key in the AppConfig, so that it's available everywhere.
in `yourapp/apps.py`
```python
from django.apps import AppConfig
import posthog
class YourAppConfig(AppConfig):
def ready(self):
posthog.api_key = 'your key'
```
Then, anywhere else in your app you can do
```python
import posthog
def homepage(request):
# example capture
posthog.capture(request.session.session_key, 'page view', ....)
```
# Development
## Naming confusion
As our open source project [PostHog](https://github.com/PostHog/posthog) shares the same module name, we create a special `posthog-analytics` package, mostly for internal use to avoid module collision. It is the exact same.
## How to release
1. Increase `VERSION` in `posthog/version.py`
2. run `make release` and `make release_analytics`
3. `git commit -am "Release X.Y.Z."` (where X.Y.Z is the new version)
4. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version).
## Thank you
This library is largely based on the `analytics-python` package.
### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
+17 -7
View File
@@ -1,20 +1,30 @@
# 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"})
# Alias a previous distinct id with a new one
posthog.alias('distinct_id', 'new_distinct_id')
print(posthog.feature_enabled("beta-feature", "distinct_id"))
# Add properties to the person
posthog.identify('distinct_id', {'email': 'something@something.com'})
print("sleeping")
time.sleep(45)
print(posthog.feature_enabled("beta-feature", "distinct_id"))
# # Alias a previous distinct id with a new one
posthog.alias("distinct_id", "new_distinct_id")
# # Add properties to the person
posthog.identify("distinct_id", {"email": "something@something.com"})
+102 -31
View File
@@ -1,30 +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: str = None
host: str = None
on_error: Callable = None
debug: bool = False
send: bool = True
sync_mode:bool = False
disabled: bool = False
api_key = None # type: str
host = None # type: str
on_error = None # type: Callable
debug = False # type: bool
send = True # type: bool
sync_mode = False # type: bool
disabled = False # type: bool
personal_api_key = None # type: str
default_client = None
def capture(distinct_id: str, event: str, properties: Optional[Dict]=None, context: Optional[Dict]=None,
timestamp: Optional[str]=None, message_id: Optional[str]=None) -> None:
def capture(
distinct_id, # type: str,
event, # type: str,
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
):
# type: (...) -> None
"""
Capture allows you to capture anything a user does within your system, which you can later use in PostHog to find patterns in usage, work out which features to improve or where people are giving up.
A `capture` call requires
- `distinct id` which uniquely identifies your user
- `event name` to make sure
- `event name` to make sure
- We recommend using [verb] [noun], like `movie played` or `movie updated` to easily identify what your events mean later on.
Optionally you can submit
@@ -35,33 +43,63 @@ def capture(distinct_id: str, event: str, properties: Optional[Dict]=None, conte
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: str, properties: Optional[Dict]=None, context: Optional[Dict]=None, timestamp: Optional[str]=None,
message_id=None) -> None:
def identify(
distinct_id, # type: str,
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
):
# type: (...) -> None
"""
Identify lets you add metadata on your users so you can more easily identify who they are in PostHog, and even do things like segment users by these properties.
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
posthog.capture('distinct id', {
posthog.identify('distinct id', {
'email': 'dwayne@gmail.com',
'name': 'Dwayne Johnson'
})
```
"""
_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 group(*args, **kwargs):
"""Send a group call."""
_proxy('group', *args, **kwargs)
_proxy("group", *args, **kwargs)
def alias(previous_id: str, distinct_id: str, context: Optional[Dict]=None, timestamp: Optional[str]=None, message_id: Optional[str]=None) -> None:
def alias(
previous_id, # type: str,
distinct_id, # type: str,
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
):
# type: (...) -> None
"""
To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call. This will allow you to answer questions like "Which marketing channels leads to users churning after a month?" or "What do users do on our website before signing up?"
@@ -78,33 +116,60 @@ def alias(previous_id: str, distinct_id: str, context: Optional[Dict]=None, time
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):
@@ -113,9 +178,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)
+187 -133
View File
@@ -1,15 +1,18 @@
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 uuid4
import requests
from dateutil.tz import tzutc
from six import string_types
from posthog.utils import guess_timezone, clean
from posthog.consumer import Consumer
from posthog.request import post
from posthog.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:
@@ -19,20 +22,41 @@ except ImportError:
ID_TYPES = (numbers.Number, string_types)
__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 +64,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 +87,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,183 +103,122 @@ 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,
'type': 'identify',
'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,
'type': 'capture',
'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 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)
require("previous_id", previous_id, ID_TYPES)
require("distinct_id", distinct_id, ID_TYPES)
msg = {
'properties': {
'distinct_id': previous_id,
'alias': distinct_id,
"properties": {
"distinct_id": previous_id,
"alias": distinct_id,
},
'timestamp': timestamp,
'context': context,
'type': 'alias',
'event': '$create_alias'
"timestamp": timestamp,
"context": context,
"event": "$create_alias",
}
return self._enqueue(msg)
def group(self, distinct_id=None, group_id=None, traits=None, context=None,
timestamp=None, message_id=None):
traits = traits or {}
context = context or {}
require('distinct_id', distinct_id, ID_TYPES)
require('group_id', group_id, ID_TYPES)
require('traits', traits, dict)
msg = {
'timestamp': timestamp,
'groupId': group_id,
'context': context,
'distinct_id': distinct_id,
'traits': traits,
'type': 'group',
'messageId': message_id,
}
return self._enqueue(msg)
def page(self, distinct_id=None, category=None, name=None, properties=None,
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)
if name:
require('name', name, string_types)
if category:
require('category', category, string_types)
require("url", url, string_types)
properties["$current_url"] = url
msg = {
'properties': properties,
'timestamp': timestamp,
'category': category,
'context': context,
'distinct_id': distinct_id,
'type': 'page',
'name': name,
'messageId': message_id,
}
return self._enqueue(msg)
def screen(self, distinct_id=None, category=None, name=None, properties=None,
context=None, timestamp=None, message_id=None):
properties = properties or {}
context = context or {}
require('distinct_id', distinct_id, ID_TYPES)
require('properties', properties, dict)
if name:
require('name', name, string_types)
if category:
require('category', category, string_types)
msg = {
'properties': properties,
'timestamp': timestamp,
'category': category,
'context': context,
'distinct_id': distinct_id,
'type': 'screen',
'name': name,
'messageId': message_id,
"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('type', msg['type'], string_types)
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['type'])
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['type'])
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):
@@ -253,7 +227,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.
@@ -272,11 +246,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)
+28 -26
View File
@@ -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
@@ -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()
+20
View File
@@ -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
View File
@@ -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()
+3 -3
View File
@@ -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 'analytics.test.' + modname
yield "posthog.test." + modname
def all():
+169 -176
View File
@@ -1,22 +1,26 @@
from datetime import date, datetime
import unittest
import six
import mock
import time
import unittest
from datetime import date, datetime
import mock
import six
from freezegun import freeze_time
from posthog.version import VERSION
from posthog.client import Client
from posthog.request import APIError
from posthog.test.utils import TEST_API_KEY
from posthog.version import VERSION
class TestClient(unittest.TestCase):
def fail(self, e, batch):
def set_fail(self, e, batch):
"""Mark the failure handler"""
print("FAIL", e, batch)
self.failed = True
def setUp(self):
self.failed = False
self.client = Client('testsecret', on_error=self.fail)
self.client = Client(TEST_API_KEY, on_error=self.set_fail)
def test_requires_api_key(self):
self.assertRaises(AssertionError, Client)
@@ -24,192 +28,128 @@ class TestClient(unittest.TestCase):
def test_empty_flush(self):
self.client.flush()
def test_basic_track(self):
def test_basic_capture(self):
client = self.client
success, msg = client.track('distinct_id', 'python test event')
success, msg = client.capture("distinct_id", "python test event")
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
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'], {})
self.assertEqual(msg['type'], 'track')
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.track(
distinct_id=157963456373623802, event='python test event')
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')
self.assertEqual(msg["distinct_id"], "157963456373623802")
def test_advanced_track(self):
def test_advanced_capture(self):
client = self.client
success, msg = client.track(
'distinct_id', 'python test event', {'property': 'value'},
{'ip': '192.168.0.1'}, datetime(2014, 9, 3),
'messageId')
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')
self.assertEqual(msg['type'], 'track')
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'})
success, msg = client.identify("distinct_id", {"trait": "value"})
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['traits'], {'trait': 'value'})
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertTrue(isinstance(msg['messageId'], str))
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'identify')
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')
"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['traits'], {'trait': 'value'})
self.assertEqual(msg['context']['library'], {
'name': 'analytics-python',
'version': VERSION
})
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'identify')
def test_basic_group(self):
client = self.client
success, msg = client.group('distinct_id', 'groupId')
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['groupId'], 'groupId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'group')
def test_advanced_group(self):
client = self.client
success, msg = client.group(
'distinct_id', 'groupId', {'trait': 'value'}, {'ip': '192.168.0.1'},
datetime(2014, 9, 3), 'messageId')
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['traits'], {'trait': 'value'})
self.assertEqual(msg['context']['library'], {
'name': 'analytics-python',
'version': VERSION
})
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'group')
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')
success, msg = client.alias("previousId", "distinct_id")
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['previousId'], 'previousId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg["properties"]["distinct_id"], "previousId")
self.assertEqual(msg["properties"]["alias"], "distinct_id")
def test_basic_page(self):
client = self.client
success, msg = client.page('distinct_id', name='name')
success, msg = client.page("distinct_id", url="https://posthog.com/contact")
self.assertFalse(self.failed)
client.flush()
self.assertTrue(success)
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'page')
self.assertEqual(msg['name'], 'name')
self.assertEqual(msg["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', 'category', 'name', {'property': 'value'},
{'ip': '192.168.0.1'}, datetime(2014, 9, 3), 'messageId')
"distinct_id",
"https://posthog.com/contact",
{"property": "value"},
{"ip": "192.168.0.1"},
datetime(2014, 9, 3),
"messageId",
)
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['properties'], {'property': 'value'})
self.assertEqual(msg['context']['library'], {
'name': 'analytics-python',
'version': VERSION
})
self.assertEqual(msg['category'], 'category')
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'page')
self.assertEqual(msg['name'], 'name')
def test_basic_screen(self):
client = self.client
success, msg = client.screen('distinct_id', name='name')
client.flush()
self.assertTrue(success)
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'screen')
self.assertEqual(msg['name'], 'name')
def test_advanced_screen(self):
client = self.client
success, msg = client.screen(
'distinct_id', 'category', 'name', {'property': 'value'},
{'ip': '192.168.0.1'}, datetime(2014, 9, 3), 'messageId')
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['properties'], {'property': 'value'})
self.assertEqual(msg['context']['library'], {
'name': 'analytics-python',
'version': VERSION
})
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['category'], 'category')
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['type'], 'screen')
self.assertEqual(msg['name'], 'name')
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'})
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()
@@ -220,7 +160,7 @@ class TestClient(unittest.TestCase):
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'})
success, msg = client.identify("distinct_id", {"trait": "value"})
client.shutdown()
# we expect two things after shutdown:
# 1. client queue is empty
@@ -230,85 +170,138 @@ class TestClient(unittest.TestCase):
self.assertFalse(consumer.is_alive())
def test_synchronous(self):
client = Client('testsecret', sync_mode=True)
client = Client(TEST_API_KEY, sync_mode=True)
success, message = client.identify('distinct_id')
success, message = client.identify("distinct_id")
self.assertFalse(client.consumers)
self.assertTrue(client.queue.empty())
self.assertTrue(success)
def test_overflow(self):
client = Client('testsecret', max_queue_size=1)
client = Client(TEST_API_KEY, max_queue_size=1)
# Ensure consumer thread is no longer uploading
client.join()
for i in range(10):
client.identify('distinct_id')
client.identify("distinct_id")
success, msg = 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_success_on_invalid_api_key(self):
client = Client('bad_key', on_error=self.fail)
client.track('distinct_id', 'event')
client.flush()
self.assertFalse(self.failed)
def test_unicode(self):
Client(six.u('unicode_key'))
Client(six.u("unicode_key"))
def test_numeric_distinct_id(self):
self.client.track(1234, 'python event')
self.client.capture(1234, "python event")
self.client.flush()
self.assertFalse(self.failed)
def test_debug(self):
Client('bad_key', debug=True)
def test_identify_with_date_object(self):
client = self.client
success, msg = client.identify(
'distinct_id',
{
'birthdate': date(1981, 2, 2),
},
)
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['traits'], {'birthdate': date(1981, 2, 2)})
Client("bad_key", debug=True)
def test_gzip(self):
client = Client('testsecret', on_error=self.fail, gzip=True)
client = Client(TEST_API_KEY, on_error=self.fail, gzip=True)
for _ in range(10):
client.identify('distinct_id', {'trait': 'value'})
client.identify("distinct_id", {"trait": "value"})
client.flush()
self.assertFalse(self.failed)
def test_user_defined_flush_at(self):
client = Client('testsecret', on_error=self.fail,
flush_at=10, flush_interval=3)
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)
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('analytics.consumer.post', side_effect=mock_post_fn) \
as mock_post:
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'})
client.identify("distinct_id", {"trait": "value"})
time.sleep(1)
self.assertEquals(mock_post.call_count, 2)
def test_user_defined_timeout(self):
client = Client('testsecret', timeout=10)
client = Client(TEST_API_KEY, timeout=10)
for consumer in client.consumers:
self.assertEquals(consumer.timeout, 10)
def test_default_timeout_15(self):
client = Client('testsecret')
client = Client(TEST_API_KEY)
for consumer in client.consumers:
self.assertEquals(consumer.timeout, 15)
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_load_feature_flags(self, patch_get, patch_poll):
patch_get.return_value = {"results": [{"id": 1, "name": "Beta Feature", "key": "beta-feature"}]}
client = Client(TEST_API_KEY, personal_api_key="test")
with freeze_time("2020-01-01T12:01:00.0000Z"):
client.load_feature_flags()
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
self.assertEqual(client._last_feature_flag_poll.isoformat(), "2020-01-01T12:01:00+00:00")
self.assertEqual(patch_poll.call_count, 1)
def test_load_feature_flags_wrong_key(self):
client = Client(TEST_API_KEY, personal_api_key=TEST_API_KEY)
with freeze_time("2020-01-01T12:01:00.0000Z"):
self.assertRaises(APIError, client.load_feature_flags)
@mock.patch("posthog.client.get")
def test_feature_enabled_simple(self, patch_get):
client = Client(TEST_API_KEY)
client.feature_flags = [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True, "rollout_percentage": 100}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
@mock.patch("posthog.client.decide")
def test_feature_enabled_request(self, patch_get):
patch_get.return_value = {"featureFlags": ["beta-feature"]}
client = Client(TEST_API_KEY)
client.feature_flags = [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": False, "rollout_percentage": 100}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_feature_enabled_doesnt_exist(self, patch_get, patch_poll):
client = Client(TEST_API_KEY, personal_api_key="test")
client.feature_flags = []
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
self.assertTrue(client.feature_enabled("doesnt-exist", "distinct_id", True))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_personal_api_key_doesnt_exist(self, patch_get, patch_poll):
client = Client(TEST_API_KEY)
client.feature_flags = []
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
self.assertTrue(client.feature_enabled("doesnt-exist", "distinct_id", True))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_load_feature_flags_error(self, patch_get, patch_poll):
def raise_effect():
raise Exception("http exception")
patch_get.return_value.raiseError.side_effect = raise_effect
client = Client(TEST_API_KEY, personal_api_key="test")
client.feature_flags = []
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_call_identify_fails(self, patch_get, patch_poll):
def raise_effect():
raise Exception("http exception")
patch_get.return_value.raiseError.side_effect = raise_effect
client = Client(TEST_API_KEY, personal_api_key="test")
client.feature_flags = [{"key": "example", "is_simple_flag": False}]
self.assertFalse(client.feature_enabled("example", "distinct_id"))
+42 -76
View File
@@ -1,22 +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
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])
@@ -24,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()
@@ -32,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, [])
@@ -41,12 +42,8 @@ class TestConsumer(unittest.TestCase):
def test_upload(self):
q = Queue()
consumer = Consumer(q, 'testsecret')
track = {
'type': 'track',
'event': 'python event',
'distinct_id': 'distinct_id'
}
consumer = Consumer(q, TEST_API_KEY)
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
q.put(track)
success = consumer.upload()
self.assertTrue(success)
@@ -57,16 +54,11 @@ class TestConsumer(unittest.TestCase):
# The consumer should upload _n_ times.
q = Queue()
flush_interval = 0.3
consumer = Consumer(q, 'testsecret', flush_at=10,
flush_interval=flush_interval)
with mock.patch('analytics.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)
@@ -77,45 +69,30 @@ class TestConsumer(unittest.TestCase):
q = Queue()
flush_interval = 0.5
flush_at = 10
consumer = Consumer(q, 'testsecret', flush_at=flush_at,
flush_interval=flush_interval)
with mock.patch('analytics.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, 'testsecret')
track = {
'type': 'track',
'event': 'python event',
'distinct_id': 'distinct_id'
}
consumer = Consumer(None, TEST_API_KEY)
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('analytics.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:
@@ -130,53 +107,45 @@ 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, 'testsecret')
self._test_request_retry(consumer, Exception('generic exception'), 2)
consumer = Consumer(None, TEST_API_KEY)
self._test_request_retry(consumer, Exception("generic exception"), 2)
# we should retry on server errors
consumer = Consumer(None, 'testsecret')
self._test_request_retry(consumer, APIError(
500, 'code', 'Internal Server Error'), 2)
consumer = Consumer(None, TEST_API_KEY)
self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 2)
# we should retry on HTTP 429 errors
consumer = Consumer(None, 'testsecret')
self._test_request_retry(consumer, APIError(
429, 'code', 'Too Many Requests'), 2)
consumer = Consumer(None, TEST_API_KEY)
self._test_request_retry(consumer, APIError(429, "Too Many Requests"), 2)
# we should NOT retry on other client errors
consumer = Consumer(None, 'testsecret')
api_error = APIError(400, 'code', 'Client Errors')
consumer = Consumer(None, TEST_API_KEY)
api_error = APIError(400, "Client Errors")
try:
self._test_request_retry(consumer, api_error, 1)
except APIError:
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, 'testsecret', retries=3)
self._test_request_retry(consumer, APIError(
500, 'code', 'Internal Server Error'), 3)
consumer = Consumer(None, TEST_API_KEY, retries=3)
self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 3)
def test_pause(self):
consumer = Consumer(None, 'testsecret')
consumer = Consumer(None, TEST_API_KEY)
consumer.pause()
self.assertFalse(consumer.running)
def test_max_batch_size(self):
q = Queue()
consumer = Consumer(
q, 'testsecret', flush_at=100000, flush_interval=3)
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)
@@ -184,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('analytics.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)
+16 -25
View File
@@ -1,49 +1,40 @@
import unittest
import analytics
import posthog
class TestModule(unittest.TestCase):
def failed(self):
self.failed = True
def setUp(self):
self.failed = False
analytics.api_key = 'testsecret'
analytics.on_error = self.failed
posthog.api_key = "testsecret"
posthog.on_error = self.failed
def test_no_api_key(self):
analytics.api_key = None
self.assertRaises(Exception, analytics.track)
posthog.api_key = None
self.assertRaises(Exception, posthog.capture)
def test_no_host(self):
analytics.host = None
self.assertRaises(Exception, analytics.track)
posthog.host = None
self.assertRaises(Exception, posthog.capture)
def test_track(self):
analytics.track('distinct_id', 'python module event')
analytics.flush()
posthog.capture("distinct_id", "python module event")
posthog.flush()
def test_identify(self):
analytics.identify('distinct_id', {'email': 'user@email.com'})
analytics.flush()
def test_group(self):
analytics.group('distinct_id', 'groupId')
analytics.flush()
posthog.identify("distinct_id", {"email": "user@email.com"})
posthog.flush()
def test_alias(self):
analytics.alias('previousId', 'distinct_id')
analytics.flush()
posthog.alias("previousId", "distinct_id")
posthog.flush()
def test_page(self):
analytics.page('distinct_id')
analytics.flush()
def test_screen(self):
analytics.screen('distinct_id')
analytics.flush()
posthog.page("distinct_id", "https://posthog.com/contact")
posthog.flush()
def test_flush(self):
analytics.flush()
posthog.flush()
+16 -25
View File
@@ -1,53 +1,44 @@
from datetime import datetime, date
import unittest
import json
import unittest
from datetime import date, datetime
import requests
from posthog.request import post, DatetimeSerializer
from posthog.request import DatetimeSerializer, batch_post
from posthog.test.utils import TEST_API_KEY
class TestRequests(unittest.TestCase):
def test_valid_request(self):
res = post(batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
}])
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, post, 'testsecret',
'https://t.posthog.com', False, '[{]')
self.assertRaises(Exception, batch_post, "testsecret", "https://t.posthog.com", False, "[{]")
def test_invalid_host(self):
self.assertRaises(Exception, post, 'testsecret',
't.posthog.com/', batch=[])
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)}
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}
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(batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
}], timeout=15)
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):
post(batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
}], timeout=0.0001)
batch_post(
"key", batch=[{"distinct_id": "distinct_id", "event": "python event", "type": "track"}], timeout=0.0001
)
+28 -25
View File
@@ -1,15 +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"
class TestUtils(unittest.TestCase):
def test_timezone_utils(self):
now = datetime.now()
utcnow = datetime.now(tz=tzutc())
@@ -24,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())
@@ -49,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))
@@ -65,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
View File
@@ -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
View File
@@ -1 +1 @@
VERSION = '1.0.9'
VERSION = "1.2.1"
+10
View File
@@ -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"
+24 -24
View File
@@ -1,4 +1,3 @@
import os
import sys
@@ -8,39 +7,40 @@ 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",
]
}
tests_require = ["mock>=2.0.0", "freezegun==0.3.15"]
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,
extras_require=extras_require,
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",
+15 -24
View File
@@ -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",
+45 -50
View File
@@ -1,42 +1,39 @@
import analytics
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)
# analytics -method=<method> -posthog-write-key=<posthogWriteKey> [options]
# 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()
@@ -45,55 +42,53 @@ def failed(status, msg):
raise Exception(msg)
def track():
analytics.track(options.distinct_id, options.event, anonymous_id=options.anonymousId,
properties=json_hash(options.properties), context=json_hash(options.context))
def capture():
posthog.capture(
options.distinct_id,
options.event,
anonymous_id=options.anonymousId,
properties=json_hash(options.properties),
context=json_hash(options.context),
)
def page():
analytics.page(options.distinct_id, name=options.name, anonymous_id=options.anonymousId,
properties=json_hash(options.properties), context=json_hash(options.context))
def screen():
analytics.screen(options.distinct_id, name=options.name, anonymous_id=options.anonymousId,
properties=json_hash(options.properties), context=json_hash(options.context))
posthog.page(
options.distinct_id,
name=options.name,
anonymous_id=options.anonymousId,
properties=json_hash(options.properties),
context=json_hash(options.context),
)
def identify():
analytics.identify(options.distinct_id, anonymous_id=options.anonymousId,
traits=json_hash(options.traits), context=json_hash(options.context))
def group():
analytics.group(options.distinct_id, options.groupId, json_hash(options.traits),
json_hash(options.context), anonymous_id=options.anonymousId)
posthog.identify(
options.distinct_id,
anonymous_id=options.anonymousId,
traits=json_hash(options.traits),
context=json_hash(options.context),
)
def unknown():
print()
analytics.api_key = options.writeKey
analytics.on_error = failed
analytics.debug = True
posthog.api_key = options.writeKey
posthog.on_error = failed
posthog.debug = True
log = logging.getLogger('posthog')
log = logging.getLogger("posthog")
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
log.addHandler(ch)
switcher = {
"track": track,
"page": page,
"screen": screen,
"identify": identify,
"group": group
}
switcher = {"capture": capture, "page": page, "identify": identify}
func = switcher.get(options.type)
if func:
func()
analytics.shutdown()
posthog.shutdown()
else:
print("Invalid Message Type " + options.type)