Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
009cac8634 | ||
|
|
ec2425996c | ||
|
|
90fa0a0604 | ||
|
|
a97fe0a40a | ||
|
|
a474fcff93 | ||
|
|
a181ba718f | ||
|
|
b996f3a4e9 | ||
|
|
6c945a0624 | ||
|
|
ab8ccb4dff | ||
|
|
870f6f8b6b | ||
|
|
9e0aeaefe6 | ||
|
|
a8409960b9 | ||
|
|
deb078293a | ||
|
|
edb8b7891e | ||
|
|
727bdb2b1e | ||
|
|
0781a1280e | ||
|
|
8b2ed8bb12 | ||
|
|
a139795a74 | ||
|
|
11f1d06761 | ||
|
|
fd321566ed | ||
|
|
83737f2477 | ||
|
|
cc5649368f | ||
|
|
7fe5045da1 | ||
|
|
2c9ad238ec | ||
|
|
de6e60f12f | ||
|
|
fd92502d99 | ||
|
|
fe6d0dc1ec | ||
|
|
fbde5cafc4 | ||
|
|
2e99081cb1 | ||
|
|
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 | ||
|
|
8977c4e3ab | ||
|
|
710ac05862 | ||
|
|
721a6aacf7 | ||
|
|
678e4ac97b | ||
|
|
72e7e4ad72 | ||
|
|
95d4375663 | ||
|
|
6e39aa0ceb | ||
|
|
c72ab9a3fd | ||
|
|
6508aa6994 | ||
|
|
3b7f37aa39 | ||
|
|
ac74ee9a5c | ||
|
|
b9e323bf47 | ||
|
|
8fee12f004 | ||
|
|
3c23f0159f | ||
|
|
c944d7df2c | ||
|
|
611e01f9eb | ||
|
|
1840bb8f57 | ||
|
|
cb06d2fd5d | ||
|
|
610fc816f1 | ||
|
|
992ab2b9a7 | ||
|
|
fd68a10e63 | ||
|
|
48c1cf3c02 | ||
|
|
b923116a9e | ||
|
|
0eed53222c | ||
|
|
1670901156 | ||
|
|
ab65f8f0f8 | ||
|
|
133e1a991c | ||
|
|
34e92615c2 | ||
|
|
cf09644d4f | ||
|
|
0cfce57d0e | ||
|
|
e9862cf671 | ||
|
|
7a93a99541 | ||
|
|
0ce011a36d |
@@ -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
|
||||
@@ -0,0 +1,38 @@
|
||||
name: 'Release'
|
||||
|
||||
on:
|
||||
- workflow_dispatch
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Publish release
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Detect version
|
||||
run: echo "REPO_VERSION=$(python3 posthog/version.py)" >> $GITHUB_ENV
|
||||
|
||||
- name: Prepare for building release
|
||||
run: pip install -U pip setuptools wheel twine
|
||||
|
||||
- name: Push release to PyPI
|
||||
run: make release && make release_analytics
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ env.REPO_VERSION }}
|
||||
release_name: ${{ env.REPO_VERSION }}
|
||||
+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
|
||||
@@ -0,0 +1,19 @@
|
||||
## 1.4.1 - 2021-05-28
|
||||
- Fix packaging issues with Sentry integrations
|
||||
|
||||
## 1.4.0 - 2021-05-18
|
||||
|
||||
- Improve support for `project_api_key` (#32)
|
||||
- Resolve polling issues with feature flags (#29)
|
||||
- Add Sentry (and Sentry+Django) integrations (#13)
|
||||
- Fix feature flag issue with no percentage rollout (#30)
|
||||
|
||||
## 1.3.1 - 2021-05-07
|
||||
|
||||
- Add `$set` and `$set_once` support (#23)
|
||||
- Add distinct ID to `$create_alias` event (#27)
|
||||
- Add `UUID` to `ID_TYPES` (#26)
|
||||
|
||||
## 1.2.1 - 2021-02-05
|
||||
|
||||
Initial release logged in CHANGELOG.md.
|
||||
@@ -1,5 +1,7 @@
|
||||
Copyright (c) 2020 PostHog (part of Hiberly Inc)
|
||||
|
||||
Copyright (c) 2013 Segment Inc. friends@segment.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
|
||||
@@ -1,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,8 @@ release_analytics:
|
||||
e2e_test:
|
||||
.buildscripts/e2e.sh
|
||||
|
||||
.PHONY: test release e2e_test
|
||||
django_example:
|
||||
python -m pip install -e ".[sentry]"
|
||||
cd sentry_django_example && python manage.py runserver 8080
|
||||
|
||||
.PHONY: test lint release e2e_test
|
||||
|
||||
@@ -1,118 +1,46 @@
|
||||
# PostHog Python
|
||||
|
||||
Official PostHog Python library to capture and send events to any PostHog instance (including PostHog.com).
|
||||
[](https://pypi.org/project/posthog/)
|
||||
|
||||
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.
|
||||
Please see the main [PostHog docs](https://posthog.com/docs).
|
||||
|
||||
## Installation
|
||||
Specifically, the [Python integration](https://posthog.com/docs/integrations/python-integration) details.
|
||||
|
||||
```bash
|
||||
pip install posthog
|
||||
```
|
||||
## Development
|
||||
|
||||
In your app, import the posthog library and set your api key **before** making any calls.
|
||||
### Testing Locally
|
||||
|
||||
```python
|
||||
import posthog
|
||||
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`
|
||||
|
||||
posthog.api_key = 'YOUR API KEY'
|
||||
```
|
||||
### Running Locally
|
||||
|
||||
You can find your key in the /setup page in PostHog.
|
||||
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.
|
||||
|
||||
To debug, you can set debug mode.
|
||||
```python
|
||||
posthog.debug = True
|
||||
```
|
||||
### Running the Django Sentry Integration Locally
|
||||
|
||||
## Making calls
|
||||
There's a sample Django project included, called `sentry_django_example`, which explains how to use PostHog with Sentry.
|
||||
|
||||
### Capture
|
||||
There's 2 places of importance (Changes required are all marked with TODO in the sample project directory)
|
||||
|
||||
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.
|
||||
1. Settings.py
|
||||
1. Input your Sentry DSN
|
||||
2. Input your Sentry Org and ProjectID details into `PosthogIntegration()`
|
||||
3. Add `POSTHOG_DJANGO` to settings.py. This allows the `PosthogDistinctIdMiddleware` to get the distinct_ids
|
||||
|
||||
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.
|
||||
2. urls.py
|
||||
1. This includes the `sentry-debug/` endpoint, which generates an exception
|
||||
|
||||
Optionally you can submit
|
||||
- `properties`, which can be a dict with any information you'd like to add
|
||||
To run things: `make django_example`. This installs the posthog-python library with the sentry-sdk add-on, and then runs the django app.
|
||||
Also start the PostHog app locally.
|
||||
Then navigate to `http://127.0.0.1:8080/sentry-debug/` and you should get an event in both Sentry and PostHog, with links to each other.
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.capture('distinct id', 'movie played', {'movie_id': '123', 'category': 'romcom'})
|
||||
```
|
||||
### Releasing Versions
|
||||
|
||||
### 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.
|
||||
Updated are released using GitHub Actions: after bumping `version.py` in `master`, go to [our release workflow's page](https://github.com/PostHog/posthog-python/actions/workflows/release.yaml) and dispatch it manually, using workflow from `master`.
|
||||
|
||||
An `identify` call requires
|
||||
- `distinct id` which uniquely identifies your user
|
||||
- `properties` with a dict with any key: value pairs
|
||||
## Questions?
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.capture('distinct id', {
|
||||
'email': 'dwayne@gmail.com',
|
||||
'name': 'Dwayne Johnson'
|
||||
})
|
||||
```
|
||||
|
||||
The most obvious place to make this call is whenever a user signs up, or when they update their information.
|
||||
|
||||
### Alias
|
||||
|
||||
To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call. This will allow you to answer questions like "Which marketing channels leads to users churning after a month?" or "What do users do on our website before signing up?"
|
||||
|
||||
In a purely back-end implementation, this means whenever an anonymous user does something, you'll want to send a session ID ([Django](https://stackoverflow.com/questions/526179/in-django-how-can-i-find-out-the-request-session-sessionid-and-use-it-as-a-vari), [Flask](https://stackoverflow.com/questions/15156132/flask-login-how-to-get-session-id)) with the capture call. Then, when that users signs up, you want to do an alias call with the session ID and the newly created user ID.
|
||||
|
||||
The same concept applies for when a user logs in.
|
||||
|
||||
An `alias` call requires
|
||||
- `previous distinct id` the unique ID of the user before
|
||||
- `distinct id` the current unique id
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.alias('anonymous session id', 'distinct id')
|
||||
```
|
||||
|
||||
## Django
|
||||
|
||||
For Django, you can do the initialisation of the key in the AppConfig, so that it's available everywhere.
|
||||
|
||||
in `yourapp/apps.py`
|
||||
```python
|
||||
from django.apps import AppConfig
|
||||
import posthog
|
||||
|
||||
class YourAppConfig(AppConfig):
|
||||
def ready(self):
|
||||
posthog.api_key = 'your key'
|
||||
```
|
||||
|
||||
Then, anywhere else in your app you can do
|
||||
```python
|
||||
import posthog
|
||||
|
||||
def homepage(request):
|
||||
# example capture
|
||||
posthog.capture(request.session.session_key, 'page view', ....)
|
||||
```
|
||||
|
||||
# Development
|
||||
|
||||
## Naming confusion
|
||||
|
||||
As our open source project [PostHog](https://github.com/PostHog/posthog) shares the same module name, we create a special `posthog-analytics` package, mostly for internal use to avoid module collision. It is the exact same.
|
||||
|
||||
## How to release
|
||||
1. Increase `VERSION` in `posthog/version.py`
|
||||
2. run `make release` and `make release_analytics`
|
||||
3. `git commit -am "Release X.Y.Z."` (where X.Y.Z is the new version)
|
||||
4. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version).
|
||||
|
||||
## Thank you
|
||||
|
||||
This library is largely based on the `analytics-python` package.
|
||||
### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
|
||||
|
||||
+41
-7
@@ -1,20 +1,54 @@
|
||||
# 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.project_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"))
|
||||
print(posthog.feature_enabled("beta-feature", "distinct_id", groups={"company": "id:5"}))
|
||||
|
||||
# Add properties to the person
|
||||
posthog.identify('distinct_id', {'email': 'something@something.com'})
|
||||
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.capture("new_distinct_id", "event2", {"property1": "value", "property2": "value"})
|
||||
posthog.capture(
|
||||
"new_distinct_id", "event-with-groups", {"property1": "value", "property2": "value"}, groups={"company": "id:5"}
|
||||
)
|
||||
|
||||
# # Add properties to the person
|
||||
posthog.identify("new_distinct_id", {"email": "something@something.com"})
|
||||
|
||||
# Add properties to a group
|
||||
posthog.group_identify("company", "id:5", {"employees": 11})
|
||||
|
||||
# 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()
|
||||
|
||||
+214
-33
@@ -1,66 +1,208 @@
|
||||
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
|
||||
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
|
||||
project_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]
|
||||
uuid=None, # type: Optional[str]
|
||||
groups=None, # type: Optional[Dict]
|
||||
):
|
||||
# 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
|
||||
- `properties`, which can be a dict with any information you'd like to add
|
||||
- `groups`, which is a dict of group type -> group key mappings
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.capture('distinct id', 'opened app')
|
||||
posthog.capture('distinct id', 'movie played', {'movie_id': '123', 'category': 'romcom'})
|
||||
|
||||
posthog.capture('distinct id', 'purchase', groups={'company': 'id:5'})
|
||||
```
|
||||
"""
|
||||
_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,
|
||||
uuid=uuid,
|
||||
groups=groups,
|
||||
)
|
||||
|
||||
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]
|
||||
uuid=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)
|
||||
|
||||
def group(*args, **kwargs):
|
||||
"""Send a group call."""
|
||||
_proxy('group', *args, **kwargs)
|
||||
_proxy(
|
||||
"identify",
|
||||
distinct_id=distinct_id,
|
||||
properties=properties,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
uuid=uuid,
|
||||
)
|
||||
|
||||
|
||||
def alias(previous_id: str, distinct_id: str, context: Optional[Dict]=None, timestamp: Optional[str]=None, message_id: Optional[str]=None) -> None:
|
||||
def set(
|
||||
distinct_id, # type: str,
|
||||
properties=None, # type: Optional[Dict]
|
||||
context=None, # type: Optional[Dict]
|
||||
timestamp=None, # type: Optional[datetime.datetime]
|
||||
uuid=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,
|
||||
uuid=uuid,
|
||||
)
|
||||
|
||||
|
||||
def set_once(
|
||||
distinct_id, # type: str,
|
||||
properties=None, # type: Optional[Dict]
|
||||
context=None, # type: Optional[Dict]
|
||||
timestamp=None, # type: Optional[datetime.datetime]
|
||||
uuid=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,
|
||||
uuid=uuid,
|
||||
)
|
||||
|
||||
|
||||
def group_identify(
|
||||
group_type, # type: str
|
||||
group_key, # type: str
|
||||
properties=None, # type: Optional[Dict]
|
||||
context=None, # type: Optional[Dict]
|
||||
timestamp=None, # type: Optional[datetime.datetime]
|
||||
uuid=None, # type: Optional[str]
|
||||
):
|
||||
# type: (...) -> None
|
||||
"""
|
||||
Set properties on a group
|
||||
|
||||
A `group_identify` call requires
|
||||
- `group_type` type of your group
|
||||
- `group_key` unique identifier of the group
|
||||
- `properties` with a dict with any key: value pairs
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.group_identify('company', 5, {
|
||||
'employees': 11,
|
||||
})
|
||||
```
|
||||
"""
|
||||
_proxy(
|
||||
"group_identify",
|
||||
group_type=group_type,
|
||||
group_key=group_key,
|
||||
properties=properties,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
uuid=uuid,
|
||||
)
|
||||
|
||||
|
||||
def alias(
|
||||
previous_id, # type: str,
|
||||
distinct_id, # type: str,
|
||||
context=None, # type: Optional[Dict]
|
||||
timestamp=None, # type: Optional[datetime.datetime]
|
||||
uuid=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?"
|
||||
|
||||
@@ -77,42 +219,81 @@ 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,
|
||||
uuid=uuid,
|
||||
)
|
||||
|
||||
|
||||
def feature_enabled(
|
||||
key, # type: str,
|
||||
distinct_id, # type: str,
|
||||
default=False, # type: bool
|
||||
groups={}, # type: dict
|
||||
):
|
||||
# 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
|
||||
if posthog.feature_enabled('groups feature', 'distinct id', groups={"organization": "id:5"}):
|
||||
# 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, groups=groups)
|
||||
|
||||
|
||||
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):
|
||||
"""Create an analytics client if one doesn't exist and send to it."""
|
||||
global default_client
|
||||
if disabled:
|
||||
return None
|
||||
if not default_client:
|
||||
default_client = Client(api_key, host=host, debug=debug,
|
||||
on_error=on_error, send=send,
|
||||
sync_mode=sync_mode)
|
||||
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,
|
||||
project_api_key=project_api_key,
|
||||
)
|
||||
|
||||
fn = getattr(default_client, method)
|
||||
fn(*args, **kwargs)
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
+258
-127
@@ -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 = project_api_key or api_key
|
||||
|
||||
require("api_key", self.api_key, string_types)
|
||||
|
||||
self.on_error = on_error
|
||||
self.debug = debug
|
||||
self.send = send
|
||||
@@ -40,6 +63,12 @@ class Client(object):
|
||||
self.host = host
|
||||
self.gzip = gzip
|
||||
self.timeout = timeout
|
||||
self.feature_flags = None
|
||||
self.poll_interval = poll_interval
|
||||
self.poller = None
|
||||
|
||||
# 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,
|
||||
self.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,181 +103,188 @@ 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, uuid=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",
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
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, uuid=None, groups=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,
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
if groups:
|
||||
require("groups", groups, dict)
|
||||
msg["properties"]["$groups"] = groups
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def set(self, distinct_id=None, properties=None, context=None, timestamp=None, uuid=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": properties,
|
||||
"event": "$set",
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def alias(self, previous_id=None, distinct_id=None, context=None,
|
||||
timestamp=None, message_id=None):
|
||||
def set_once(self, distinct_id=None, properties=None, context=None, timestamp=None, uuid=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_once": properties,
|
||||
"event": "$set_once",
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def group_identify(self, group_type=None, group_key=None, properties=None, context=None, timestamp=None, uuid=None):
|
||||
properties = properties or {}
|
||||
context = context or {}
|
||||
require("group_type", group_type, ID_TYPES)
|
||||
require("group_key", group_key, ID_TYPES)
|
||||
require("properties", properties, dict)
|
||||
|
||||
msg = {
|
||||
"event": "$groupidentify",
|
||||
"properties": {
|
||||
"$group_type": group_type,
|
||||
"$group_key": group_key,
|
||||
"$group_set": properties,
|
||||
},
|
||||
'timestamp': timestamp,
|
||||
'context': context,
|
||||
'type': 'alias',
|
||||
'event': '$create_alias'
|
||||
"distinct_id": "${}_{}".format(group_type, group_key),
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
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 {}
|
||||
def alias(self, previous_id=None, distinct_id=None, context=None, timestamp=None, uuid=None):
|
||||
context = context or {}
|
||||
|
||||
require('distinct_id', distinct_id, ID_TYPES)
|
||||
require('group_id', group_id, ID_TYPES)
|
||||
require('traits', traits, dict)
|
||||
require("previous_id", previous_id, ID_TYPES)
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
|
||||
msg = {
|
||||
'timestamp': timestamp,
|
||||
'groupId': group_id,
|
||||
'context': context,
|
||||
'distinct_id': distinct_id,
|
||||
'traits': traits,
|
||||
'type': 'group',
|
||||
'messageId': message_id,
|
||||
"properties": {
|
||||
"distinct_id": previous_id,
|
||||
"alias": distinct_id,
|
||||
},
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"event": "$create_alias",
|
||||
"distinct_id": previous_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, uuid=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,
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
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')
|
||||
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)
|
||||
msg['library'] = 'posthog-python'
|
||||
msg['library_version'] = VERSION
|
||||
msg["timestamp"] = timestamp.isoformat()
|
||||
|
||||
msg['distinct_id'] = stringify_id(msg.get('distinct_id', None))
|
||||
# only send if "uuid" is truthy
|
||||
if "uuid" in msg:
|
||||
uuid = msg.pop("uuid")
|
||||
if uuid:
|
||||
msg["uuid"] = stringify_id(uuid)
|
||||
|
||||
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 = 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):
|
||||
@@ -251,7 +293,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.
|
||||
@@ -265,16 +307,105 @@ class Client(object):
|
||||
# consumer thread has not started
|
||||
pass
|
||||
|
||||
if self.poller:
|
||||
self.poller.stop()
|
||||
|
||||
def shutdown(self):
|
||||
"""Flush all messages and cleanly shutdown the client"""
|
||||
self.flush()
|
||||
self.join()
|
||||
|
||||
def _load_feature_flags(self):
|
||||
try:
|
||||
flags = get(self.personal_api_key, f"/api/feature_flag/?token={self.api_key}", self.host)["results"]
|
||||
self.feature_flags = [flag for flag in flags if flag["active"]]
|
||||
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()
|
||||
if not (self.poller and self.poller.is_alive()):
|
||||
self.poller = Poller(interval=timedelta(seconds=self.poll_interval), execute=self._load_feature_flags)
|
||||
self.poller.start()
|
||||
|
||||
def feature_enabled(self, key, distinct_id, default=False, *, groups={}):
|
||||
require("key", key, string_types)
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
require("groups", groups, dict)
|
||||
|
||||
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:
|
||||
for flag in self.feature_flags:
|
||||
if flag["key"] == key:
|
||||
feature_flag = flag
|
||||
break
|
||||
else:
|
||||
return default
|
||||
|
||||
if feature_flag.get("is_simple_flag"):
|
||||
response = _hash(key, distinct_id) <= ((feature_flag.get("rollout_percentage", 100) or 100) / 100)
|
||||
else:
|
||||
try:
|
||||
request_data = {
|
||||
"distinct_id": distinct_id,
|
||||
"personal_api_key": self.personal_api_key,
|
||||
"groups": groups,
|
||||
}
|
||||
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)
|
||||
|
||||
|
||||
|
||||
+31
-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,25 +117,22 @@ 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):
|
||||
# retry on server errors and client errors
|
||||
# with 429 status code (rate limited),
|
||||
# don't retry on other client errors
|
||||
if exc.status == "N/A":
|
||||
return False
|
||||
return (400 <= exc.status < 500) and exc.status != 429
|
||||
else:
|
||||
# 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 = True # Make daemon to not interfere with program exit
|
||||
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()
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
POSTHOG_ID_TAG = "posthog_distinct_id"
|
||||
@@ -0,0 +1,28 @@
|
||||
from django.conf import settings
|
||||
from sentry_sdk import configure_scope
|
||||
|
||||
from posthog.sentry import POSTHOG_ID_TAG
|
||||
|
||||
GET_DISTINCT_ID = getattr(settings, "POSTHOG_DJANGO", {}).get("distinct_id")
|
||||
|
||||
|
||||
def get_distinct_id(request):
|
||||
if not GET_DISTINCT_ID:
|
||||
return None
|
||||
try:
|
||||
return GET_DISTINCT_ID(request)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class PosthogDistinctIdMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
with configure_scope() as scope:
|
||||
distinct_id = get_distinct_id(request)
|
||||
if distinct_id:
|
||||
scope.set_tag(POSTHOG_ID_TAG, distinct_id)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -0,0 +1,53 @@
|
||||
from sentry_sdk._types import MYPY
|
||||
from sentry_sdk.client import Client
|
||||
from sentry_sdk.hub import Hub
|
||||
from sentry_sdk.integrations import Integration
|
||||
from sentry_sdk.scope import add_global_event_processor
|
||||
from sentry_sdk.utils import Dsn
|
||||
|
||||
import posthog
|
||||
from posthog.request import DEFAULT_HOST
|
||||
from posthog.sentry import POSTHOG_ID_TAG
|
||||
|
||||
if MYPY:
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sentry_sdk._types import Event, Hint
|
||||
|
||||
|
||||
class PostHogIntegration(Integration):
|
||||
identifier = "posthog-python"
|
||||
organization = None # The Sentry organization, used to send a direct link from PostHog to Sentry
|
||||
project_id = None # The Sentry project id, used to send a direct link from PostHog to Sentry
|
||||
prefix = "https://sentry.io/organizations/" # URL of a hosted sentry instance (default: https://sentry.io/organizations/)
|
||||
|
||||
@staticmethod
|
||||
def setup_once():
|
||||
@add_global_event_processor
|
||||
def processor(event, hint):
|
||||
# type: (Event, Optional[Hint]) -> Optional[Event]
|
||||
if Hub.current.get_integration(PostHogIntegration) is not None:
|
||||
if event.get("level") != "error":
|
||||
return event
|
||||
|
||||
if event.get("tags", {}).get(POSTHOG_ID_TAG):
|
||||
posthog_distinct_id = event["tags"][POSTHOG_ID_TAG]
|
||||
event["tags"]["PostHog URL"] = f"{posthog.host or DEFAULT_HOST}/person/{posthog_distinct_id}"
|
||||
|
||||
properties = {
|
||||
"$sentry_event_id": event["event_id"],
|
||||
"$sentry_exception": event["exception"],
|
||||
}
|
||||
|
||||
if PostHogIntegration.organization:
|
||||
project_id = PostHogIntegration.project_id or (
|
||||
not not Hub.current.client.dsn and Dsn(Hub.current.client.dsn).project_id
|
||||
)
|
||||
if project_id:
|
||||
properties[
|
||||
"$sentry_url"
|
||||
] = f"{PostHogIntegration.prefix}{PostHogIntegration.organization}/issues/?project={project_id}&query={event['event_id']}"
|
||||
|
||||
posthog.capture(posthog_distinct_id, "$exception", properties)
|
||||
|
||||
return event
|
||||
@@ -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():
|
||||
|
||||
@@ -1,316 +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
|
||||
|
||||
|
||||
class TestClient(unittest.TestCase):
|
||||
|
||||
def fail(self, e, batch):
|
||||
"""Mark the failure handler"""
|
||||
self.failed = True
|
||||
|
||||
def setUp(self):
|
||||
self.failed = False
|
||||
self.client = Client('testsecret', 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_track(self):
|
||||
client = self.client
|
||||
success, msg = client.track('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')
|
||||
|
||||
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')
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg['distinct_id'], '157963456373623802')
|
||||
|
||||
def test_advanced_track(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')
|
||||
|
||||
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['context']['library'], {
|
||||
'name': 'analytics-python',
|
||||
'version': VERSION
|
||||
})
|
||||
self.assertEqual(msg['messageId'], 'messageId')
|
||||
self.assertEqual(msg['distinct_id'], 'distinct_id')
|
||||
self.assertEqual(msg['type'], 'track')
|
||||
|
||||
def test_basic_identify(self):
|
||||
client = self.client
|
||||
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')
|
||||
|
||||
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['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')
|
||||
|
||||
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['previousId'], 'previousId')
|
||||
self.assertEqual(msg['distinct_id'], 'distinct_id')
|
||||
|
||||
def test_basic_page(self):
|
||||
client = self.client
|
||||
success, msg = client.page('distinct_id', name='name')
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
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('testsecret', 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('testsecret', 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_success_on_invalid_api_key(self):
|
||||
client = Client('bad_key', on_error=self.fail)
|
||||
client.track('distinct_id', 'event')
|
||||
client.flush()
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
def test_unicode(self):
|
||||
Client(six.u('unicode_key'))
|
||||
|
||||
def test_numeric_distinct_id(self):
|
||||
self.client.track(1234, 'python event')
|
||||
self.client.flush()
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
def test_debug(self):
|
||||
Client('bad_key', debug=True)
|
||||
|
||||
def test_identify_with_date_object(self):
|
||||
client = self.client
|
||||
success, msg = client.identify(
|
||||
'distinct_id',
|
||||
{
|
||||
'birthdate': date(1981, 2, 2),
|
||||
},
|
||||
)
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg['traits'], {'birthdate': date(1981, 2, 2)})
|
||||
|
||||
def test_gzip(self):
|
||||
client = Client('testsecret', on_error=self.fail, gzip=True)
|
||||
for _ in range(10):
|
||||
client.identify('distinct_id', {'trait': 'value'})
|
||||
client.flush()
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
def test_user_defined_flush_at(self):
|
||||
client = Client('testsecret', on_error=self.fail,
|
||||
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('analytics.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('testsecret', timeout=10)
|
||||
for consumer in client.consumers:
|
||||
self.assertEquals(consumer.timeout, 10)
|
||||
|
||||
def test_default_timeout_15(self):
|
||||
client = Client('testsecret')
|
||||
for consumer in client.consumers:
|
||||
self.assertEquals(consumer.timeout, 15)
|
||||
@@ -1,49 +0,0 @@
|
||||
import unittest
|
||||
|
||||
import analytics
|
||||
|
||||
|
||||
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
|
||||
|
||||
def test_no_api_key(self):
|
||||
analytics.api_key = None
|
||||
self.assertRaises(Exception, analytics.track)
|
||||
|
||||
def test_no_host(self):
|
||||
analytics.host = None
|
||||
self.assertRaises(Exception, analytics.track)
|
||||
|
||||
def test_track(self):
|
||||
analytics.track('distinct_id', 'python module event')
|
||||
analytics.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()
|
||||
|
||||
def test_alias(self):
|
||||
analytics.alias('previousId', 'distinct_id')
|
||||
analytics.flush()
|
||||
|
||||
def test_page(self):
|
||||
analytics.page('distinct_id')
|
||||
analytics.flush()
|
||||
|
||||
def test_screen(self):
|
||||
analytics.screen('distinct_id')
|
||||
analytics.flush()
|
||||
|
||||
def test_flush(self):
|
||||
analytics.flush()
|
||||
@@ -1,53 +0,0 @@
|
||||
from datetime import datetime, date
|
||||
import unittest
|
||||
import json
|
||||
import requests
|
||||
|
||||
from posthog.request import post, DatetimeSerializer
|
||||
|
||||
|
||||
class TestRequests(unittest.TestCase):
|
||||
|
||||
def test_valid_request(self):
|
||||
res = post(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(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)
|
||||
@@ -0,0 +1,486 @@
|
||||
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.assertIsNone(msg.get("uuid"))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
|
||||
def test_basic_capture_with_uuid(self):
|
||||
client = self.client
|
||||
uuid = str(uuid4())
|
||||
success, msg = client.capture("distinct_id", "python test event", uuid=uuid)
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg["event"], "python test event")
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertEqual(msg["uuid"], uuid)
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
|
||||
def test_basic_capture_with_project_api_key(self):
|
||||
|
||||
client = Client(project_api_key=TEST_API_KEY, on_error=self.set_fail)
|
||||
|
||||
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.assertIsNone(msg.get("uuid"))
|
||||
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),
|
||||
"new-uuid",
|
||||
)
|
||||
|
||||
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["uuid"], "new-uuid")
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertTrue("$groups" not in msg["properties"])
|
||||
|
||||
def test_groups_capture(self):
|
||||
success, msg = self.client.capture(
|
||||
"distinct_id",
|
||||
"test_event",
|
||||
groups={"company": "id:5", "instance": "app.posthog.com"},
|
||||
)
|
||||
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(msg["properties"]["$groups"], {"company": "id:5", "instance": "app.posthog.com"})
|
||||
|
||||
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.assertIsNone(msg.get("uuid"))
|
||||
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), "new-uuid"
|
||||
)
|
||||
|
||||
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["uuid"], "new-uuid")
|
||||
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.assertIsNone(msg.get("uuid"))
|
||||
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), "new-uuid"
|
||||
)
|
||||
|
||||
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["uuid"], "new-uuid")
|
||||
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.assertIsNone(msg.get("uuid"))
|
||||
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), "new-uuid"
|
||||
)
|
||||
|
||||
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["uuid"], "new-uuid")
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_basic_group_identify(self):
|
||||
success, msg = self.client.group_identify("organization", "id:5")
|
||||
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(msg["event"], "$groupidentify")
|
||||
self.assertEqual(msg["distinct_id"], "$organization_id:5")
|
||||
self.assertEqual(
|
||||
msg["properties"],
|
||||
{
|
||||
"$group_type": "organization",
|
||||
"$group_key": "id:5",
|
||||
"$group_set": {},
|
||||
"$lib": "posthog-python",
|
||||
"$lib_version": VERSION,
|
||||
},
|
||||
)
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertIsNone(msg.get("uuid"))
|
||||
|
||||
def test_advanced_group_identify(self):
|
||||
success, msg = self.client.group_identify(
|
||||
"organization", "id:5", {"trait": "value"}, {"ip": "192.168.0.1"}, datetime(2014, 9, 3), "new-uuid"
|
||||
)
|
||||
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(msg["event"], "$groupidentify")
|
||||
self.assertEqual(msg["distinct_id"], "$organization_id:5")
|
||||
self.assertEqual(
|
||||
msg["properties"],
|
||||
{
|
||||
"$group_type": "organization",
|
||||
"$group_key": "id:5",
|
||||
"$group_set": {"trait": "value"},
|
||||
"$lib": "posthog-python",
|
||||
"$lib_version": VERSION,
|
||||
},
|
||||
)
|
||||
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
||||
self.assertEqual(msg["context"]["ip"], "192.168.0.1")
|
||||
|
||||
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),
|
||||
"new-uuid",
|
||||
)
|
||||
|
||||
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["uuid"], "new-uuid")
|
||||
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.assertEqual(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.assertEqual(mock_post.call_count, 2)
|
||||
|
||||
def test_user_defined_timeout(self):
|
||||
client = Client(TEST_API_KEY, timeout=10)
|
||||
for consumer in client.consumers:
|
||||
self.assertEqual(consumer.timeout, 10)
|
||||
|
||||
def test_default_timeout_15(self):
|
||||
client = Client(TEST_API_KEY)
|
||||
for consumer in client.consumers:
|
||||
self.assertEqual(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", "active": True},
|
||||
{"id": 2, "name": "Alpha Feature", "key": "alpha-feature", "active": False},
|
||||
]
|
||||
}
|
||||
client = Client(TEST_API_KEY, personal_api_key="test")
|
||||
with freeze_time("2020-01-01T12:01:00.0000Z"):
|
||||
client.load_feature_flags()
|
||||
self.assertEqual(len(client.feature_flags), 1)
|
||||
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.get")
|
||||
def test_feature_enabled_simple_with_project_api_key(self, patch_get):
|
||||
client = Client(project_api_key=TEST_API_KEY, on_error=self.set_fail)
|
||||
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_decide):
|
||||
patch_decide.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.get")
|
||||
def test_feature_enabled_simple_without_rollout_percentage(self, patch_get):
|
||||
client = Client(TEST_API_KEY)
|
||||
client.feature_flags = [{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True}]
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
def test_feature_enabled_simple_with_none_rollout_percentage(self, patch_get):
|
||||
client = Client(TEST_API_KEY)
|
||||
client.feature_flags = [
|
||||
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True, "rollout_percantage": None}
|
||||
]
|
||||
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,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.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,15 +153,12 @@ 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)
|
||||
q.join()
|
||||
self.assertEquals(mock_post.call_count, 2)
|
||||
self.assertEqual(mock_post.call_count, 2)
|
||||
@@ -0,0 +1,40 @@
|
||||
import unittest
|
||||
|
||||
import posthog
|
||||
|
||||
|
||||
class TestModule(unittest.TestCase):
|
||||
def failed(self):
|
||||
self.failed = True
|
||||
|
||||
def setUp(self):
|
||||
self.failed = False
|
||||
posthog.api_key = "testsecret"
|
||||
posthog.on_error = self.failed
|
||||
|
||||
def test_no_api_key(self):
|
||||
posthog.api_key = None
|
||||
self.assertRaises(Exception, posthog.capture)
|
||||
|
||||
def test_no_host(self):
|
||||
posthog.host = None
|
||||
self.assertRaises(Exception, posthog.capture)
|
||||
|
||||
def test_track(self):
|
||||
posthog.capture("distinct_id", "python module event")
|
||||
posthog.flush()
|
||||
|
||||
def test_identify(self):
|
||||
posthog.identify("distinct_id", {"email": "user@email.com"})
|
||||
posthog.flush()
|
||||
|
||||
def test_alias(self):
|
||||
posthog.alias("previousId", "distinct_id")
|
||||
posthog.flush()
|
||||
|
||||
def test_page(self):
|
||||
posthog.page("distinct_id", "https://posthog.com/contact")
|
||||
posthog.flush()
|
||||
|
||||
def test_flush(self):
|
||||
posthog.flush()
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
+4
-1
@@ -1 +1,4 @@
|
||||
VERSION = '1.0.6'
|
||||
VERSION = "1.4.6"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(VERSION, end="")
|
||||
|
||||
@@ -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"
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sentry_django_example.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for sentry_django_example project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sentry_django_example.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Django settings for sentry_django_example project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.2.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-4kzfiq7vb(t0+jbl#vq)u=%06ouf)n*=l%730c8=tk(wkm9i9o"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# PostHog Setup (can be a separate app)
|
||||
import posthog
|
||||
|
||||
# You can find this key on the /setup page in PostHog
|
||||
posthog.api_key = "LXP6nQXvo-2TCqGVrWvPah8uJIyVykoMmhnEkEBi5PA" # TODO: replace with your 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"
|
||||
|
||||
from posthog.sentry.posthog_integration import PostHogIntegration
|
||||
|
||||
PostHogIntegration.organization = "posthog" # TODO: your sentry organization
|
||||
# PostHogIntegration.prefix = # TODO: your self hosted Sentry url. (default: https://sentry.io/organizations/)
|
||||
|
||||
# Since Sentry doesn't allow Integrations configuration (see https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/__init__.py#L171-L183)
|
||||
# we work around this by setting static class variables beforehand
|
||||
|
||||
# Sentry Setup
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn="https://27ac54f7f4cf484abf1335436b0c52e5@o344752.ingest.sentry.io/5624115", # TODO: your Sentry DSN here
|
||||
integrations=[DjangoIntegration(), PostHogIntegration()],
|
||||
# Set traces_sample_rate to 1.0 to capture 100%
|
||||
# of transactions for performance monitoring.
|
||||
# We recommend adjusting this value in production.
|
||||
traces_sample_rate=1.0,
|
||||
# If you wish to associate users to errors (assuming you are using
|
||||
# django.contrib.auth) you may enable sending PII data.
|
||||
send_default_pii=True,
|
||||
)
|
||||
|
||||
POSTHOG_DJANGO = {
|
||||
"distinct_id": lambda request: str(uuid4()) # TODO: your logic for generating unique ID, given the request object
|
||||
}
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"posthog.sentry.django.PosthogDistinctIdMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "sentry_django_example.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "sentry_django_example.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
@@ -0,0 +1,27 @@
|
||||
"""sentry_django_example URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
|
||||
def trigger_error(request):
|
||||
division_by_zero = 1 / 0
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("sentry-debug/", trigger_error),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for sentry_django_example project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sentry_django_example.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -8,39 +7,39 @@ 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.10.0,<2.0.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", "pytest"],
|
||||
"sentry": ["sentry-sdk", "django"],
|
||||
}
|
||||
|
||||
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", "posthog.sentry"],
|
||||
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",
|
||||
|
||||
+18
-24
@@ -1,4 +1,3 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -8,39 +7,34 @@ 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", "posthoganalytics.sentry"],
|
||||
license="MIT License",
|
||||
install_requires=install_requires,
|
||||
tests_require=tests_require,
|
||||
description='Integrate PostHog into any python application.',
|
||||
extras_require={
|
||||
"sentry": ["sentry-sdk", "django"],
|
||||
},
|
||||
description="Integrate PostHog into any python application.",
|
||||
long_description=long_description,
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
+59
-48
@@ -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,69 @@ 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))
|
||||
posthog.identify(
|
||||
options.distinct_id,
|
||||
anonymous_id=options.anonymousId,
|
||||
traits=json_hash(options.traits),
|
||||
context=json_hash(options.context),
|
||||
)
|
||||
|
||||
|
||||
def group():
|
||||
analytics.group(options.distinct_id, options.groupId, json_hash(options.traits),
|
||||
json_hash(options.context), anonymous_id=options.anonymousId)
|
||||
def 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():
|
||||
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, "set_once": set_once, "set": set}
|
||||
|
||||
func = switcher.get(options.type)
|
||||
if func:
|
||||
func()
|
||||
analytics.shutdown()
|
||||
posthog.shutdown()
|
||||
else:
|
||||
print("Invalid Message Type " + options.type)
|
||||
|
||||
Reference in New Issue
Block a user