Compare commits

...
10 Commits
Author SHA1 Message Date
Dylan MartinandGitHub 9e1bb8c58a fix(flags): bump the version (#148) 2024-11-27 17:15:38 -05:00
fb57de2e12 fix(flags): correctly emit feature flag events with the FF response on get_feature_flag_payload calls (#143)
* this is the fix, needs tests

* fix test

* tests

* yeah

* please work

* ran the formatter

* code review feedback

* how'd this get here

* bump version add changelog

* Update CHANGELOG.md

Co-authored-by: David Newell <d.newell1@outlook.com>

---------

Co-authored-by: David Newell <d.newell1@outlook.com>
2024-11-25 14:51:06 -05:00
db565bc0fd fix(err): fix distinct_id, set personless and use a uuid (#144)
Co-authored-by: David Newell <david@posthog.com>
2024-11-25 12:09:57 +00:00
David NewellandGitHub 8ae3f2b623 chore: add type to stack (#142) 2024-11-19 12:46:52 +00:00
David NewellandGitHub 39f72a0070 chore: add lang to frames (#139) 2024-10-24 16:18:02 +01:00
David NewellandGitHub ee0305993d feat: add super properties (#138) 2024-10-03 17:07:47 +01:00
28c4802d9b Remove deprecated datetime.utcnow() in favour of datetime.now(tz=tzutc()) (#124)
Co-authored-by: Neil Kakkar <neilkakkar@gmail.com>
2024-09-24 11:09:15 +01:00
Neil KakkarandGitHub 67a343f242 fix(errors): Make sure project root exists to judge in app frames (#136)
* fix(errors): Make sure project root exists to judge in app frames

* prep release
2024-09-16 11:11:28 +01:00
Neil KakkarandGitHub 1521621d66 fix: Update Django integration for manual capture (#135) 2024-09-10 09:27:47 +01:00
Neil KakkarandGitHub 39070babfb feat(errors): Add manual exception capture (#134)
* feat(errors): Add manual exception capture

* prep release

* use backwards compatible helper

* add tests
2024-09-09 11:40:51 +01:00
10 changed files with 498 additions and 69 deletions
+44 -5
View File
@@ -1,3 +1,39 @@
## 3.7.4 - 2024-11-25
1. Fix bug where this SDK incorrectly sent feature flag events with null values when calling `get_feature_flag_payload`.
## 3.7.3 - 2024-11-25
1. Use personless mode when sending an exception without a provided `distinct_id`.
## 3.7.2 - 2024-11-19
1. Add `type` property to exception stacks.
## 3.7.1 - 2024-10-24
1. Add `platform` property to each frame of exception stacks.
## 3.7.0 - 2024-10-03
1. Adds a new `super_properties` parameter on the client that are appended to every /capture call.
## 3.6.7 - 2024-09-24
1. Remove deprecated datetime.utcnow() in favour of datetime.now(tz=tzutc())
## 3.6.6 - 2024-09-16
1. Fix manual capture support for in app frames
## 3.6.5 - 2024-09-10
1. Fix django integration support for manual exception capture.
## 3.6.4 - 2024-09-05
1. Add manual exception capture.
## 3.6.3 - 2024-09-03
1. Make sure setup.py for posthoganalytics package also discovers the new exception integration package.
@@ -50,7 +86,6 @@
1. Return success/failure with all capture calls from module functions
## 3.3.1 - 2024-01-10
1. Make sure we don't override any existing feature flag properties when adding locally evaluated feature flag properties.
@@ -76,6 +111,7 @@
1. Restore how feature flags work when the client library is disabled: All requests return `None` and no events are sent when the client is disabled.
2. Add a `feature_flag_definitions()` debug option, which returns currently loaded feature flag definitions. You can use this to more cleverly decide when to request local evaluation of feature flags.
## 3.0.0 - 2023-04-14
Breaking change:
@@ -116,7 +152,6 @@ posthog = Posthog('api_key', disable_geoip=False)
1. Log instead of raise error on posthog personal api key errors
2. Remove upper bound on backoff dependency
## 2.3.0 - 2023-01-31
1. Add support for returning payloads of matched feature flags
@@ -132,6 +167,7 @@ Changes:
Changes:
1. Fixes issues with date comparison.
## 2.1.1 - 2022-09-14
Changes:
@@ -144,6 +180,7 @@ Changes:
1. Feature flag defaults have been removed
2. Setup logging only when debug mode is enabled.
## 2.0.1 - 2022-08-04
- Make poll_interval configurable
@@ -156,7 +193,7 @@ Breaking changes:
1. The minimum version requirement for PostHog servers is now 1.38. If you're using PostHog Cloud, you satisfy this requirement automatically.
2. Feature flag defaults apply only when there's an error fetching feature flag results. Earlier, if the default was set to `True`, even if a flag resolved to `False`, the default would override this.
**Note: These are removed in 2.0.2**
**Note: These are removed in 2.0.2**
3. Feature flag remote evaluation doesn't require a personal API key.
New Changes:
@@ -164,18 +201,20 @@ New Changes:
1. You can now evaluate feature flags locally (i.e. without sending a request to your PostHog servers) by setting a personal API key, and passing in groups and person properties to `is_feature_enabled` and `get_feature_flag` calls.
2. Introduces a `get_all_flags` method that returns all feature flags. This is useful for when you want to seed your frontend with some initial flags, given a user ID.
## 1.4.9 - 2022-06-13
- Support for sending feature flags with capture calls
## 1.4.8 - 2022-05-12
- Support multi variate feature flags
## 1.4.7 - 2022-04-25
- Allow feature flags usage without project_api_key
## 1.4.1 - 2021-05-28
- Fix packaging issues with Sentry integrations
## 1.4.0 - 2021-05-18
+48
View File
@@ -20,9 +20,12 @@ project_api_key = None # type: Optional[str]
poll_interval = 30 # type: int
disable_geoip = True # type: bool
feature_flags_request_timeout_seconds = 3 # type: int
super_properties = None # type: Optional[Dict]
# Currently alpha, use at your own risk
enable_exception_autocapture = False # type: bool
exception_autocapture_integrations = [] # type: List[Integrations]
# Used to determine in app paths for exception autocapture. Defaults to the current working directory
project_root = None # type: Optional[str]
default_client = None # type: Optional[Client]
@@ -251,6 +254,50 @@ def alias(
)
def capture_exception(
exception=None, # type: Optional[BaseException]
distinct_id=None, # type: Optional[str]
properties=None, # type: Optional[Dict]
context=None, # type: Optional[Dict]
timestamp=None, # type: Optional[datetime.datetime]
uuid=None, # type: Optional[str]
groups=None, # type: Optional[Dict]
):
# type: (...) -> Tuple[bool, dict]
"""
capture_exception allows you to capture exceptions that happen in your code. This is useful for debugging and understanding what errors your users are encountering.
This function never raises an exception, even if it fails to send the event.
A `capture_exception` call does not require any fields, but we recommend sending:
- `distinct id` which uniquely identifies your user for which this exception happens
- `exception` to specify the exception to capture. If not provided, the current exception is captured via `sys.exc_info()`
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
try:
1 / 0
except Exception as e:
posthog.capture_exception(e, 'my specific distinct id')
posthog.capture_exception(distinct_id='my specific distinct id')
```
"""
return _proxy(
"capture_exception",
exception=exception,
distinct_id=distinct_id,
properties=properties,
context=context,
timestamp=timestamp,
uuid=uuid,
groups=groups,
)
def feature_enabled(
key, # type: str
distinct_id, # type: str
@@ -458,6 +505,7 @@ def _proxy(method, *args, **kwargs):
disabled=disabled,
disable_geoip=disable_geoip,
feature_flags_request_timeout_seconds=feature_flags_request_timeout_seconds,
super_properties=super_properties,
# TODO: Currently this monitoring begins only when the Client is initialised (which happens when you do something with the SDK)
# This kind of initialisation is very annoying for exception capture. We need to figure out a way around this,
# or deprecate this proxy option fully (it's already in the process of deprecation, no new clients should be using this method since like 5-6 months)
+128 -14
View File
@@ -1,18 +1,21 @@
import atexit
import logging
import numbers
import os
import sys
from datetime import datetime, timedelta
from uuid import UUID
from uuid import UUID, uuid4
from dateutil.tz import tzutc
from six import string_types
from posthog.consumer import Consumer
from posthog.exception_capture import ExceptionCapture
from posthog.exception_utils import exc_info_from_error, exceptions_from_error_tuple, handle_in_app
from posthog.feature_flags import InconclusiveMatchError, match_feature_flag_properties
from posthog.poller import Poller
from posthog.request import APIError, batch_post, decide, determine_server_host, get
from posthog.utils import SizeLimitedDict, clean, guess_timezone
from posthog.request import DEFAULT_HOST, APIError, batch_post, decide, determine_server_host, get
from posthog.utils import SizeLimitedDict, clean, guess_timezone, remove_trailing_slash
from posthog.version import VERSION
try:
@@ -52,8 +55,10 @@ class Client(object):
disable_geoip=True,
historical_migration=False,
feature_flags_request_timeout_seconds=3,
super_properties=None,
enable_exception_autocapture=False,
exception_autocapture_integrations=None,
project_root=None,
):
self.queue = queue.Queue(max_queue_size)
@@ -67,7 +72,7 @@ class Client(object):
self.send = send
self.sync_mode = sync_mode
# Used for session replay URL generation - we don't want the server host here.
self.raw_host = host
self.raw_host = host or DEFAULT_HOST
self.host = determine_server_host(host)
self.gzip = gzip
self.timeout = timeout
@@ -82,10 +87,19 @@ class Client(object):
self.disabled = disabled
self.disable_geoip = disable_geoip
self.historical_migration = historical_migration
self.super_properties = super_properties
self.enable_exception_autocapture = enable_exception_autocapture
self.exception_autocapture_integrations = exception_autocapture_integrations
self.exception_capture = None
if project_root is None:
try:
project_root = os.getcwd()
except Exception:
project_root = None
self.project_root = project_root
# personal_api_key: This should be a generated Personal API Key, private
self.personal_api_key = personal_api_key
if debug:
@@ -159,6 +173,15 @@ class Client(object):
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties, disable_geoip)
return resp_data["featureFlagPayloads"]
def get_feature_flags_and_payloads(
self, distinct_id, groups=None, person_properties=None, group_properties=None, disable_geoip=None
):
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties, disable_geoip)
return {
"featureFlags": resp_data["featureFlags"],
"featureFlagPayloads": resp_data["featureFlagPayloads"],
}
def get_decide(self, distinct_id, groups=None, person_properties=None, group_properties=None, disable_geoip=None):
require("distinct_id", distinct_id, ID_TYPES)
@@ -345,6 +368,65 @@ class Client(object):
return self._enqueue(msg, disable_geoip)
def capture_exception(
self,
exception=None,
distinct_id=None,
properties=None,
context=None,
timestamp=None,
uuid=None,
groups=None,
):
# this function shouldn't ever throw an error, so it logs exceptions instead of raising them.
# this is important to ensure we don't unexpectedly re-raise exceptions in the user's code.
try:
properties = properties or {}
# if there's no distinct_id, we'll generate one and set personless mode
# via $process_person_profile = false
if distinct_id is None:
properties["$process_person_profile"] = False
distinct_id = uuid4()
require("distinct_id", distinct_id, ID_TYPES)
require("properties", properties, dict)
if exception is not None:
exc_info = exc_info_from_error(exception)
else:
exc_info = sys.exc_info()
if exc_info is None or exc_info == (None, None, None):
self.log.warning("No exception information available")
return
# Format stack trace for cymbal
all_exceptions_with_trace = exceptions_from_error_tuple(exc_info)
# Add in-app property to frames in the exceptions
event = handle_in_app(
{
"exception": {
"values": all_exceptions_with_trace,
},
},
project_root=self.project_root,
)
all_exceptions_with_trace_and_in_app = event["exception"]["values"]
properties = {
"$exception_type": all_exceptions_with_trace_and_in_app[0].get("type"),
"$exception_message": all_exceptions_with_trace_and_in_app[0].get("value"),
"$exception_list": all_exceptions_with_trace_and_in_app,
"$exception_personURL": f"{remove_trailing_slash(self.raw_host)}/project/{self.api_key}/person/{distinct_id}",
**properties,
}
return self.capture(distinct_id, "$exception", properties, context, timestamp, uuid, groups)
except Exception as e:
self.log.exception(f"Failed to capture exception: {e}")
def _enqueue(self, msg, disable_geoip):
"""Push a new `msg` onto the queue, return `(success, msg)`"""
@@ -353,7 +435,7 @@ class Client(object):
timestamp = msg["timestamp"]
if timestamp is None:
timestamp = datetime.utcnow().replace(tzinfo=tzutc())
timestamp = datetime.now(tz=tzutc())
require("timestamp", timestamp, datetime)
require("context", msg["context"], dict)
@@ -379,6 +461,9 @@ class Client(object):
if disable_geoip:
msg["properties"]["$geoip_disable"] = True
if self.super_properties:
msg["properties"] = {**msg["properties"], **self.super_properties}
msg["distinct_id"] = stringify_id(msg.get("distinct_id", None))
msg = clean(msg)
@@ -477,7 +562,7 @@ class Client(object):
)
self.log.warning(e)
self._last_feature_flag_poll = datetime.utcnow().replace(tzinfo=tzutc())
self._last_feature_flag_poll = datetime.now(tz=tzutc())
def load_feature_flags(self):
if not self.personal_api_key:
@@ -670,23 +755,52 @@ class Client(object):
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
send_feature_flag_events=send_feature_flag_events,
only_evaluate_locally=True,
send_feature_flag_events=False,
# Disable automatic sending of feature flag events because we're manually handling event dispatch.
# This prevents sending events with empty data when `get_feature_flag` cannot be evaluated locally.
only_evaluate_locally=True, # Enable local evaluation of feature flags to avoid making multiple requests to `/decide`.
disable_geoip=disable_geoip,
)
response = None
payload = None
if match_value is not None:
response = self._compute_payload_locally(key, match_value)
payload = self._compute_payload_locally(key, match_value)
if response is None and not only_evaluate_locally:
decide_payloads = self.get_feature_payloads(
distinct_id, groups, person_properties, group_properties, disable_geoip
flag_was_locally_evaluated = payload is not None
if not flag_was_locally_evaluated and not only_evaluate_locally:
try:
responses_and_payloads = self.get_feature_flags_and_payloads(
distinct_id, groups, person_properties, group_properties, disable_geoip
)
response = responses_and_payloads["featureFlags"].get(key, None)
payload = responses_and_payloads["featureFlagPayloads"].get(str(key).lower(), None)
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Unable to get feature flags and payloads: {e}")
feature_flag_reported_key = f"{key}_{str(response)}"
if (
feature_flag_reported_key not in self.distinct_ids_feature_flags_reported[distinct_id]
and send_feature_flag_events # noqa: W503
):
self.capture(
distinct_id,
"$feature_flag_called",
{
"$feature_flag": key,
"$feature_flag_response": response,
"$feature_flag_payload": payload,
"locally_evaluated": flag_was_locally_evaluated,
f"$feature/{key}": response,
},
groups=groups,
disable_geoip=disable_geoip,
)
response = decide_payloads.get(str(key).lower(), None)
self.distinct_ids_feature_flags_reported[distinct_id].add(feature_flag_reported_key)
return response
return payload
def _compute_payload_locally(self, key, match_value):
payload = None
+6 -42
View File
@@ -4,9 +4,6 @@ import threading
from enum import Enum
from typing import TYPE_CHECKING, List, Optional
from posthog.exception_utils import exceptions_from_error_tuple, handle_in_app
from posthog.utils import remove_trailing_slash
if TYPE_CHECKING:
from posthog.client import Client
@@ -15,9 +12,6 @@ class Integrations(str, Enum):
Django = "django"
DEFAULT_DISTINCT_ID = "python-exceptions"
class ExceptionCapture:
# TODO: Add client side rate limiting to prevent spamming the server with exceptions
@@ -49,52 +43,22 @@ class ExceptionCapture:
def exception_handler(self, exc_type, exc_value, exc_traceback):
# don't affect default behaviour.
self.capture_exception(exc_type, exc_value, exc_traceback)
self.capture_exception((exc_type, exc_value, exc_traceback))
self.original_excepthook(exc_type, exc_value, exc_traceback)
def thread_exception_handler(self, args):
self.capture_exception(args.exc_type, args.exc_value, args.exc_traceback)
self.capture_exception((args.exc_type, args.exc_value, args.exc_traceback))
def exception_receiver(self, exc_info, extra_properties):
if "distinct_id" in extra_properties:
metadata = {"distinct_id": extra_properties["distinct_id"]}
else:
metadata = None
self.capture_exception(exc_info[0], exc_info[1], exc_info[2], metadata)
self.capture_exception((exc_info[0], exc_info[1], exc_info[2]), metadata)
def capture_exception(self, exc_type, exc_value, exc_traceback, metadata=None):
def capture_exception(self, exception, metadata=None):
try:
# if hasattr(sys, "ps1"):
# # Disable the excepthook for interactive Python shells
# return
# Format stack trace like sentry
all_exceptions_with_trace = exceptions_from_error_tuple((exc_type, exc_value, exc_traceback))
# Add in-app property to frames in the exceptions
event = handle_in_app(
{
"exception": {
"values": all_exceptions_with_trace,
},
}
)
all_exceptions_with_trace_and_in_app = event["exception"]["values"]
distinct_id = metadata.get("distinct_id") if metadata else DEFAULT_DISTINCT_ID
# Make sure we have a distinct_id if its empty in metadata
distinct_id = distinct_id or DEFAULT_DISTINCT_ID
properties = {
"$exception_type": all_exceptions_with_trace_and_in_app[0].get("type"),
"$exception_message": all_exceptions_with_trace_and_in_app[0].get("value"),
"$exception_list": all_exceptions_with_trace_and_in_app,
"$exception_personURL": f"{remove_trailing_slash(self.client.raw_host)}/project/{self.client.api_key}/person/{distinct_id}",
}
# TODO: What distinct id should we attach these server-side exceptions to?
# Any heuristic seems prone to errors - how can we know if exception occurred in the context of a user that captured some other event?
self.client.capture(distinct_id, "$exception", properties=properties)
distinct_id = metadata.get("distinct_id") if metadata else None
self.client.capture_exception(exception, distinct_id)
except Exception as e:
self.log.exception(f"Failed to capture exception: {e}")
+3 -2
View File
@@ -369,6 +369,7 @@ def serialize_frame(
tb_lineno = frame.f_lineno
rv = {
"platform": "python",
"filename": filename_for_module(module, abs_path) or None,
"abs_path": os.path.abspath(abs_path) if abs_path else None,
"function": function or "<unknown>",
@@ -417,7 +418,7 @@ def current_stacktrace(
frames.reverse()
return {"frames": frames}
return {"frames": frames, "type": "raw"}
def get_errno(exc_value):
@@ -503,7 +504,7 @@ def single_exception_from_error_tuple(
]
if frames:
exception_value["stacktrace"] = {"frames": frames}
exception_value["stacktrace"] = {"frames": frames, "type": "raw"}
return exception_value
+1 -1
View File
@@ -37,7 +37,7 @@ def post(
"""Post the `kwargs` to the API"""
log = logging.getLogger("posthog")
body = kwargs
body["sentAt"] = datetime.utcnow().replace(tzinfo=tzutc()).isoformat()
body["sentAt"] = datetime.now(tz=tzutc()).isoformat()
url = remove_trailing_slash(host or DEFAULT_HOST) + path
body["api_key"] = api_key
data = json.dumps(body, cls=DatetimeSerializer)
+178
View File
@@ -84,6 +84,184 @@ class TestClient(unittest.TestCase):
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
def test_basic_super_properties(self):
client = Client(FAKE_TEST_API_KEY, super_properties={"source": "repo-name"})
_, msg = client.capture("distinct_id", "python test event")
client.flush()
self.assertEqual(msg["event"], "python test event")
self.assertEqual(msg["properties"]["source"], "repo-name")
_, msg = client.identify("distinct_id", {"trait": "value"})
client.flush()
self.assertEqual(msg["$set"]["trait"], "value")
self.assertEqual(msg["properties"]["source"], "repo-name")
def test_basic_capture_exception(self):
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
client = self.client
exception = Exception("test exception")
client.capture_exception(exception, distinct_id="distinct_id")
self.assertTrue(patch_capture.called)
capture_call = patch_capture.call_args[0]
self.assertEqual(capture_call[0], "distinct_id")
self.assertEqual(capture_call[1], "$exception")
self.assertEqual(
capture_call[2],
{
"$exception_type": "Exception",
"$exception_message": "test exception",
"$exception_list": [
{
"mechanism": {"type": "generic", "handled": True},
"module": None,
"type": "Exception",
"value": "test exception",
}
],
"$exception_personURL": "https://us.i.posthog.com/project/random_key/person/distinct_id",
},
)
def test_basic_capture_exception_with_distinct_id(self):
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
client = self.client
exception = Exception("test exception")
client.capture_exception(exception, "distinct_id")
self.assertTrue(patch_capture.called)
capture_call = patch_capture.call_args[0]
self.assertEqual(capture_call[0], "distinct_id")
self.assertEqual(capture_call[1], "$exception")
self.assertEqual(
capture_call[2],
{
"$exception_type": "Exception",
"$exception_message": "test exception",
"$exception_list": [
{
"mechanism": {"type": "generic", "handled": True},
"module": None,
"type": "Exception",
"value": "test exception",
}
],
"$exception_personURL": "https://us.i.posthog.com/project/random_key/person/distinct_id",
},
)
def test_basic_capture_exception_with_correct_host_generation(self):
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://aloha.com")
exception = Exception("test exception")
client.capture_exception(exception, "distinct_id")
self.assertTrue(patch_capture.called)
capture_call = patch_capture.call_args[0]
self.assertEqual(capture_call[0], "distinct_id")
self.assertEqual(capture_call[1], "$exception")
self.assertEqual(
capture_call[2],
{
"$exception_type": "Exception",
"$exception_message": "test exception",
"$exception_list": [
{
"mechanism": {"type": "generic", "handled": True},
"module": None,
"type": "Exception",
"value": "test exception",
}
],
"$exception_personURL": "https://aloha.com/project/random_key/person/distinct_id",
},
)
def test_basic_capture_exception_with_correct_host_generation_for_server_hosts(self):
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, host="https://app.posthog.com")
exception = Exception("test exception")
client.capture_exception(exception, "distinct_id")
self.assertTrue(patch_capture.called)
capture_call = patch_capture.call_args[0]
self.assertEqual(capture_call[0], "distinct_id")
self.assertEqual(capture_call[1], "$exception")
self.assertEqual(
capture_call[2],
{
"$exception_type": "Exception",
"$exception_message": "test exception",
"$exception_list": [
{
"mechanism": {"type": "generic", "handled": True},
"module": None,
"type": "Exception",
"value": "test exception",
}
],
"$exception_personURL": "https://app.posthog.com/project/random_key/person/distinct_id",
},
)
def test_basic_capture_exception_with_no_exception_given(self):
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
client = self.client
try:
raise Exception("test exception")
except Exception:
client.capture_exception(distinct_id="distinct_id")
self.assertTrue(patch_capture.called)
capture_call = patch_capture.call_args[0]
self.assertEqual(capture_call[0], "distinct_id")
self.assertEqual(capture_call[1], "$exception")
self.assertEqual(capture_call[2]["$exception_type"], "Exception")
self.assertEqual(capture_call[2]["$exception_message"], "test exception")
self.assertEqual(capture_call[2]["$exception_list"][0]["mechanism"]["type"], "generic")
self.assertEqual(capture_call[2]["$exception_list"][0]["mechanism"]["handled"], True)
self.assertEqual(capture_call[2]["$exception_list"][0]["module"], None)
self.assertEqual(capture_call[2]["$exception_list"][0]["type"], "Exception")
self.assertEqual(capture_call[2]["$exception_list"][0]["value"], "test exception")
self.assertEqual(
capture_call[2]["$exception_list"][0]["stacktrace"]["type"],
"raw",
)
self.assertEqual(
capture_call[2]["$exception_list"][0]["stacktrace"]["frames"][0]["filename"],
"posthog/test/test_client.py",
)
self.assertEqual(
capture_call[2]["$exception_list"][0]["stacktrace"]["frames"][0]["function"],
"test_basic_capture_exception_with_no_exception_given",
)
self.assertEqual(
capture_call[2]["$exception_list"][0]["stacktrace"]["frames"][0]["module"], "posthog.test.test_client"
)
self.assertEqual(capture_call[2]["$exception_list"][0]["stacktrace"]["frames"][0]["in_app"], True)
def test_basic_capture_exception_with_no_exception_happening(self):
with mock.patch.object(Client, "capture", return_value=None) as patch_capture:
with self.assertLogs("posthog", level="WARNING") as logs:
client = self.client
client.capture_exception()
self.assertFalse(patch_capture.called)
self.assertEqual(
logs.output[0],
"WARNING:posthog:No exception information available",
)
@mock.patch("posthog.client.decide")
def test_basic_capture_with_feature_flags(self, patch_decide):
patch_decide.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
+2 -2
View File
@@ -29,7 +29,7 @@ def test_excepthook(tmpdir):
assert b"LOL" in output
assert b"DEBUG:posthog:data uploaded successfully" in output
assert (
b'"$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"filename": "app.py", "abs_path"'
b'"$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"platform": "python", "filename": "app.py", "abs_path"'
in output
)
@@ -58,6 +58,6 @@ def test_trying_to_use_django_integration(tmpdir):
assert b"LOL" in output
assert b"DEBUG:posthog:data uploaded successfully" in output
assert (
b'"$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"filename": "app.py", "abs_path"'
b'"$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"platform": "python", "filename": "app.py", "abs_path"'
in output
)
+87 -2
View File
@@ -1632,9 +1632,10 @@ class TestLocalEvaluation(unittest.TestCase):
)
self.assertEqual(patch_decide.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.decide")
def test_boolean_feature_flag_payload_decide(self, patch_decide):
patch_decide.return_value = {"featureFlagPayloads": {"person-flag": 300}}
def test_boolean_feature_flag_payload_decide(self, patch_decide, patch_capture):
patch_decide.return_value = {"featureFlags": {"person-flag": True}, "featureFlagPayloads": {"person-flag": 300}}
self.assertEqual(
self.client.get_feature_flag_payload(
"person-flag", "some-distinct-id", person_properties={"region": "USA"}
@@ -1649,6 +1650,8 @@ class TestLocalEvaluation(unittest.TestCase):
300,
)
self.assertEqual(patch_decide.call_count, 2)
self.assertEqual(patch_capture.call_count, 1)
patch_capture.reset_mock()
@mock.patch("posthog.client.decide")
def test_multivariate_feature_flag_payloads(self, patch_decide):
@@ -2334,6 +2337,88 @@ class TestCaptureCalls(unittest.TestCase):
disable_geoip=None,
)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.decide")
def test_capture_is_called_in_get_feature_flag_payload(self, patch_decide, patch_capture):
patch_decide.return_value = {
"featureFlags": {"person-flag": True},
"featureFlagPayloads": {"person-flag": 300},
}
client = Client(api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "person-flag",
"is_simple_flag": False,
"active": True,
"filters": {
"groups": [
{
"properties": [{"key": "region", "value": "USA"}],
"rollout_percentage": 100,
}
],
},
}
]
# Call get_feature_flag_payload with match_value=None to trigger get_feature_flag
client.get_feature_flag_payload(
key="person-flag", distinct_id="some-distinct-id", person_properties={"region": "USA", "name": "Aloha"}
)
# Assert that capture was called once, with the correct parameters
self.assertEqual(patch_capture.call_count, 1)
patch_capture.assert_called_with(
"some-distinct-id",
"$feature_flag_called",
{
"$feature_flag": "person-flag",
"$feature_flag_response": True,
"$feature_flag_payload": 300,
"locally_evaluated": False,
"$feature/person-flag": True,
},
groups={},
disable_geoip=None,
)
# Reset mocks for further tests
patch_capture.reset_mock()
patch_decide.reset_mock()
# Call get_feature_flag_payload again for the same user; capture should not be called again because we've already reported an event for this distinct_id + flag
client.get_feature_flag_payload(
key="person-flag", distinct_id="some-distinct-id", person_properties={"region": "USA", "name": "Aloha"}
)
self.assertEqual(patch_capture.call_count, 0)
patch_capture.reset_mock()
# Call get_feature_flag_payload for a different user; capture should be called
client.get_feature_flag_payload(
key="person-flag", distinct_id="some-distinct-id2", person_properties={"region": "USA", "name": "Aloha"}
)
self.assertEqual(patch_capture.call_count, 1)
patch_capture.assert_called_with(
"some-distinct-id2",
"$feature_flag_called",
{
"$feature_flag": "person-flag",
"$feature_flag_response": True,
"$feature_flag_payload": 300,
"locally_evaluated": False,
"$feature/person-flag": True,
},
groups={},
disable_geoip=None,
)
patch_capture.reset_mock()
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.decide")
def test_disable_geoip_get_flag_capture_call(self, patch_decide, patch_capture):
+1 -1
View File
@@ -1,4 +1,4 @@
VERSION = "3.6.3"
VERSION = "3.7.4"
if __name__ == "__main__":
print(VERSION, end="") # noqa: T201