Compare commits

..
53 Commits
Author SHA1 Message Date
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
31 changed files with 971 additions and 108 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
- name: Install requirements.txt dependencies with pip
run: |
python -m pip install -e .
python -m pip install -e .[test]
- name: Run posthog tests
run: |
+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 }}
+3 -1
View File
@@ -5,7 +5,7 @@ dist/
MANIFEST
build/
.eggs/
.coverage/
.coverage
.vscode/
env/
venv/
@@ -13,3 +13,5 @@ flake8.out
pylint.out
posthog-analytics
.idea
.python-version
.coverage
+25
View File
@@ -0,0 +1,25 @@
## 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
+39 -2
View File
@@ -1,8 +1,45 @@
# 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`, 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?
+27 -3
View File
@@ -6,7 +6,7 @@ import time
import posthog
# You can find this key on the /setup page in PostHog
posthog.api_key = ""
posthog.project_api_key = ""
posthog.personal_api_key = ""
# Where you host PostHog, with no trailing /.
@@ -17,14 +17,38 @@ posthog.host = "http://127.0.0.1:8000"
posthog.capture("distinct_id", "event", {"property1": "value", "property2": "value"})
print(posthog.feature_enabled("beta-feature", "distinct_id"))
print(posthog.feature_enabled("beta-feature", "distinct_id", groups={"company": "id:5"}))
print("sleeping")
time.sleep(45)
time.sleep(5)
print(posthog.feature_enabled("beta-feature", "distinct_id"))
# # Alias a previous distinct id with a new one
posthog.alias("distinct_id", "new_distinct_id")
posthog.capture("new_distinct_id", "event2", {"property1": "value", "property2": "value"})
posthog.capture(
"new_distinct_id", "event-with-groups", {"property1": "value", "property2": "value"}, groups={"company": "id:5"}
)
# # Add properties to the person
posthog.identify("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})
time.sleep(3)
posthog.set_once(
"new_distinct_id", {"self_serve_signup": False}
) # this will not change the property (because it was already set)
posthog.set("new_distinct_id", {"current_browser": "Chrome"})
posthog.set("new_distinct_id", {"current_browser": "Firefox"})
# posthog.shutdown()
+121 -14
View File
@@ -14,17 +14,19 @@ send = True # type: bool
sync_mode = False # type: bool
disabled = False # type: bool
personal_api_key = None # type: str
project_api_key = None # type: str
default_client = None
def capture(
distinct_id, # type: str,
event, # type: str,
distinct_id, # type: str
event, # type: str
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
uuid=None, # type: Optional[str]
groups=None, # type: Optional[Dict]
):
# type: (...) -> None
"""
@@ -32,15 +34,19 @@ def capture(
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(
@@ -50,16 +56,17 @@ def capture(
properties=properties,
context=context,
timestamp=timestamp,
message_id=message_id,
uuid=uuid,
groups=groups,
)
def identify(
distinct_id, # type: str,
distinct_id, # type: str
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
uuid=None, # type: Optional[str]
):
# type: (...) -> None
"""
@@ -83,13 +90,109 @@ def identify(
properties=properties,
context=context,
timestamp=timestamp,
message_id=message_id,
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(
@@ -97,7 +200,7 @@ def alias(
distinct_id, # type: str,
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
message_id=None, # type: Optional[str]
uuid=None, # type: Optional[str]
):
# type: (...) -> None
"""
@@ -122,7 +225,7 @@ def alias(
distinct_id=distinct_id,
context=context,
timestamp=timestamp,
message_id=message_id,
uuid=uuid,
)
@@ -130,6 +233,7 @@ def feature_enabled(
key, # type: str,
distinct_id, # type: str,
default=False, # type: bool
groups={}, # type: dict
):
# type: (...) -> bool
"""
@@ -139,11 +243,13 @@ def feature_enabled(
```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, default=default)
return _proxy("feature_enabled", key=key, distinct_id=distinct_id, default=default, groups=groups)
def page(*args, **kwargs):
@@ -186,6 +292,7 @@ def _proxy(method, *args, **kwargs):
send=send,
sync_mode=sync_mode,
personal_api_key=personal_api_key,
project_api_key=project_api_key,
)
fn = getattr(default_client, method)
+114 -46
View File
@@ -3,9 +3,8 @@ import hashlib
import logging
import numbers
from datetime import datetime, timedelta
from uuid import uuid4
from uuid import UUID, uuid4
import requests
from dateutil.tz import tzutc
from six import string_types
@@ -21,7 +20,7 @@ except ImportError:
import Queue as queue
ID_TYPES = (numbers.Number, string_types)
ID_TYPES = (numbers.Number, string_types, UUID)
__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)
@@ -53,7 +52,7 @@ class Client(object):
self.queue = queue.Queue(max_queue_size)
# api_key: This should be the Team API Key (token), public
self.api_key = api_key or project_api_key
self.api_key = project_api_key or api_key
require("api_key", self.api_key, string_types)
@@ -66,6 +65,7 @@ class Client(object):
self.timeout = timeout
self.feature_flags = None
self.poll_interval = poll_interval
self.poller = None
# personal_api_key: This should be a generated Personal API Key, private
self.personal_api_key = personal_api_key
@@ -88,7 +88,7 @@ class Client(object):
self.consumers = []
consumer = Consumer(
self.queue,
api_key,
self.api_key,
host=host,
on_error=on_error,
flush_at=flush_at,
@@ -103,7 +103,7 @@ 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)
@@ -115,12 +115,14 @@ class Client(object):
"distinct_id": distinct_id,
"$set": properties,
"event": "$identify",
"messageId": message_id,
"uuid": uuid,
}
return self._enqueue(msg)
def capture(self, distinct_id=None, event=None, properties=None, context=None, timestamp=None, message_id=None):
def capture(
self, distinct_id=None, event=None, properties=None, context=None, timestamp=None, uuid=None, groups=None
):
properties = properties or {}
context = context or {}
require("distinct_id", distinct_id, ID_TYPES)
@@ -133,12 +135,72 @@ class Client(object):
"context": context,
"distinct_id": distinct_id,
"event": event,
"messageId": message_id,
"uuid": uuid,
}
if groups:
require("groups", groups, dict)
msg["properties"]["$groups"] = groups
return self._enqueue(msg)
def set(self, distinct_id=None, properties=None, context=None, timestamp=None, uuid=None):
properties = properties or {}
context = context or {}
require("distinct_id", distinct_id, ID_TYPES)
require("properties", properties, dict)
msg = {
"timestamp": timestamp,
"context": context,
"distinct_id": distinct_id,
"$set": properties,
"event": "$set",
"uuid": uuid,
}
return self._enqueue(msg)
def alias(self, previous_id=None, distinct_id=None, context=None, timestamp=None, message_id=None):
def set_once(self, distinct_id=None, properties=None, context=None, timestamp=None, uuid=None):
properties = properties or {}
context = context or {}
require("distinct_id", distinct_id, ID_TYPES)
require("properties", properties, dict)
msg = {
"timestamp": timestamp,
"context": context,
"distinct_id": distinct_id,
"$set_once": properties,
"event": "$set_once",
"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,
},
"distinct_id": "${}_{}".format(group_type, group_key),
"timestamp": timestamp,
"context": context,
"uuid": uuid,
}
return self._enqueue(msg)
def alias(self, previous_id=None, distinct_id=None, context=None, timestamp=None, uuid=None):
context = context or {}
require("previous_id", previous_id, ID_TYPES)
@@ -152,11 +214,12 @@ class Client(object):
"timestamp": timestamp,
"context": context,
"event": "$create_alias",
"distinct_id": previous_id,
}
return self._enqueue(msg)
def page(self, distinct_id=None, url=None, properties=None, context=None, timestamp=None, message_id=None):
def page(self, distinct_id=None, url=None, properties=None, context=None, timestamp=None, uuid=None):
properties = properties or {}
context = context or {}
@@ -172,7 +235,7 @@ class Client(object):
"timestamp": timestamp,
"context": context,
"distinct_id": distinct_id,
"messageId": message_id,
"uuid": uuid,
}
return self._enqueue(msg)
@@ -182,9 +245,6 @@ class Client(object):
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)
@@ -192,7 +252,13 @@ class Client(object):
# add common
timestamp = guess_timezone(timestamp)
msg["timestamp"] = timestamp.isoformat()
msg["messageId"] = stringify_id(message_id)
# 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"
@@ -241,6 +307,9 @@ 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()
@@ -248,7 +317,8 @@ class Client(object):
def _load_feature_flags(self):
try:
self.feature_flags = get(self.personal_api_key, "/api/feature_flag/", self.host)["results"]
flags = get(self.personal_api_key, f"/api/feature_flag/?token={self.api_key}", self.host)["results"]
self.feature_flags = [flag for flag in flags if flag["active"]]
except APIError as e:
if e.status == 401:
raise APIError(
@@ -275,44 +345,42 @@ class Client(object):
return
self._load_feature_flags()
poller = Poller(interval=timedelta(seconds=self.poll_interval), execute=self._load_feature_flags)
poller.start()
if not (self.poller and self.poller.is_alive()):
self.poller = Poller(interval=timedelta(seconds=self.poll_interval), execute=self._load_feature_flags)
self.poller.start()
def feature_enabled(self, key, distinct_id, default=False):
def feature_enabled(self, key, distinct_id, default=False, *, groups={}):
require("key", key, string_types)
require("distinct_id", distinct_id, ID_TYPES)
require("groups", groups, dict)
if not self.personal_api_key:
self.log.warning("[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags.")
if not self.feature_flags:
if self.feature_flags == None and self.personal_api_key:
self.load_feature_flags()
response = None
# If loading in previous line failed
if not self.feature_flags:
response = default
else:
if self.feature_flags:
for flag in self.feature_flags:
if flag["key"] == key:
feature_flag = flag
if feature_flag.get("is_simple_flag"):
response = _hash(key, distinct_id) <= (feature_flag.get("rollout_percentage", 100) / 100)
if response == None:
try:
feature_flag = [flag for flag in self.feature_flags if flag["key"] == key][0]
except IndexError:
return default
if feature_flag.get("is_simple_flag") and feature_flag.get("rollout_percentage"):
response = _hash(key, distinct_id) <= (feature_flag["rollout_percentage"] / 100)
request_data = {
"distinct_id": distinct_id,
"personal_api_key": self.personal_api_key,
"groups": groups,
}
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
except Exception as e:
response = default
self.log.warning(
"[FEATURE FLAGS] Unable to get data for flag %s, because of the following error:" % key
)
self.log.warning(e)
else:
try:
request_data = {
"distinct_id": distinct_id,
"personal_api_key": self.personal_api_key,
}
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
response = key in resp_data["featureFlags"]
except Exception as e:
response = default
self.log.warning(
"[FEATURE FLAGS] Unable to get data for flag %s, because of the following error:" % key
)
self.log.warning(e)
response = resp_data["featureFlags"].get(key, default)
self.capture(distinct_id, "$feature_flag_called", {"$feature_flag": key, "$feature_flag_response": response})
return response
+3 -1
View File
@@ -117,13 +117,15 @@ 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)
+1 -1
View File
@@ -4,7 +4,7 @@ import threading
class Poller(threading.Thread):
def __init__(self, interval, execute, *args, **kwargs):
threading.Thread.__init__(self)
self.daemon = False
self.daemon = True # Make daemon to not interfere with program exit
self.stopped = threading.Event()
self.interval = interval
self.execute = execute
+1 -1
View File
@@ -68,7 +68,7 @@ def _process_response(
def decide(api_key: str, host: Optional[str] = None, gzip: bool = False, timeout: int = 15, **kwargs) -> Any:
"""Post the `kwargs to the decide API endpoint"""
res = post(api_key, host, "/decide/", gzip, timeout, **kwargs)
res = post(api_key, host, "/decide/?v=2", gzip, timeout, **kwargs)
return _process_response(res, success_message="Feature flags decided successfully")
+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
@@ -1,6 +1,7 @@
import time
import unittest
from datetime import date, datetime
from uuid import uuid4
import mock
import six
@@ -8,7 +9,7 @@ from freezegun import freeze_time
from posthog.client import Client
from posthog.request import APIError
from posthog.test.utils import TEST_API_KEY
from posthog.test.test_utils import TEST_API_KEY
from posthog.version import VERSION
@@ -37,7 +38,38 @@ class TestClient(unittest.TestCase):
self.assertEqual(msg["event"], "python test event")
self.assertTrue(isinstance(msg["timestamp"], str))
self.assertTrue(isinstance(msg["messageId"], str))
self.assertIsNone(msg.get("uuid"))
self.assertEqual(msg["distinct_id"], "distinct_id")
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
def test_basic_capture_with_uuid(self):
client = self.client
uuid = str(uuid4())
success, msg = client.capture("distinct_id", "python test event", uuid=uuid)
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg["event"], "python test event")
self.assertTrue(isinstance(msg["timestamp"], str))
self.assertEqual(msg["uuid"], uuid)
self.assertEqual(msg["distinct_id"], "distinct_id")
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
def test_basic_capture_with_project_api_key(self):
client = Client(project_api_key=TEST_API_KEY, on_error=self.set_fail)
success, msg = client.capture("distinct_id", "python test event")
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
self.assertEqual(msg["event"], "python test event")
self.assertTrue(isinstance(msg["timestamp"], str))
self.assertIsNone(msg.get("uuid"))
self.assertEqual(msg["distinct_id"], "distinct_id")
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
@@ -61,7 +93,7 @@ class TestClient(unittest.TestCase):
{"property": "value"},
{"ip": "192.168.0.1"},
datetime(2014, 9, 3),
"messageId",
"new-uuid",
)
self.assertTrue(success)
@@ -72,8 +104,19 @@ class TestClient(unittest.TestCase):
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["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
@@ -84,13 +127,13 @@ class TestClient(unittest.TestCase):
self.assertEqual(msg["$set"]["trait"], "value")
self.assertTrue(isinstance(msg["timestamp"], str))
self.assertTrue(isinstance(msg["messageId"], 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), "messageId"
"distinct_id", {"trait": "value"}, {"ip": "192.168.0.1"}, datetime(2014, 9, 3), "new-uuid"
)
self.assertTrue(success)
@@ -101,9 +144,107 @@ class TestClient(unittest.TestCase):
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["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")
@@ -122,6 +263,16 @@ class TestClient(unittest.TestCase):
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(
@@ -130,7 +281,7 @@ class TestClient(unittest.TestCase):
{"property": "value"},
{"ip": "192.168.0.1"},
datetime(2014, 9, 3),
"messageId",
"new-uuid",
)
self.assertTrue(success)
@@ -142,7 +293,7 @@ class TestClient(unittest.TestCase):
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["uuid"], "new-uuid")
self.assertEqual(msg["distinct_id"], "distinct_id")
def test_flush(self):
@@ -211,7 +362,7 @@ class TestClient(unittest.TestCase):
client = Client(TEST_API_KEY, on_error=self.fail, flush_at=10, flush_interval=3)
def mock_post_fn(*args, **kwargs):
self.assertEquals(len(kwargs["batch"]), 10)
self.assertEqual(len(kwargs["batch"]), 10)
# the post function should be called 2 times, with a batch size of 10
# each time.
@@ -219,25 +370,31 @@ class TestClient(unittest.TestCase):
for _ in range(20):
client.identify("distinct_id", {"trait": "value"})
time.sleep(1)
self.assertEquals(mock_post.call_count, 2)
self.assertEqual(mock_post.call_count, 2)
def test_user_defined_timeout(self):
client = Client(TEST_API_KEY, timeout=10)
for consumer in client.consumers:
self.assertEquals(consumer.timeout, 10)
self.assertEqual(consumer.timeout, 10)
def test_default_timeout_15(self):
client = Client(TEST_API_KEY)
for consumer in client.consumers:
self.assertEquals(consumer.timeout, 15)
self.assertEqual(consumer.timeout, 15)
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_load_feature_flags(self, patch_get, patch_poll):
patch_get.return_value = {"results": [{"id": 1, "name": "Beta Feature", "key": "beta-feature"}]}
patch_get.return_value = {
"results": [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True},
{"id": 2, "name": "Alpha Feature", "key": "alpha-feature", "active": False},
]
}
client = Client(TEST_API_KEY, personal_api_key="test")
with freeze_time("2020-01-01T12:01:00.0000Z"):
client.load_feature_flags()
self.assertEqual(len(client.feature_flags), 1)
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
self.assertEqual(client._last_feature_flag_poll.isoformat(), "2020-01-01T12:01:00+00:00")
self.assertEqual(patch_poll.call_count, 1)
@@ -247,26 +404,70 @@ class TestClient(unittest.TestCase):
with freeze_time("2020-01-01T12:01:00.0000Z"):
self.assertRaises(APIError, client.load_feature_flags)
@mock.patch("posthog.client.decide")
@mock.patch("posthog.client.get")
def test_feature_enabled_simple(self, patch_get):
def test_feature_enabled_simple(self, patch_get, patch_decide):
client = Client(TEST_API_KEY)
client.feature_flags = [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True, "rollout_percentage": 100}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
self.assertEqual(patch_decide.call_count, 0)
@mock.patch("posthog.client.decide")
@mock.patch("posthog.client.get")
def test_feature_enabled_simple_is_false(self, patch_get, patch_decide):
client = Client(TEST_API_KEY)
client.feature_flags = [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True, "rollout_percentage": 0}
]
self.assertFalse(client.feature_enabled("beta-feature", "distinct_id"))
self.assertEqual(patch_decide.call_count, 0)
@mock.patch("posthog.client.get")
def test_feature_enabled_simple_with_project_api_key(self, patch_get):
client = Client(project_api_key=TEST_API_KEY, on_error=self.set_fail)
client.feature_flags = [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True, "rollout_percentage": 100}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
@mock.patch("posthog.client.decide")
def test_feature_enabled_request(self, patch_get):
patch_get.return_value = {"featureFlags": ["beta-feature"]}
def test_feature_enabled_request(self, patch_decide):
patch_decide.return_value = {"featureFlags": {"beta-feature": True}}
client = Client(TEST_API_KEY)
client.feature_flags = [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": False, "rollout_percentage": 100}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.decide")
def test_feature_enabled_request_multi_variate(self, patch_decide):
patch_decide.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
client = Client(TEST_API_KEY)
client.feature_flags = [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": False, "rollout_percentage": 100}
]
self.assertEqual(client.feature_enabled("beta-feature", "distinct_id"), "variant-1")
@mock.patch("posthog.client.get")
def test_feature_enabled_doesnt_exist(self, patch_get, patch_poll):
def test_feature_enabled_simple_without_rollout_percentage(self, patch_get):
client = Client(TEST_API_KEY)
client.feature_flags = [{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True}]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
@mock.patch("posthog.client.get")
def test_feature_enabled_simple_with_none_rollout_percentage(self, patch_get):
client = Client(TEST_API_KEY)
client.feature_flags = [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True, "rollout_percantage": None}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.decide")
def test_feature_enabled_doesnt_exist(self, patch_decide, patch_poll):
patch_decide.return_value = {"featureFlags": {}}
client = Client(TEST_API_KEY, personal_api_key="test")
client.feature_flags = []
@@ -274,13 +475,14 @@ class TestClient(unittest.TestCase):
self.assertTrue(client.feature_enabled("doesnt-exist", "distinct_id", True))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_personal_api_key_doesnt_exist(self, patch_get, patch_poll):
@mock.patch("posthog.client.decide")
def test_personal_api_key_doesnt_exist(self, patch_decide, patch_poll):
client = Client(TEST_API_KEY)
client.feature_flags = []
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
self.assertTrue(client.feature_enabled("doesnt-exist", "distinct_id", True))
patch_decide.return_value = {"featureFlags": {"feature-flag": True}}
self.assertTrue(client.feature_enabled("feature-flag", "distinct_id"))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
@@ -11,7 +11,7 @@ except ImportError:
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):
@@ -161,4 +161,4 @@ class TestConsumer(unittest.TestCase):
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)
@@ -5,7 +5,7 @@ from datetime import date, datetime
import requests
from posthog.request import DatetimeSerializer, batch_post
from posthog.test.utils import TEST_API_KEY
from posthog.test.test_utils import TEST_API_KEY
class TestRequests(unittest.TestCase):
+4 -1
View File
@@ -1 +1,4 @@
VERSION = "1.2.1"
VERSION = "1.4.8"
if __name__ == "__main__":
print(VERSION, end="")
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()
+5 -6
View File
@@ -14,18 +14,18 @@ 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"]
extras_require = {
"dev": [
"black",
"isort",
"pre-commit",
]
],
"test": ["mock>=2.0.0", "freezegun==0.3.15", "pylint", "flake8", "coverage", "pytest"],
"sentry": ["sentry-sdk", "django"],
}
tests_require = ["mock>=2.0.0", "freezegun==0.3.15"]
setup(
name="posthog",
version=VERSION,
@@ -35,11 +35,10 @@ setup(
maintainer="PostHog",
maintainer_email="hey@posthog.com",
test_suite="posthog.test.all",
packages=["posthog", "posthog.test"],
packages=["posthog", "posthog.test", "posthog.sentry"],
license="MIT License",
install_requires=install_requires,
extras_require=extras_require,
tests_require=tests_require,
description="Integrate PostHog into any python application.",
long_description=long_description,
classifiers=[
+4 -1
View File
@@ -27,10 +27,13 @@ setup(
maintainer="PostHog",
maintainer_email="hey@posthog.com",
test_suite="posthoganalytics.test.all",
packages=["posthoganalytics", "posthoganalytics.test"],
packages=["posthoganalytics", "posthoganalytics.test", "posthoganalytics.sentry"],
license="MIT License",
install_requires=install_requires,
tests_require=tests_require,
extras_require={
"sentry": ["sentry-sdk", "django"],
},
description="Integrate PostHog into any python application.",
long_description=long_description,
classifiers=[
+17 -1
View File
@@ -71,6 +71,22 @@ def identify():
)
def set_once():
posthog.set_once(
options.distinct_id,
properties=json_hash(options.traits),
context=json_hash(options.context),
)
def set():
posthog.set(
options.distinct_id,
properties=json_hash(options.traits),
context=json_hash(options.context),
)
def unknown():
print()
@@ -84,7 +100,7 @@ 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: