Compare commits
33
Commits
code-style
...
1.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd321566ed | ||
|
|
83737f2477 | ||
|
|
cc5649368f | ||
|
|
7fe5045da1 | ||
|
|
2c9ad238ec | ||
|
|
de6e60f12f | ||
|
|
fd92502d99 | ||
|
|
fe6d0dc1ec | ||
|
|
fbde5cafc4 | ||
|
|
2e99081cb1 | ||
|
|
372fb74637 | ||
|
|
ba11548089 | ||
|
|
49d0821e27 | ||
|
|
b4489f1dca | ||
|
|
8040964761 | ||
|
|
4f853403b9 | ||
|
|
ac61fb0e01 | ||
|
|
7196dc6048 | ||
|
|
45303b899e | ||
|
|
7e463ccad6 | ||
|
|
41dec34929 | ||
|
|
0bf9db0108 | ||
|
|
e8308360bb | ||
|
|
15ebe85a78 | ||
|
|
dbc22d2f9f | ||
|
|
f3ee238823 | ||
|
|
71d81b2da9 | ||
|
|
5493029577 | ||
|
|
d66f944571 | ||
|
|
9d620967f8 | ||
|
|
563404f914 | ||
|
|
d15aac41a9 | ||
|
|
8a3e28b949 |
@@ -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: |
|
||||
|
||||
+3
-1
@@ -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
|
||||
@@ -0,0 +1,19 @@
|
||||
## 1.4.1 - 2021-05-28
|
||||
- Fix packaging issues with Sentry integrations
|
||||
|
||||
## 1.4.0 - 2021-05-18
|
||||
|
||||
- Improve support for `project_api_key` (#32)
|
||||
- Resolve polling issues with feature flags (#29)
|
||||
- Add Sentry (and Sentry+Django) integrations (#13)
|
||||
- Fix feature flag issue with no percentage rollout (#30)
|
||||
|
||||
## 1.3.1 - 2021-05-07
|
||||
|
||||
- Add `$set` and `$set_once` support (#23)
|
||||
- Add distinct ID to `$create_alias` event (#27)
|
||||
- Add `UUID` to `ID_TYPES` (#26)
|
||||
|
||||
## 1.2.1 - 2021-02-05
|
||||
|
||||
Initial release logged in CHANGELOG.md.
|
||||
@@ -1,7 +1,10 @@
|
||||
test:
|
||||
lint:
|
||||
pylint --rcfile=.pylintrc --reports=y --exit-zero analytics | tee pylint.out
|
||||
flake8 --max-complexity=10 --statistics analytics > flake8.out || true
|
||||
coverage run --branch --include=analytics/\* --omit=*/test* setup.py test
|
||||
|
||||
test:
|
||||
coverage run -m pytest
|
||||
coverage report
|
||||
|
||||
release:
|
||||
rm -rf dist/*
|
||||
@@ -26,4 +29,8 @@ release_analytics:
|
||||
e2e_test:
|
||||
.buildscripts/e2e.sh
|
||||
|
||||
.PHONY: test release e2e_test
|
||||
django_example:
|
||||
python -m pip install -e ".[sentry]"
|
||||
cd sentry_django_example && python manage.py runserver 8080
|
||||
|
||||
.PHONY: test lint release e2e_test
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# PostHog Python
|
||||
|
||||
[](https://pypi.org/project/posthog/)
|
||||
|
||||
Please see the main [PostHog docs](https://posthog.com/docs).
|
||||
|
||||
Specifically, the [Python integration](https://posthog.com/docs/integrations/python-integration) details.
|
||||
|
||||
## Questions?
|
||||
|
||||
### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
|
||||
|
||||
# Local Development
|
||||
|
||||
## Testing Locally
|
||||
|
||||
1. Run `python3 -m venv env` (creates virtual environment called "env")
|
||||
2. Run `source env/bin/activate` (activates the virtual environment)
|
||||
3. Run `python3 -m pip install -e ".[test]"` (installs the package in develop mode, along with test dependencies)
|
||||
4. Run `make test`
|
||||
|
||||
## Running Locally
|
||||
|
||||
Assuming you have a [local version of PostHog](https://posthog.com/docs/developing-locally) running, you can run `python3 example.py` to see the library in action.
|
||||
|
||||
## 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.
|
||||
|
||||
+20
-3
@@ -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 /.
|
||||
@@ -19,12 +19,29 @@ posthog.capture("distinct_id", "event", {"property1": "value", "property2": "val
|
||||
print(posthog.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
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"})
|
||||
|
||||
# # Add properties to the person
|
||||
posthog.identify("distinct_id", {"email": "something@something.com"})
|
||||
posthog.identify("new_distinct_id", {"email": "something@something.com"})
|
||||
|
||||
# properties set only once to the person
|
||||
posthog.set_once("new_distinct_id", {"self_serve_signup": True})
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
posthog.set_once(
|
||||
"new_distinct_id", {"self_serve_signup": False}
|
||||
) # this will not change the property (because it was already set)
|
||||
|
||||
posthog.set("new_distinct_id", {"current_browser": "Chrome"})
|
||||
posthog.set("new_distinct_id", {"current_browser": "Firefox"})
|
||||
|
||||
# posthog.shutdown()
|
||||
|
||||
+71
-3
@@ -14,13 +14,14 @@ 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]
|
||||
@@ -32,7 +33,7 @@ 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
|
||||
@@ -87,6 +88,72 @@ def identify(
|
||||
)
|
||||
|
||||
|
||||
def set(
|
||||
distinct_id, # type: str,
|
||||
properties=None, # type: Optional[Dict]
|
||||
context=None, # type: Optional[Dict]
|
||||
timestamp=None, # type: Optional[datetime.datetime]
|
||||
message_id=None, # type: Optional[str]
|
||||
):
|
||||
# type: (...) -> None
|
||||
"""
|
||||
Set properties on a user record.
|
||||
This will overwrite previous people property values, just like `identify`.
|
||||
|
||||
A `set` call requires
|
||||
- `distinct id` which uniquely identifies your user
|
||||
- `properties` with a dict with any key: value pairs
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.set('distinct id', {
|
||||
'current_browser': 'Chrome',
|
||||
})
|
||||
```
|
||||
"""
|
||||
_proxy(
|
||||
"set",
|
||||
distinct_id=distinct_id,
|
||||
properties=properties,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
message_id=message_id,
|
||||
)
|
||||
|
||||
|
||||
def set_once(
|
||||
distinct_id, # type: str,
|
||||
properties=None, # type: Optional[Dict]
|
||||
context=None, # type: Optional[Dict]
|
||||
timestamp=None, # type: Optional[datetime.datetime]
|
||||
message_id=None, # type: Optional[str]
|
||||
):
|
||||
# type: (...) -> None
|
||||
"""
|
||||
Set properties on a user record, only if they do not yet exist.
|
||||
This will not overwrite previous people property values, unlike `identify`.
|
||||
|
||||
A `set_once` call requires
|
||||
- `distinct id` which uniquely identifies your user
|
||||
- `properties` with a dict with any key: value pairs
|
||||
|
||||
For example:
|
||||
```python
|
||||
posthog.set_once('distinct id', {
|
||||
'referred_by': 'friend',
|
||||
})
|
||||
```
|
||||
"""
|
||||
_proxy(
|
||||
"set_once",
|
||||
distinct_id=distinct_id,
|
||||
properties=properties,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
message_id=message_id,
|
||||
)
|
||||
|
||||
|
||||
def group(*args, **kwargs):
|
||||
"""Send a group call."""
|
||||
_proxy("group", *args, **kwargs)
|
||||
@@ -186,6 +253,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)
|
||||
|
||||
+47
-8
@@ -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,
|
||||
@@ -138,6 +138,40 @@ class Client(object):
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def set(self, distinct_id=None, properties=None, context=None, timestamp=None, message_id=None):
|
||||
properties = properties or {}
|
||||
context = context or {}
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
require("properties", properties, dict)
|
||||
|
||||
msg = {
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"distinct_id": distinct_id,
|
||||
"$set": properties,
|
||||
"event": "$set",
|
||||
"messageId": message_id,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def set_once(self, distinct_id=None, properties=None, context=None, timestamp=None, message_id=None):
|
||||
properties = properties or {}
|
||||
context = context or {}
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
require("properties", properties, dict)
|
||||
|
||||
msg = {
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"distinct_id": distinct_id,
|
||||
"$set_once": properties,
|
||||
"event": "$set_once",
|
||||
"messageId": message_id,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def alias(self, previous_id=None, distinct_id=None, context=None, timestamp=None, message_id=None):
|
||||
context = context or {}
|
||||
|
||||
@@ -152,6 +186,7 @@ class Client(object):
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"event": "$create_alias",
|
||||
"distinct_id": previous_id,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
@@ -241,6 +276,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()
|
||||
@@ -275,8 +313,9 @@ 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):
|
||||
require("key", key, string_types)
|
||||
@@ -297,7 +336,7 @@ class Client(object):
|
||||
return default
|
||||
|
||||
if feature_flag.get("is_simple_flag"):
|
||||
response = _hash(key, distinct_id) <= (feature_flag["rollout_percentage"] / 100)
|
||||
response = _hash(key, distinct_id) <= ((feature_flag.get("rollout_percentage", 100) or 100) / 100)
|
||||
else:
|
||||
try:
|
||||
request_data = {
|
||||
|
||||
+3
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
POSTHOG_ID_TAG = "posthog_distinct_id"
|
||||
@@ -0,0 +1,28 @@
|
||||
from django.conf import settings
|
||||
from sentry_sdk import configure_scope
|
||||
|
||||
from posthog.sentry import POSTHOG_ID_TAG
|
||||
|
||||
GET_DISTINCT_ID = getattr(settings, "POSTHOG_DJANGO", {}).get("distinct_id")
|
||||
|
||||
|
||||
def get_distinct_id(request):
|
||||
if not GET_DISTINCT_ID:
|
||||
return None
|
||||
try:
|
||||
return GET_DISTINCT_ID(request)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class PosthogDistinctIdMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
with configure_scope() as scope:
|
||||
distinct_id = get_distinct_id(request)
|
||||
if distinct_id:
|
||||
scope.set_tag(POSTHOG_ID_TAG, distinct_id)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -0,0 +1,47 @@
|
||||
from sentry_sdk._types import MYPY
|
||||
from sentry_sdk.hub import Hub
|
||||
from sentry_sdk.integrations import Integration
|
||||
from sentry_sdk.scope import add_global_event_processor
|
||||
|
||||
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 self-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 and PostHogIntegration.project_id:
|
||||
properties[
|
||||
"$sentry_url"
|
||||
] = f"{PostHogIntegration.prefix}{PostHogIntegration.organization}/issues/?project={PostHogIntegration.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
|
||||
|
||||
|
||||
@@ -42,6 +43,22 @@ class TestClient(unittest.TestCase):
|
||||
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.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
|
||||
@@ -104,6 +121,64 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["messageId"], "messageId")
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_basic_set(self):
|
||||
client = self.client
|
||||
success, msg = client.set("distinct_id", {"trait": "value"})
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg["$set"]["trait"], "value")
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertTrue(isinstance(msg["messageId"], str))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_advanced_set(self):
|
||||
client = self.client
|
||||
success, msg = client.set(
|
||||
"distinct_id", {"trait": "value"}, {"ip": "192.168.0.1"}, datetime(2014, 9, 3), "messageId"
|
||||
)
|
||||
|
||||
self.assertTrue(success)
|
||||
|
||||
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
||||
self.assertEqual(msg["context"]["ip"], "192.168.0.1")
|
||||
self.assertEqual(msg["$set"]["trait"], "value")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertEqual(msg["messageId"], "messageId")
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_basic_set_once(self):
|
||||
client = self.client
|
||||
success, msg = client.set_once("distinct_id", {"trait": "value"})
|
||||
client.flush()
|
||||
self.assertTrue(success)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
self.assertEqual(msg["$set_once"]["trait"], "value")
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertTrue(isinstance(msg["messageId"], str))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_advanced_set_once(self):
|
||||
client = self.client
|
||||
success, msg = client.set_once(
|
||||
"distinct_id", {"trait": "value"}, {"ip": "192.168.0.1"}, datetime(2014, 9, 3), "messageId"
|
||||
)
|
||||
|
||||
self.assertTrue(success)
|
||||
|
||||
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
||||
self.assertEqual(msg["context"]["ip"], "192.168.0.1")
|
||||
self.assertEqual(msg["$set_once"]["trait"], "value")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertEqual(msg["messageId"], "messageId")
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_basic_alias(self):
|
||||
client = self.client
|
||||
success, msg = client.alias("previousId", "distinct_id")
|
||||
@@ -122,6 +197,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(
|
||||
@@ -255,15 +340,37 @@ class TestClient(unittest.TestCase):
|
||||
]
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
def test_feature_enabled_simple_with_project_api_key(self, patch_get):
|
||||
client = Client(project_api_key=TEST_API_KEY, on_error=self.set_fail)
|
||||
client.feature_flags = [
|
||||
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True, "rollout_percentage": 100}
|
||||
]
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_feature_enabled_request(self, patch_get):
|
||||
patch_get.return_value = {"featureFlags": ["beta-feature"]}
|
||||
def test_feature_enabled_request(self, patch_decide):
|
||||
patch_decide.return_value = {"featureFlags": ["beta-feature"]}
|
||||
client = Client(TEST_API_KEY)
|
||||
client.feature_flags = [
|
||||
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": False, "rollout_percentage": 100}
|
||||
]
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
def test_feature_enabled_simple_without_rollout_percentage(self, patch_get):
|
||||
client = Client(TEST_API_KEY)
|
||||
client.feature_flags = [{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True}]
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
def test_feature_enabled_simple_with_none_rollout_percentage(self, patch_get):
|
||||
client = Client(TEST_API_KEY)
|
||||
client.feature_flags = [
|
||||
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True, "rollout_percantage": None}
|
||||
]
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
def test_feature_enabled_doesnt_exist(self, patch_get, patch_poll):
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
+1
-1
@@ -1 +1 @@
|
||||
VERSION = "1.1.3"
|
||||
VERSION = "1.4.1"
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sentry_django_example.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for sentry_django_example project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sentry_django_example.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
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.project_id = "5624115" # TODO: your sentry projectID
|
||||
# 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()
|
||||
@@ -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", "python-dateutil>2.1"]
|
||||
|
||||
extras_require = {
|
||||
"dev": [
|
||||
"black",
|
||||
"isort",
|
||||
"pre-commit",
|
||||
]
|
||||
],
|
||||
"test": ["mock>=2.0.0", "freezegun==0.3.15", "pylint", "flake8", "coverage"],
|
||||
"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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user