Compare commits

..
Author SHA1 Message Date
Luke Harries d747bcea20 reformat with black 2023-01-24 17:59:54 +00:00
Luke Harries 364760b83d added regression tests 2023-01-24 17:11:24 +00:00
Luke Harries 33ccc37d4a fix alias 2023-01-24 17:03:27 +00:00
Neil KakkarandGitHub ae8817b611 feat(flags): Add support for variant overrides (#77) 2022-11-14 12:15:33 +00:00
Neil KakkarandGitHub acad2b142e fix: datetime comparison issues (#75) 2022-09-15 15:26:38 +01:00
Neil KakkarandGitHub cb62570e69 Update CHANGELOG.md 2022-09-14 14:22:28 +01:00
Eric DuongandGitHub 33645ecd3c feat: add date comparison to local evaluation (#74) 2022-09-14 13:53:44 +01:00
Neil KakkarandGitHub 81debcef27 fix: Remove defaults for feature flag calls (#72) 2022-08-12 12:20:54 +01:00
Neil KakkarandGitHub dac06bab18 fix(feature-flags): Add more options to make using library easier (#70) 2022-08-04 13:53:20 +01:00
Neil KakkarandGitHub 2dc1298620 Bump version to 2.0.0 and add breaking changes changelog (#69) 2022-08-02 12:10:32 +01:00
Neil KakkarandGitHub 2c6b675be7 feat(feature-flags): Enable local evaluation of flags (#68) 2022-07-29 14:10:51 +01:00
3a6fd07951 Add get feature flag method (#67)
* get feature flag method

* remove groups param from method

* use personal api key

* add groups

* add capture

* black reformat

* black?

* Update posthog/client.py

Co-authored-by: Neil Kakkar <neilkakkar@gmail.com>

* add to init

* formatting

Co-authored-by: Neil Kakkar <neilkakkar@gmail.com>
2022-06-30 11:48:53 -04:00
Neil KakkarandGitHub de0ccd29d3 bump version to 1.4.9 (#66) 2022-06-13 12:18:50 +01:00
addd2e3340 Have an option to send feature variants with the .capture(...) calls (#65)
Co-authored-by: Utku Zihnioglu <utku@webshare.io>
2022-06-13 12:13:35 +01:00
Tim GlaserandGitHub 9d2fa72753 bump version 1.4.8 (#61)
* bump version 1.4.8

* Update CHANGELOG.md
2022-05-12 08:12:11 +01:00
306fb2a1fa Feature: Enable multi variate feature flags for Python library (#60)
* Capturing $feature_flag_called at the end of client.feature_enabled method

* Enabling multi-variants for feature flags

Co-authored-by: Utku Zihnioglu <utku@webshare.io>
Co-authored-by: Tim Glaser <tim@posthog.com>
2022-05-12 08:10:54 +01:00
faffd1f88a Capturing $feature_flag_called at the end of client.feature_enabled method (#57)
Co-authored-by: Utku Zihnioglu <utku@webshare.io>
2022-05-12 08:05:26 +01:00
Tim Glaser ab1399d88f Bump version 1.4.7 2022-04-25 14:08:08 +01:00
Tim GlaserandGitHub 1777b7062e fix: Personal api key not required (#56)
* fix: Personal api key not required

* formatting

* Fix false
2022-04-25 14:07:08 +01:00
Tim GlaserandGitHub 565bb8a0eb docs:Remove id: from example (#55) 2022-04-11 16:01:27 +02:00
Marius Andra 009cac8634 1.4.6 2022-03-30 08:45:14 +02:00
Marius AndraandGitHub ec2425996c Support custom UUID values (#53)
* feat(uuid): add support for custom uuids

* fix deprecated assert

* do not send "none" uuid

* you can have it in any color you want, as long as it's black
2022-03-30 08:42:43 +02:00
Paolo D'AmicoandGitHub 90fa0a0604 Update version.py (#49) 2022-01-05 06:46:28 -06:00
Paolo D'AmicoandGitHub a97fe0a40a Use Sentry DSN to obtain project ID (#48) 2022-01-05 06:15:51 -06:00
Karl-Aksel PuulmannandGitHub a474fcff93 Update version to 1.4.4 2021-11-25 11:45:18 +02:00
Karl-Aksel PuulmannandGitHub a181ba718f Groups: Feature flags support (#45)
* Fix a documentation typo

* Feature flags & groups support

* Update examples
2021-11-25 09:55:40 +02:00
Karl-Aksel PuulmannandGitHub b996f3a4e9 Update version to 1.4.3 2021-10-28 18:42:58 +03:00
Karl-Aksel PuulmannandGitHub 6c945a0624 Basic group analytics support (#44)
* Make setup instructions work

* Add basic group analytics support to library

* Resolve formatting issues
2021-10-27 10:25:33 +03:00
Michael Matloka ab8ccb4dff Update pip install 2021-06-22 14:41:11 +02:00
Michael MatlokaandGitHub 870f6f8b6b Update README.md 2021-06-22 14:39:02 +02:00
Michael MatlokaandGitHub 9e0aeaefe6 Update README.md 2021-06-22 14:38:44 +02:00
Michael MatlokaandGitHub a8409960b9 Bump version to 1.4.2 2021-06-22 14:38:15 +02:00
Michael Matloka deb078293a Use PyPI API token instead of username and password 2021-06-22 14:30:53 +02:00
Michael Matloka edb8b7891e Ensure wheel 2021-06-22 14:24:27 +02:00
Michael Matloka 727bdb2b1e Fix make target in release workflow 2021-06-22 14:21:01 +02:00
Michael MatlokaandGitHub 0781a1280e Add release workflow (#42)
* Add release workflow

* Update README.md
2021-06-22 14:19:31 +02:00
Yakko MajuriandGitHub 8b2ed8bb12 Fix integrity issues (#41)
* Fix integrity issues

* fix black, tests

* add test for new behavior

* run black properly

* remove accidental commit
2021-06-16 11:06:14 -03:00
Yakko MajuriandGitHub a139795a74 Better handling on feature_enabled (#40) 2021-06-07 11:39:17 +01:00
Mandeep GillandGitHub 11f1d06761 Remove pinning of backoff dependency (#38) 2021-06-04 14:26:36 -03:00
Neil Kakkar fd321566ed Release 1.4.1 2021-05-28 10:19:56 +01:00
Mandeep GillandGitHub 83737f2477 Bump backoff dependency to 1.10.0 (#36) 2021-05-26 19:02:15 +02:00
Michael MatlokaandGitHub cc5649368f Fix 1.4.0 date in CHANGELOG.md 2021-05-20 15:44:53 +02:00
Michael MatlokaandGitHub 7fe5045da1 Update CHANGELOG.md 2021-05-18 17:21:59 +02:00
Michael MatlokaandGitHub 2c9ad238ec Add PyPI badge to README 2021-05-18 17:20:36 +02:00
Michael Matloka de6e60f12f Release 1.4.0 2021-05-18 17:16:37 +02:00
Neil KakkarandGitHub fd92502d99 Add support for project_api_key. (#32)
* add support for project_api_key

* rm keys

* lint
2021-05-18 17:09:07 +02:00
Neil KakkarandGitHub fe6d0dc1ec Resolve polling issues with feature flags (#29)
* resolve polling issues

* lint
2021-05-17 14:09:20 +01:00
fbde5cafc4 Add python sentry (and sentry & django) integrations (#13)
* Add python sentry (and sentry & django) integrations

* example for django-sentry application and PosthogIntegration code

Co-authored-by: Neil Kakkar <neilkakkar@gmail.com>
2021-05-17 14:08:46 +01:00
Neil KakkarandGitHub 2e99081cb1 Fix feature flag issue with no % rollout (#30)
* fix feature flag issue with no % rollout

* black
2021-05-14 11:52:06 -03:00
Neil Kakkar 372fb74637 Release 1.3.1 2021-05-07 12:22:14 +01:00
Neil KakkarandGitHub ba11548089 revert test change (#28) 2021-05-07 12:39:50 +02:00
Neil KakkarandGitHub 49d0821e27 Merge pull request #23 from PostHog/set_once
Add $set and $set_once support
2021-05-07 11:06:04 +01:00
Neil Kakkar b4489f1dca black 2021-05-07 10:57:53 +01:00
Neil Kakkar 8040964761 Add set and set_once to simulator.py 2021-05-07 10:54:10 +01:00
Tim GlaserandGitHub 4f853403b9 Merge pull request #27 from gagantrivedi/fix/alias
fix: Add distinct id to $create_alias event
2021-05-07 11:35:00 +02:00
Gagan ac61fb0e01 chore: Reformat consumer with black 2021-05-07 14:52:25 +05:30
Gagan 7196dc6048 fix: Add distinct id to $create_alias event 2021-05-07 11:22:03 +05:30
Neil Kakkar 45303b899e black 2021-05-06 14:05:57 +01:00
Neil Kakkar 7e463ccad6 resolve conflicts with master 2021-05-06 13:59:46 +01:00
Neil Kakkar 41dec34929 remove API key 2021-05-06 13:53:18 +01:00
Neil Kakkar 0bf9db0108 add functionality+tests for set and set_once 2021-05-06 13:52:23 +01:00
Neil Kakkar e8308360bb cleanup tests, add instructions to readme 2021-05-06 11:43:13 +01:00
Paolo D'AmicoandGitHub 15ebe85a78 Merge pull request #26 from PostHog/distinct-uuid 2021-04-01 17:59:29 -07:00
Michael Matloka dbc22d2f9f Add UUID to ID_TYPES 2021-04-01 17:31:48 +02:00
Paolo D'Amico f3ee238823 black 2021-02-08 11:21:55 +01:00
Paolo D'Amico 71d81b2da9 update tests 2021-02-08 11:18:55 +01:00
Paolo D'Amico 5493029577 add $set_once support 2021-02-08 11:16:27 +01:00
Michael Matloka d66f944571 Bump version to 1.2.1 2021-02-05 16:27:51 +01:00
Michael MatlokaandGitHub 9d620967f8 Merge pull request #22 from PostHog/fix-possibly-null-percentage
Add condition for rollout-percentage
2021-01-28 15:01:30 +01:00
Eric 563404f914 add condition 2021-01-25 19:57:21 -05:00
Yakko MajuriandGitHub d15aac41a9 Update README.md 2021-01-11 17:19:21 -03:00
Michael MatlokaandGitHub 8a3e28b949 Merge pull request #21 from PostHog/code-style
Black and isort all the things
2021-01-05 12:25:57 +01:00
Michael Matloka 1aa0d6335c Add CI job for this 2021-01-05 12:21:39 +01:00
Michael Matloka 3b46c60cf1 Normalize strings to double quotes 2021-01-05 12:09:44 +01:00
Michael Matloka 4ad8cbfa58 Fix weird concat 2021-01-05 12:07:05 +01:00
Michael Matloka 984a679b19 Black and isort all the things! 2021-01-03 05:37:51 +01:00
Michael Matloka dd1bad6175 Setup black, isort and pre-commit 2021-01-03 05:36:26 +01:00
Michael MatlokaandGitHub e6f71e4cc3 Merge pull request #20 from Hungsiro506/patch-1
Fix post() returning None on non-200 response
2021-01-03 05:07:51 +01:00
Michael Matloka 17874cb131 Update .gitignore 2021-01-03 05:00:41 +01:00
Michael Matloka 8366e09df9 Add typing to request.py 2021-01-03 04:58:55 +01:00
Hưng VũandGitHub e28b237ff1 Update request.py
-- Fix post method return None
2020-12-30 17:43:08 +07:00
Michael MatlokaandGitHub 05fde2a51e Merge pull request #18 from PostHog/better-errors
Add project_api_key, improve errors
2020-12-15 12:01:28 +01:00
Michael MatlokaandGitHub 2be04f3b8b Merge pull request #14 from PostHog/uuid-property
Allow passing in UUID as property
2020-12-15 11:21:31 +01:00
Michael Matloka d52b605742 Rerun CI 2020-12-15 11:14:18 +01:00
yakkomajuri 7ee3002c6f update generic error handler 2020-12-04 11:10:22 +00:00
yakkomajuri d1bc9135c7 specify json return 2020-12-04 10:29:07 +00:00
yakkomajuri 111813296c add project_api_key to config 2020-12-04 09:58:43 +00:00
yakkomajuri 31acda73a3 Better API error handling 2020-12-04 09:34:23 +00:00
Paolo D'AmicoandGitHub 888457387b Merge pull request #17 from PostHog/v1.1.3 2020-11-23 09:33:50 -06:00
Paolo D'Amico fe45ff2ab0 version bump 2020-11-23 10:08:04 -05:00
Marius AndraandGitHub 221d7f09f3 Do not start Feature Flag polling if no API Key (#15)
* jetbrains idea .gitignore

* do not start the poller if no personal api key
2020-11-23 15:37:52 +01:00
Michael Matloka a5f2e030b5 Allow passing in UUID as property 2020-11-03 17:14:00 +01:00
Tim GlaserandGitHub 98d2d4cc05 Merge pull request #12 from PostHog/investigate
Send distinctID to the decide endpoint to determin if user should have features enabled
2020-09-30 14:34:43 +01:00
James Greenhill b7c1572c32 bump version to 1.1.2 2020-09-30 14:25:49 +01:00
James Greenhill 16acf2e278 better testing and org 2020-09-30 14:20:50 +01:00
James Greenhill df9ae05202 fix tests 2020-09-30 14:10:34 +01:00
James Greenhill b05ee3884a Send distinctID to the decide endpoint to determin if user should have features enabled 2020-09-30 13:53:47 +01:00
James GreenhillandGitHub 144a7744e4 Merge pull request #11 from PostHog/none
Default to False if the feature flag key does not exist. None is confusing
2020-09-29 17:06:53 +01:00
James Greenhill ca979f06c8 type annotations 2020-09-29 17:02:39 +01:00
James Greenhill caa64fb6fe Default to False if the feature flag key does not exist. None is confusing 2020-09-29 16:07:43 +01:00
Tim GlaserandGitHub e007dee07e Merge pull request #10 from PostHog/warn-instead-of-error
Warn instead of error
2020-09-29 13:16:06 +02:00
Tim Glaser 1de9553d61 Fix tests 2020-09-29 12:13:28 +01:00
Tim Glaser d447d170fa Warn instead of error 2020-09-29 11:57:44 +01:00
Tim GlaserandGitHub d92c398c0f Merge pull request #9 from PostHog/feature-flags
Feature flags
2020-09-17 10:17:42 +02:00
Tim Glaser b331c4aae3 Add error handling and timeouts 2020-09-17 10:13:03 +02:00
Yakko Majuri 5e70ca84bb minor changes 2020-09-14 09:37:14 +00:00
Yakko Majuri adef8d4928 fix comment 2020-09-14 08:26:40 +00:00
Tim GlaserandGitHub 8682091eec Merge pull request #8 from PostHog/add-tests
Fix tests and clean up integration
2020-09-11 15:39:51 +02:00
Yakko Majuri 721a6aacf7 fix action 2020-09-11 08:58:14 +00:00
Tim Glaser 72e7e4ad72 typo 2020-09-10 16:52:31 +02:00
Tim Glaser 95d4375663 Fix tests 2020-09-10 16:39:27 +02:00
Tim Glaser 6e39aa0ceb Add poller 2020-09-10 16:35:23 +02:00
Tim Glaser c72ab9a3fd Make feature flags work 2020-09-10 16:30:54 +02:00
41 changed files with 6051 additions and 763 deletions
+61
View File
@@ -0,0 +1,61 @@
name: CI
on:
- pull_request
jobs:
code-quality:
name: Code quality checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dev dependencies
run: |
python -m pip install -e .[dev]
if: steps.cache.outputs.cache-hit != 'true'
- name: Check formatting with black
run: |
black --check .
- name: Check import order with isort
run: |
isort --check-only .
tests:
name: Python tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install requirements.txt dependencies with pip
run: |
python -m pip install -e .[test]
- name: Run posthog tests
run: |
python setup.py test
+38
View File
@@ -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 }}
-27
View File
@@ -1,27 +0,0 @@
name: Backend CI
on:
- pull_request
jobs:
tests:
name: Python tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install requirements.txt dependencies with pip
run: |
python -m pip install -e .
- name: Run posthog tests
run: |
python setup.py test
+8 -5
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
repos:
- repo: https://github.com/psf/black
rev: stable
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.7.0
hooks:
- id: isort
+73
View File
@@ -0,0 +1,73 @@
## 2.2.0 - 2022-11-14
Changes:
1. Add support for feature flag variant overrides with local evaluation
## 2.1.2 - 2022-09-15
Changes:
1. Fixes issues with date comparison.
## 2.1.1 - 2022-09-14
Changes:
1. Feature flags local evaluation now supports date property filters as well. Accepts both strings and datetime objects.
## 2.1.0 - 2022-08-11
Changes:
1. Feature flag defaults have been removed
2. Setup logging only when debug mode is enabled.
## 2.0.1 - 2022-08-04
- Make poll_interval configurable
- Add `send_feature_flag_events` parameter to feature flag calls, which determine whether the `$feature_flag_called` event should be sent or not.
- Add `only_evaluate_locally` parameter to feature flag calls, which determines whether the feature flag should only be evaluated locally or not.
## 2.0.0 - 2022-08-02
Breaking changes:
1. The minimum version requirement for PostHog servers is now 1.38. If you're using PostHog Cloud, you satisfy this requirement automatically.
2. Feature flag defaults apply only when there's an error fetching feature flag results. Earlier, if the default was set to `True`, even if a flag resolved to `False`, the default would override this.
**Note: These are removed in 2.0.2**
3. Feature flag remote evaluation doesn't require a personal API key.
New Changes:
1. You can now evaluate feature flags locally (i.e. without sending a request to your PostHog servers) by setting a personal API key, and passing in groups and person properties to `is_feature_enabled` and `get_feature_flag` calls.
2. Introduces a `get_all_flags` method that returns all feature flags. This is useful for when you want to seed your frontend with some initial flags, given a user ID.
## 1.4.9 - 2022-06-13
- Support for sending feature flags with capture calls
## 1.4.8 - 2022-05-12
- Support multi variate feature flags
## 1.4.7 - 2022-04-25
- Allow feature flags usage without project_api_key
## 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.
+10 -3
View File
@@ -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
+43 -2
View File
@@ -1,5 +1,46 @@
# PostHog Python
Please see the main [PostHog docs](https://posthog.com/docs).
[![PyPI](https://img.shields.io/pypi/v/posthog)](https://pypi.org/project/posthog/)
Specifically, the [Python integration](https://posthog.com/docs/integrations/python-integration) details.
Please see the [Python integration docs](https://posthog.com/docs/integrations/python-integration) for details.
## Development
### Testing Locally
1. Run `python3 -m venv env` (creates virtual environment called "env")
2. Run `source env/bin/activate` (activates the virtual environment)
3. Run `python3 -m pip install -e ".[test]"` (installs the package in develop mode, along with test dependencies)
4. Run `make test`
1. To run a specific test do `pytest -k test_no_api_key`
### Running Locally
Assuming you have a [local version of PostHog](https://posthog.com/docs/developing-locally) running, you can run `python3 example.py` to see the library in action.
### Running the Django Sentry Integration Locally
There's a sample Django project included, called `sentry_django_example`, which explains how to use PostHog with Sentry.
There's 2 places of importance (Changes required are all marked with TODO in the sample project directory)
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
2. urls.py
1. This includes the `sentry-debug/` endpoint, which generates an exception
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.
### Releasing Versions
Updated are released using GitHub Actions: after bumping `version.py` in `master` and adding to `CHANGELOG.md`, 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`.
## Questions?
### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
+84 -6
View File
@@ -1,20 +1,98 @@
# PostHog Python library example
# Import the library
import time
import posthog
posthog.debug = True
# You can find this key on the /setup page in PostHog
posthog.api_key = '<your key>'
posthog.project_api_key = "phc_gtWmTq3Pgl06u4sZY3TRcoQfp42yfuXHKoe8ZVSR6Kh"
posthog.personal_api_key = "phx_fiRCOQkTA3o2ePSdLrFDAILLHjMu2Mv52vUi8MNruIm"
# 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://localhost:8000"
posthog.poll_interval = 10
print(
posthog.feature_enabled(
"person-on-events-enabled",
"12345",
groups={"organization": str("0182ee91-8ef7-0000-4cb9-fedc5f00926a")},
group_properties={
"organization": {
"id": "0182ee91-8ef7-0000-4cb9-fedc5f00926a",
"created_at": "2022-06-30 11:44:52.984121+00:00",
}
},
only_evaluate_locally=True,
)
)
exit()
# Capture an event
posthog.capture('distinct_id', 'event', {'property1': 'value', 'property2': 'value'})
posthog.capture("distinct_id", "event", {"property1": "value", "property2": "value"}, send_feature_flags=True)
# # 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-groups", "distinct_id", groups={"company": "id:5"}))
print(posthog.feature_enabled("beta-feature", "distinct_id"))
# Add an alias_id to a distinct_id
posthog.alias("distinct_id", "alias_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('distinct_id', {'email': 'something@something.com'})
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})
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"})
# #############################################################################
# Make sure you have a personal API key for the examples below
# Local Evaluation
# If flag has City=Sydney, this call doesn't go to `/decide`
print(posthog.feature_enabled("test-flag", "distinct_id_random_22", person_properties={"$geoip_city_name": "Sydney"}))
print(
posthog.feature_enabled(
"test-flag",
"distinct_id_random_22",
person_properties={"$geoip_city_name": "Sydney"},
only_evaluate_locally=True,
)
)
print(posthog.get_all_flags("distinct_id_random_22"))
print(posthog.get_all_flags("distinct_id_random_22", only_evaluate_locally=True))
print(
posthog.get_all_flags(
"distinct_id_random_22", person_properties={"$geoip_city_name": "Sydney"}, only_evaluate_locally=True
)
)
posthog.shutdown()
+299 -49
View File
@@ -1,63 +1,83 @@
from typing import Callable, Dict, Optional
from posthog.version import VERSION
from posthog.client import Client
from typing import Optional, Dict, Callable
from posthog.version import VERSION
__version__ = VERSION
"""Settings."""
api_key = None # type: str
host = None # type: str
on_error = None # type: Callable
debug = False # type: bool
send = True # type: bool
sync_mode = False # type: bool
disabled = False # type: bool
api_key = None # type: str
host = None # type: str
on_error = None # type: Callable
debug = False # type: bool
send = True # type: bool
sync_mode = False # type: bool
disabled = False # type: bool
personal_api_key = None # type: str
project_api_key = None # type: str
poll_interval = 30 # type: int
default_client = None
def capture(
distinct_id, # type: str,
event, # type: str,
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
):
distinct_id, # type: str
event, # type: str
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
uuid=None, # type: Optional[str]
groups=None, # type: Optional[Dict]
send_feature_flags=False,
):
# 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,
send_feature_flags=send_feature_flags,
)
def identify(
distinct_id, # type: str,
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
):
distinct_id, # type: str
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
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
@@ -67,20 +87,124 @@ def identify(
})
```
"""
_proxy('identify', distinct_id=distinct_id, properties=properties, context=context, timestamp=timestamp, message_id=message_id)
_proxy(
"identify",
distinct_id=distinct_id,
properties=properties,
context=context,
timestamp=timestamp,
uuid=uuid,
)
def group(*args, **kwargs):
"""Send a group call."""
_proxy('group', *args, **kwargs)
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]
message_id=None, # type: Optional[str]
):
distinct_id, # type: str
previous_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?"
@@ -90,41 +214,159 @@ def alias(
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
- `distinct id` the current unique id of the user (normally the id in your database)
- `alias distinct id` the alias id you want to attach to the user, such as the anonymous session id or another ID like their email
For example:
```python
posthog.alias('anonymous session id', 'distinct id')
posthog.alias('distinct id', 'anonymous session id')
```
or
```python
posthog.alias('distinct id', 'users-email@posthog.com')
```
"""
_proxy('alias', previous_id=previous_id, distinct_id=distinct_id, context=context, timestamp=timestamp, message_id=message_id)
_proxy(
"alias",
distinct_id=distinct_id,
previous_id=previous_id,
context=context,
timestamp=timestamp,
uuid=uuid,
)
def feature_enabled(
key, # type: str,
distinct_id, # type: str,
groups={}, # type: dict
person_properties={}, # type: dict
group_properties={}, # type: dict
only_evaluate_locally=False, # type: bool
send_feature_flag_events=True, # type: bool
):
# type: (...) -> bool
"""
Use feature flags to enable or disable features for users.
For example:
```python
if posthog.feature_enabled('beta feature', 'distinct id'):
# do something
if posthog.feature_enabled('groups feature', 'distinct id', groups={"organization": "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,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
send_feature_flag_events=send_feature_flag_events,
)
def get_feature_flag(
key, # type: str,
distinct_id, # type: str,
groups={}, # type: dict
person_properties={}, # type: dict
group_properties={}, # type: dict
only_evaluate_locally=False, # type: bool
send_feature_flag_events=True, # type: bool
):
"""
Get feature flag variant for users. Used with experiments.
Example:
```python
if posthog.get_feature_flag('beta-feature', 'distinct_id') == 'test-variant':
# do test variant code
if posthog.get_feature_flag('beta-feature', 'distinct_id') == 'control':
# do control code
```
`groups` are a mapping from group type to group key. So, if you have a group type of "organization" and a group key of "5",
you would pass groups={"organization": "5"}.
`group_properties` take the format: { group_type_name: { group_properties } }
So, for example, if you have the group type "organization" and the group key "5", with the properties name, and employee count,
you'll send these as:
```python
group_properties={"organization": {"name": "PostHog", "employees": 11}}
```
"""
return _proxy(
"get_feature_flag",
key=key,
distinct_id=distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
send_feature_flag_events=send_feature_flag_events,
)
def get_all_flags(
distinct_id, # type: str,
groups={}, # type: dict
person_properties={}, # type: dict
group_properties={}, # type: dict
only_evaluate_locally=False, # type: bool
):
"""
Get all flags for a given user.
Example:
```python
flags = posthog.get_all_flags('distinct_id')
```
flags are key-value pairs where the key is the flag key and the value is the flag variant, or True, or False.
"""
return _proxy(
"get_all_flags",
distinct_id=distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
)
def page(*args, **kwargs):
"""Send a page call."""
_proxy('page', *args, **kwargs)
_proxy("page", *args, **kwargs)
def screen(*args, **kwargs):
"""Send a screen call."""
_proxy('screen', *args, **kwargs)
_proxy("screen", *args, **kwargs)
def flush():
"""Tell the client to flush."""
_proxy('flush')
_proxy("flush")
def join():
"""Block program until the client clears the queue"""
_proxy('join')
_proxy("join")
def shutdown():
"""Flush all messages and cleanly shutdown the client"""
_proxy('flush')
_proxy('join')
_proxy("flush")
_proxy("join")
def _proxy(method, *args, **kwargs):
@@ -133,9 +375,17 @@ def _proxy(method, *args, **kwargs):
if disabled:
return None
if not default_client:
default_client = Client(api_key, host=host, debug=debug,
on_error=on_error, send=send,
sync_mode=sync_mode)
default_client = Client(
api_key,
host=host,
debug=debug,
on_error=on_error,
send=send,
sync_mode=sync_mode,
personal_api_key=personal_api_key,
project_api_key=project_api_key,
poll_interval=poll_interval,
)
fn = getattr(default_client, method)
fn(*args, **kwargs)
return fn(*args, **kwargs)
+430 -81
View File
@@ -1,15 +1,17 @@
from datetime import datetime
from uuid import uuid4
import atexit
import logging
import numbers
import atexit
from datetime import datetime, timedelta
from uuid import UUID
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.feature_flags import InconclusiveMatchError, match_feature_flag_properties
from posthog.poller import Poller
from posthog.request import APIError, batch_post, decide, get
from posthog.utils import SizeLimitedDict, 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)
MAX_DICT_SIZE = 50_000
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,9 +63,21 @@ class Client(object):
self.host = host
self.gzip = gzip
self.timeout = timeout
self.feature_flags = None
self.group_type_mapping = None
self.poll_interval = poll_interval
self.poller = None
self.distinct_ids_feature_flags_reported = SizeLimitedDict(MAX_DICT_SIZE, set)
# personal_api_key: This should be a generated Personal API Key, private
self.personal_api_key = personal_api_key
if debug:
# Ensures that debug level messages are logged when debug mode is on.
# Otherwise, defaults to WARNING level. See https://docs.python.org/3/howto/logging.html#what-happens-if-no-configuration-is-provided
logging.basicConfig()
self.log.setLevel(logging.DEBUG)
else:
self.log.setLevel(logging.WARNING)
if sync_mode:
self.consumers = None
@@ -58,9 +93,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,127 +109,223 @@ 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,
'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 get_feature_variants(self, distinct_id, groups=None, person_properties=None, group_properties=None):
require("distinct_id", distinct_id, ID_TYPES)
if groups:
require("groups", groups, dict)
else:
groups = {}
request_data = {
"distinct_id": distinct_id,
"groups": groups,
"person_properties": person_properties,
"group_properties": group_properties,
}
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
return resp_data["featureFlags"]
def capture(
self,
distinct_id=None,
event=None,
properties=None,
context=None,
timestamp=None,
uuid=None,
groups=None,
send_feature_flags=False,
):
properties = properties or {}
context = context or {}
require('distinct_id', distinct_id, ID_TYPES)
require('properties', properties, dict)
require('event', event, string_types)
require("distinct_id", distinct_id, ID_TYPES)
require("properties", properties, dict)
require("event", event, string_types)
msg = {
'properties': properties,
'timestamp': timestamp,
'context': context,
'distinct_id': distinct_id,
'event': event,
'messageId': message_id,
"properties": properties,
"timestamp": timestamp,
"context": context,
"distinct_id": distinct_id,
"event": event,
"uuid": uuid,
}
if groups:
require("groups", groups, dict)
msg["properties"]["$groups"] = groups
if send_feature_flags:
try:
feature_variants = self.get_feature_variants(distinct_id, groups)
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Unable to get feature variants: {e}")
else:
for feature, variant in feature_variants.items():
msg["properties"]["$feature/{}".format(feature)] = variant
msg["properties"]["$active_feature_flags"] = list(feature_variants.keys())
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,
'event': '$create_alias'
"distinct_id": "${}_{}".format(group_type, group_key),
"timestamp": timestamp,
"context": context,
"uuid": uuid,
}
return self._enqueue(msg)
def page(self, distinct_id=None, url=None, properties=None,
context=None, timestamp=None, message_id=None):
def alias(self, distinct_id=None, previous_id=None, context=None, timestamp=None, uuid=None):
context = context or {}
require("previous_id", previous_id, ID_TYPES)
require("distinct_id", distinct_id, ID_TYPES)
msg = {
"properties": {
"distinct_id": distinct_id,
"alias": previous_id,
},
"timestamp": timestamp,
"context": context,
"event": "$create_alias",
"distinct_id": distinct_id,
}
return self._enqueue(msg)
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)
require('url', url, string_types)
properties['$current_url'] = url
require("url", url, string_types)
properties["$current_url"] = url
msg = {
'event': '$pageview',
'properties': properties,
'timestamp': timestamp,
'context': context,
'distinct_id': distinct_id,
'messageId': message_id,
"event": "$pageview",
"properties": properties,
"timestamp": timestamp,
"context": context,
"distinct_id": distinct_id,
"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('timestamp', timestamp, datetime)
require('context', msg['context'], dict)
require("timestamp", timestamp, datetime)
require("context", msg["context"], dict)
# add common
timestamp = guess_timezone(timestamp)
msg['timestamp'] = timestamp.isoformat()
msg['messageId'] = stringify_id(message_id)
if not msg.get('properties'):
msg['properties'] = {}
msg['properties']['$lib'] = 'posthog-python'
msg['properties']['$lib_version'] = VERSION
msg["timestamp"] = timestamp.isoformat()
msg['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['event'])
post(self.api_key, self.host, gzip=self.gzip,
timeout=self.timeout, batch=[msg])
self.log.debug("enqueued with blocking %s.", msg["event"])
batch_post(self.api_key, self.host, gzip=self.gzip, timeout=self.timeout, batch=[msg])
return True, msg
try:
self.queue.put(msg, block=False)
self.log.debug('enqueued %s.', msg['event'])
self.log.debug("enqueued %s.", msg["event"])
return True, msg
except queue.Full:
self.log.warning('analytics-python queue is full')
self.log.warning("analytics-python queue is full")
return False, msg
def flush(self):
@@ -197,7 +334,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.
@@ -211,16 +348,228 @@ 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:
response = get(
self.personal_api_key, f"/api/feature_flag/local_evaluation/?token={self.api_key}", self.host
)
self.feature_flags = response["flags"] or []
self.group_type_mapping = response["group_type_mapping"] or {}
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:
self.log.error(f"[FEATURE FLAGS] Error loading feature flags: {e}")
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 _compute_flag_locally(self, feature_flag, distinct_id, *, groups={}, person_properties={}, group_properties={}):
if feature_flag.get("ensure_experience_continuity", False):
raise InconclusiveMatchError("Flag has experience continuity enabled")
if not feature_flag.get("active"):
return False
flag_filters = feature_flag.get("filters") or {}
aggregation_group_type_index = flag_filters.get("aggregation_group_type_index")
if aggregation_group_type_index is not None:
group_name = self.group_type_mapping.get(str(aggregation_group_type_index))
if not group_name:
self.log.warning(
f"[FEATURE FLAGS] Unknown group type index {aggregation_group_type_index} for feature flag {feature_flag['key']}"
)
# failover to `/decide/`
raise InconclusiveMatchError("Flag has unknown group type index")
if group_name not in groups:
# Group flags are never enabled in `groups` aren't passed in
# don't failover to `/decide/`, since response will be the same
self.log.warning(
f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
)
return False
focused_group_properties = group_properties[group_name]
return match_feature_flag_properties(feature_flag, groups[group_name], focused_group_properties)
else:
return match_feature_flag_properties(feature_flag, distinct_id, person_properties)
def feature_enabled(
self,
key,
distinct_id,
*,
groups={},
person_properties={},
group_properties={},
only_evaluate_locally=False,
send_feature_flag_events=True,
):
response = self.get_feature_flag(
key,
distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
send_feature_flag_events=send_feature_flag_events,
)
if response is None:
return None
return bool(response)
def get_feature_flag(
self,
key,
distinct_id,
*,
groups={},
person_properties={},
group_properties={},
only_evaluate_locally=False,
send_feature_flag_events=True,
):
require("key", key, string_types)
require("distinct_id", distinct_id, ID_TYPES)
require("groups", groups, dict)
if self.feature_flags == None and self.personal_api_key:
self.load_feature_flags()
response = None
# If loading in previous line failed
if self.feature_flags:
for flag in self.feature_flags:
if flag["key"] == key:
try:
response = self._compute_flag_locally(
flag,
distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
)
self.log.debug(f"Successfully computed flag locally: {key} -> {response}")
except InconclusiveMatchError as e:
self.log.debug(f"Failed to compute flag {key} locally: {e}")
continue
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Error while computing variant locally: {e}")
continue
flag_was_locally_evaluated = response is not None
if not flag_was_locally_evaluated and not only_evaluate_locally:
try:
feature_flags = self.get_feature_variants(
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
)
response = feature_flags.get(key)
if response is None:
response = False
self.log.debug(f"Successfully computed flag remotely: #{key} -> #{response}")
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Unable to get flag remotely: {e}")
feature_flag_reported_key = f"{key}_{str(response)}"
if (
feature_flag_reported_key not in self.distinct_ids_feature_flags_reported[distinct_id]
and send_feature_flag_events
):
self.capture(
distinct_id,
"$feature_flag_called",
{
"$feature_flag": key,
"$feature_flag_response": response,
"locally_evaluated": flag_was_locally_evaluated,
},
groups=groups,
)
self.distinct_ids_feature_flags_reported[distinct_id].add(feature_flag_reported_key)
return response
def get_all_flags(
self, distinct_id, *, groups={}, person_properties={}, group_properties={}, only_evaluate_locally=False
):
require("distinct_id", distinct_id, ID_TYPES)
require("groups", groups, dict)
if self.feature_flags == None and self.personal_api_key:
self.load_feature_flags()
response = {}
fallback_to_decide = False
# If loading in previous line failed
if self.feature_flags:
for flag in self.feature_flags:
try:
response[flag["key"]] = self._compute_flag_locally(
flag,
distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
)
except InconclusiveMatchError as e:
# No need to log this, since it's just telling us to fall back to `/decide`
fallback_to_decide = True
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Error while computing variant: {e}")
fallback_to_decide = True
else:
fallback_to_decide = True
if fallback_to_decide and not only_evaluate_locally:
try:
feature_flags = self.get_feature_variants(
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
)
response = {**response, **feature_flags}
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Unable to get feature variants: {e}")
return response
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
View File
@@ -1,10 +1,11 @@
import json
import logging
from threading import Thread
import monotonic
import backoff
import json
from posthog.request import post, APIError, DatetimeSerializer
import backoff
import monotonic
from posthog.request import APIError, DatetimeSerializer, batch_post
try:
from queue import Empty
@@ -20,11 +21,21 @@ BATCH_SIZE_LIMIT = 475000
class Consumer(Thread):
"""Consumes the messages from the client's queue."""
log = logging.getLogger('posthog')
def __init__(self, queue, api_key, flush_at=100, host=None,
on_error=None, flush_interval=0.5, gzip=False, retries=10,
timeout=15):
log = logging.getLogger("posthog")
def __init__(
self,
queue,
api_key,
flush_at=100,
host=None,
on_error=None,
flush_interval=0.5,
gzip=False,
retries=10,
timeout=15,
):
"""Create a consumer thread."""
Thread.__init__(self)
# Make consumer a daemon thread so that it doesn't block program exit
@@ -46,11 +57,11 @@ class Consumer(Thread):
def run(self):
"""Runs the consumer."""
self.log.debug('consumer is running...')
self.log.debug("consumer is running...")
while self.running:
self.upload()
self.log.debug('consumer exited.')
self.log.debug("consumer exited.")
def pause(self):
"""Pause the consumer."""
@@ -67,7 +78,7 @@ class Consumer(Thread):
self.request(batch)
success = True
except Exception as e:
self.log.error('error uploading: %s', e)
self.log.error("error uploading: %s", e)
success = False
if self.on_error:
self.on_error(e, batch)
@@ -90,19 +101,15 @@ class Consumer(Thread):
if elapsed >= self.flush_interval:
break
try:
item = queue.get(
block=True, timeout=self.flush_interval - elapsed)
item_size = len(json.dumps(
item, cls=DatetimeSerializer).encode())
item = queue.get(block=True, timeout=self.flush_interval - elapsed)
item_size = len(json.dumps(item, cls=DatetimeSerializer).encode())
if item_size > MAX_MSG_SIZE:
self.log.error(
'Item exceeds 32kb limit, dropping. (%s)', str(item))
self.log.error("Item exceeds 32kb limit, dropping. (%s)", str(item))
continue
items.append(item)
total_size += item_size
if total_size >= BATCH_SIZE_LIMIT:
self.log.debug(
'hit batch size limit (size: %d)', total_size)
self.log.debug("hit batch size limit (size: %d)", total_size)
break
except Empty:
break
@@ -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()
+177
View File
@@ -0,0 +1,177 @@
import datetime
import hashlib
import re
from dateutil import parser
from posthog.utils import convert_to_datetime_aware, is_valid_regex
__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)
class InconclusiveMatchError(Exception):
pass
# 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, salt=""):
hash_key = f"{key}.{distinct_id}{salt}"
hash_val = int(hashlib.sha1(hash_key.encode("utf-8")).hexdigest()[:15], 16)
return hash_val / __LONG_SCALE__
def get_matching_variant(flag, distinct_id):
hash_value = _hash(flag["key"], distinct_id, salt="variant")
for variant in variant_lookup_table(flag):
if hash_value >= variant["value_min"] and hash_value < variant["value_max"]:
return variant["key"]
return None
def variant_lookup_table(feature_flag):
lookup_table = []
value_min = 0
multivariates = ((feature_flag.get("filters") or {}).get("multivariate") or {}).get("variants") or []
for variant in multivariates:
value_max = value_min + variant["rollout_percentage"] / 100
lookup_table.append({"value_min": value_min, "value_max": value_max, "key": variant["key"]})
value_min = value_max
return lookup_table
def match_feature_flag_properties(flag, distinct_id, properties):
flag_conditions = (flag.get("filters") or {}).get("groups") or []
is_inconclusive = False
# Stable sort conditions with variant overrides to the top. This ensures that if overrides are present, they are
# evaluated first, and the variant override is applied to the first matching condition.
sorted_flag_conditions = sorted(
flag_conditions,
key=lambda condition: 0 if condition.get("variant") else 1,
)
for condition in sorted_flag_conditions:
try:
# if any one condition resolves to True, we can shortcircuit and return
# the matching variant
if is_condition_match(flag, distinct_id, condition, properties):
variant_override = condition.get("variant")
# Some filters can be explicitly set to null, which require accessing variants like so
flag_variants = ((flag.get("filters") or {}).get("multivariate") or {}).get("variants") or []
if variant_override and variant_override in [variant["key"] for variant in flag_variants]:
variant = variant_override
else:
variant = get_matching_variant(flag, distinct_id)
return variant or True
except InconclusiveMatchError:
is_inconclusive = True
if is_inconclusive:
raise InconclusiveMatchError("Can't determine if feature flag is enabled or not with given properties")
# We can only return False when either all conditions are False, or
# no condition was inconclusive.
return False
def is_condition_match(feature_flag, distinct_id, condition, properties):
rollout_percentage = condition.get("rollout_percentage")
if len(condition.get("properties") or []) > 0:
if not all(match_property(prop, properties) for prop in condition.get("properties")):
return False
elif rollout_percentage is None:
return True
if rollout_percentage is not None and _hash(feature_flag["key"], distinct_id) > (rollout_percentage / 100):
return False
return True
def match_property(property, property_values) -> bool:
# only looks for matches where key exists in override_property_values
# doesn't support operator is_not_set
key = property.get("key")
operator = property.get("operator") or "exact"
value = property.get("value")
if key not in property_values:
raise InconclusiveMatchError("can't match properties without a given property value")
if operator == "is_not_set":
raise InconclusiveMatchError("can't match properties with operator is_not_set")
override_value = property_values[key]
if operator == "exact":
if isinstance(value, list):
return override_value in value
return value == override_value
if operator == "is_not":
if isinstance(value, list):
return override_value not in value
return value != override_value
if operator == "is_set":
return key in property_values
if operator == "icontains":
return str(value).lower() in str(override_value).lower()
if operator == "not_icontains":
return str(value).lower() not in str(override_value).lower()
if operator == "regex":
return is_valid_regex(str(value)) and re.compile(str(value)).search(str(override_value)) is not None
if operator == "not_regex":
return is_valid_regex(str(value)) and re.compile(str(value)).search(str(override_value)) is None
if operator == "gt":
return type(override_value) == type(value) and override_value > value
if operator == "gte":
return type(override_value) == type(value) and override_value >= value
if operator == "lt":
return type(override_value) == type(value) and override_value < value
if operator == "lte":
return type(override_value) == type(value) and override_value <= value
if operator in ["is_date_before", "is_date_after"]:
try:
parsed_date = parser.parse(value)
parsed_date = convert_to_datetime_aware(parsed_date)
except Exception:
raise InconclusiveMatchError("The date set on the flag is not a valid format")
if isinstance(override_value, datetime.datetime):
override_date = convert_to_datetime_aware(override_value)
if operator == "is_date_before":
return override_date < parsed_date
else:
return override_date > parsed_date
elif isinstance(override_value, datetime.date):
if operator == "is_date_before":
return override_value < parsed_date.date()
else:
return override_value > parsed_date.date()
elif isinstance(override_value, str):
try:
override_date = parser.parse(override_value)
override_date = convert_to_datetime_aware(override_date)
if operator == "is_date_before":
return override_date < parsed_date
else:
return override_date > parsed_date
except Exception:
raise InconclusiveMatchError("The date provided is not a valid format")
else:
raise InconclusiveMatchError("The date provided must be a string or date object")
return False
+20
View File
@@ -0,0 +1,20 @@
import threading
class Poller(threading.Thread):
def __init__(self, interval, execute, *args, **kwargs):
threading.Thread.__init__(self)
self.daemon = 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)
+62 -33
View File
@@ -1,69 +1,98 @@
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 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'])
except ValueError:
raise APIError(res.status_code, 'unknown', res.text)
log.debug("received response: %s", payload)
raise APIError(res.status_code, payload["detail"])
except (KeyError, ValueError):
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/?v=2", gzip, timeout, **kwargs)
return _process_response(res, success_message="Feature flags decided successfully")
def batch_post(
api_key: str, host: Optional[str] = None, gzip: bool = False, timeout: int = 15, **kwargs
) -> requests.Response:
"""Post the `kwargs` to the batch API endpoint for events"""
res = post(api_key, host, "/batch/", gzip, timeout, **kwargs)
return _process_response(res, success_message="data uploaded successfully", return_json=False)
def get(api_key: str, url: str, host: Optional[str] = None, timeout: Optional[int] = None) -> requests.Response:
url = remove_trailing_slash(host or DEFAULT_HOST) + url
res = requests.get(url, headers={"Authorization": "Bearer %s" % api_key, "User-Agent": USER_AGENT}, timeout=timeout)
return _process_response(res, success_message=f"GET {url} completed successfully")
class APIError(Exception):
def __init__(self, status, code, message):
def __init__(self, status: Union[int, str], message: str):
self.message = message
self.status = status
self.code = code
def __str__(self):
msg = "[PostHog] {0}: {1} ({2})"
return msg.format(self.code, self.message, self.status)
msg = "[PostHog] {0} ({1})"
return msg.format(self.message, self.status)
class DatetimeSerializer(json.JSONEncoder):
def default(self, obj):
def default(self, obj: Any):
if isinstance(obj, (date, datetime)):
return obj.isoformat()
+1
View File
@@ -0,0 +1 @@
POSTHOG_ID_TAG = "posthog_distinct_id"
+28
View File
@@ -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
+53
View File
@@ -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
+3 -3
View File
@@ -1,12 +1,12 @@
import unittest
import pkgutil
import logging
import pkgutil
import sys
import unittest
def all_names():
for _, modname, _ in pkgutil.iter_modules(__path__):
yield 'posthog.test.' + modname
yield "posthog.test." + modname
def all():
-223
View File
@@ -1,223 +0,0 @@
from datetime import date, datetime
import unittest
import six
import mock
import time
from posthog.version import VERSION
from posthog.client import Client
from posthog.test.utils import TEST_API_KEY
class TestClient(unittest.TestCase):
def fail(self, e, batch):
"""Mark the failure handler"""
print('FAIL', e, batch)
self.failed = True
def setUp(self):
self.failed = False
self.client = Client(TEST_API_KEY, on_error=self.fail)
def test_requires_api_key(self):
self.assertRaises(AssertionError, Client)
def test_empty_flush(self):
self.client.flush()
def test_basic_capture(self):
client = self.client
success, msg = client.capture('distinct_id', 'python test event')
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['event'], 'python test event')
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertTrue(isinstance(msg['messageId'], str))
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
self.assertEqual(msg['properties']['$lib_version'], VERSION)
def test_stringifies_distinct_id(self):
# A large number that loses precision in node:
# node -e "console.log(157963456373623802 + 1)" > 157963456373623800
client = self.client
success, msg = client.capture(
distinct_id=157963456373623802, event='python test event')
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['distinct_id'], '157963456373623802')
def test_advanced_capture(self):
client = self.client
success, msg = client.capture(
'distinct_id', 'python test event', {'property': 'value'},
{'ip': '192.168.0.1'}, datetime(2014, 9, 3),
'messageId')
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['properties']['property'], 'value')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['event'], 'python test event')
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
self.assertEqual(msg['properties']['$lib_version'], VERSION)
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
def test_basic_identify(self):
client = self.client
success, msg = client.identify('distinct_id', {'trait': 'value'})
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['$set']['trait'], 'value')
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertTrue(isinstance(msg['messageId'], str))
self.assertEqual(msg['distinct_id'], 'distinct_id')
def test_advanced_identify(self):
client = self.client
success, msg = client.identify(
'distinct_id', {'trait': 'value'}, {'ip': '192.168.0.1'},
datetime(2014, 9, 3), 'messageId')
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['$set']['trait'], 'value')
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
self.assertEqual(msg['properties']['$lib_version'], VERSION)
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
def test_basic_alias(self):
client = self.client
success, msg = client.alias('previousId', 'distinct_id')
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg['properties']['distinct_id'], 'previousId')
self.assertEqual(msg['properties']['alias'], 'distinct_id')
def test_basic_page(self):
client = self.client
success, msg = client.page('distinct_id', url='https://posthog.com/contact')
self.assertFalse(self.failed)
client.flush()
self.assertTrue(success)
self.assertEqual(msg['distinct_id'], 'distinct_id')
self.assertEqual(msg['properties']['$current_url'], 'https://posthog.com/contact')
def test_advanced_page(self):
client = self.client
success, msg = client.page(
'distinct_id', 'https://posthog.com/contact', {'property': 'value'},
{'ip': '192.168.0.1'}, datetime(2014, 9, 3), 'messageId')
self.assertTrue(success)
self.assertEqual(msg['timestamp'], '2014-09-03T00:00:00+00:00')
self.assertEqual(msg['context']['ip'], '192.168.0.1')
self.assertEqual(msg['properties']['$current_url'], 'https://posthog.com/contact')
self.assertEqual(msg['properties']['property'], 'value')
self.assertEqual(msg['properties']['$lib'], 'posthog-python')
self.assertEqual(msg['properties']['$lib_version'], VERSION)
self.assertTrue(isinstance(msg['timestamp'], str))
self.assertEqual(msg['messageId'], 'messageId')
self.assertEqual(msg['distinct_id'], 'distinct_id')
def test_flush(self):
client = self.client
# set up the consumer with more requests than a single batch will allow
for i in range(1000):
success, msg = client.identify('distinct_id', {'trait': 'value'})
# We can't reliably assert that the queue is non-empty here; that's
# a race condition. We do our best to load it up though.
client.flush()
# Make sure that the client queue is empty after flushing
self.assertTrue(client.queue.empty())
def test_shutdown(self):
client = self.client
# set up the consumer with more requests than a single batch will allow
for i in range(1000):
success, msg = client.identify('distinct_id', {'trait': 'value'})
client.shutdown()
# we expect two things after shutdown:
# 1. client queue is empty
# 2. consumer thread has stopped
self.assertTrue(client.queue.empty())
for consumer in client.consumers:
self.assertFalse(consumer.is_alive())
def test_synchronous(self):
client = Client(TEST_API_KEY, sync_mode=True)
success, message = client.identify('distinct_id')
self.assertFalse(client.consumers)
self.assertTrue(client.queue.empty())
self.assertTrue(success)
def test_overflow(self):
client = Client(TEST_API_KEY, max_queue_size=1)
# Ensure consumer thread is no longer uploading
client.join()
for i in range(10):
client.identify('distinct_id')
success, msg = client.identify('distinct_id')
# Make sure we are informed that the queue is at capacity
self.assertFalse(success)
def test_unicode(self):
Client(six.u('unicode_key'))
def test_numeric_distinct_id(self):
self.client.capture(1234, 'python event')
self.client.flush()
self.assertFalse(self.failed)
def test_debug(self):
Client('bad_key', debug=True)
def test_gzip(self):
client = Client(TEST_API_KEY, on_error=self.fail, gzip=True)
for _ in range(10):
client.identify('distinct_id', {'trait': 'value'})
client.flush()
self.assertFalse(self.failed)
def test_user_defined_flush_at(self):
client = Client(TEST_API_KEY, on_error=self.fail,
flush_at=10, flush_interval=3)
def mock_post_fn(*args, **kwargs):
self.assertEquals(len(kwargs['batch']), 10)
# the post function should be called 2 times, with a batch size of 10
# each time.
with mock.patch('posthog.consumer.post', side_effect=mock_post_fn) \
as mock_post:
for _ in range(20):
client.identify('distinct_id', {'trait': 'value'})
time.sleep(1)
self.assertEquals(mock_post.call_count, 2)
def test_user_defined_timeout(self):
client = Client(TEST_API_KEY, timeout=10)
for consumer in client.consumers:
self.assertEquals(consumer.timeout, 10)
def test_default_timeout_15(self):
client = Client(TEST_API_KEY)
for consumer in client.consumers:
self.assertEquals(consumer.timeout, 15)
-54
View File
@@ -1,54 +0,0 @@
from datetime import datetime, date
import unittest
import json
import requests
from posthog.request import post, DatetimeSerializer
from posthog.test.utils import TEST_API_KEY
class TestRequests(unittest.TestCase):
def test_valid_request(self):
res = post(TEST_API_KEY, batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
}])
self.assertEqual(res.status_code, 200)
def test_invalid_request_error(self):
self.assertRaises(Exception, post, 'testsecret',
'https://t.posthog.com', False, '[{]')
def test_invalid_host(self):
self.assertRaises(Exception, post, 'testsecret',
't.posthog.com/', batch=[])
def test_datetime_serialization(self):
data = {'created': datetime(2012, 3, 4, 5, 6, 7, 891011)}
result = json.dumps(data, cls=DatetimeSerializer)
self.assertEqual(result, '{"created": "2012-03-04T05:06:07.891011"}')
def test_date_serialization(self):
today = date.today()
data = {'created': today}
result = json.dumps(data, cls=DatetimeSerializer)
expected = '{"created": "%s"}' % today.isoformat()
self.assertEqual(result, expected)
def test_should_not_timeout(self):
res = post(TEST_API_KEY, batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
}], timeout=15)
self.assertEqual(res.status_code, 200)
def test_should_timeout(self):
with self.assertRaises(requests.ReadTimeout):
post('key', batch=[{
'distinct_id': 'distinct_id',
'event': 'python event',
'type': 'track'
}], timeout=0.0001)
+467
View File
@@ -0,0 +1,467 @@
import time
import unittest
from datetime import date, datetime
from unittest.mock import MagicMock
from uuid import uuid4
import mock
import six
from posthog.client import Client
from posthog.test.test_utils import FAKE_TEST_API_KEY
from posthog.version import VERSION
class TestClient(unittest.TestCase):
@classmethod
def setUpClass(cls):
# This ensures no real HTTP POST requests are made
cls.client_post_patcher = mock.patch("posthog.client.batch_post")
cls.consumer_post_patcher = mock.patch("posthog.consumer.batch_post")
cls.client_post_patcher.start()
cls.consumer_post_patcher.start()
@classmethod
def tearDownClass(cls):
cls.client_post_patcher.stop()
cls.consumer_post_patcher.stop()
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(FAKE_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=FAKE_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)
@mock.patch("posthog.client.decide")
def test_basic_capture_with_feature_flags(self, patch_decide):
patch_decide.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY)
success, msg = client.capture("distinct_id", "python test event", send_feature_flags=True)
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)
self.assertEqual(msg["properties"]["$feature/beta-feature"], "random-variant")
self.assertEqual(msg["properties"]["$active_feature_flags"], ["beta-feature"])
self.assertEqual(patch_decide.call_count, 1)
@mock.patch("posthog.client.decide")
def test_basic_capture_with_feature_flags_switched_off_doesnt_send_them(self, patch_decide):
patch_decide.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY)
success, msg = client.capture("distinct_id", "python test event", send_feature_flags=False)
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)
self.assertTrue("$feature/beta-feature" not in msg["properties"])
self.assertTrue("$active_feature_flags" not in msg["properties"])
self.assertEqual(patch_decide.call_count, 0)
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(FAKE_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(FAKE_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(FAKE_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(FAKE_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(FAKE_TEST_API_KEY, timeout=10)
for consumer in client.consumers:
self.assertEqual(consumer.timeout, 10)
def test_default_timeout_15(self):
client = Client(FAKE_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_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(FAKE_TEST_API_KEY, personal_api_key="test")
client.feature_flags = [{"key": "example", "is_simple_flag": False}]
self.assertFalse(client.feature_enabled("example", "distinct_id"))
def test_alias_doesnt_regress_positional_args(self):
client = self.client
success, msg = client.alias("distinct_id", "alias_id")
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg["properties"]["distinct_id"], "distinct_id")
self.assertEqual(msg["properties"]["alias"], "alias_id")
def test_alias_doesnt_regress_named_args(self):
client = self.client
success, msg = client.alias(distinct_id="distinct_id", previous_id="alias_id")
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg["properties"]["distinct_id"], "distinct_id")
self.assertEqual(msg["properties"]["alias"], "alias_id")
@@ -1,23 +1,23 @@
import unittest
import mock
import time
import json
import time
import unittest
import mock
try:
from queue import Queue
except ImportError:
from Queue import Queue
from posthog.consumer import Consumer, MAX_MSG_SIZE
from posthog.consumer import MAX_MSG_SIZE, Consumer
from posthog.request import APIError
from posthog.test.utils import TEST_API_KEY
from posthog.test.test_utils import TEST_API_KEY
class TestConsumer(unittest.TestCase):
def test_next(self):
q = Queue()
consumer = Consumer(q, '')
consumer = Consumer(q, "")
q.put(1)
next = consumer.next()
self.assertEqual(next, [1])
@@ -25,7 +25,7 @@ class TestConsumer(unittest.TestCase):
def test_next_limit(self):
q = Queue()
flush_at = 50
consumer = Consumer(q, '', flush_at)
consumer = Consumer(q, "", flush_at)
for i in range(10000):
q.put(i)
next = consumer.next()
@@ -33,8 +33,8 @@ class TestConsumer(unittest.TestCase):
def test_dropping_oversize_msg(self):
q = Queue()
consumer = Consumer(q, '')
oversize_msg = {'m': 'x' * MAX_MSG_SIZE}
consumer = Consumer(q, "")
oversize_msg = {"m": "x" * MAX_MSG_SIZE}
q.put(oversize_msg)
next = consumer.next()
self.assertEqual(next, [])
@@ -43,11 +43,7 @@ class TestConsumer(unittest.TestCase):
def test_upload(self):
q = Queue()
consumer = Consumer(q, TEST_API_KEY)
track = {
'type': 'track',
'event': 'python event',
'distinct_id': 'distinct_id'
}
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
q.put(track)
success = consumer.upload()
self.assertTrue(success)
@@ -58,16 +54,11 @@ class TestConsumer(unittest.TestCase):
# The consumer should upload _n_ times.
q = Queue()
flush_interval = 0.3
consumer = Consumer(q, TEST_API_KEY, flush_at=10,
flush_interval=flush_interval)
with mock.patch('posthog.consumer.post') as mock_post:
consumer = Consumer(q, TEST_API_KEY, flush_at=10, flush_interval=flush_interval)
with mock.patch("posthog.consumer.batch_post") as mock_post:
consumer.start()
for i in range(0, 3):
track = {
'type': 'track',
'event': 'python event %d' % i,
'distinct_id': 'distinct_id'
}
track = {"type": "track", "event": "python event %d" % i, "distinct_id": "distinct_id"}
q.put(track)
time.sleep(flush_interval * 1.1)
self.assertEqual(mock_post.call_count, 3)
@@ -78,45 +69,30 @@ class TestConsumer(unittest.TestCase):
q = Queue()
flush_interval = 0.5
flush_at = 10
consumer = Consumer(q, TEST_API_KEY, flush_at=flush_at,
flush_interval=flush_interval)
with mock.patch('posthog.consumer.post') as mock_post:
consumer = Consumer(q, TEST_API_KEY, flush_at=flush_at, flush_interval=flush_interval)
with mock.patch("posthog.consumer.batch_post") as mock_post:
consumer.start()
for i in range(0, flush_at * 2):
track = {
'type': 'track',
'event': 'python event %d' % i,
'distinct_id': 'distinct_id'
}
track = {"type": "track", "event": "python event %d" % i, "distinct_id": "distinct_id"}
q.put(track)
time.sleep(flush_interval * 1.1)
self.assertEqual(mock_post.call_count, 2)
def test_request(self):
consumer = Consumer(None, TEST_API_KEY)
track = {
'type': 'track',
'event': 'python event',
'distinct_id': 'distinct_id'
}
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
consumer.request([track])
def _test_request_retry(self, consumer,
expected_exception, exception_count):
def _test_request_retry(self, consumer, expected_exception, exception_count):
def mock_post(*args, **kwargs):
mock_post.call_count += 1
if mock_post.call_count <= exception_count:
raise expected_exception
mock_post.call_count = 0
with mock.patch('posthog.consumer.post',
mock.Mock(side_effect=mock_post)):
track = {
'type': 'track',
'event': 'python event',
'distinct_id': 'distinct_id'
}
with mock.patch("posthog.consumer.batch_post", mock.Mock(side_effect=mock_post)):
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
# request() should succeed if the number of exceptions raised is
# less than the retries paramater.
if exception_count <= consumer.retries:
@@ -131,38 +107,35 @@ class TestConsumer(unittest.TestCase):
self.assertEqual(exc, expected_exception)
else:
self.fail(
"request() should raise an exception if still failing "
"after %d retries" % consumer.retries)
"request() should raise an exception if still failing after %d retries" % consumer.retries
)
def test_request_retry(self):
# we should retry on general errors
consumer = Consumer(None, TEST_API_KEY)
self._test_request_retry(consumer, Exception('generic exception'), 2)
self._test_request_retry(consumer, Exception("generic exception"), 2)
# we should retry on server errors
consumer = Consumer(None, TEST_API_KEY)
self._test_request_retry(consumer, APIError(
500, 'code', 'Internal Server Error'), 2)
self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 2)
# we should retry on HTTP 429 errors
consumer = Consumer(None, TEST_API_KEY)
self._test_request_retry(consumer, APIError(
429, 'code', 'Too Many Requests'), 2)
self._test_request_retry(consumer, APIError(429, "Too Many Requests"), 2)
# we should NOT retry on other client errors
consumer = Consumer(None, TEST_API_KEY)
api_error = APIError(400, 'code', 'Client Errors')
api_error = APIError(400, "Client Errors")
try:
self._test_request_retry(consumer, api_error, 1)
except APIError:
pass
else:
self.fail('request() should not retry on client errors')
self.fail("request() should not retry on client errors")
# test for number of exceptions raise > retries value
consumer = Consumer(None, TEST_API_KEY, retries=3)
self._test_request_retry(consumer, APIError(
500, 'code', 'Internal Server Error'), 3)
self._test_request_retry(consumer, APIError(500, "Internal Server Error"), 3)
def test_pause(self):
consumer = Consumer(None, TEST_API_KEY)
@@ -171,13 +144,8 @@ class TestConsumer(unittest.TestCase):
def test_max_batch_size(self):
q = Queue()
consumer = Consumer(
q, TEST_API_KEY, flush_at=100000, flush_interval=3)
track = {
'type': 'track',
'event': 'python event',
'distinct_id': 'distinct_id'
}
consumer = Consumer(q, TEST_API_KEY, flush_at=100000, flush_interval=3)
track = {"type": "track", "event": "python event", "distinct_id": "distinct_id"}
msg_size = len(json.dumps(track).encode())
# number of messages in a maximum-size batch
n_msgs = int(475000 / msg_size)
@@ -185,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('posthog.request._session.post',
side_effect=mock_post_fn) as mock_post:
with mock.patch("posthog.request._session.post", side_effect=mock_post_fn) as mock_post:
consumer.start()
for _ in range(0, n_msgs + 2):
q.put(track)
q.join()
self.assertEquals(mock_post.call_count, 2)
self.assertEqual(mock_post.call_count, 2)
File diff suppressed because it is too large Load Diff
@@ -4,13 +4,12 @@ import posthog
class TestModule(unittest.TestCase):
def failed(self):
self.failed = True
def setUp(self):
self.failed = False
posthog.api_key = 'testsecret'
posthog.api_key = "testsecret"
posthog.on_error = self.failed
def test_no_api_key(self):
@@ -22,19 +21,19 @@ class TestModule(unittest.TestCase):
self.assertRaises(Exception, posthog.capture)
def test_track(self):
posthog.capture('distinct_id', 'python module event')
posthog.capture("distinct_id", "python module event")
posthog.flush()
def test_identify(self):
posthog.identify('distinct_id', {'email': 'user@email.com'})
posthog.identify("distinct_id", {"email": "user@email.com"})
posthog.flush()
def test_alias(self):
posthog.alias('previousId', 'distinct_id')
posthog.alias("previousId", "distinct_id")
posthog.flush()
def test_page(self):
posthog.page('distinct_id', 'https://posthog.com/contact')
posthog.page("distinct_id", "https://posthog.com/contact")
posthog.flush()
def test_flush(self):
+44
View File
@@ -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
)
+101
View File
@@ -0,0 +1,101 @@
import unittest
from datetime import date, datetime, timedelta
from decimal import Decimal
from uuid import UUID
import six
from dateutil.tz import tzutc
from posthog import utils
TEST_API_KEY = "kOOlRy2QlMY9jHZQv0bKz0FZyazBUoY8Arj0lFVNjs4"
FAKE_TEST_API_KEY = "random_key"
class TestUtils(unittest.TestCase):
def test_timezone_utils(self):
now = datetime.now()
utcnow = datetime.now(tz=tzutc())
self.assertTrue(utils.is_naive(now))
self.assertFalse(utils.is_naive(utcnow))
fixed = utils.guess_timezone(now)
self.assertFalse(utils.is_naive(fixed))
shouldnt_be_edited = utils.guess_timezone(utcnow)
self.assertEqual(utcnow, shouldnt_be_edited)
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,
}
complicated = {
"exception": Exception("This should show up"),
"timedelta": timedelta(microseconds=20),
"list": [1, 2, 3],
}
combined = dict(simple.items())
combined.update(complicated.items())
pre_clean_keys = combined.keys()
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(),
}
self.assertEqual(dict_with_dates, utils.clean(dict_with_dates))
def test_bytes(self):
if six.PY3:
item = bytes(10)
else:
item = bytearray(10)
utils.clean(item)
def test_clean_fn(self):
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)
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"))
class TestSizeLimitedDict(unittest.TestCase):
def test_size_limited_dict(self):
size = 10
values = utils.SizeLimitedDict(size, lambda _: -1)
for i in range(100):
values[i] = i
self.assertEqual(values[i], i)
self.assertEqual(len(values), i % size + 1)
if i % size == 0:
# old numbers should've been removed
self.assertIsNone(values.get(i - 1))
self.assertIsNone(values.get(i - 3))
self.assertIsNone(values.get(i - 5))
self.assertIsNone(values.get(i - 9))
-79
View File
@@ -1,79 +0,0 @@
from datetime import date, datetime, timedelta
from decimal import Decimal
import unittest
from dateutil.tz import tzutc
import six
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())
self.assertTrue(utils.is_naive(now))
self.assertFalse(utils.is_naive(utcnow))
fixed = utils.guess_timezone(now)
self.assertFalse(utils.is_naive(fixed))
shouldnt_be_edited = utils.guess_timezone(utcnow)
self.assertEqual(utcnow, shouldnt_be_edited)
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
}
complicated = {
'exception': Exception('This should show up'),
'timedelta': timedelta(microseconds=20),
'list': [1, 2, 3]
}
combined = dict(simple.items())
combined.update(complicated.items())
pre_clean_keys = combined.keys()
utils.clean(combined)
self.assertEqual(combined.keys(), pre_clean_keys)
def test_clean_with_dates(self):
dict_with_dates = {
'birthdate': date(1980, 1, 1),
'registration': datetime.utcnow(),
}
self.assertEqual(dict_with_dates, utils.clean(dict_with_dates))
def test_bytes(self):
if six.PY3:
item = bytes(10)
else:
item = bytearray(10)
utils.clean(item)
def test_clean_fn(self):
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)
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'))
+43 -13
View File
@@ -1,12 +1,15 @@
from dateutil.tz import tzlocal, tzutc
from datetime import date, datetime
from decimal import Decimal
import logging
import numbers
import re
from collections import defaultdict
from datetime import date, datetime, timezone
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 +20,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 +41,7 @@ def guess_timezone(dt):
def remove_trailing_slash(host):
if host.endswith('/'):
if host.endswith("/"):
return host[:-1]
return host
@@ -47,8 +49,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 +72,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 +86,32 @@ 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
def is_valid_regex(value) -> bool:
try:
re.compile(value)
return True
except re.error:
return False
class SizeLimitedDict(defaultdict):
def __init__(self, max_size, *args, **kwargs):
super().__init__(*args, **kwargs)
self.max_size = max_size
def __setitem__(self, key, value):
if len(self) >= self.max_size:
self.clear()
super().__setitem__(key, value)
def convert_to_datetime_aware(date_obj):
if date_obj.tzinfo is None:
date_obj = date_obj.replace(tzinfo=timezone.utc)
return date_obj
+4 -1
View File
@@ -1 +1,4 @@
VERSION = '1.0.11'
VERSION = "2.2.0"
if __name__ == "__main__":
print(VERSION, end="")
+10
View File
@@ -0,0 +1,10 @@
[tool.black]
line-length = 120
[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 8
ensure_newline_before_comments = true
line_length = 120
virtual_env = "env"
View File
+22
View File
@@ -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()
+24 -25
View File
@@ -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
View File
@@ -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",
+55 -32
View File
@@ -1,42 +1,39 @@
import posthog
import argparse
import json
import logging
__name__ = 'simulator.py'
__version__ = '0.0.1'
__description__ = 'scripting simulator'
import posthog
__name__ = "simulator.py"
__version__ = "0.0.1"
__description__ = "scripting simulator"
def json_hash(str):
if str:
return json.loads(str)
# posthog -method=<method> -posthog-write-key=<posthogWriteKey> [options]
parser = argparse.ArgumentParser(description='send a posthog message')
parser = argparse.ArgumentParser(description="send a posthog message")
parser.add_argument('--writeKey', help='the posthog writeKey')
parser.add_argument('--type', help='The posthog message type')
parser.add_argument("--writeKey", help="the posthog writeKey")
parser.add_argument("--type", help="The posthog message type")
parser.add_argument('--distinct_id', help='the user id to send the event as')
parser.add_argument(
'--anonymousId', help='the anonymous user id to send the event as')
parser.add_argument(
'--context', help='additional context for the event (JSON-encoded)')
parser.add_argument("--distinct_id", help="the user id to send the event as")
parser.add_argument("--anonymousId", help="the anonymous user id to send the event as")
parser.add_argument("--context", help="additional context for the event (JSON-encoded)")
parser.add_argument('--event', help='the event name to send with the event')
parser.add_argument(
'--properties', help='the event properties to send (JSON-encoded)')
parser.add_argument("--event", help="the event name to send with the event")
parser.add_argument("--properties", help="the event properties to send (JSON-encoded)")
parser.add_argument(
'--name', help='name of the screen or page to send with the message')
parser.add_argument("--name", help="name of the screen or page to send with the message")
parser.add_argument(
'--traits', help='the identify/group traits to send (JSON-encoded)')
parser.add_argument("--traits", help="the identify/group traits to send (JSON-encoded)")
parser.add_argument('--groupId', help='the group id')
parser.add_argument("--groupId", help="the group id")
options = parser.parse_args()
@@ -46,18 +43,48 @@ def failed(status, msg):
def capture():
posthog.capture(options.distinct_id, options.event, anonymous_id=options.anonymousId,
properties=json_hash(options.properties), context=json_hash(options.context))
posthog.capture(
options.distinct_id,
options.event,
anonymous_id=options.anonymousId,
properties=json_hash(options.properties),
context=json_hash(options.context),
)
def page():
posthog.page(options.distinct_id, name=options.name, anonymous_id=options.anonymousId,
properties=json_hash(options.properties), context=json_hash(options.context))
posthog.page(
options.distinct_id,
name=options.name,
anonymous_id=options.anonymousId,
properties=json_hash(options.properties),
context=json_hash(options.context),
)
def identify():
posthog.identify(options.distinct_id, anonymous_id=options.anonymousId,
traits=json_hash(options.traits), context=json_hash(options.context))
posthog.identify(
options.distinct_id,
anonymous_id=options.anonymousId,
traits=json_hash(options.traits),
context=json_hash(options.context),
)
def set_once():
posthog.set_once(
options.distinct_id,
properties=json_hash(options.traits),
context=json_hash(options.context),
)
def set():
posthog.set(
options.distinct_id,
properties=json_hash(options.traits),
context=json_hash(options.context),
)
def unknown():
@@ -68,16 +95,12 @@ posthog.api_key = options.writeKey
posthog.on_error = failed
posthog.debug = True
log = logging.getLogger('posthog')
log = logging.getLogger("posthog")
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
log.addHandler(ch)
switcher = {
"capture": capture,
"page": page,
"identify": identify
}
switcher = {"capture": capture, "page": page, "identify": identify, "set_once": set_once, "set": set}
func = switcher.get(options.type)
if func: