Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab1399d88f | ||
|
|
1777b7062e | ||
|
|
565bb8a0eb | ||
|
|
009cac8634 | ||
|
|
ec2425996c | ||
|
|
90fa0a0604 | ||
|
|
a97fe0a40a | ||
|
|
a474fcff93 | ||
|
|
a181ba718f | ||
|
|
b996f3a4e9 | ||
|
|
6c945a0624 | ||
|
|
ab8ccb4dff | ||
|
|
870f6f8b6b | ||
|
|
9e0aeaefe6 | ||
|
|
a8409960b9 | ||
|
|
deb078293a | ||
|
|
edb8b7891e | ||
|
|
727bdb2b1e | ||
|
|
0781a1280e | ||
|
|
8b2ed8bb12 | ||
|
|
a139795a74 | ||
|
|
11f1d06761 |
@@ -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 }}
|
||||
@@ -1,3 +1,6 @@
|
||||
## 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
|
||||
|
||||
|
||||
@@ -2,28 +2,24 @@
|
||||
|
||||
[](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.
|
||||
Please see the [Python integration docs](https://posthog.com/docs/integrations/python-integration) for details.
|
||||
|
||||
## Questions?
|
||||
## Development
|
||||
|
||||
### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
|
||||
|
||||
# Local Development
|
||||
|
||||
## Testing Locally
|
||||
### 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
|
||||
### 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
|
||||
### 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.
|
||||
|
||||
@@ -40,3 +36,11 @@ There's 2 places of importance (Changes required are all marked with TODO in the
|
||||
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?
|
||||
|
||||
### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ)
|
||||
|
||||
@@ -17,6 +17,7 @@ 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(5)
|
||||
@@ -28,10 +29,16 @@ print(posthog.feature_enabled("beta-feature", "distinct_id"))
|
||||
posthog.alias("distinct_id", "new_distinct_id")
|
||||
|
||||
posthog.capture("new_distinct_id", "event2", {"property1": "value", "property2": "value"})
|
||||
posthog.capture(
|
||||
"new_distinct_id", "event-with-groups", {"property1": "value", "property2": "value"}, groups={"company": "id:5"}
|
||||
)
|
||||
|
||||
# # Add properties to the person
|
||||
posthog.identify("new_distinct_id", {"email": "something@something.com"})
|
||||
|
||||
# Add properties to a group
|
||||
posthog.group_identify("company", "id:5", {"employees": 11})
|
||||
|
||||
# properties set only once to the person
|
||||
posthog.set_once("new_distinct_id", {"self_serve_signup": True})
|
||||
|
||||
|
||||
+54
-15
@@ -25,7 +25,8 @@ def capture(
|
||||
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
|
||||
"""
|
||||
@@ -38,10 +39,14 @@ def capture(
|
||||
|
||||
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(
|
||||
@@ -51,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
|
||||
"""
|
||||
@@ -84,7 +90,7 @@ def identify(
|
||||
properties=properties,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
message_id=message_id,
|
||||
uuid=uuid,
|
||||
)
|
||||
|
||||
|
||||
@@ -93,7 +99,7 @@ def set(
|
||||
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
|
||||
"""
|
||||
@@ -117,7 +123,7 @@ def set(
|
||||
properties=properties,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
message_id=message_id,
|
||||
uuid=uuid,
|
||||
)
|
||||
|
||||
|
||||
@@ -126,7 +132,7 @@ def set_once(
|
||||
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
|
||||
"""
|
||||
@@ -150,13 +156,43 @@ def set_once(
|
||||
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 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(
|
||||
@@ -164,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
|
||||
"""
|
||||
@@ -189,7 +225,7 @@ def alias(
|
||||
distinct_id=distinct_id,
|
||||
context=context,
|
||||
timestamp=timestamp,
|
||||
message_id=message_id,
|
||||
uuid=uuid,
|
||||
)
|
||||
|
||||
|
||||
@@ -197,6 +233,7 @@ def feature_enabled(
|
||||
key, # type: str,
|
||||
distinct_id, # type: str,
|
||||
default=False, # type: bool
|
||||
groups={}, # type: dict
|
||||
):
|
||||
# type: (...) -> bool
|
||||
"""
|
||||
@@ -206,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):
|
||||
|
||||
+75
-42
@@ -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,16 @@ 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, message_id=None):
|
||||
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)
|
||||
@@ -150,12 +156,12 @@ class Client(object):
|
||||
"distinct_id": distinct_id,
|
||||
"$set": properties,
|
||||
"event": "$set",
|
||||
"messageId": message_id,
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def set_once(self, distinct_id=None, properties=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)
|
||||
@@ -167,12 +173,34 @@ class Client(object):
|
||||
"distinct_id": distinct_id,
|
||||
"$set_once": properties,
|
||||
"event": "$set_once",
|
||||
"messageId": message_id,
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
|
||||
def alias(self, previous_id=None, distinct_id=None, context=None, timestamp=None, message_id=None):
|
||||
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)
|
||||
@@ -191,7 +219,7 @@ class Client(object):
|
||||
|
||||
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 {}
|
||||
|
||||
@@ -207,7 +235,7 @@ class Client(object):
|
||||
"timestamp": timestamp,
|
||||
"context": context,
|
||||
"distinct_id": distinct_id,
|
||||
"messageId": message_id,
|
||||
"uuid": uuid,
|
||||
}
|
||||
|
||||
return self._enqueue(msg)
|
||||
@@ -217,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)
|
||||
@@ -227,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"
|
||||
@@ -286,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(
|
||||
@@ -317,40 +349,41 @@ class Client(object):
|
||||
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"):
|
||||
response = _hash(key, distinct_id) <= ((feature_flag.get("rollout_percentage", 100) or 100) / 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)
|
||||
if key in resp_data["featureFlags"]:
|
||||
return True
|
||||
else:
|
||||
return default
|
||||
|
||||
self.capture(distinct_id, "$feature_flag_called", {"$feature_flag": key, "$feature_flag_response": response})
|
||||
return response
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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
|
||||
@@ -17,7 +19,7 @@ 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/)
|
||||
prefix = "https://sentry.io/organizations/" # URL of a hosted sentry instance (default: https://sentry.io/organizations/)
|
||||
|
||||
@staticmethod
|
||||
def setup_once():
|
||||
@@ -37,10 +39,14 @@ class PostHogIntegration(Integration):
|
||||
"$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']}"
|
||||
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)
|
||||
|
||||
|
||||
+113
-27
@@ -38,7 +38,22 @@ 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)
|
||||
@@ -54,7 +69,7 @@ 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)
|
||||
@@ -78,7 +93,7 @@ class TestClient(unittest.TestCase):
|
||||
{"property": "value"},
|
||||
{"ip": "192.168.0.1"},
|
||||
datetime(2014, 9, 3),
|
||||
"messageId",
|
||||
"new-uuid",
|
||||
)
|
||||
|
||||
self.assertTrue(success)
|
||||
@@ -89,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
|
||||
@@ -101,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)
|
||||
@@ -118,7 +144,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_basic_set(self):
|
||||
@@ -130,13 +156,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_set(self):
|
||||
client = self.client
|
||||
success, msg = client.set(
|
||||
"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)
|
||||
@@ -147,7 +173,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_basic_set_once(self):
|
||||
@@ -159,13 +185,13 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertEqual(msg["$set_once"]["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_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"
|
||||
"distinct_id", {"trait": "value"}, {"ip": "192.168.0.1"}, datetime(2014, 9, 3), "new-uuid"
|
||||
)
|
||||
|
||||
self.assertTrue(success)
|
||||
@@ -176,9 +202,49 @@ 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_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")
|
||||
@@ -215,7 +281,7 @@ class TestClient(unittest.TestCase):
|
||||
{"property": "value"},
|
||||
{"ip": "192.168.0.1"},
|
||||
datetime(2014, 9, 3),
|
||||
"messageId",
|
||||
"new-uuid",
|
||||
)
|
||||
|
||||
self.assertTrue(success)
|
||||
@@ -227,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):
|
||||
@@ -296,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.
|
||||
@@ -304,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)
|
||||
@@ -332,13 +404,25 @@ 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):
|
||||
@@ -372,8 +456,9 @@ class TestClient(unittest.TestCase):
|
||||
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):
|
||||
@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 = []
|
||||
|
||||
@@ -381,13 +466,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"]}
|
||||
|
||||
self.assertTrue(client.feature_enabled("feature-flag", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
|
||||
@@ -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)
|
||||
|
||||
+4
-1
@@ -1 +1,4 @@
|
||||
VERSION = "1.4.1"
|
||||
VERSION = "1.4.7"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(VERSION, end="")
|
||||
|
||||
@@ -44,7 +44,6 @@ 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)
|
||||
|
||||
@@ -14,7 +14,7 @@ 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.10.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": [
|
||||
@@ -22,7 +22,7 @@ extras_require = {
|
||||
"isort",
|
||||
"pre-commit",
|
||||
],
|
||||
"test": ["mock>=2.0.0", "freezegun==0.3.15", "pylint", "flake8", "coverage"],
|
||||
"test": ["mock>=2.0.0", "freezegun==0.3.15", "pylint", "flake8", "coverage", "pytest"],
|
||||
"sentry": ["sentry-sdk", "django"],
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user