Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81fb40cde3 | ||
|
|
001ac2f65b | ||
|
|
b5f5dade80 | ||
|
|
29a74b090a | ||
|
|
24318fe57c | ||
|
|
e14fe3824d | ||
|
|
894c008b5d | ||
|
|
867d7d0aed | ||
|
|
afb978e1f8 | ||
|
|
d73abc6d93 | ||
|
|
a96ed6616c | ||
|
|
fb8677bd18 | ||
|
|
369b530db9 | ||
|
|
451cbc85be | ||
|
|
af050f03f8 | ||
|
|
37beabd1d3 | ||
|
|
9214babf55 | ||
|
|
a2ea0bbfe7 | ||
|
|
91f8c39c53 | ||
|
|
c689530d82 | ||
|
|
ad2ef1ef39 | ||
|
|
54a0d98d6f | ||
|
|
6ba2d8af81 | ||
|
|
d6c97b371f | ||
|
|
ef7c513b59 | ||
|
|
c877700b43 | ||
|
|
0fa03d328e | ||
|
|
0639f0f31a | ||
|
|
f8a5f109c1 | ||
|
|
005709ff76 | ||
|
|
100e0c08a4 | ||
|
|
2791ee0175 | ||
|
|
07a5261f0f | ||
|
|
f409515bfa | ||
|
|
c50b7b2e78 | ||
|
|
cbc74eb1d4 | ||
|
|
786ef4d612 | ||
|
|
cd351653c0 | ||
|
|
ea320eab5a | ||
|
|
ea56c52be4 | ||
|
|
6d3e769256 | ||
|
|
ef6ef12d81 | ||
|
|
0dab1e7f34 | ||
|
|
982af288d2 | ||
|
|
cbdb9adf40 | ||
|
|
664696f3eb | ||
|
|
ad27f5ef47 | ||
|
|
c5d0280e3b | ||
|
|
af034835aa | ||
|
|
649e04335d | ||
|
|
f21a5984e3 | ||
|
|
b64c2d97f9 | ||
|
|
16cd501674 | ||
|
|
ddfa1ee93c | ||
|
|
4bbccef790 | ||
|
|
6fe1102993 | ||
|
|
af561acd9d | ||
|
|
b8073984eb | ||
|
|
082a0e67cd | ||
|
|
da9b730da1 | ||
|
|
381dc1c9ab | ||
|
|
e24b1038f3 | ||
|
|
f1b454b10d | ||
|
|
304218f78f | ||
|
|
671af32b02 | ||
|
|
50a177e39a | ||
|
|
b7dfe68da7 | ||
|
|
b997548400 | ||
|
|
e54f8a3811 | ||
|
|
e38bfb5e49 | ||
|
|
b2c4043e09 | ||
|
|
f2f4fa6874 | ||
|
|
748553236d | ||
|
|
15a9c1b861 | ||
|
|
9972fa3f0b | ||
|
|
99422f02ab | ||
|
|
1dcfb99413 | ||
|
|
4303f78759 | ||
|
|
1ec7afdc5f | ||
|
|
3a8876d1a9 | ||
|
|
a338772b84 | ||
|
|
08bf769a6c | ||
|
|
8c64f43476 | ||
|
|
bb5f1b7130 | ||
|
|
34540a5d93 | ||
|
|
0e502fa086 | ||
|
|
aa04cd6a7f | ||
|
|
c141ab0c77 | ||
|
|
997f8228da | ||
|
|
183ecc8edf | ||
|
|
2ff0810472 | ||
|
|
2cb5f02b1b | ||
|
|
450d96a5f1 | ||
|
|
5b4bab5fd7 | ||
|
|
156170e34b | ||
|
|
141ced6402 | ||
|
|
b1c193bcb1 | ||
|
|
5e6f642650 | ||
|
|
c892dc7a8a | ||
|
|
52e023072e | ||
|
|
19d0562d41 | ||
|
|
ccbd9c23b7 | ||
|
|
71919f7125 | ||
|
|
4d9a4b689e | ||
|
|
890bb4b5f8 | ||
|
|
98a5bca68d | ||
|
|
898859b546 | ||
|
|
c3ec12c4a6 | ||
|
|
65bf587fee | ||
|
|
6b28b80b7a | ||
|
|
466bc358b2 | ||
|
|
2508f0193d | ||
|
|
7e6009bc3e | ||
|
|
63d8c8dc0f | ||
|
|
2d418d1224 | ||
|
|
a70ed0506e | ||
|
|
b7817791aa | ||
|
|
b7c71b09aa | ||
|
|
4dacaed27b | ||
|
|
6947dc6635 | ||
|
|
44dddb1a4a | ||
|
|
413fb5af77 | ||
|
|
7f087e25b3 | ||
|
|
ae38bbabc1 | ||
|
|
cb75671cd9 | ||
|
|
edc0d1a1ed | ||
|
|
82a7f7c171 | ||
|
|
698f79b257 | ||
|
|
947ee46661 | ||
|
|
d5bac13022 | ||
|
|
f73774f616 | ||
|
|
6ecd4f7efb | ||
|
|
864d718412 | ||
|
|
5a5326b66a | ||
|
|
3289cfe3af | ||
|
|
2fb3c980a5 | ||
|
|
f8efe050f2 | ||
|
|
f600b0e7de | ||
|
|
4ef129d646 | ||
|
|
633504ea56 | ||
|
|
1830d7917d | ||
|
|
70da872508 | ||
|
|
ae0a4e7b57 | ||
|
|
66bf50c6ac | ||
|
|
5f5b68cbbb | ||
|
|
aa494972b0 | ||
|
|
59174b81b5 | ||
|
|
25b221cc2e | ||
|
|
18f3f89759 | ||
|
|
a13acafb81 | ||
|
|
eb3b524d72 | ||
|
|
d5da699816 | ||
|
|
b521c2acbd | ||
|
|
fe0a680e3f | ||
|
|
eba2b112dc | ||
|
|
5c2897a767 | ||
|
|
c85e361642 | ||
|
|
c2afc4d27a | ||
|
|
4625267f16 | ||
|
|
2e4ec31e8e | ||
|
|
5b853fe9a1 | ||
|
|
4521db34a1 | ||
|
|
5c84dcfccf | ||
|
|
c4ca591227 | ||
|
|
00c476328c | ||
|
|
7e018382ab | ||
|
|
312140ff25 | ||
|
|
7db912f06f | ||
|
|
3b07b6ed47 | ||
|
|
8b136d433b | ||
|
|
ce2a9bebc6 | ||
|
|
6fb9e6d4c0 | ||
|
|
c62e60b9d3 | ||
|
|
93d10d4d10 | ||
|
|
a03e2a066f | ||
|
|
fc8812806c | ||
|
|
9b43c0544d | ||
|
|
ed12f1b208 | ||
|
|
af6882ea00 | ||
|
|
8253c66412 | ||
|
|
11461049fd | ||
|
|
74ea024265 | ||
|
|
265de15b6b | ||
|
|
62618b7547 | ||
|
|
849a81decf | ||
|
|
c08d05d894 | ||
|
|
c07e7c87ee | ||
|
|
0a3c06086a | ||
|
|
8ce657630f | ||
|
|
28ef3c7e8e | ||
|
|
00d2f9066f | ||
|
|
3a07d907f7 | ||
|
|
bd08b51ac8 | ||
|
|
a5c738f2cb | ||
|
|
e337ff51a9 | ||
|
|
4ab73eeac2 | ||
|
|
7635457f90 | ||
|
|
041adad7fb | ||
|
|
2c972ab7bc | ||
|
|
7b16bcb62e | ||
|
|
dc765e6ee1 | ||
|
|
f36daa199f | ||
|
|
44a8637e5a | ||
|
|
f874fe53cb | ||
|
|
f84ad25963 | ||
|
|
f05e716224 | ||
|
|
a74b8a7fcc | ||
|
|
38eba50f21 | ||
|
|
beac1539dc | ||
|
|
0375c7eb7f | ||
|
|
9705b53da6 | ||
|
|
b91c5acc95 | ||
|
|
50e1df93a0 | ||
|
|
0634ee8cbd | ||
|
|
99c72f6ef0 | ||
|
|
30b8ec24e7 | ||
|
|
0692b846af | ||
|
|
49de45888e | ||
|
|
53de042ffb | ||
|
|
7c7287f303 | ||
|
|
198aecf9c7 | ||
|
|
277868e12a | ||
|
|
833833bfbc | ||
|
|
999aead498 | ||
|
|
07dbf46fbb | ||
|
|
4aa32ee6fa | ||
|
|
869f87af26 | ||
|
|
9a12b71dcc | ||
|
|
8e05c1951c | ||
|
|
91ac7bc82f | ||
|
|
ef0b92d9ff | ||
|
|
267ab0440b | ||
|
|
cd510cc386 | ||
|
|
c377be7140 | ||
|
|
113de04672 | ||
|
|
2adef77a18 | ||
|
|
211075dabe | ||
|
|
a38360d087 | ||
|
|
e6e4368852 | ||
|
|
93623e8a5a | ||
|
|
bead19a9c2 | ||
|
|
cc0568b0a6 | ||
|
|
ddd744b51a | ||
|
|
5c4cc35bfe | ||
|
|
cde24f1525 | ||
|
|
fdc154deb9 | ||
|
|
65abf5f34a | ||
|
|
e4358be3d2 | ||
|
|
5e73f7b018 | ||
|
|
2898d08d8f | ||
|
|
6afa42bb2e | ||
|
|
9886e6fbaa | ||
|
|
b468a9064a | ||
|
|
7576234533 | ||
|
|
7cd12b1421 | ||
|
|
749f7b5940 | ||
|
|
98e82f9ae6 | ||
|
|
32ad34f8b1 | ||
|
|
e0ae599d97 | ||
|
|
07396e64a2 | ||
|
|
0e264a8cb2 | ||
|
|
6785720db1 | ||
|
|
97955fe0c9 | ||
|
|
1b21afe570 | ||
|
|
af424c833d | ||
|
|
40b5bea6c6 | ||
|
|
230d9c2dbb | ||
|
|
635db706e8 | ||
|
|
4b9667ccb3 | ||
|
|
80a38aca2c | ||
|
|
5159528070 | ||
|
|
e72101c03b | ||
|
|
cf886de444 | ||
|
|
8196da17cd | ||
|
|
e6116112f1 | ||
|
|
fac1a3a16d | ||
|
|
2679ca6f38 | ||
|
|
508182500b | ||
|
|
a543fb74f9 | ||
|
|
9916f64e8e | ||
|
|
22489ffe9f | ||
|
|
289a26c2fe | ||
|
|
ccee7b666b | ||
|
|
5302613832 | ||
|
|
a7804baa52 | ||
|
|
d226d469e3 | ||
|
|
c078c0944c | ||
|
|
80523e03a2 | ||
|
|
e76bfb16ee | ||
|
|
57e30aca08 | ||
|
|
36b87e5d0b | ||
|
|
51f3cf6d06 | ||
|
|
34ee449a7e | ||
|
|
ce5aff1fe8 | ||
|
|
93ae071693 | ||
|
|
5f1ba52ad5 | ||
|
|
5d6672315c | ||
|
|
f490dd571a | ||
|
|
78c7cb9ab2 | ||
|
|
f0fd79841d | ||
|
|
59dba76dea | ||
|
|
ccde499d53 | ||
|
|
62ea178d23 | ||
|
|
0ec3a21498 | ||
|
|
796e5df626 | ||
|
|
7f20286af7 | ||
|
|
594b5f37e6 | ||
|
|
a6ebb02d43 | ||
|
|
9e31b3fdbb | ||
|
|
85feb84d87 | ||
|
|
6ca9b0002a | ||
|
|
0723332b09 | ||
|
|
c9ede4b953 | ||
|
|
f7626714e2 | ||
|
|
8ac068d9a7 | ||
|
|
521a508c0b | ||
|
|
589475336b | ||
|
|
bd6f14a7cd | ||
|
|
41b3668b77 | ||
|
|
c333f434fd | ||
|
|
4d3707d31c | ||
|
|
d4fa6c6df7 | ||
|
|
69d416e897 | ||
|
|
e37c75e509 | ||
|
|
0617277bd0 | ||
|
|
2b620e0f68 | ||
|
|
b239002483 | ||
|
|
e706401495 | ||
|
|
7596674578 | ||
|
|
b37047ab4d | ||
|
|
9df7af697b | ||
|
|
16685b1875 | ||
|
|
953140e207 | ||
|
|
280e16fb2e | ||
|
|
4050c060d7 | ||
|
|
fa2e69d158 | ||
|
|
4c40410f4c | ||
|
|
5261de0d29 | ||
|
|
ca2673ddc5 | ||
|
|
2262a155c6 | ||
|
|
223e12efdd | ||
|
|
1089749f72 | ||
|
|
69eb0696c0 | ||
|
|
a26908d48d | ||
|
|
a6350c9362 | ||
|
|
a702f9ed94 | ||
|
|
0813f26a1d | ||
|
|
7ecdba1a5d | ||
|
|
8ecd73336b | ||
|
|
6d80cadf3c | ||
|
|
5354f9aec8 | ||
|
|
55ff3d94af | ||
|
|
d06c99a00c | ||
|
|
f914eb4254 | ||
|
|
8e16d82553 | ||
|
|
0341dbc7db | ||
|
|
46f8519310 | ||
|
|
37c7360b2c | ||
|
|
1901eafbd9 | ||
|
|
07dfe8704f | ||
|
|
aa435f373f | ||
|
|
fc9d579791 | ||
|
|
93636181db | ||
|
|
78658eb034 | ||
|
|
3dbfb2f826 | ||
|
|
8761d1745a | ||
|
|
c9ae5f73ee | ||
|
|
6f6a25f063 | ||
|
|
dce9af2fae | ||
|
|
92c80f75bd | ||
|
|
960941f534 | ||
|
|
fe3907d5f7 | ||
|
|
54b6e83947 | ||
|
|
e93e0f9db0 | ||
|
|
39670b2f01 | ||
|
|
caa33cd1b9 | ||
|
|
7098a2d29a | ||
|
|
aac318f81f | ||
|
|
6882dbffbc | ||
|
|
adf10b3d86 | ||
|
|
37b462120f | ||
|
|
268384611a | ||
|
|
c4cab81f90 | ||
|
|
1f51afc1b4 | ||
|
|
57bc6320e2 | ||
|
|
6fa49c3f22 | ||
|
|
c3695c5f13 | ||
|
|
e6b84635f9 | ||
|
|
450383943b | ||
|
|
053f20b846 | ||
|
|
03f0223697 | ||
|
|
df5f0003cd | ||
|
|
9fed4e34f5 | ||
|
|
e5a4c896d0 | ||
|
|
421de30e0a | ||
|
|
84c05f2266 | ||
|
|
c52dfdd81e | ||
|
|
b42214f804 | ||
|
|
cd66eacac6 | ||
|
|
4f2bad3fd2 | ||
|
|
467b04403a | ||
|
|
3bf1b5666c | ||
|
|
fdbebf4147 | ||
|
|
0d4a1c9444 | ||
|
|
c8526e15da | ||
|
|
02a19c3d44 | ||
|
|
a8b0a7e12c | ||
|
|
730a84ae5e | ||
|
|
28e76606a9 | ||
|
|
e34cf44205 | ||
|
|
45ae2891ce | ||
|
|
16e4d7e433 | ||
|
|
58d9e63834 | ||
|
|
ca684c5d2c | ||
|
|
a9e427bac4 | ||
|
|
9c6595f66d | ||
|
|
8df4c44be3 | ||
|
|
34de69d736 | ||
|
|
ae3edee81f | ||
|
|
e23c5a481b | ||
|
|
341e6763d3 | ||
|
|
6ea2efcd9c | ||
|
|
a514ff8ff2 | ||
|
|
023c33a7de |
+6
-6
@@ -1,11 +1,11 @@
|
||||
# PostHog API Configuration
|
||||
# Hanzo Insights API Configuration
|
||||
# Copy this file to .env and update with your actual values
|
||||
|
||||
# Your project API key (found on the /setup page in PostHog)
|
||||
POSTHOG_PROJECT_API_KEY=phc_your_project_api_key_here
|
||||
# Your project API key (found on the setup page in Insights)
|
||||
INSIGHTS_PROJECT_API_KEY=hi_your_project_api_key_here
|
||||
|
||||
# Your personal API key (for local evaluation and other advanced features)
|
||||
POSTHOG_PERSONAL_API_KEY=phx_your_personal_api_key_here
|
||||
INSIGHTS_PERSONAL_API_KEY=phx_your_personal_api_key_here
|
||||
|
||||
# PostHog host URL (remove this line if using posthog.com)
|
||||
POSTHOG_HOST=http://localhost:8000
|
||||
# Insights host URL (remove this line if using insights.hanzo.ai)
|
||||
INSIGHTS_HOST=http://localhost:8000
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
pypi/posthog: patch
|
||||
---
|
||||
|
||||
feat(llma): support fetching versioned prompts from the prompts sdk
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
# Before Send Hook
|
||||
|
||||
The `before_send` parameter allows you to modify or filter events before they are sent to PostHog. This is useful for:
|
||||
The `before_send` parameter allows you to modify or filter events before they are sent to Insights. This is useful for:
|
||||
|
||||
- **Privacy**: Removing or masking sensitive data (PII)
|
||||
- **Filtering**: Dropping unwanted events (test events, internal users, etc.)
|
||||
@@ -10,12 +10,12 @@ The `before_send` parameter allows you to modify or filter events before they ar
|
||||
## Basic Usage
|
||||
|
||||
```python
|
||||
import posthog
|
||||
import hanzo_insights
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
def my_before_send(event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Process event before sending to PostHog.
|
||||
Process event before sending to Insights.
|
||||
|
||||
Args:
|
||||
event: The event dictionary containing 'event', 'distinct_id', 'properties', etc.
|
||||
@@ -27,7 +27,7 @@ def my_before_send(event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return event
|
||||
|
||||
# Initialize client with before_send hook
|
||||
client = posthog.Client(
|
||||
client = hanzo_insights.Client(
|
||||
api_key="your-project-api-key",
|
||||
before_send=my_before_send
|
||||
)
|
||||
@@ -166,7 +166,7 @@ def should_drop_event(event: dict[str, Any]) -> bool:
|
||||
|
||||
## Error Handling
|
||||
|
||||
If your `before_send` function raises an exception, PostHog will:
|
||||
If your `before_send` function raises an exception, Insights will:
|
||||
|
||||
1. Log the error
|
||||
2. Continue with the original, unmodified event
|
||||
@@ -184,7 +184,7 @@ def risky_before_send(event: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||
## Complete Example
|
||||
|
||||
```python
|
||||
import posthog
|
||||
import hanzo_insights
|
||||
from typing import Optional, Any
|
||||
import re
|
||||
|
||||
@@ -227,7 +227,7 @@ def production_before_send(event: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||
return event # Return original event on error
|
||||
|
||||
# Usage
|
||||
client = posthog.Client(
|
||||
client = hanzo_insights.Client(
|
||||
api_key="your-api-key",
|
||||
before_send=production_before_send
|
||||
)
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# posthog
|
||||
|
||||
## 7.9.7 — 2026-03-05
|
||||
|
||||
### Patch changes
|
||||
|
||||
- [b206669](https://github.com/posthog/posthog-python/commit/b206669bf62c923346ad28881dc4694d933ca424) fix(llma): use distinct_id from outer context if not provided, fix $process_person_profile for context-based identity — Thanks @ethanporcaro for your first contribution 🎉!
|
||||
- [a99c7d7](https://github.com/posthog/posthog-python/commit/a99c7d73b1e0ef1f35d856c82ace21237ee253a3) Add warning log for local flag evaluation cold start — Thanks @dmarticus!
|
||||
|
||||
## 7.9.6 — 2026-03-02
|
||||
|
||||
### Patch changes
|
||||
|
||||
- [8d83315](https://github.com/posthog/posthog-python/commit/8d83315b67c21eb9e7d6c17bae27ada98ca2643d) add PROPERTY_OPERATORS constant for match_property — Thanks @dmarticus!
|
||||
|
||||
## 7.9.5 — 2026-03-02
|
||||
|
||||
### Patch changes
|
||||
|
||||
- [830244b](https://github.com/posthog/posthog-python/commit/830244bd409b1992ae2e49610f8f87d2cdfc8096) add semver targeting support to local evaluation — Thanks @dmarticus!
|
||||
|
||||
## 7.9.4 — 2026-02-25
|
||||
|
||||
### Patch changes
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# LLM.md - Hanzo Insights Python SDK
|
||||
|
||||
## Overview
|
||||
Integrate Hanzo Insights into any Python application. Package name: `hanzo-insights` on PyPI.
|
||||
|
||||
## Tech Stack
|
||||
- **Language**: Python 3.10+
|
||||
- **Package**: `hanzo_insights` (import name), `hanzo-insights` (pip name)
|
||||
|
||||
## Build & Run
|
||||
```bash
|
||||
uv sync
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## Structure
|
||||
```
|
||||
posthog-python/
|
||||
hanzo_insights/ # Main package
|
||||
__init__.py # Module-level API, Insights class
|
||||
client.py # Client class
|
||||
ai/ # AI provider integrations (OpenAI, Anthropic, Gemini, LangChain)
|
||||
integrations/ # Framework integrations (Django middleware)
|
||||
test/ # Tests
|
||||
examples/
|
||||
integration_tests/
|
||||
pyproject.toml # Package config (name: hanzo-insights)
|
||||
setup.py # Legacy setup
|
||||
```
|
||||
|
||||
## Key Files
|
||||
- `pyproject.toml` -- Package config, dependencies, test config
|
||||
- `hanzo_insights/__init__.py` -- Public API surface
|
||||
- `hanzo_insights/client.py` -- Client implementation
|
||||
|
||||
## Rebrand Notes
|
||||
- Main class: `Insights` (no backward compat aliases)
|
||||
- Django middleware: `InsightsContextMiddleware` (no backward compat aliases)
|
||||
- OpenAI Agents: `InsightsTracingProcessor` (no backward compat aliases)
|
||||
- `$lib` protocol value: `insights-python`
|
||||
- Ingestion URLs: `us.i.insights.hanzo.ai` / `eu.i.insights.hanzo.ai`
|
||||
- AI wrapper kwargs: `insights_*` (e.g. `insights_distinct_id`, `insights_trace_id`)
|
||||
- Exception attrs: `__insights_exception_captured`, `__insights_exception_uuid`
|
||||
- Context var: `insights_context_stack`
|
||||
- Redis prefix: `insights:flags:`
|
||||
- Redaction sentinels: `$$_insights_redacted_*`, `$$_insights_value_too_long_*`
|
||||
- Django settings: `INSIGHTS_MW_*` only (no `POSTHOG_MW_*` fallback)
|
||||
- Django headers: `X-INSIGHTS-SESSION-ID`, `X-INSIGHTS-DISTINCT-ID` only
|
||||
@@ -9,37 +9,38 @@ build_release:
|
||||
rm -rf dist/*
|
||||
python setup.py sdist bdist_wheel
|
||||
|
||||
# Builds the `posthoganalytics` PyPI package, which is a mirror of `posthog`
|
||||
# published under a different name for internal use by posthog/posthog.
|
||||
# Builds the `posthoganalytics` PyPI package, which is a mirror of `hanzo_insights`
|
||||
# published under a different name for backward compatibility with the upstream
|
||||
# posthog/posthog project.
|
||||
#
|
||||
# The process works in three phases:
|
||||
# 1. posthog -> posthoganalytics: Copy the source, rewrite all imports,
|
||||
# remove the original posthog/ dir, and build the dist.
|
||||
# 2. posthoganalytics -> posthog: Reverse the import rewrites, copy
|
||||
# everything back into posthog/, and clean up.
|
||||
# 1. hanzo_insights -> posthoganalytics: Copy the source, rewrite all imports,
|
||||
# remove the original hanzo_insights/ dir, and build the dist.
|
||||
# 2. posthoganalytics -> hanzo_insights: Reverse the import rewrites, copy
|
||||
# everything back into hanzo_insights/, and clean up.
|
||||
# 3. Restore pyproject.toml from backup (setup_analytics.py modifies it).
|
||||
#
|
||||
# This ensures the working tree is left in the same state it started in.
|
||||
#
|
||||
# NOTE: This target clears dist/ before building. In the release workflow,
|
||||
# `build_release` (posthog) must be published BEFORE running this target,
|
||||
# otherwise the posthog dist artifacts will be lost.
|
||||
# `build_release` (hanzo_insights) must be published BEFORE running this target,
|
||||
# otherwise the hanzo_insights dist artifacts will be lost.
|
||||
build_release_analytics:
|
||||
rm -rf dist
|
||||
rm -rf build
|
||||
rm -rf posthoganalytics
|
||||
mkdir posthoganalytics
|
||||
cp -r posthog/* posthoganalytics/
|
||||
find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthog /from posthoganalytics /g' {} \;
|
||||
find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthog\./from posthoganalytics\./g' {} \;
|
||||
cp -r hanzo_insights/* posthoganalytics/
|
||||
find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from hanzo_insights /from posthoganalytics /g' {} \;
|
||||
find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from hanzo_insights\./from posthoganalytics\./g' {} \;
|
||||
find ./posthoganalytics -name "*.bak" -delete
|
||||
rm -rf posthog
|
||||
rm -rf hanzo_insights
|
||||
python setup_analytics.py sdist bdist_wheel
|
||||
mkdir posthog
|
||||
find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthoganalytics /from posthog /g' {} \;
|
||||
find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthoganalytics\./from posthog\./g' {} \;
|
||||
mkdir hanzo_insights
|
||||
find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthoganalytics /from hanzo_insights /g' {} \;
|
||||
find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthoganalytics\./from hanzo_insights\./g' {} \;
|
||||
find ./posthoganalytics -name "*.bak" -delete
|
||||
cp -r posthoganalytics/* posthog/
|
||||
cp -r posthoganalytics/* hanzo_insights/
|
||||
rm -rf posthoganalytics
|
||||
rm -f pyproject.toml
|
||||
cp pyproject.toml.backup pyproject.toml
|
||||
@@ -54,14 +55,14 @@ prep_local:
|
||||
cp -r . ../posthog-python-local/
|
||||
cd ../posthog-python-local && rm -rf dist build posthoganalytics .git
|
||||
cd ../posthog-python-local && mkdir posthoganalytics
|
||||
cd ../posthog-python-local && cp -r posthog/* posthoganalytics/
|
||||
cd ../posthog-python-local && find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthog /from posthoganalytics /g' {} \;
|
||||
cd ../posthog-python-local && find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthog\./from posthoganalytics\./g' {} \;
|
||||
cd ../posthog-python-local && cp -r hanzo_insights/* posthoganalytics/
|
||||
cd ../posthog-python-local && find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from hanzo_insights /from posthoganalytics /g' {} \;
|
||||
cd ../posthog-python-local && find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from hanzo_insights\./from posthoganalytics\./g' {} \;
|
||||
cd ../posthog-python-local && find ./posthoganalytics -name "*.bak" -delete
|
||||
cd ../posthog-python-local && rm -rf posthog
|
||||
cd ../posthog-python-local && rm -rf hanzo_insights
|
||||
cd ../posthog-python-local && sed -i.bak 's/from version import VERSION/from posthoganalytics.version import VERSION/' setup_analytics.py
|
||||
cd ../posthog-python-local && rm setup_analytics.py.bak
|
||||
cd ../posthog-python-local && sed -i.bak 's/"posthog"/"posthoganalytics"/' setup.py
|
||||
cd ../posthog-python-local && sed -i.bak 's/"hanzo_insights"/"posthoganalytics"/' setup.py
|
||||
cd ../posthog-python-local && rm setup.py.bak
|
||||
cd ../posthog-python-local && python -c "import setup_analytics" 2>/dev/null || true
|
||||
@echo "Local copy created at ../posthog-python-local"
|
||||
|
||||
@@ -1,41 +1,51 @@
|
||||
# PostHog Python
|
||||
# Hanzo Insights Python SDK
|
||||
|
||||
<p align="center">
|
||||
<img alt="posthoglogo" src="https://user-images.githubusercontent.com/65415371/205059737-c8a4f836-4889-4654-902e-f302b187b6a0.png">
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://pypi.org/project/posthog/"><img alt="pypi installs" src="https://img.shields.io/pypi/v/posthog"/></a>
|
||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/posthog/posthog-python">
|
||||
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/posthog/posthog-python"/>
|
||||
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/posthog/posthog-python"/>
|
||||
</p>
|
||||
Integrate [Hanzo Insights](https://insights.hanzo.ai) into any Python application.
|
||||
|
||||
Please see the [Python integration docs](https://posthog.com/docs/integrations/python-integration) for details.
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hanzo-insights
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from hanzo_insights import Insights
|
||||
|
||||
client = Insights('<your_project_api_key>', host='https://insights.hanzo.ai')
|
||||
|
||||
# Capture an event
|
||||
client.capture('user_123', 'purchase', properties={'product': 'widget'})
|
||||
|
||||
# Feature flags
|
||||
if client.feature_enabled('new-checkout', 'user_123'):
|
||||
show_new_checkout()
|
||||
```
|
||||
|
||||
## Module-level usage
|
||||
|
||||
```python
|
||||
import hanzo_insights
|
||||
|
||||
hanzo_insights.api_key = '<your_project_api_key>'
|
||||
hanzo_insights.host = 'https://insights.hanzo.ai'
|
||||
|
||||
hanzo_insights.capture('movie_played', distinct_id='user_123', properties={'movie_id': '42'})
|
||||
hanzo_insights.shutdown()
|
||||
```
|
||||
|
||||
## Python Version Support
|
||||
|
||||
| SDK Version | Python Versions Supported | Notes |
|
||||
| ------------- | ---------------------------- | -------------------------- |
|
||||
| 7.3.1+ | 3.10, 3.11, 3.12, 3.13, 3.14 | Added Python 3.14 support |
|
||||
| 7.0.0 - 7.0.1 | 3.10, 3.11, 3.12, 3.13 | Dropped Python 3.9 support |
|
||||
| 4.0.1 - 6.x | 3.9, 3.10, 3.11, 3.12, 3.13 | Python 3.9+ required |
|
||||
| SDK Version | Python Versions Supported |
|
||||
| -------------- | ----------------------------- |
|
||||
| 7.3.1+ | 3.10, 3.11, 3.12, 3.13, 3.14 |
|
||||
| 7.0.0 - 7.0.1 | 3.10, 3.11, 3.12, 3.13 |
|
||||
| 4.0.1 - 6.x | 3.9, 3.10, 3.11, 3.12, 3.13 |
|
||||
|
||||
## Development
|
||||
|
||||
### Testing Locally
|
||||
|
||||
We recommend using [uv](https://docs.astral.sh/uv/). It's super fast.
|
||||
|
||||
1. Run `uv venv env` (creates virtual environment called "env")
|
||||
- or `python3 -m venv env`
|
||||
2. Run `source env/bin/activate` (activates the virtual environment)
|
||||
3. Run `uv sync --extra dev --extra test` (installs the package in develop mode, along with test dependencies)
|
||||
- or `pip install -e ".[dev,test]"`
|
||||
4. you have to run `pre-commit install` to have auto linting pre commit
|
||||
5. Run `make test`
|
||||
6. To run a specific test do `pytest -k test_no_api_key`
|
||||
|
||||
## PostHog recommends `uv` so...
|
||||
We use [uv](https://docs.astral.sh/uv/).
|
||||
|
||||
```bash
|
||||
uv python install 3.12
|
||||
@@ -47,34 +57,14 @@ pre-commit install
|
||||
make test
|
||||
```
|
||||
|
||||
### Running Locally
|
||||
### Running Tests
|
||||
|
||||
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.
|
||||
|
||||
### Testing changes locally with the PostHog app
|
||||
|
||||
You can run `make prep_local`, and it'll create a new folder alongside the SDK repo one called `posthog-python-local`, which you can then import into the posthog project by changing pyproject.toml to look like this:
|
||||
|
||||
```toml
|
||||
dependencies = [
|
||||
...
|
||||
"posthoganalytics" #NOTE: no version number
|
||||
...
|
||||
]
|
||||
...
|
||||
[tools.uv.sources]
|
||||
posthoganalytics = { path = "../posthog-python-local" }
|
||||
```bash
|
||||
make test
|
||||
# or run a specific test:
|
||||
pytest -k test_no_api_key
|
||||
```
|
||||
|
||||
This'll let you build and test SDK changes fully locally, incorporating them into your local posthog app stack. It mainly takes care of the `posthog -> posthoganalytics` module renaming. You'll need to re-run `make prep_local` each time you make a change, and re-run `uv sync --active` in the posthog app project.
|
||||
## License
|
||||
|
||||
## Releasing
|
||||
|
||||
This repository uses [Sampo](https://github.com/bruits/sampo) for versioning, changelogs, and publishing to crates.io.
|
||||
|
||||
1. When making changes, include a changeset: `sampo add`
|
||||
2. Create a PR with your changes and the changeset file
|
||||
3. Add the `release` label and merge to `main`
|
||||
4. Approve the release in Slack when prompted — this triggers version bump, crates.io publish, git tag, and GitHub Release
|
||||
|
||||
You can also trigger a release manually via the workflow's `workflow_dispatch` trigger (still requires pending changesets).
|
||||
MIT
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
source bin/helpers/_utils.sh
|
||||
set_source_and_root_dir
|
||||
|
||||
flake8 posthog --ignore E501,W503
|
||||
flake8 hanzo_insights --ignore E501,W503
|
||||
mypy --no-site-packages --config-file mypy.ini . | mypy-baseline filter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#/ Usage: bin/docs
|
||||
#/ Description: Generate documentation for the PostHog Python SDK
|
||||
#/ Description: Generate documentation for the Insights Python SDK
|
||||
source bin/helpers/_utils.sh
|
||||
set_source_and_root_dir
|
||||
ensure_virtual_env
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"""
|
||||
Constants for PostHog Python SDK documentation generation.
|
||||
Constants for Insights Python SDK documentation generation.
|
||||
"""
|
||||
|
||||
from typing import Dict, Union
|
||||
from posthog.version import VERSION
|
||||
from hanzo_insights.version import VERSION
|
||||
|
||||
# Documentation generation metadata
|
||||
DOCUMENTATION_METADATA = {
|
||||
"hogRef": "0.3",
|
||||
"slugPrefix": "posthog-python",
|
||||
"specUrl": "https://github.com/PostHog/posthog-python",
|
||||
"slugPrefix": "insights-python",
|
||||
"specUrl": "https://github.com/Insights/insights-python",
|
||||
}
|
||||
|
||||
# Docstring parsing patterns for new format
|
||||
@@ -29,8 +29,8 @@ DOCSTRING_PATTERNS = {
|
||||
# Output file configuration
|
||||
OUTPUT_CONFIG: Dict[str, Union[str, int]] = {
|
||||
"output_dir": "./references",
|
||||
"filename": f"posthog-python-references-{VERSION}.json",
|
||||
"filename_latest": "posthog-python-references-latest.json",
|
||||
"filename": f"insights-python-references-{VERSION}.json",
|
||||
"filename_latest": "insights-python-references-latest.json",
|
||||
"indent": 2,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate comprehensive SDK documentation JSON from PostHog Python SDK.
|
||||
Generate comprehensive SDK documentation JSON from Insights Python SDK.
|
||||
This script inspects the code and docstrings to create documentation in the specified format.
|
||||
"""
|
||||
|
||||
@@ -337,19 +337,19 @@ def analyze_type(cls) -> dict:
|
||||
def generate_sdk_documentation():
|
||||
"""Generate complete SDK documentation in the requested format."""
|
||||
|
||||
# Import PostHog components
|
||||
import posthog
|
||||
from posthog.client import Client
|
||||
import posthog.types as types_module
|
||||
import posthog.args as args_module
|
||||
from posthog.version import VERSION
|
||||
# Import Insights components
|
||||
import hanzo_insights
|
||||
from hanzo_insights.client import Client
|
||||
import hanzo_insights.types as types_module
|
||||
import hanzo_insights.args as args_module
|
||||
from hanzo_insights.version import VERSION
|
||||
|
||||
# Main SDK info
|
||||
sdk_info = {
|
||||
"version": VERSION,
|
||||
"id": "posthog-python",
|
||||
"title": "PostHog Python SDK",
|
||||
"description": "Integrate PostHog into any python application.",
|
||||
"id": "insights-python",
|
||||
"title": "Insights Python SDK",
|
||||
"description": "Integrate Insights into any python application.",
|
||||
"slugPrefix": DOCUMENTATION_METADATA["slugPrefix"],
|
||||
"specUrl": DOCUMENTATION_METADATA["specUrl"],
|
||||
}
|
||||
@@ -357,7 +357,7 @@ def generate_sdk_documentation():
|
||||
# Collect types
|
||||
types_list = []
|
||||
|
||||
# Types from posthog.types
|
||||
# Types from hanzo_insights.types
|
||||
for name in dir(types_module):
|
||||
obj = getattr(types_module, name)
|
||||
if inspect.isclass(obj) and not name.startswith("_"):
|
||||
@@ -367,7 +367,7 @@ def generate_sdk_documentation():
|
||||
except Exception as e:
|
||||
print(f"Error analyzing type {name}: {e}")
|
||||
|
||||
# Types from posthog.args
|
||||
# Types from hanzo_insights.args
|
||||
for name in dir(args_module):
|
||||
obj = getattr(args_module, name)
|
||||
if inspect.isclass(obj) and not name.startswith("_"):
|
||||
@@ -388,26 +388,26 @@ def generate_sdk_documentation():
|
||||
# Collect classes
|
||||
classes_list = []
|
||||
|
||||
# Main PostHog class (renamed from Client)
|
||||
# Main Insights class (renamed from Client)
|
||||
client_class = analyze_class(Client)
|
||||
client_class["id"] = "PostHog"
|
||||
client_class["title"] = "PostHog"
|
||||
client_class["id"] = "Insights"
|
||||
client_class["title"] = "Insights"
|
||||
classes_list.append(client_class)
|
||||
|
||||
# Global module functions (functions callable as posthog.function_name)
|
||||
# Global module functions (functions callable as hanzo_insights.function_name)
|
||||
global_functions = []
|
||||
for func_name in dir(posthog):
|
||||
for func_name in dir(hanzo_insights):
|
||||
# Skip private functions and non-callables
|
||||
if func_name.startswith("_") or not callable(getattr(posthog, func_name)):
|
||||
if func_name.startswith("_") or not callable(getattr(hanzo_insights, func_name)):
|
||||
continue
|
||||
|
||||
func = getattr(posthog, func_name)
|
||||
# Only include functions actually defined in the posthog module (not imported)
|
||||
func = getattr(hanzo_insights, func_name)
|
||||
# Only include functions actually defined in the hanzo_insights module (not imported)
|
||||
# and exclude class references
|
||||
if (
|
||||
func_name not in ["Client", "Posthog"]
|
||||
func_name not in ["Client", "Insights"]
|
||||
and hasattr(func, "__module__")
|
||||
and func.__module__ == "posthog"
|
||||
and func.__module__ == "hanzo_insights"
|
||||
):
|
||||
try:
|
||||
func_info = analyze_function(func, func_name)
|
||||
@@ -421,8 +421,8 @@ def generate_sdk_documentation():
|
||||
classes_list.append(
|
||||
{
|
||||
"id": "PostHogModule",
|
||||
"title": "PostHog Module Functions",
|
||||
"description": "Global functions available in the PostHog module",
|
||||
"title": "Insights Module Functions",
|
||||
"description": "Global functions available in the Insights module",
|
||||
"functions": global_functions,
|
||||
}
|
||||
)
|
||||
@@ -443,7 +443,7 @@ def generate_sdk_documentation():
|
||||
|
||||
# Create the final structure
|
||||
result = {
|
||||
"id": "posthog-python",
|
||||
"id": "insights-python",
|
||||
"hogRef": DOCUMENTATION_METADATA["hogRef"],
|
||||
"info": sdk_info,
|
||||
"types": types_list,
|
||||
@@ -455,7 +455,7 @@ def generate_sdk_documentation():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Generating PostHog Python SDK documentation...")
|
||||
print("Generating Insights Python SDK documentation...")
|
||||
|
||||
try:
|
||||
documentation = generate_sdk_documentation()
|
||||
|
||||
+87
-87
@@ -1,18 +1,18 @@
|
||||
# PostHog Python library example
|
||||
# Hanzo Insights Python library example
|
||||
#
|
||||
# This script demonstrates various PostHog Python SDK capabilities including:
|
||||
# This script demonstrates various Hanzo Insights Python SDK capabilities including:
|
||||
# - Basic event capture and user identification
|
||||
# - Feature flag local evaluation
|
||||
# - Feature flag payloads
|
||||
# - Context management and tagging
|
||||
#
|
||||
# Setup:
|
||||
# 1. Copy .env.example to .env and fill in your PostHog credentials
|
||||
# 1. Copy .env.example to .env and fill in your Insights credentials
|
||||
# 2. Run this script and choose from the interactive menu
|
||||
|
||||
import os
|
||||
|
||||
import posthog
|
||||
import hanzo_insights
|
||||
|
||||
|
||||
def load_env_file():
|
||||
@@ -31,30 +31,30 @@ def load_env_file():
|
||||
load_env_file()
|
||||
|
||||
# Get configuration
|
||||
project_key = os.getenv("POSTHOG_PROJECT_API_KEY", "")
|
||||
personal_api_key = os.getenv("POSTHOG_PERSONAL_API_KEY", "")
|
||||
host = os.getenv("POSTHOG_HOST", "http://localhost:8000")
|
||||
project_key = os.getenv("INSIGHTS_PROJECT_API_KEY", "")
|
||||
personal_api_key = os.getenv("INSIGHTS_PERSONAL_API_KEY", "")
|
||||
host = os.getenv("INSIGHTS_HOST", "http://localhost:8000")
|
||||
|
||||
# Check if project key is provided (required)
|
||||
if not project_key:
|
||||
print("❌ Missing PostHog project API key!")
|
||||
print(" Please set POSTHOG_PROJECT_API_KEY environment variable")
|
||||
print("❌ Missing Insights project API key!")
|
||||
print(" Please set INSIGHTS_PROJECT_API_KEY environment variable")
|
||||
print(" or copy .env.example to .env and fill in your values")
|
||||
exit(1)
|
||||
|
||||
# Configure PostHog with credentials
|
||||
posthog.debug = False
|
||||
posthog.api_key = project_key
|
||||
posthog.project_api_key = project_key
|
||||
posthog.host = host
|
||||
posthog.poll_interval = 10
|
||||
# Configure Insights with credentials
|
||||
hanzo_insights.debug = False
|
||||
hanzo_insights.api_key = project_key
|
||||
hanzo_insights.project_api_key = project_key
|
||||
hanzo_insights.host = host
|
||||
hanzo_insights.poll_interval = 10
|
||||
|
||||
# Check if personal API key is available for local evaluation
|
||||
local_eval_available = bool(personal_api_key)
|
||||
if personal_api_key:
|
||||
posthog.personal_api_key = personal_api_key
|
||||
hanzo_insights.personal_api_key = personal_api_key
|
||||
|
||||
print("🔑 PostHog Configuration:")
|
||||
print("🔑 Insights Configuration:")
|
||||
print(f" Project API Key: {project_key[:9]}...")
|
||||
if local_eval_available:
|
||||
print(" Personal API Key: [SET]")
|
||||
@@ -63,7 +63,7 @@ else:
|
||||
print(f" Host: {host}\n")
|
||||
|
||||
# Display menu and get user choice
|
||||
print("🚀 PostHog Python SDK Demo - Choose an example to run:\n")
|
||||
print("🚀 Hanzo Insights Python SDK Demo - Choose an example to run:\n")
|
||||
print("1. Identify and capture examples")
|
||||
local_eval_note = "" if local_eval_available else " [requires personal API key]"
|
||||
print(f"2. Feature flag local evaluation examples{local_eval_note}")
|
||||
@@ -79,11 +79,11 @@ if choice == "1":
|
||||
print("IDENTIFY AND CAPTURE EXAMPLES")
|
||||
print("=" * 60)
|
||||
|
||||
posthog.debug = True
|
||||
hanzo_insights.debug = True
|
||||
|
||||
# Capture an event
|
||||
print("📊 Capturing events...")
|
||||
posthog.capture(
|
||||
hanzo_insights.capture(
|
||||
"event",
|
||||
distinct_id="distinct_id",
|
||||
properties={"property1": "value", "property2": "value"},
|
||||
@@ -92,14 +92,14 @@ if choice == "1":
|
||||
|
||||
# Alias a previous distinct id with a new one
|
||||
print("🔗 Creating alias...")
|
||||
posthog.alias("distinct_id", "new_distinct_id")
|
||||
hanzo_insights.alias("distinct_id", "new_distinct_id")
|
||||
|
||||
posthog.capture(
|
||||
hanzo_insights.capture(
|
||||
"event2",
|
||||
distinct_id="new_distinct_id",
|
||||
properties={"property1": "value", "property2": "value"},
|
||||
)
|
||||
posthog.capture(
|
||||
hanzo_insights.capture(
|
||||
"event-with-groups",
|
||||
distinct_id="new_distinct_id",
|
||||
properties={"property1": "value", "property2": "value"},
|
||||
@@ -108,28 +108,28 @@ if choice == "1":
|
||||
|
||||
# Add properties to the person
|
||||
print("👤 Identifying user...")
|
||||
posthog.set(
|
||||
hanzo_insights.set(
|
||||
distinct_id="new_distinct_id", properties={"email": "something@something.com"}
|
||||
)
|
||||
|
||||
# Add properties to a group
|
||||
print("🏢 Identifying group...")
|
||||
posthog.group_identify("company", "id:5", {"employees": 11})
|
||||
hanzo_insights.group_identify("company", "id:5", {"employees": 11})
|
||||
|
||||
# Properties set only once to the person
|
||||
print("🔒 Setting properties once...")
|
||||
posthog.set_once(
|
||||
hanzo_insights.set_once(
|
||||
distinct_id="new_distinct_id", properties={"self_serve_signup": True}
|
||||
)
|
||||
|
||||
# This will not change the property (because it was already set)
|
||||
posthog.set_once(
|
||||
hanzo_insights.set_once(
|
||||
distinct_id="new_distinct_id", properties={"self_serve_signup": False}
|
||||
)
|
||||
|
||||
print("🔄 Updating properties...")
|
||||
posthog.set(distinct_id="new_distinct_id", properties={"current_browser": "Chrome"})
|
||||
posthog.set(
|
||||
hanzo_insights.set(distinct_id="new_distinct_id", properties={"current_browser": "Chrome"})
|
||||
hanzo_insights.set(
|
||||
distinct_id="new_distinct_id", properties={"current_browser": "Firefox"}
|
||||
)
|
||||
|
||||
@@ -137,45 +137,45 @@ elif choice == "2":
|
||||
if not local_eval_available:
|
||||
print("\n❌ This example requires a personal API key for local evaluation.")
|
||||
print(
|
||||
" Set POSTHOG_PERSONAL_API_KEY environment variable to run this example."
|
||||
" Set INSIGHTS_PERSONAL_API_KEY environment variable to run this example."
|
||||
)
|
||||
posthog.shutdown()
|
||||
hanzo_insights.shutdown()
|
||||
exit(1)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("FEATURE FLAG LOCAL EVALUATION EXAMPLES")
|
||||
print("=" * 60)
|
||||
|
||||
posthog.debug = True
|
||||
hanzo_insights.debug = True
|
||||
|
||||
print("🏁 Testing basic feature flags...")
|
||||
print(
|
||||
f"beta-feature for 'distinct_id': {posthog.feature_enabled('beta-feature', 'distinct_id')}"
|
||||
f"beta-feature for 'distinct_id': {hanzo_insights.feature_enabled('beta-feature', 'distinct_id')}"
|
||||
)
|
||||
print(
|
||||
f"beta-feature for 'new_distinct_id': {posthog.feature_enabled('beta-feature', 'new_distinct_id')}"
|
||||
f"beta-feature for 'new_distinct_id': {hanzo_insights.feature_enabled('beta-feature', 'new_distinct_id')}"
|
||||
)
|
||||
print(
|
||||
f"beta-feature with groups: {posthog.feature_enabled('beta-feature-groups', 'distinct_id', groups={'company': 'id:5'})}"
|
||||
f"beta-feature with groups: {hanzo_insights.feature_enabled('beta-feature-groups', 'distinct_id', groups={'company': 'id:5'})}"
|
||||
)
|
||||
|
||||
print("\n🌍 Testing location-based flags...")
|
||||
# Assume test-flag has `City Name = Sydney` as a person property set
|
||||
print(
|
||||
f"Sydney user: {posthog.feature_enabled('test-flag', 'random_id_12345', person_properties={'$geoip_city_name': 'Sydney'})}"
|
||||
f"Sydney user: {hanzo_insights.feature_enabled('test-flag', 'random_id_12345', person_properties={'$geoip_city_name': 'Sydney'})}"
|
||||
)
|
||||
|
||||
print(
|
||||
f"Sydney user (local only): {posthog.feature_enabled('test-flag', 'distinct_id_random_22', person_properties={'$geoip_city_name': 'Sydney'}, only_evaluate_locally=True)}"
|
||||
f"Sydney user (local only): {hanzo_insights.feature_enabled('test-flag', 'distinct_id_random_22', person_properties={'$geoip_city_name': 'Sydney'}, only_evaluate_locally=True)}"
|
||||
)
|
||||
|
||||
print("\n📋 Getting all flags...")
|
||||
print(f"All flags: {posthog.get_all_flags('distinct_id_random_22')}")
|
||||
print(f"All flags: {hanzo_insights.get_all_flags('distinct_id_random_22')}")
|
||||
print(
|
||||
f"All flags (local): {posthog.get_all_flags('distinct_id_random_22', only_evaluate_locally=True)}"
|
||||
f"All flags (local): {hanzo_insights.get_all_flags('distinct_id_random_22', only_evaluate_locally=True)}"
|
||||
)
|
||||
print(
|
||||
f"All flags with properties: {posthog.get_all_flags('distinct_id_random_22', person_properties={'$geoip_city_name': 'Sydney'}, only_evaluate_locally=True)}"
|
||||
f"All flags with properties: {hanzo_insights.get_all_flags('distinct_id_random_22', person_properties={'$geoip_city_name': 'Sydney'}, only_evaluate_locally=True)}"
|
||||
)
|
||||
|
||||
elif choice == "3":
|
||||
@@ -183,22 +183,22 @@ elif choice == "3":
|
||||
print("FEATURE FLAG PAYLOAD EXAMPLES")
|
||||
print("=" * 60)
|
||||
|
||||
posthog.debug = True
|
||||
hanzo_insights.debug = True
|
||||
|
||||
print("📦 Testing feature flag payloads...")
|
||||
print(
|
||||
f"beta-feature payload: {posthog.get_feature_flag_payload('beta-feature', 'distinct_id')}"
|
||||
f"beta-feature payload: {hanzo_insights.get_feature_flag_payload('beta-feature', 'distinct_id')}"
|
||||
)
|
||||
print(
|
||||
f"All flags and payloads: {posthog.get_all_flags_and_payloads('distinct_id')}"
|
||||
f"All flags and payloads: {hanzo_insights.get_all_flags_and_payloads('distinct_id')}"
|
||||
)
|
||||
print(
|
||||
f"Remote config payload: {posthog.get_remote_config_payload('encrypted_payload_flag_key')}"
|
||||
f"Remote config payload: {hanzo_insights.get_remote_config_payload('encrypted_payload_flag_key')}"
|
||||
)
|
||||
|
||||
# Get feature flag result with all details (enabled, variant, payload, key, reason)
|
||||
print("\n🔍 Getting detailed flag result...")
|
||||
result = posthog.get_feature_flag_result("beta-feature", "distinct_id")
|
||||
result = hanzo_insights.get_feature_flag_result("beta-feature", "distinct_id")
|
||||
if result:
|
||||
print(f"Flag key: {result.key}")
|
||||
print(f"Flag enabled: {result.enabled}")
|
||||
@@ -212,9 +212,9 @@ elif choice == "4":
|
||||
if not local_eval_available:
|
||||
print("\n❌ This example requires a personal API key for local evaluation.")
|
||||
print(
|
||||
" Set POSTHOG_PERSONAL_API_KEY environment variable to run this example."
|
||||
" Set INSIGHTS_PERSONAL_API_KEY environment variable to run this example."
|
||||
)
|
||||
posthog.shutdown()
|
||||
hanzo_insights.shutdown()
|
||||
exit(1)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
@@ -234,10 +234,10 @@ elif choice == "4":
|
||||
print(" - Rollout: 100%")
|
||||
print("")
|
||||
|
||||
posthog.debug = True
|
||||
hanzo_insights.debug = True
|
||||
|
||||
# Test @example.com user (should satisfy dependency if flags exist)
|
||||
result1 = posthog.feature_enabled(
|
||||
result1 = hanzo_insights.feature_enabled(
|
||||
"test-flag-dependency",
|
||||
"example_user",
|
||||
person_properties={"email": "user@example.com"},
|
||||
@@ -246,7 +246,7 @@ elif choice == "4":
|
||||
print(f"✅ @example.com user (test-flag-dependency): {result1}")
|
||||
|
||||
# Test non-example.com user (dependency should not be satisfied)
|
||||
result2 = posthog.feature_enabled(
|
||||
result2 = hanzo_insights.feature_enabled(
|
||||
"test-flag-dependency",
|
||||
"regular_user",
|
||||
person_properties={"email": "user@other.com"},
|
||||
@@ -255,13 +255,13 @@ elif choice == "4":
|
||||
print(f"❌ Regular user (test-flag-dependency): {result2}")
|
||||
|
||||
# Test beta-feature directly for comparison
|
||||
beta1 = posthog.feature_enabled(
|
||||
beta1 = hanzo_insights.feature_enabled(
|
||||
"beta-feature",
|
||||
"example_user",
|
||||
person_properties={"email": "user@example.com"},
|
||||
only_evaluate_locally=True,
|
||||
)
|
||||
beta2 = posthog.feature_enabled(
|
||||
beta2 = hanzo_insights.feature_enabled(
|
||||
"beta-feature",
|
||||
"regular_user",
|
||||
person_properties={"email": "user@other.com"},
|
||||
@@ -303,7 +303,7 @@ elif choice == "4":
|
||||
print("")
|
||||
|
||||
# Test pineapple -> blue -> breaking-bad chain
|
||||
dependent_result3 = posthog.get_feature_flag(
|
||||
dependent_result3 = hanzo_insights.get_feature_flag(
|
||||
"multivariate-root-flag",
|
||||
"regular_user",
|
||||
person_properties={"email": "pineapple@example.com"},
|
||||
@@ -317,7 +317,7 @@ elif choice == "4":
|
||||
print("✅ 'multivariate-root-flag' with email pineapple@example.com succeeded")
|
||||
|
||||
# Test mango -> red -> the-wire chain
|
||||
dependent_result4 = posthog.get_feature_flag(
|
||||
dependent_result4 = hanzo_insights.get_feature_flag(
|
||||
"multivariate-root-flag",
|
||||
"regular_user",
|
||||
person_properties={"email": "mango@example.com"},
|
||||
@@ -336,19 +336,19 @@ elif choice == "4":
|
||||
("pineapple@example.com", ["pineapple", "blue", "breaking-bad"]),
|
||||
("mango@example.com", ["mango", "red", "the-wire"]),
|
||||
]:
|
||||
leaf = posthog.get_feature_flag(
|
||||
leaf = hanzo_insights.get_feature_flag(
|
||||
"multivariate-leaf-flag",
|
||||
"regular_user",
|
||||
person_properties={"email": email},
|
||||
only_evaluate_locally=True,
|
||||
)
|
||||
intermediate = posthog.get_feature_flag(
|
||||
intermediate = hanzo_insights.get_feature_flag(
|
||||
"multivariate-intermediate-flag",
|
||||
"regular_user",
|
||||
person_properties={"email": email},
|
||||
only_evaluate_locally=True,
|
||||
)
|
||||
root = posthog.get_feature_flag(
|
||||
root = hanzo_insights.get_feature_flag(
|
||||
"multivariate-root-flag",
|
||||
"regular_user",
|
||||
person_properties={"email": email},
|
||||
@@ -373,7 +373,7 @@ elif choice == "5":
|
||||
print("CONTEXT MANAGEMENT AND TAGGING EXAMPLES")
|
||||
print("=" * 60)
|
||||
|
||||
posthog.debug = True
|
||||
hanzo_insights.debug = True
|
||||
|
||||
print("🏷️ Testing context management...")
|
||||
print(
|
||||
@@ -384,12 +384,12 @@ elif choice == "5":
|
||||
# and tagged with the context tags. Other events captured will also be tagged with the context tags. By default,
|
||||
# the new context inherits tags from the parent context.
|
||||
try:
|
||||
with posthog.new_context():
|
||||
posthog.tag("transaction_id", "abc123")
|
||||
posthog.tag("some_arbitrary_value", {"tags": "can be dicts"})
|
||||
with hanzo_insights.new_context():
|
||||
hanzo_insights.tag("transaction_id", "abc123")
|
||||
hanzo_insights.tag("some_arbitrary_value", {"tags": "can be dicts"})
|
||||
|
||||
# This event will be captured with the tags set above
|
||||
posthog.capture("order_processed")
|
||||
hanzo_insights.capture("order_processed")
|
||||
print("✅ Event captured with inherited context tags")
|
||||
# This exception will be captured with the tags set above
|
||||
# raise Exception("Order processing failed")
|
||||
@@ -398,30 +398,30 @@ elif choice == "5":
|
||||
|
||||
# Use fresh=True to start with a clean context (no inherited tags)
|
||||
try:
|
||||
with posthog.new_context(fresh=True):
|
||||
posthog.tag("session_id", "xyz789")
|
||||
with hanzo_insights.new_context(fresh=True):
|
||||
hanzo_insights.tag("session_id", "xyz789")
|
||||
# Only session_id tag will be present, no inherited tags
|
||||
posthog.capture("session_event")
|
||||
hanzo_insights.capture("session_event")
|
||||
print("✅ Event captured with fresh context tags")
|
||||
# raise Exception("Session handling failed")
|
||||
except Exception as e:
|
||||
print(f"Exception captured: {e}")
|
||||
|
||||
# You can also use the `@posthog.scoped()` decorator to enter a new context.
|
||||
# You can also use the `@hanzo_insights.scoped()` decorator to enter a new context.
|
||||
# By default, it inherits tags from the parent context
|
||||
@posthog.scoped()
|
||||
@hanzo_insights.scoped()
|
||||
def process_order(order_id):
|
||||
posthog.tag("order_id", order_id)
|
||||
posthog.capture("order_step_completed")
|
||||
hanzo_insights.tag("order_id", order_id)
|
||||
hanzo_insights.capture("order_step_completed")
|
||||
print(f"✅ Order {order_id} processed with scoped context")
|
||||
# Exception will be captured and tagged automatically
|
||||
# raise Exception("Order processing failed")
|
||||
|
||||
# Use fresh=True to start with a clean context (no inherited tags)
|
||||
@posthog.scoped(fresh=True)
|
||||
@hanzo_insights.scoped(fresh=True)
|
||||
def process_payment(payment_id):
|
||||
posthog.tag("payment_id", payment_id)
|
||||
posthog.capture("payment_processed")
|
||||
hanzo_insights.tag("payment_id", payment_id)
|
||||
hanzo_insights.capture("payment_processed")
|
||||
print(f"✅ Payment {payment_id} processed with fresh scoped context")
|
||||
# Only payment_id tag will be present, no inherited tags
|
||||
# raise Exception("Payment processing failed")
|
||||
@@ -436,18 +436,18 @@ elif choice == "6":
|
||||
|
||||
# Run example 1
|
||||
print(f"\n{'🔸' * 20} IDENTIFY AND CAPTURE {'🔸' * 20}")
|
||||
posthog.debug = True
|
||||
hanzo_insights.debug = True
|
||||
print("📊 Capturing events...")
|
||||
posthog.capture(
|
||||
hanzo_insights.capture(
|
||||
"event",
|
||||
distinct_id="distinct_id",
|
||||
properties={"property1": "value", "property2": "value"},
|
||||
send_feature_flags=True,
|
||||
)
|
||||
print("🔗 Creating alias...")
|
||||
posthog.alias("distinct_id", "new_distinct_id")
|
||||
hanzo_insights.alias("distinct_id", "new_distinct_id")
|
||||
print("👤 Identifying user...")
|
||||
posthog.set(
|
||||
hanzo_insights.set(
|
||||
distinct_id="new_distinct_id", properties={"email": "something@something.com"}
|
||||
)
|
||||
|
||||
@@ -455,27 +455,27 @@ elif choice == "6":
|
||||
if local_eval_available:
|
||||
print(f"\n{'🔸' * 20} FEATURE FLAGS {'🔸' * 20}")
|
||||
print("🏁 Testing basic feature flags...")
|
||||
print(f"beta-feature: {posthog.feature_enabled('beta-feature', 'distinct_id')}")
|
||||
print(f"beta-feature: {hanzo_insights.feature_enabled('beta-feature', 'distinct_id')}")
|
||||
print(
|
||||
f"Sydney user: {posthog.feature_enabled('test-flag', 'random_id_12345', person_properties={'$geoip_city_name': 'Sydney'})}"
|
||||
f"Sydney user: {hanzo_insights.feature_enabled('test-flag', 'random_id_12345', person_properties={'$geoip_city_name': 'Sydney'})}"
|
||||
)
|
||||
|
||||
# Run example 3
|
||||
print(f"\n{'🔸' * 20} PAYLOADS {'🔸' * 20}")
|
||||
print("📦 Testing payloads...")
|
||||
print(f"Payload: {posthog.get_feature_flag_payload('beta-feature', 'distinct_id')}")
|
||||
print(f"Payload: {hanzo_insights.get_feature_flag_payload('beta-feature', 'distinct_id')}")
|
||||
|
||||
# Run example 4 (requires local evaluation)
|
||||
if local_eval_available:
|
||||
print(f"\n{'🔸' * 20} FLAG DEPENDENCIES {'🔸' * 20}")
|
||||
print("🔗 Testing flag dependencies...")
|
||||
result1 = posthog.feature_enabled(
|
||||
result1 = hanzo_insights.feature_enabled(
|
||||
"test-flag-dependency",
|
||||
"demo_user",
|
||||
person_properties={"email": "user@example.com"},
|
||||
only_evaluate_locally=True,
|
||||
)
|
||||
result2 = posthog.feature_enabled(
|
||||
result2 = hanzo_insights.feature_enabled(
|
||||
"test-flag-dependency",
|
||||
"demo_user2",
|
||||
person_properties={"email": "user@other.com"},
|
||||
@@ -486,23 +486,23 @@ elif choice == "6":
|
||||
# Run example 5
|
||||
print(f"\n{'🔸' * 20} CONTEXT MANAGEMENT {'🔸' * 20}")
|
||||
print("🏷️ Testing context management...")
|
||||
with posthog.new_context():
|
||||
posthog.tag("demo_run", "all_examples")
|
||||
posthog.capture("demo_completed")
|
||||
with hanzo_insights.new_context():
|
||||
hanzo_insights.tag("demo_run", "all_examples")
|
||||
hanzo_insights.capture("demo_completed")
|
||||
print("✅ Demo completed with context tags")
|
||||
|
||||
elif choice == "7":
|
||||
print("👋 Goodbye!")
|
||||
posthog.shutdown()
|
||||
hanzo_insights.shutdown()
|
||||
exit()
|
||||
|
||||
else:
|
||||
print("❌ Invalid choice. Please run again and select 1-7.")
|
||||
posthog.shutdown()
|
||||
hanzo_insights.shutdown()
|
||||
exit()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ Example completed!")
|
||||
print("=" * 60)
|
||||
|
||||
posthog.shutdown()
|
||||
hanzo_insights.shutdown()
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"""
|
||||
Redis-based distributed cache for PostHog feature flag definitions.
|
||||
Redis-based distributed cache for Insights feature flag definitions.
|
||||
|
||||
This example demonstrates how to implement a FlagDefinitionCacheProvider
|
||||
using Redis for multi-instance deployments (leader election pattern).
|
||||
|
||||
Usage:
|
||||
import redis
|
||||
from posthog import Posthog
|
||||
from hanzo_insights import Insights
|
||||
|
||||
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
|
||||
cache = RedisFlagCache(redis_client, service_key="my-service")
|
||||
|
||||
posthog = Posthog(
|
||||
client = Insights(
|
||||
"<project_api_key>",
|
||||
personal_api_key="<personal_api_key>",
|
||||
flag_definition_cache_provider=cache,
|
||||
@@ -24,17 +24,17 @@ Requirements:
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from posthog import FlagDefinitionCacheData, FlagDefinitionCacheProvider
|
||||
from hanzo_insights import FlagDefinitionCacheData, FlagDefinitionCacheProvider
|
||||
from redis import Redis
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RedisFlagCache(FlagDefinitionCacheProvider):
|
||||
"""
|
||||
A distributed cache for PostHog feature flag definitions using Redis.
|
||||
A distributed cache for Insights feature flag definitions using Redis.
|
||||
|
||||
In a multi-instance deployment (e.g., multiple serverless functions or containers),
|
||||
we want only ONE instance to poll PostHog for flag updates, while all instances
|
||||
we want only ONE instance to poll Insights for flag updates, while all instances
|
||||
share the cached results. This prevents N instances from making N redundant API calls.
|
||||
|
||||
The implementation uses leader election:
|
||||
@@ -83,8 +83,8 @@ class RedisFlagCache(FlagDefinitionCacheProvider):
|
||||
Examples: "my-api-prod", "checkout-service", "staging".
|
||||
|
||||
Redis Keys Created:
|
||||
- posthog:flags:{service_key} - Cached flag definitions (JSON)
|
||||
- posthog:flags:{service_key}:lock - Leader election lock
|
||||
- insights:flags:{service_key} - Cached flag definitions (JSON)
|
||||
- insights:flags:{service_key}:lock - Leader election lock
|
||||
|
||||
Example:
|
||||
redis_client = redis.Redis(
|
||||
@@ -95,8 +95,8 @@ class RedisFlagCache(FlagDefinitionCacheProvider):
|
||||
cache = RedisFlagCache(redis_client, service_key="my-api-prod")
|
||||
"""
|
||||
self._redis = redis
|
||||
self._cache_key = f"posthog:flags:{service_key}"
|
||||
self._lock_key = f"posthog:flags:{service_key}:lock"
|
||||
self._cache_key = f"insights:flags:{service_key}"
|
||||
self._lock_key = f"insights:flags:{service_key}:lock"
|
||||
self._instance_id = str(uuid.uuid4())
|
||||
self._try_lead = self._redis.register_script(self._LUA_TRY_LEAD)
|
||||
self._stop_lead = self._redis.register_script(self._LUA_STOP_LEAD)
|
||||
@@ -113,7 +113,7 @@ class RedisFlagCache(FlagDefinitionCacheProvider):
|
||||
|
||||
def should_fetch_flag_definitions(self) -> bool:
|
||||
"""
|
||||
Determines if this instance should fetch flag definitions from PostHog.
|
||||
Determines if this instance should fetch flag definitions from Insights.
|
||||
|
||||
Atomically either:
|
||||
- Acquires the lock if no one holds it, OR
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test script for PostHog remote config endpoint.
|
||||
Simple test script for Insights remote config endpoint.
|
||||
"""
|
||||
|
||||
import posthog
|
||||
import hanzo_insights
|
||||
|
||||
# Initialize PostHog client
|
||||
posthog.api_key = "phc_..."
|
||||
posthog.personal_api_key = "phs_..." # or "phx_..."
|
||||
posthog.host = "http://localhost:8000" # or "https://us.posthog.com"
|
||||
posthog.debug = True
|
||||
# Initialize Insights client
|
||||
hanzo_insights.api_key = "phc_..."
|
||||
hanzo_insights.personal_api_key = "phs_..." # or "phx_..."
|
||||
hanzo_insights.host = "http://localhost:8000" # or "https://us.insights.hanzo.ai"
|
||||
hanzo_insights.debug = True
|
||||
|
||||
|
||||
def test_remote_config():
|
||||
@@ -21,7 +21,7 @@ def test_remote_config():
|
||||
|
||||
try:
|
||||
# Get remote config payload
|
||||
payload = posthog.get_remote_config_payload(flag_key)
|
||||
payload = hanzo_insights.get_remote_config_payload(flag_key)
|
||||
print(f"✅ Success! Remote config payload for '{flag_key}': {payload}")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -3,66 +3,66 @@ from typing import Any, Callable, Dict, Optional # noqa: F401
|
||||
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from posthog.args import ExceptionArg, OptionalCaptureArgs, OptionalSetArgs
|
||||
from posthog.client import Client
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.args import ExceptionArg, OptionalCaptureArgs, OptionalSetArgs
|
||||
from hanzo_insights.client import Client
|
||||
from hanzo_insights.contexts import (
|
||||
identify_context as inner_identify_context,
|
||||
)
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
new_context as inner_new_context,
|
||||
)
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
scoped as inner_scoped,
|
||||
)
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
set_capture_exception_code_variables_context as inner_set_capture_exception_code_variables_context,
|
||||
)
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
set_code_variables_ignore_patterns_context as inner_set_code_variables_ignore_patterns_context,
|
||||
)
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
set_code_variables_mask_patterns_context as inner_set_code_variables_mask_patterns_context,
|
||||
)
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
set_context_device_id as inner_set_context_device_id,
|
||||
)
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
set_context_session as inner_set_context_session,
|
||||
)
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
tag as inner_tag,
|
||||
)
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
get_tags as inner_get_tags,
|
||||
)
|
||||
from posthog.exception_utils import (
|
||||
from hanzo_insights.exception_utils import (
|
||||
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
|
||||
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
|
||||
)
|
||||
from posthog.feature_flags import (
|
||||
from hanzo_insights.feature_flags import (
|
||||
InconclusiveMatchError as InconclusiveMatchError,
|
||||
)
|
||||
from posthog.feature_flags import (
|
||||
from hanzo_insights.feature_flags import (
|
||||
RequiresServerEvaluation as RequiresServerEvaluation,
|
||||
)
|
||||
from posthog.flag_definition_cache import (
|
||||
from hanzo_insights.flag_definition_cache import (
|
||||
FlagDefinitionCacheData as FlagDefinitionCacheData,
|
||||
FlagDefinitionCacheProvider as FlagDefinitionCacheProvider,
|
||||
)
|
||||
from posthog.request import (
|
||||
from hanzo_insights.request import (
|
||||
disable_connection_reuse as disable_connection_reuse,
|
||||
enable_keep_alive as enable_keep_alive,
|
||||
set_socket_options as set_socket_options,
|
||||
SocketOptions as SocketOptions,
|
||||
)
|
||||
from posthog.types import (
|
||||
from hanzo_insights.types import (
|
||||
FeatureFlag,
|
||||
FlagsAndPayloads,
|
||||
)
|
||||
from posthog.types import (
|
||||
from hanzo_insights.types import (
|
||||
FeatureFlagResult as FeatureFlagResult,
|
||||
)
|
||||
from posthog.version import VERSION
|
||||
from hanzo_insights.version import VERSION
|
||||
|
||||
__version__ = VERSION
|
||||
|
||||
@@ -76,11 +76,11 @@ def new_context(fresh=False, capture_exceptions=True, client=None):
|
||||
Args:
|
||||
fresh: Whether to start with a fresh context (default: False)
|
||||
capture_exceptions: Whether to capture exceptions raised within the context (default: True)
|
||||
client: Optional Posthog client instance to use for this context (default: None)
|
||||
client: Optional Insights client instance to use for this context (default: None)
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import new_context, tag, capture
|
||||
from hanzo_insights import new_context, tag, capture
|
||||
with new_context():
|
||||
tag("request_id", "123")
|
||||
capture("event_name", properties={"property": "value"})
|
||||
@@ -100,11 +100,11 @@ def scoped(fresh=False, capture_exceptions=True):
|
||||
|
||||
Args:
|
||||
fresh: Whether to start with a fresh context (default: False)
|
||||
capture_exceptions: Whether to capture and track exceptions with posthog error tracking (default: True)
|
||||
capture_exceptions: Whether to capture and track exceptions with Insights error tracking (default: True)
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import scoped, tag, capture
|
||||
from hanzo_insights import scoped, tag, capture
|
||||
@scoped()
|
||||
def process_payment(payment_id):
|
||||
tag("payment_id", payment_id)
|
||||
@@ -126,7 +126,7 @@ def set_context_session(session_id: str):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import set_context_session
|
||||
from hanzo_insights import set_context_session
|
||||
set_context_session("session_123")
|
||||
```
|
||||
|
||||
@@ -146,7 +146,7 @@ def set_context_device_id(device_id: str):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import set_context_device_id
|
||||
from hanzo_insights import set_context_device_id
|
||||
set_context_device_id("device_123")
|
||||
```
|
||||
|
||||
@@ -165,7 +165,7 @@ def identify_context(distinct_id: str):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import identify_context
|
||||
from hanzo_insights import identify_context
|
||||
identify_context("user_123")
|
||||
```
|
||||
|
||||
@@ -206,7 +206,7 @@ def tag(name: str, value: Any):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import tag
|
||||
from hanzo_insights import tag
|
||||
tag("user_id", "123")
|
||||
```
|
||||
|
||||
@@ -263,7 +263,7 @@ in_app_modules = None # type: Optional[list[str]]
|
||||
|
||||
|
||||
# NOTE - this and following functions take unpacked kwargs because we needed to make
|
||||
# it impossible to write `posthog.capture(distinct-id, event-name)` - basically, to enforce
|
||||
# it impossible to write `hanzo_insights.capture(distinct-id, event-name)` - basically, to enforce
|
||||
# the breaking change made between 5.3.0 and 6.0.0. This decision can be unrolled in later
|
||||
# versions, without a breaking change, to get back the type information in function signatures
|
||||
def capture(event: str, **kwargs: Unpack[OptionalCaptureArgs]) -> Optional[str]:
|
||||
@@ -280,12 +280,12 @@ def capture(event: str, **kwargs: Unpack[OptionalCaptureArgs]) -> Optional[str]:
|
||||
disable_geoip: Whether to disable GeoIP lookup
|
||||
|
||||
Details:
|
||||
Capture allows you to capture anything a user does within your system, which you can later use in PostHog to find patterns in usage, work out which features to improve or where people are giving up. A capture call requires an 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. Capture takes a number of optional arguments, which are defined by the `OptionalCaptureArgs` type.
|
||||
Capture allows you to capture anything a user does within your system, which you can later use in Insights to find patterns in usage, work out which features to improve or where people are giving up. A capture call requires an 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. Capture takes a number of optional arguments, which are defined by the `OptionalCaptureArgs` type.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
# Context and capture usage
|
||||
from posthog import new_context, identify_context, tag_context, capture
|
||||
from hanzo_insights import new_context, identify_context, tag_context, capture
|
||||
# Enter a new context (e.g. a request/response cycle, an instance of a background job, etc)
|
||||
with new_context():
|
||||
# Associate this context with some user, by distinct_id
|
||||
@@ -312,7 +312,7 @@ def capture(event: str, **kwargs: Unpack[OptionalCaptureArgs]) -> Optional[str]:
|
||||
```
|
||||
```python
|
||||
# Set event properties
|
||||
from posthog import capture
|
||||
from hanzo_insights import capture
|
||||
capture(
|
||||
"user_signed_up",
|
||||
distinct_id="distinct_id_of_the_user",
|
||||
@@ -339,7 +339,7 @@ def set(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
|
||||
Examples:
|
||||
```python
|
||||
# Set person properties
|
||||
from posthog import capture
|
||||
from hanzo_insights import capture
|
||||
capture(
|
||||
'distinct_id',
|
||||
event='event_name',
|
||||
@@ -366,7 +366,7 @@ def set_once(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
|
||||
Examples:
|
||||
```python
|
||||
# Set property once
|
||||
from posthog import capture
|
||||
from hanzo_insights import capture
|
||||
capture(
|
||||
'distinct_id',
|
||||
event='event_name',
|
||||
@@ -406,7 +406,7 @@ def group_identify(
|
||||
Examples:
|
||||
```python
|
||||
# Group identify
|
||||
from posthog import group_identify
|
||||
from hanzo_insights import group_identify
|
||||
group_identify('company', 'company_id_in_your_db', {
|
||||
'name': 'Awesome Inc.',
|
||||
'employees': 11
|
||||
@@ -451,7 +451,7 @@ def alias(
|
||||
Examples:
|
||||
```python
|
||||
# Alias user
|
||||
from posthog import alias
|
||||
from hanzo_insights import alias
|
||||
alias(previous_id='distinct_id', distinct_id='alias_id')
|
||||
```
|
||||
Category:
|
||||
@@ -479,12 +479,12 @@ def capture_exception(
|
||||
exception: The exception to capture. If not provided, the current exception is captured via `sys.exc_info()`
|
||||
|
||||
Details:
|
||||
Capture exception is idempotent - if it is called twice with the same exception instance, only a occurrence will be tracked in posthog. This is because, generally, contexts will cause exceptions to be captured automatically. However, to ensure you track an exception, if you catch and do not re-raise it, capturing it manually is recommended, unless you are certain it will have crossed a context boundary (e.g. by existing a `with posthog.new_context():` block already). If the passed exception was raised and caught, the captured stack trace will consist of every frame between where the exception was raised and the point at which it is captured (the "traceback"). If the passed exception was never raised, e.g. if you call `posthog.capture_exception(ValueError("Some Error"))`, the stack trace captured will be the full stack trace at the moment the exception was captured. Note that heavy use of contexts will lead to truncated stack traces, as the exception will be captured by the context entered most recently, which may not be the point you catch the exception for the final time in your code. It's recommended to use contexts sparingly, for this reason. `capture_exception` takes the same set of optional arguments as `capture`.
|
||||
Capture exception is idempotent - if it is called twice with the same exception instance, only a occurrence will be tracked in hanzo_insights. This is because, generally, contexts will cause exceptions to be captured automatically. However, to ensure you track an exception, if you catch and do not re-raise it, capturing it manually is recommended, unless you are certain it will have crossed a context boundary (e.g. by existing a `with hanzo_insights.new_context():` block already). If the passed exception was raised and caught, the captured stack trace will consist of every frame between where the exception was raised and the point at which it is captured (the "traceback"). If the passed exception was never raised, e.g. if you call `hanzo_insights.capture_exception(ValueError("Some Error"))`, the stack trace captured will be the full stack trace at the moment the exception was captured. Note that heavy use of contexts will lead to truncated stack traces, as the exception will be captured by the context entered most recently, which may not be the point you catch the exception for the final time in your code. It's recommended to use contexts sparingly, for this reason. `capture_exception` takes the same set of optional arguments as `capture`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
# Capture exception
|
||||
from posthog import capture_exception
|
||||
from hanzo_insights import capture_exception
|
||||
try:
|
||||
risky_operation()
|
||||
except Exception as e:
|
||||
@@ -523,12 +523,12 @@ def feature_enabled(
|
||||
disable_geoip: Whether to disable GeoIP lookup
|
||||
|
||||
Details:
|
||||
You can call `posthog.load_feature_flags()` before to make sure you're not doing unexpected requests.
|
||||
You can call `hanzo_insights.load_feature_flags()` before to make sure you're not doing unexpected requests.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
# Boolean feature flag
|
||||
from posthog import feature_enabled, get_feature_flag_payload
|
||||
from hanzo_insights import feature_enabled, get_feature_flag_payload
|
||||
is_my_flag_enabled = feature_enabled('flag-key', 'distinct_id_of_your_user')
|
||||
if is_my_flag_enabled:
|
||||
matched_flag_payload = get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
|
||||
@@ -575,12 +575,12 @@ def get_feature_flag(
|
||||
disable_geoip: Whether to disable GeoIP lookup
|
||||
|
||||
Details:
|
||||
`groups` are a mapping from group type to group key. So, if you have a group type of "organization" and a group key of "5", you would pass groups={"organization": "5"}. `group_properties` take the format: { group_type_name: { group_properties } }. So, for example, if you have the group type "organization" and the group key "5", with the properties name, and employee count, you'll send these as: group_properties={"organization": {"name": "PostHog", "employees": 11}}.
|
||||
`groups` are a mapping from group type to group key. So, if you have a group type of "organization" and a group key of "5", you would pass groups={"organization": "5"}. `group_properties` take the format: { group_type_name: { group_properties } }. So, for example, if you have the group type "organization" and the group key "5", with the properties name, and employee count, you'll send these as: group_properties={"organization": {"name": "Hanzo", "employees": 11}}.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
# Multivariate feature flag
|
||||
from posthog import get_feature_flag, get_feature_flag_payload
|
||||
from hanzo_insights import get_feature_flag, get_feature_flag_payload
|
||||
enabled_variant = get_feature_flag('flag-key', 'distinct_id_of_your_user')
|
||||
if enabled_variant == 'variant-key':
|
||||
matched_flag_payload = get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
|
||||
@@ -628,7 +628,7 @@ def get_all_flags(
|
||||
Examples:
|
||||
```python
|
||||
# All flags for user
|
||||
from posthog import get_all_flags
|
||||
from hanzo_insights import get_all_flags
|
||||
get_all_flags('distinct_id_of_your_user')
|
||||
```
|
||||
Category:
|
||||
@@ -670,7 +670,7 @@ def get_feature_flag_result(
|
||||
|
||||
Example:
|
||||
```python
|
||||
result = posthog.get_feature_flag_result('beta-feature', 'distinct_id')
|
||||
result = hanzo_insights.get_feature_flag_result('beta-feature', 'distinct_id')
|
||||
if result and result.enabled:
|
||||
# Use the variant and payload
|
||||
print(f"Variant: {result.variant}")
|
||||
@@ -768,7 +768,7 @@ def feature_flag_definitions():
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import feature_flag_definitions
|
||||
from hanzo_insights import feature_flag_definitions
|
||||
definitions = feature_flag_definitions()
|
||||
```
|
||||
|
||||
@@ -780,11 +780,11 @@ def feature_flag_definitions():
|
||||
|
||||
def load_feature_flags():
|
||||
"""
|
||||
Load feature flag definitions from PostHog.
|
||||
Load feature flag definitions from the server.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import load_feature_flags
|
||||
from hanzo_insights import load_feature_flags
|
||||
load_feature_flags()
|
||||
```
|
||||
|
||||
@@ -800,7 +800,7 @@ def flush():
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import flush
|
||||
from hanzo_insights import flush
|
||||
flush()
|
||||
```
|
||||
|
||||
@@ -816,7 +816,7 @@ def join():
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import join
|
||||
from hanzo_insights import join
|
||||
join()
|
||||
```
|
||||
|
||||
@@ -832,7 +832,7 @@ def shutdown():
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import shutdown
|
||||
from hanzo_insights import shutdown
|
||||
shutdown()
|
||||
```
|
||||
|
||||
@@ -888,5 +888,7 @@ def _proxy(method, *args, **kwargs):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
|
||||
class Posthog(Client):
|
||||
class Insights(Client):
|
||||
"""Hanzo Insights client for product analytics."""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,3 @@
|
||||
from hanzo_insights.ai.prompts import Prompts
|
||||
|
||||
__all__ = ["Prompts"]
|
||||
@@ -10,38 +10,38 @@ import time
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from posthog.ai.types import StreamingContentBlock, TokenUsage, ToolInProgress
|
||||
from posthog.ai.utils import (
|
||||
from hanzo_insights.ai.types import StreamingContentBlock, TokenUsage, ToolInProgress
|
||||
from hanzo_insights.ai.utils import (
|
||||
call_llm_and_track_usage,
|
||||
merge_usage_stats,
|
||||
)
|
||||
from posthog.ai.anthropic.anthropic_converter import (
|
||||
from hanzo_insights.ai.anthropic.anthropic_converter import (
|
||||
extract_anthropic_usage_from_event,
|
||||
handle_anthropic_content_block_start,
|
||||
handle_anthropic_text_delta,
|
||||
handle_anthropic_tool_delta,
|
||||
finalize_anthropic_tool_input,
|
||||
)
|
||||
from posthog.ai.sanitization import sanitize_anthropic
|
||||
from posthog.client import Client as PostHogClient
|
||||
from posthog import setup
|
||||
from hanzo_insights.ai.sanitization import sanitize_anthropic
|
||||
from hanzo_insights.client import Client as InsightsClient
|
||||
from hanzo_insights import setup
|
||||
|
||||
|
||||
class Anthropic(anthropic.Anthropic):
|
||||
"""
|
||||
A wrapper around the Anthropic SDK that automatically sends LLM usage events to PostHog.
|
||||
A wrapper around the Anthropic SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
"""
|
||||
Args:
|
||||
posthog_client: PostHog client for tracking usage
|
||||
insights_client: Insights client for tracking usage
|
||||
**kwargs: Additional arguments passed to the Anthropic client
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
self.messages = WrappedMessages(self)
|
||||
|
||||
|
||||
@@ -50,46 +50,46 @@ class WrappedMessages(Messages):
|
||||
|
||||
def create(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Create a message using Anthropic's API while tracking usage in PostHog.
|
||||
Create a message using Anthropic's API while tracking usage in Insights.
|
||||
|
||||
Args:
|
||||
posthog_distinct_id: Optional ID to associate with the usage event
|
||||
posthog_trace_id: Optional trace UUID for linking events
|
||||
posthog_properties: Optional dictionary of extra properties to include in the event
|
||||
posthog_privacy_mode: Whether to redact sensitive information in tracking
|
||||
posthog_groups: Optional group analytics properties
|
||||
insights_distinct_id: Optional ID to associate with the usage event
|
||||
insights_trace_id: Optional trace UUID for linking events
|
||||
insights_properties: Optional dictionary of extra properties to include in the event
|
||||
insights_privacy_mode: Whether to redact sensitive information in tracking
|
||||
insights_groups: Optional group analytics properties
|
||||
**kwargs: Arguments passed to Anthropic's messages.create
|
||||
"""
|
||||
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
if kwargs.get("stream", False):
|
||||
return self._create_streaming(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return call_llm_and_track_usage(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"anthropic",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
super().create,
|
||||
**kwargs,
|
||||
@@ -97,32 +97,32 @@ class WrappedMessages(Messages):
|
||||
|
||||
def stream(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
return self._create_streaming(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _create_streaming(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
**kwargs: Any,
|
||||
):
|
||||
start_time = time.time()
|
||||
@@ -188,11 +188,11 @@ class WrappedMessages(Messages):
|
||||
latency = end_time - start_time
|
||||
|
||||
self._capture_streaming_event(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
kwargs,
|
||||
usage_stats,
|
||||
latency,
|
||||
@@ -204,23 +204,23 @@ class WrappedMessages(Messages):
|
||||
|
||||
def _capture_streaming_event(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
kwargs: Dict[str, Any],
|
||||
usage_stats: TokenUsage,
|
||||
latency: float,
|
||||
content_blocks: List[StreamingContentBlock],
|
||||
accumulated_content: str,
|
||||
):
|
||||
from posthog.ai.types import StreamingEventData
|
||||
from posthog.ai.anthropic.anthropic_converter import (
|
||||
from hanzo_insights.ai.types import StreamingEventData
|
||||
from hanzo_insights.ai.anthropic.anthropic_converter import (
|
||||
format_anthropic_streaming_input,
|
||||
format_anthropic_streaming_output_complete,
|
||||
)
|
||||
from posthog.ai.utils import capture_streaming_event
|
||||
from hanzo_insights.ai.utils import capture_streaming_event
|
||||
|
||||
# Prepare standardized event data
|
||||
formatted_input = format_anthropic_streaming_input(kwargs)
|
||||
@@ -237,11 +237,11 @@ class WrappedMessages(Messages):
|
||||
),
|
||||
usage_stats=usage_stats,
|
||||
latency=latency,
|
||||
distinct_id=posthog_distinct_id,
|
||||
trace_id=posthog_trace_id,
|
||||
properties=posthog_properties,
|
||||
privacy_mode=posthog_privacy_mode,
|
||||
groups=posthog_groups,
|
||||
distinct_id=insights_distinct_id,
|
||||
trace_id=insights_trace_id,
|
||||
properties=insights_properties,
|
||||
privacy_mode=insights_privacy_mode,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
# Use the common capture function
|
||||
+69
-69
@@ -10,38 +10,38 @@ import time
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from posthog import setup
|
||||
from posthog.ai.types import StreamingContentBlock, TokenUsage, ToolInProgress
|
||||
from posthog.ai.utils import (
|
||||
from hanzo_insights import setup
|
||||
from hanzo_insights.ai.types import StreamingContentBlock, TokenUsage, ToolInProgress
|
||||
from hanzo_insights.ai.utils import (
|
||||
call_llm_and_track_usage_async,
|
||||
merge_usage_stats,
|
||||
)
|
||||
from posthog.ai.anthropic.anthropic_converter import (
|
||||
from hanzo_insights.ai.anthropic.anthropic_converter import (
|
||||
extract_anthropic_usage_from_event,
|
||||
handle_anthropic_content_block_start,
|
||||
handle_anthropic_text_delta,
|
||||
handle_anthropic_tool_delta,
|
||||
finalize_anthropic_tool_input,
|
||||
)
|
||||
from posthog.ai.sanitization import sanitize_anthropic
|
||||
from posthog.client import Client as PostHogClient
|
||||
from hanzo_insights.ai.sanitization import sanitize_anthropic
|
||||
from hanzo_insights.client import Client as InsightsClient
|
||||
|
||||
|
||||
class AsyncAnthropic(anthropic.AsyncAnthropic):
|
||||
"""
|
||||
An async wrapper around the Anthropic SDK that automatically sends LLM usage events to PostHog.
|
||||
An async wrapper around the Anthropic SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
"""
|
||||
Args:
|
||||
posthog_client: PostHog client for tracking usage
|
||||
insights_client: Insights client for tracking usage
|
||||
**kwargs: Additional arguments passed to the Anthropic client
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
self.messages = AsyncWrappedMessages(self)
|
||||
|
||||
|
||||
@@ -50,46 +50,46 @@ class AsyncWrappedMessages(AsyncMessages):
|
||||
|
||||
async def create(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Create a message using Anthropic's API while tracking usage in PostHog.
|
||||
Create a message using Anthropic's API while tracking usage in Insights.
|
||||
|
||||
Args:
|
||||
posthog_distinct_id: Optional ID to associate with the usage event
|
||||
posthog_trace_id: Optional trace UUID for linking events
|
||||
posthog_properties: Optional dictionary of extra properties to include in the event
|
||||
posthog_privacy_mode: Whether to redact sensitive information in tracking
|
||||
posthog_groups: Optional group analytics properties
|
||||
insights_distinct_id: Optional ID to associate with the usage event
|
||||
insights_trace_id: Optional trace UUID for linking events
|
||||
insights_properties: Optional dictionary of extra properties to include in the event
|
||||
insights_privacy_mode: Whether to redact sensitive information in tracking
|
||||
insights_groups: Optional group analytics properties
|
||||
**kwargs: Arguments passed to Anthropic's messages.create
|
||||
"""
|
||||
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
if kwargs.get("stream", False):
|
||||
return await self._create_streaming(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return await call_llm_and_track_usage_async(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"anthropic",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
super().create,
|
||||
**kwargs,
|
||||
@@ -97,32 +97,32 @@ class AsyncWrappedMessages(AsyncMessages):
|
||||
|
||||
async def stream(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
return await self._create_streaming(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def _create_streaming(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
**kwargs: Any,
|
||||
):
|
||||
start_time = time.time()
|
||||
@@ -188,11 +188,11 @@ class AsyncWrappedMessages(AsyncMessages):
|
||||
latency = end_time - start_time
|
||||
|
||||
await self._capture_streaming_event(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
kwargs,
|
||||
usage_stats,
|
||||
latency,
|
||||
@@ -204,23 +204,23 @@ class AsyncWrappedMessages(AsyncMessages):
|
||||
|
||||
async def _capture_streaming_event(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
kwargs: Dict[str, Any],
|
||||
usage_stats: TokenUsage,
|
||||
latency: float,
|
||||
content_blocks: List[StreamingContentBlock],
|
||||
accumulated_content: str,
|
||||
):
|
||||
from posthog.ai.types import StreamingEventData
|
||||
from posthog.ai.anthropic.anthropic_converter import (
|
||||
from hanzo_insights.ai.types import StreamingEventData
|
||||
from hanzo_insights.ai.anthropic.anthropic_converter import (
|
||||
format_anthropic_streaming_input,
|
||||
format_anthropic_streaming_output_complete,
|
||||
)
|
||||
from posthog.ai.utils import capture_streaming_event
|
||||
from hanzo_insights.ai.utils import capture_streaming_event
|
||||
|
||||
# Prepare standardized event data
|
||||
formatted_input = format_anthropic_streaming_input(kwargs)
|
||||
@@ -237,11 +237,11 @@ class AsyncWrappedMessages(AsyncMessages):
|
||||
),
|
||||
usage_stats=usage_stats,
|
||||
latency=latency,
|
||||
distinct_id=posthog_distinct_id,
|
||||
trace_id=posthog_trace_id,
|
||||
properties=posthog_properties,
|
||||
privacy_mode=posthog_privacy_mode,
|
||||
groups=posthog_groups,
|
||||
distinct_id=insights_distinct_id,
|
||||
trace_id=insights_trace_id,
|
||||
properties=insights_properties,
|
||||
privacy_mode=insights_privacy_mode,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
# Use the common capture function
|
||||
+6
-6
@@ -2,13 +2,13 @@
|
||||
Anthropic-specific conversion utilities.
|
||||
|
||||
This module handles the conversion of Anthropic API responses and inputs
|
||||
into standardized formats for PostHog tracking.
|
||||
into standardized formats for Insights tracking.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from posthog.ai.types import (
|
||||
from hanzo_insights.ai.types import (
|
||||
FormattedContentItem,
|
||||
FormattedFunctionCall,
|
||||
FormattedMessage,
|
||||
@@ -17,7 +17,7 @@ from posthog.ai.types import (
|
||||
TokenUsage,
|
||||
ToolInProgress,
|
||||
)
|
||||
from posthog.ai.utils import serialize_raw_usage
|
||||
from hanzo_insights.ai.utils import serialize_raw_usage
|
||||
|
||||
|
||||
def format_anthropic_response(response: Any) -> List[FormattedMessage]:
|
||||
@@ -425,9 +425,9 @@ def format_anthropic_streaming_input(kwargs: Dict[str, Any]) -> Any:
|
||||
kwargs: Keyword arguments passed to Anthropic API
|
||||
|
||||
Returns:
|
||||
Formatted input ready for PostHog tracking
|
||||
Formatted input ready for Insights tracking
|
||||
"""
|
||||
from posthog.ai.utils import merge_system_prompt
|
||||
from hanzo_insights.ai.utils import merge_system_prompt
|
||||
|
||||
return merge_system_prompt(kwargs, "anthropic")
|
||||
|
||||
@@ -445,7 +445,7 @@ def format_anthropic_streaming_output_complete(
|
||||
accumulated_content: Raw accumulated text content as fallback
|
||||
|
||||
Returns:
|
||||
Formatted messages ready for PostHog tracking
|
||||
Formatted messages ready for Insights tracking
|
||||
"""
|
||||
formatted_content = format_anthropic_streaming_content(content_blocks)
|
||||
|
||||
+20
-20
@@ -7,59 +7,59 @@ except ImportError:
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from posthog.ai.anthropic.anthropic import WrappedMessages
|
||||
from posthog.ai.anthropic.anthropic_async import AsyncWrappedMessages
|
||||
from posthog.client import Client as PostHogClient
|
||||
from posthog import setup
|
||||
from hanzo_insights.ai.anthropic.anthropic import WrappedMessages
|
||||
from hanzo_insights.ai.anthropic.anthropic_async import AsyncWrappedMessages
|
||||
from hanzo_insights.client import Client as InsightsClient
|
||||
from hanzo_insights import setup
|
||||
|
||||
|
||||
class AnthropicBedrock(anthropic.AnthropicBedrock):
|
||||
"""
|
||||
A wrapper around the Anthropic Bedrock SDK that automatically sends LLM usage events to PostHog.
|
||||
A wrapper around the Anthropic Bedrock SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
self.messages = WrappedMessages(self)
|
||||
|
||||
|
||||
class AsyncAnthropicBedrock(anthropic.AsyncAnthropicBedrock):
|
||||
"""
|
||||
A wrapper around the Anthropic Bedrock SDK that automatically sends LLM usage events to PostHog.
|
||||
A wrapper around the Anthropic Bedrock SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
self.messages = AsyncWrappedMessages(self)
|
||||
|
||||
|
||||
class AnthropicVertex(anthropic.AnthropicVertex):
|
||||
"""
|
||||
A wrapper around the Anthropic Vertex SDK that automatically sends LLM usage events to PostHog.
|
||||
A wrapper around the Anthropic Vertex SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
self.messages = WrappedMessages(self)
|
||||
|
||||
|
||||
class AsyncAnthropicVertex(anthropic.AsyncAnthropicVertex):
|
||||
"""
|
||||
A wrapper around the Anthropic Vertex SDK that automatically sends LLM usage events to PostHog.
|
||||
A wrapper around the Anthropic Vertex SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
self.messages = AsyncWrappedMessages(self)
|
||||
@@ -3,8 +3,8 @@ import time
|
||||
import uuid
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from posthog.ai.types import TokenUsage, StreamingEventData
|
||||
from posthog.ai.utils import merge_system_prompt
|
||||
from hanzo_insights.ai.types import TokenUsage, StreamingEventData
|
||||
from hanzo_insights.ai.utils import merge_system_prompt
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
@@ -13,40 +13,40 @@ except ImportError:
|
||||
"Please install the Google Gemini SDK to use this feature: 'pip install google-genai'"
|
||||
)
|
||||
|
||||
from posthog import setup
|
||||
from posthog.ai.utils import (
|
||||
from hanzo_insights import setup
|
||||
from hanzo_insights.ai.utils import (
|
||||
call_llm_and_track_usage,
|
||||
capture_streaming_event,
|
||||
merge_usage_stats,
|
||||
)
|
||||
from posthog.ai.gemini.gemini_converter import (
|
||||
from hanzo_insights.ai.gemini.gemini_converter import (
|
||||
extract_gemini_usage_from_chunk,
|
||||
extract_gemini_content_from_chunk,
|
||||
format_gemini_streaming_output,
|
||||
)
|
||||
from posthog.ai.sanitization import sanitize_gemini
|
||||
from posthog.client import Client as PostHogClient
|
||||
from hanzo_insights.ai.sanitization import sanitize_gemini
|
||||
from hanzo_insights.client import Client as InsightsClient
|
||||
|
||||
|
||||
class Client:
|
||||
"""
|
||||
A drop-in replacement for genai.Client that automatically sends LLM usage events to PostHog.
|
||||
A drop-in replacement for genai.Client that automatically sends LLM usage events to Insights.
|
||||
|
||||
Usage:
|
||||
client = Client(
|
||||
api_key="your_api_key",
|
||||
posthog_client=posthog_client,
|
||||
posthog_distinct_id="default_user", # Optional defaults
|
||||
posthog_properties={"team": "ai"} # Optional defaults
|
||||
insights_client=insights_client,
|
||||
insights_distinct_id="default_user", # Optional defaults
|
||||
insights_properties={"team": "ai"} # Optional defaults
|
||||
)
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello world"],
|
||||
posthog_distinct_id="specific_user" # Override default
|
||||
insights_distinct_id="specific_user" # Override default
|
||||
)
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -57,11 +57,11 @@ class Client:
|
||||
location: Optional[str] = None,
|
||||
debug_config: Optional[Any] = None,
|
||||
http_options: Optional[Any] = None,
|
||||
posthog_client: Optional[PostHogClient] = None,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_client: Optional[InsightsClient] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -73,18 +73,18 @@ class Client:
|
||||
location: GCP location for Vertex AI
|
||||
debug_config: Debug configuration for the client
|
||||
http_options: HTTP options for the client
|
||||
posthog_client: PostHog client for tracking usage
|
||||
posthog_distinct_id: Default distinct ID for all calls (can be overridden per call)
|
||||
posthog_properties: Default properties for all calls (can be overridden per call)
|
||||
posthog_privacy_mode: Default privacy mode for all calls (can be overridden per call)
|
||||
posthog_groups: Default groups for all calls (can be overridden per call)
|
||||
insights_client: Insights client for tracking usage
|
||||
insights_distinct_id: Default distinct ID for all calls (can be overridden per call)
|
||||
insights_properties: Default properties for all calls (can be overridden per call)
|
||||
insights_privacy_mode: Default privacy mode for all calls (can be overridden per call)
|
||||
insights_groups: Default groups for all calls (can be overridden per call)
|
||||
**kwargs: Additional arguments (for future compatibility)
|
||||
"""
|
||||
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
|
||||
if self._ph_client is None:
|
||||
raise ValueError("posthog_client is required for PostHog tracking")
|
||||
raise ValueError("insights_client is required for Insights tracking")
|
||||
|
||||
self.models = Models(
|
||||
api_key=api_key,
|
||||
@@ -94,21 +94,21 @@ class Client:
|
||||
location=location,
|
||||
debug_config=debug_config,
|
||||
http_options=http_options,
|
||||
posthog_client=self._ph_client,
|
||||
posthog_distinct_id=posthog_distinct_id,
|
||||
posthog_properties=posthog_properties,
|
||||
posthog_privacy_mode=posthog_privacy_mode,
|
||||
posthog_groups=posthog_groups,
|
||||
insights_client=self._ph_client,
|
||||
insights_distinct_id=insights_distinct_id,
|
||||
insights_properties=insights_properties,
|
||||
insights_privacy_mode=insights_privacy_mode,
|
||||
insights_groups=insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class Models:
|
||||
"""
|
||||
Models interface that mimics genai.Client().models with PostHog tracking.
|
||||
Models interface that mimics genai.Client().models with Insights tracking.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient # Not None after __init__ validation
|
||||
_ph_client: InsightsClient # Not None after __init__ validation
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -119,11 +119,11 @@ class Models:
|
||||
location: Optional[str] = None,
|
||||
debug_config: Optional[Any] = None,
|
||||
http_options: Optional[Any] = None,
|
||||
posthog_client: Optional[PostHogClient] = None,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_client: Optional[InsightsClient] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -135,24 +135,24 @@ class Models:
|
||||
location: GCP location for Vertex AI
|
||||
debug_config: Debug configuration for the client
|
||||
http_options: HTTP options for the client
|
||||
posthog_client: PostHog client for tracking usage
|
||||
posthog_distinct_id: Default distinct ID for all calls
|
||||
posthog_properties: Default properties for all calls
|
||||
posthog_privacy_mode: Default privacy mode for all calls
|
||||
posthog_groups: Default groups for all calls
|
||||
insights_client: Insights client for tracking usage
|
||||
insights_distinct_id: Default distinct ID for all calls
|
||||
insights_properties: Default properties for all calls
|
||||
insights_privacy_mode: Default privacy mode for all calls
|
||||
insights_groups: Default groups for all calls
|
||||
**kwargs: Additional arguments (for future compatibility)
|
||||
"""
|
||||
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
|
||||
if self._ph_client is None:
|
||||
raise ValueError("posthog_client is required for PostHog tracking")
|
||||
raise ValueError("insights_client is required for Insights tracking")
|
||||
|
||||
# Store default PostHog settings
|
||||
self._default_distinct_id = posthog_distinct_id
|
||||
self._default_properties = posthog_properties or {}
|
||||
self._default_privacy_mode = posthog_privacy_mode
|
||||
self._default_groups = posthog_groups
|
||||
# Store default Insights settings
|
||||
self._default_distinct_id = insights_distinct_id
|
||||
self._default_properties = insights_properties or {}
|
||||
self._default_privacy_mode = insights_privacy_mode
|
||||
self._default_groups = insights_groups
|
||||
|
||||
# Build genai.Client arguments
|
||||
client_args: Dict[str, Any] = {}
|
||||
@@ -196,7 +196,7 @@ class Models:
|
||||
self._client = genai.Client(**client_args)
|
||||
self._base_url = "https://generativelanguage.googleapis.com"
|
||||
|
||||
def _merge_posthog_params(
|
||||
def _merge_insights_params(
|
||||
self,
|
||||
call_distinct_id: Optional[str],
|
||||
call_trace_id: Optional[str],
|
||||
@@ -204,7 +204,7 @@ class Models:
|
||||
call_privacy_mode: Optional[bool],
|
||||
call_groups: Optional[Dict[str, Any]],
|
||||
):
|
||||
"""Merge call-level PostHog parameters with client defaults."""
|
||||
"""Merge call-level Insights parameters with client defaults."""
|
||||
|
||||
# Use call-level values if provided, otherwise fall back to defaults
|
||||
distinct_id = (
|
||||
@@ -234,38 +234,38 @@ class Models:
|
||||
self,
|
||||
model: str,
|
||||
contents,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: Optional[bool] = None,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: Optional[bool] = None,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Generate content using Gemini's API while tracking usage in PostHog.
|
||||
Generate content using Gemini's API while tracking usage in Insights.
|
||||
|
||||
This method signature exactly matches genai.Client().models.generate_content()
|
||||
with additional PostHog tracking parameters.
|
||||
with additional Insights tracking parameters.
|
||||
|
||||
Args:
|
||||
model: The model to use (e.g., 'gemini-2.0-flash')
|
||||
contents: The input content for generation
|
||||
posthog_distinct_id: ID to associate with the usage event (overrides client default)
|
||||
posthog_trace_id: Trace UUID for linking events (auto-generated if not provided)
|
||||
posthog_properties: Extra properties to include in the event (merged with client defaults)
|
||||
posthog_privacy_mode: Whether to redact sensitive information (overrides client default)
|
||||
posthog_groups: Group analytics properties (overrides client default)
|
||||
insights_distinct_id: ID to associate with the usage event (overrides client default)
|
||||
insights_trace_id: Trace UUID for linking events (auto-generated if not provided)
|
||||
insights_properties: Extra properties to include in the event (merged with client defaults)
|
||||
insights_privacy_mode: Whether to redact sensitive information (overrides client default)
|
||||
insights_groups: Group analytics properties (overrides client default)
|
||||
**kwargs: Arguments passed to Gemini's generate_content
|
||||
"""
|
||||
|
||||
# Merge PostHog parameters
|
||||
# Merge Insights parameters
|
||||
distinct_id, trace_id, properties, privacy_mode, groups = (
|
||||
self._merge_posthog_params(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
self._merge_insights_params(
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -380,7 +380,7 @@ class Models:
|
||||
capture_streaming_event(self._ph_client, event_data)
|
||||
|
||||
def _format_input(self, contents, **kwargs):
|
||||
"""Format input contents for PostHog tracking"""
|
||||
"""Format input contents for Insights tracking"""
|
||||
|
||||
# Create kwargs dict with contents for merge_system_prompt
|
||||
input_kwargs = {"contents": contents, **kwargs}
|
||||
@@ -390,21 +390,21 @@ class Models:
|
||||
self,
|
||||
model: str,
|
||||
contents,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: Optional[bool] = None,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: Optional[bool] = None,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
# Merge PostHog parameters
|
||||
# Merge Insights parameters
|
||||
distinct_id, trace_id, properties, privacy_mode, groups = (
|
||||
self._merge_posthog_params(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
self._merge_insights_params(
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -3,8 +3,8 @@ import time
|
||||
import uuid
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from posthog.ai.types import TokenUsage, StreamingEventData
|
||||
from posthog.ai.utils import merge_system_prompt
|
||||
from hanzo_insights.ai.types import TokenUsage, StreamingEventData
|
||||
from hanzo_insights.ai.utils import merge_system_prompt
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
@@ -13,40 +13,40 @@ except ImportError:
|
||||
"Please install the Google Gemini SDK to use this feature: 'pip install google-genai'"
|
||||
)
|
||||
|
||||
from posthog import setup
|
||||
from posthog.ai.utils import (
|
||||
from hanzo_insights import setup
|
||||
from hanzo_insights.ai.utils import (
|
||||
call_llm_and_track_usage_async,
|
||||
capture_streaming_event,
|
||||
merge_usage_stats,
|
||||
)
|
||||
from posthog.ai.gemini.gemini_converter import (
|
||||
from hanzo_insights.ai.gemini.gemini_converter import (
|
||||
extract_gemini_usage_from_chunk,
|
||||
extract_gemini_content_from_chunk,
|
||||
format_gemini_streaming_output,
|
||||
)
|
||||
from posthog.ai.sanitization import sanitize_gemini
|
||||
from posthog.client import Client as PostHogClient
|
||||
from hanzo_insights.ai.sanitization import sanitize_gemini
|
||||
from hanzo_insights.client import Client as InsightsClient
|
||||
|
||||
|
||||
class AsyncClient:
|
||||
"""
|
||||
An async drop-in replacement for genai.Client that automatically sends LLM usage events to PostHog.
|
||||
An async drop-in replacement for genai.Client that automatically sends LLM usage events to Insights.
|
||||
|
||||
Usage:
|
||||
client = AsyncClient(
|
||||
api_key="your_api_key",
|
||||
posthog_client=posthog_client,
|
||||
posthog_distinct_id="default_user", # Optional defaults
|
||||
posthog_properties={"team": "ai"} # Optional defaults
|
||||
insights_client=insights_client,
|
||||
insights_distinct_id="default_user", # Optional defaults
|
||||
insights_properties={"team": "ai"} # Optional defaults
|
||||
)
|
||||
response = await client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello world"],
|
||||
posthog_distinct_id="specific_user" # Override default
|
||||
insights_distinct_id="specific_user" # Override default
|
||||
)
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -57,11 +57,11 @@ class AsyncClient:
|
||||
location: Optional[str] = None,
|
||||
debug_config: Optional[Any] = None,
|
||||
http_options: Optional[Any] = None,
|
||||
posthog_client: Optional[PostHogClient] = None,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_client: Optional[InsightsClient] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -73,18 +73,18 @@ class AsyncClient:
|
||||
location: GCP location for Vertex AI
|
||||
debug_config: Debug configuration for the client
|
||||
http_options: HTTP options for the client
|
||||
posthog_client: PostHog client for tracking usage
|
||||
posthog_distinct_id: Default distinct ID for all calls (can be overridden per call)
|
||||
posthog_properties: Default properties for all calls (can be overridden per call)
|
||||
posthog_privacy_mode: Default privacy mode for all calls (can be overridden per call)
|
||||
posthog_groups: Default groups for all calls (can be overridden per call)
|
||||
insights_client: Insights client for tracking usage
|
||||
insights_distinct_id: Default distinct ID for all calls (can be overridden per call)
|
||||
insights_properties: Default properties for all calls (can be overridden per call)
|
||||
insights_privacy_mode: Default privacy mode for all calls (can be overridden per call)
|
||||
insights_groups: Default groups for all calls (can be overridden per call)
|
||||
**kwargs: Additional arguments (for future compatibility)
|
||||
"""
|
||||
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
|
||||
if self._ph_client is None:
|
||||
raise ValueError("posthog_client is required for PostHog tracking")
|
||||
raise ValueError("insights_client is required for Insights tracking")
|
||||
|
||||
self.models = AsyncModels(
|
||||
api_key=api_key,
|
||||
@@ -94,21 +94,21 @@ class AsyncClient:
|
||||
location=location,
|
||||
debug_config=debug_config,
|
||||
http_options=http_options,
|
||||
posthog_client=self._ph_client,
|
||||
posthog_distinct_id=posthog_distinct_id,
|
||||
posthog_properties=posthog_properties,
|
||||
posthog_privacy_mode=posthog_privacy_mode,
|
||||
posthog_groups=posthog_groups,
|
||||
insights_client=self._ph_client,
|
||||
insights_distinct_id=insights_distinct_id,
|
||||
insights_properties=insights_properties,
|
||||
insights_privacy_mode=insights_privacy_mode,
|
||||
insights_groups=insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class AsyncModels:
|
||||
"""
|
||||
Async Models interface that mimics genai.Client().aio.models with PostHog tracking.
|
||||
Async Models interface that mimics genai.Client().aio.models with Insights tracking.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient # Not None after __init__ validation
|
||||
_ph_client: InsightsClient # Not None after __init__ validation
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -119,11 +119,11 @@ class AsyncModels:
|
||||
location: Optional[str] = None,
|
||||
debug_config: Optional[Any] = None,
|
||||
http_options: Optional[Any] = None,
|
||||
posthog_client: Optional[PostHogClient] = None,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_client: Optional[InsightsClient] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -135,24 +135,24 @@ class AsyncModels:
|
||||
location: GCP location for Vertex AI
|
||||
debug_config: Debug configuration for the client
|
||||
http_options: HTTP options for the client
|
||||
posthog_client: PostHog client for tracking usage
|
||||
posthog_distinct_id: Default distinct ID for all calls
|
||||
posthog_properties: Default properties for all calls
|
||||
posthog_privacy_mode: Default privacy mode for all calls
|
||||
posthog_groups: Default groups for all calls
|
||||
insights_client: Insights client for tracking usage
|
||||
insights_distinct_id: Default distinct ID for all calls
|
||||
insights_properties: Default properties for all calls
|
||||
insights_privacy_mode: Default privacy mode for all calls
|
||||
insights_groups: Default groups for all calls
|
||||
**kwargs: Additional arguments (for future compatibility)
|
||||
"""
|
||||
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
|
||||
if self._ph_client is None:
|
||||
raise ValueError("posthog_client is required for PostHog tracking")
|
||||
raise ValueError("insights_client is required for Insights tracking")
|
||||
|
||||
# Store default PostHog settings
|
||||
self._default_distinct_id = posthog_distinct_id
|
||||
self._default_properties = posthog_properties or {}
|
||||
self._default_privacy_mode = posthog_privacy_mode
|
||||
self._default_groups = posthog_groups
|
||||
# Store default Insights settings
|
||||
self._default_distinct_id = insights_distinct_id
|
||||
self._default_properties = insights_properties or {}
|
||||
self._default_privacy_mode = insights_privacy_mode
|
||||
self._default_groups = insights_groups
|
||||
|
||||
# Build genai.Client arguments
|
||||
client_args: Dict[str, Any] = {}
|
||||
@@ -196,7 +196,7 @@ class AsyncModels:
|
||||
self._client = genai.Client(**client_args)
|
||||
self._base_url = "https://generativelanguage.googleapis.com"
|
||||
|
||||
def _merge_posthog_params(
|
||||
def _merge_insights_params(
|
||||
self,
|
||||
call_distinct_id: Optional[str],
|
||||
call_trace_id: Optional[str],
|
||||
@@ -204,7 +204,7 @@ class AsyncModels:
|
||||
call_privacy_mode: Optional[bool],
|
||||
call_groups: Optional[Dict[str, Any]],
|
||||
):
|
||||
"""Merge call-level PostHog parameters with client defaults."""
|
||||
"""Merge call-level Insights parameters with client defaults."""
|
||||
|
||||
# Use call-level values if provided, otherwise fall back to defaults
|
||||
distinct_id = (
|
||||
@@ -234,38 +234,38 @@ class AsyncModels:
|
||||
self,
|
||||
model: str,
|
||||
contents,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: Optional[bool] = None,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: Optional[bool] = None,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Generate content using Gemini's API while tracking usage in PostHog.
|
||||
Generate content using Gemini's API while tracking usage in Insights.
|
||||
|
||||
This method signature exactly matches genai.Client().aio.models.generate_content()
|
||||
with additional PostHog tracking parameters.
|
||||
with additional Insights tracking parameters.
|
||||
|
||||
Args:
|
||||
model: The model to use (e.g., 'gemini-2.0-flash')
|
||||
contents: The input content for generation
|
||||
posthog_distinct_id: ID to associate with the usage event (overrides client default)
|
||||
posthog_trace_id: Trace UUID for linking events (auto-generated if not provided)
|
||||
posthog_properties: Extra properties to include in the event (merged with client defaults)
|
||||
posthog_privacy_mode: Whether to redact sensitive information (overrides client default)
|
||||
posthog_groups: Group analytics properties (overrides client default)
|
||||
insights_distinct_id: ID to associate with the usage event (overrides client default)
|
||||
insights_trace_id: Trace UUID for linking events (auto-generated if not provided)
|
||||
insights_properties: Extra properties to include in the event (merged with client defaults)
|
||||
insights_privacy_mode: Whether to redact sensitive information (overrides client default)
|
||||
insights_groups: Group analytics properties (overrides client default)
|
||||
**kwargs: Arguments passed to Gemini's generate_content
|
||||
"""
|
||||
|
||||
# Merge PostHog parameters
|
||||
# Merge Insights parameters
|
||||
distinct_id, trace_id, properties, privacy_mode, groups = (
|
||||
self._merge_posthog_params(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
self._merge_insights_params(
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -383,7 +383,7 @@ class AsyncModels:
|
||||
capture_streaming_event(self._ph_client, event_data)
|
||||
|
||||
def _format_input(self, contents, **kwargs):
|
||||
"""Format input contents for PostHog tracking"""
|
||||
"""Format input contents for Insights tracking"""
|
||||
|
||||
# Create kwargs dict with contents for merge_system_prompt
|
||||
input_kwargs = {"contents": contents, **kwargs}
|
||||
@@ -393,21 +393,21 @@ class AsyncModels:
|
||||
self,
|
||||
model: str,
|
||||
contents,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: Optional[bool] = None,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: Optional[bool] = None,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
# Merge PostHog parameters
|
||||
# Merge Insights parameters
|
||||
distinct_id, trace_id, properties, privacy_mode, groups = (
|
||||
self._merge_posthog_params(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
self._merge_insights_params(
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
)
|
||||
)
|
||||
|
||||
+5
-5
@@ -2,17 +2,17 @@
|
||||
Gemini-specific conversion utilities.
|
||||
|
||||
This module handles the conversion of Gemini API responses and inputs
|
||||
into standardized formats for PostHog tracking.
|
||||
into standardized formats for Insights tracking.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, TypedDict, Union
|
||||
|
||||
from posthog.ai.types import (
|
||||
from hanzo_insights.ai.types import (
|
||||
FormattedContentItem,
|
||||
FormattedMessage,
|
||||
TokenUsage,
|
||||
)
|
||||
from posthog.ai.utils import serialize_raw_usage
|
||||
from hanzo_insights.ai.utils import serialize_raw_usage
|
||||
|
||||
|
||||
class GeminiPart(TypedDict, total=False):
|
||||
@@ -349,7 +349,7 @@ def format_gemini_input_with_system(
|
||||
if system_instruction is not None:
|
||||
has_system = any(msg.get("role") == "system" for msg in formatted_messages)
|
||||
if not has_system:
|
||||
from posthog.ai.types import FormattedMessage
|
||||
from hanzo_insights.ai.types import FormattedMessage
|
||||
|
||||
system_message: FormattedMessage = {
|
||||
"role": "system",
|
||||
@@ -362,7 +362,7 @@ def format_gemini_input_with_system(
|
||||
|
||||
def format_gemini_input(contents: Any) -> List[FormattedMessage]:
|
||||
"""
|
||||
Format Gemini input contents into standardized message format for PostHog tracking.
|
||||
Format Gemini input contents into standardized message format for Insights tracking.
|
||||
|
||||
This function handles various input formats:
|
||||
- String inputs
|
||||
@@ -41,12 +41,12 @@ from langchain_core.messages import (
|
||||
from langchain_core.outputs import ChatGeneration, LLMResult
|
||||
from pydantic import BaseModel
|
||||
|
||||
from posthog import setup
|
||||
from posthog.ai.sanitization import sanitize_langchain
|
||||
from posthog.ai.utils import get_model_params, with_privacy_mode
|
||||
from posthog.client import Client
|
||||
from hanzo_insights import setup
|
||||
from hanzo_insights.ai.sanitization import sanitize_langchain
|
||||
from hanzo_insights.ai.utils import get_model_params, with_privacy_mode
|
||||
from hanzo_insights.client import Client
|
||||
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -79,8 +79,8 @@ class GenerationMetadata(SpanMetadata):
|
||||
"""Base URL of the provider's API used in the run."""
|
||||
tools: Optional[List[Dict[str, Any]]] = None
|
||||
"""Tools provided to the model."""
|
||||
posthog_properties: Optional[Dict[str, Any]] = None
|
||||
"""PostHog properties of the run."""
|
||||
insights_properties: Optional[Dict[str, Any]] = None
|
||||
"""Insights properties of the run."""
|
||||
|
||||
|
||||
RunMetadata = Union[SpanMetadata, GenerationMetadata]
|
||||
@@ -89,11 +89,11 @@ RunMetadataStorage = Dict[UUID, RunMetadata]
|
||||
|
||||
class CallbackHandler(BaseCallbackHandler):
|
||||
"""
|
||||
The PostHog LLM observability callback handler for LangChain.
|
||||
The Insights LLM observability callback handler for LangChain.
|
||||
"""
|
||||
|
||||
_ph_client: Client
|
||||
"""PostHog client instance."""
|
||||
"""Insights client instance."""
|
||||
|
||||
_distinct_id: Optional[Union[str, int, UUID]]
|
||||
"""Distinct ID of the user to associate the trace with."""
|
||||
@@ -131,12 +131,12 @@ class CallbackHandler(BaseCallbackHandler):
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
client: PostHog client instance.
|
||||
client: Insights client instance.
|
||||
distinct_id: Optional distinct ID of the user to associate the trace with.
|
||||
trace_id: Optional trace ID to use for the event.
|
||||
properties: Optional additional metadata to use for the trace.
|
||||
privacy_mode: Whether to redact the input and output of the trace.
|
||||
groups: Optional additional PostHog groups to use for the trace.
|
||||
groups: Optional additional Insights groups to use for the trace.
|
||||
"""
|
||||
self._ph_client = client or setup()
|
||||
self._distinct_id = distinct_id
|
||||
@@ -423,7 +423,7 @@ class CallbackHandler(BaseCallbackHandler):
|
||||
if provider := metadata.get("ls_provider"):
|
||||
generation.provider = provider
|
||||
|
||||
generation.posthog_properties = metadata.get("posthog_properties")
|
||||
generation.insights_properties = metadata.get("insights_properties")
|
||||
try:
|
||||
base_url = serialized["kwargs"]["openai_api_base"]
|
||||
if base_url is not None:
|
||||
@@ -578,8 +578,8 @@ class CallbackHandler(BaseCallbackHandler):
|
||||
"$ai_framework": "langchain",
|
||||
}
|
||||
|
||||
if isinstance(run.posthog_properties, dict):
|
||||
event_properties.update(run.posthog_properties)
|
||||
if isinstance(run.insights_properties, dict):
|
||||
event_properties.update(run.insights_properties)
|
||||
|
||||
if run.tools:
|
||||
event_properties["$ai_tools"] = run.tools
|
||||
@@ -2,7 +2,7 @@ import time
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from posthog.ai.types import TokenUsage
|
||||
from hanzo_insights.ai.types import TokenUsage
|
||||
|
||||
try:
|
||||
import openai
|
||||
@@ -11,40 +11,40 @@ except ImportError:
|
||||
"Please install the OpenAI SDK to use this feature: 'pip install openai'"
|
||||
)
|
||||
|
||||
from posthog.ai.utils import (
|
||||
from hanzo_insights.ai.utils import (
|
||||
call_llm_and_track_usage,
|
||||
extract_available_tool_calls,
|
||||
merge_usage_stats,
|
||||
with_privacy_mode,
|
||||
)
|
||||
from posthog.ai.openai.openai_converter import (
|
||||
from hanzo_insights.ai.openai.openai_converter import (
|
||||
extract_openai_usage_from_chunk,
|
||||
extract_openai_content_from_chunk,
|
||||
extract_openai_tool_calls_from_chunk,
|
||||
accumulate_openai_tool_calls,
|
||||
)
|
||||
from posthog.ai.sanitization import sanitize_openai, sanitize_openai_response
|
||||
from posthog.client import Client as PostHogClient
|
||||
from posthog import setup
|
||||
from hanzo_insights.ai.sanitization import sanitize_openai, sanitize_openai_response
|
||||
from hanzo_insights.client import Client as InsightsClient
|
||||
from hanzo_insights import setup
|
||||
|
||||
|
||||
class OpenAI(openai.OpenAI):
|
||||
"""
|
||||
A wrapper around the OpenAI SDK that automatically sends LLM usage events to PostHog.
|
||||
A wrapper around the OpenAI SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
"""
|
||||
Args:
|
||||
api_key: OpenAI API key.
|
||||
posthog_client: If provided, events will be captured via this client instead of the global `posthog`.
|
||||
insights_client: If provided, events will be captured via this client instead of the global client.
|
||||
**openai_config: Any additional keyword args to set on openai (e.g. organization="xxx").
|
||||
"""
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
|
||||
# Store original objects after parent initialization (only if they exist)
|
||||
self._original_chat = getattr(self, "chat", None)
|
||||
@@ -67,7 +67,7 @@ class OpenAI(openai.OpenAI):
|
||||
|
||||
|
||||
class WrappedResponses:
|
||||
"""Wrapper for OpenAI responses that tracks usage in PostHog."""
|
||||
"""Wrapper for OpenAI responses that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: OpenAI, original_responses):
|
||||
self._client = client
|
||||
@@ -79,34 +79,34 @@ class WrappedResponses:
|
||||
|
||||
def create(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
if kwargs.get("stream", False):
|
||||
return self._create_streaming(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return call_llm_and_track_usage(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"openai",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
self._original.create,
|
||||
**kwargs,
|
||||
@@ -114,11 +114,11 @@ class WrappedResponses:
|
||||
|
||||
def _create_streaming(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
**kwargs: Any,
|
||||
):
|
||||
start_time = time.time()
|
||||
@@ -160,11 +160,11 @@ class WrappedResponses:
|
||||
latency = end_time - start_time
|
||||
output = final_content
|
||||
self._capture_streaming_event(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
kwargs,
|
||||
usage_stats,
|
||||
latency,
|
||||
@@ -177,11 +177,11 @@ class WrappedResponses:
|
||||
|
||||
def _capture_streaming_event(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
kwargs: Dict[str, Any],
|
||||
usage_stats: TokenUsage,
|
||||
latency: float,
|
||||
@@ -189,12 +189,12 @@ class WrappedResponses:
|
||||
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
|
||||
model_from_response: Optional[str] = None,
|
||||
):
|
||||
from posthog.ai.types import StreamingEventData
|
||||
from posthog.ai.openai.openai_converter import (
|
||||
from hanzo_insights.ai.types import StreamingEventData
|
||||
from hanzo_insights.ai.openai.openai_converter import (
|
||||
format_openai_streaming_input,
|
||||
format_openai_streaming_output,
|
||||
)
|
||||
from posthog.ai.utils import capture_streaming_event
|
||||
from hanzo_insights.ai.utils import capture_streaming_event
|
||||
|
||||
# Prepare standardized event data
|
||||
formatted_input = format_openai_streaming_input(kwargs, "responses")
|
||||
@@ -212,11 +212,11 @@ class WrappedResponses:
|
||||
formatted_output=format_openai_streaming_output(output, "responses"),
|
||||
usage_stats=usage_stats,
|
||||
latency=latency,
|
||||
distinct_id=posthog_distinct_id,
|
||||
trace_id=posthog_trace_id,
|
||||
properties=posthog_properties,
|
||||
privacy_mode=posthog_privacy_mode,
|
||||
groups=posthog_groups,
|
||||
distinct_id=insights_distinct_id,
|
||||
trace_id=insights_trace_id,
|
||||
properties=insights_properties,
|
||||
privacy_mode=insights_privacy_mode,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
# Use the common capture function
|
||||
@@ -224,35 +224,35 @@ class WrappedResponses:
|
||||
|
||||
def parse(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Parse structured output using OpenAI's 'responses.parse' method, but also track usage in PostHog.
|
||||
Parse structured output using OpenAI's 'responses.parse' method, but also track usage in Insights.
|
||||
|
||||
Args:
|
||||
posthog_distinct_id: Optional ID to associate with the usage event.
|
||||
posthog_trace_id: Optional trace UUID for linking events.
|
||||
posthog_properties: Optional dictionary of extra properties to include in the event.
|
||||
posthog_privacy_mode: Whether to anonymize the input and output.
|
||||
posthog_groups: Optional dictionary of groups to associate with the event.
|
||||
insights_distinct_id: Optional ID to associate with the usage event.
|
||||
insights_trace_id: Optional trace UUID for linking events.
|
||||
insights_properties: Optional dictionary of extra properties to include in the event.
|
||||
insights_privacy_mode: Whether to anonymize the input and output.
|
||||
insights_groups: Optional dictionary of groups to associate with the event.
|
||||
**kwargs: Any additional parameters for the OpenAI Responses Parse API.
|
||||
|
||||
Returns:
|
||||
The response from OpenAI's responses.parse call.
|
||||
"""
|
||||
return call_llm_and_track_usage(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"openai",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
self._original.parse,
|
||||
**kwargs,
|
||||
@@ -260,7 +260,7 @@ class WrappedResponses:
|
||||
|
||||
|
||||
class WrappedChat:
|
||||
"""Wrapper for OpenAI chat that tracks usage in PostHog."""
|
||||
"""Wrapper for OpenAI chat that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: OpenAI, original_chat):
|
||||
self._client = client
|
||||
@@ -276,7 +276,7 @@ class WrappedChat:
|
||||
|
||||
|
||||
class WrappedCompletions:
|
||||
"""Wrapper for OpenAI chat completions that tracks usage in PostHog."""
|
||||
"""Wrapper for OpenAI chat completions that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: OpenAI, original_completions):
|
||||
self._client = client
|
||||
@@ -288,34 +288,34 @@ class WrappedCompletions:
|
||||
|
||||
def create(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
if kwargs.get("stream", False):
|
||||
return self._create_streaming(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return call_llm_and_track_usage(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"openai",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
self._original.create,
|
||||
**kwargs,
|
||||
@@ -323,11 +323,11 @@ class WrappedCompletions:
|
||||
|
||||
def _create_streaming(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
**kwargs: Any,
|
||||
):
|
||||
start_time = time.time()
|
||||
@@ -385,11 +385,11 @@ class WrappedCompletions:
|
||||
)
|
||||
|
||||
self._capture_streaming_event(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
kwargs,
|
||||
usage_stats,
|
||||
latency,
|
||||
@@ -403,11 +403,11 @@ class WrappedCompletions:
|
||||
|
||||
def _capture_streaming_event(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
kwargs: Dict[str, Any],
|
||||
usage_stats: TokenUsage,
|
||||
latency: float,
|
||||
@@ -416,12 +416,12 @@ class WrappedCompletions:
|
||||
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
|
||||
model_from_response: Optional[str] = None,
|
||||
):
|
||||
from posthog.ai.types import StreamingEventData
|
||||
from posthog.ai.openai.openai_converter import (
|
||||
from hanzo_insights.ai.types import StreamingEventData
|
||||
from hanzo_insights.ai.openai.openai_converter import (
|
||||
format_openai_streaming_input,
|
||||
format_openai_streaming_output,
|
||||
)
|
||||
from posthog.ai.utils import capture_streaming_event
|
||||
from hanzo_insights.ai.utils import capture_streaming_event
|
||||
|
||||
# Prepare standardized event data
|
||||
formatted_input = format_openai_streaming_input(kwargs, "chat")
|
||||
@@ -439,11 +439,11 @@ class WrappedCompletions:
|
||||
formatted_output=format_openai_streaming_output(output, "chat", tool_calls),
|
||||
usage_stats=usage_stats,
|
||||
latency=latency,
|
||||
distinct_id=posthog_distinct_id,
|
||||
trace_id=posthog_trace_id,
|
||||
properties=posthog_properties,
|
||||
privacy_mode=posthog_privacy_mode,
|
||||
groups=posthog_groups,
|
||||
distinct_id=insights_distinct_id,
|
||||
trace_id=insights_trace_id,
|
||||
properties=insights_properties,
|
||||
privacy_mode=insights_privacy_mode,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
# Use the common capture function
|
||||
@@ -451,7 +451,7 @@ class WrappedCompletions:
|
||||
|
||||
|
||||
class WrappedEmbeddings:
|
||||
"""Wrapper for OpenAI embeddings that tracks usage in PostHog."""
|
||||
"""Wrapper for OpenAI embeddings that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: OpenAI, original_embeddings):
|
||||
self._client = client
|
||||
@@ -463,30 +463,30 @@ class WrappedEmbeddings:
|
||||
|
||||
def create(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Create an embedding using OpenAI's 'embeddings.create' method, but also track usage in PostHog.
|
||||
Create an embedding using OpenAI's 'embeddings.create' method, but also track usage in Insights.
|
||||
|
||||
Args:
|
||||
posthog_distinct_id: Optional ID to associate with the usage event.
|
||||
posthog_trace_id: Optional trace UUID for linking events.
|
||||
posthog_properties: Optional dictionary of extra properties to include in the event.
|
||||
posthog_privacy_mode: Whether to anonymize the input and output.
|
||||
posthog_groups: Optional dictionary of groups to associate with the event.
|
||||
insights_distinct_id: Optional ID to associate with the usage event.
|
||||
insights_trace_id: Optional trace UUID for linking events.
|
||||
insights_properties: Optional dictionary of extra properties to include in the event.
|
||||
insights_privacy_mode: Whether to anonymize the input and output.
|
||||
insights_groups: Optional dictionary of groups to associate with the event.
|
||||
**kwargs: Any additional parameters for the OpenAI Embeddings API.
|
||||
|
||||
Returns:
|
||||
The response from OpenAI's embeddings.create call.
|
||||
"""
|
||||
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
start_time = time.time()
|
||||
response = self._original.create(**kwargs)
|
||||
@@ -508,34 +508,34 @@ class WrappedEmbeddings:
|
||||
"$ai_model": kwargs.get("model"),
|
||||
"$ai_input": with_privacy_mode(
|
||||
self._client._ph_client,
|
||||
posthog_privacy_mode,
|
||||
insights_privacy_mode,
|
||||
sanitize_openai_response(kwargs.get("input")),
|
||||
),
|
||||
"$ai_http_status": 200,
|
||||
"$ai_input_tokens": usage_stats.get("prompt_tokens", 0),
|
||||
"$ai_latency": latency,
|
||||
"$ai_trace_id": posthog_trace_id,
|
||||
"$ai_trace_id": insights_trace_id,
|
||||
"$ai_base_url": str(self._client.base_url),
|
||||
**(posthog_properties or {}),
|
||||
**(insights_properties or {}),
|
||||
}
|
||||
|
||||
if posthog_distinct_id is None:
|
||||
if insights_distinct_id is None:
|
||||
event_properties["$process_person_profile"] = False
|
||||
|
||||
# Send capture event for embeddings
|
||||
if hasattr(self._client._ph_client, "capture"):
|
||||
self._client._ph_client.capture(
|
||||
distinct_id=posthog_distinct_id or posthog_trace_id,
|
||||
distinct_id=insights_distinct_id or insights_trace_id,
|
||||
event="$ai_embedding",
|
||||
properties=event_properties,
|
||||
groups=posthog_groups,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class WrappedBeta:
|
||||
"""Wrapper for OpenAI beta features that tracks usage in PostHog."""
|
||||
"""Wrapper for OpenAI beta features that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: OpenAI, original_beta):
|
||||
self._client = client
|
||||
@@ -551,7 +551,7 @@ class WrappedBeta:
|
||||
|
||||
|
||||
class WrappedBetaChat:
|
||||
"""Wrapper for OpenAI beta chat that tracks usage in PostHog."""
|
||||
"""Wrapper for OpenAI beta chat that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: OpenAI, original_beta_chat):
|
||||
self._client = client
|
||||
@@ -567,7 +567,7 @@ class WrappedBetaChat:
|
||||
|
||||
|
||||
class WrappedBetaCompletions:
|
||||
"""Wrapper for OpenAI beta chat completions that tracks usage in PostHog."""
|
||||
"""Wrapper for OpenAI beta chat completions that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: OpenAI, original_beta_completions):
|
||||
self._client = client
|
||||
@@ -579,21 +579,21 @@ class WrappedBetaCompletions:
|
||||
|
||||
def parse(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
return call_llm_and_track_usage(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"openai",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
self._original.parse,
|
||||
**kwargs,
|
||||
@@ -2,7 +2,7 @@ import time
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from posthog.ai.types import TokenUsage
|
||||
from hanzo_insights.ai.types import TokenUsage
|
||||
|
||||
try:
|
||||
import openai
|
||||
@@ -11,43 +11,43 @@ except ImportError:
|
||||
"Please install the OpenAI SDK to use this feature: 'pip install openai'"
|
||||
)
|
||||
|
||||
from posthog import setup
|
||||
from posthog.ai.utils import (
|
||||
from hanzo_insights import setup
|
||||
from hanzo_insights.ai.utils import (
|
||||
call_llm_and_track_usage_async,
|
||||
extract_available_tool_calls,
|
||||
get_model_params,
|
||||
merge_usage_stats,
|
||||
with_privacy_mode,
|
||||
)
|
||||
from posthog.ai.openai.openai_converter import (
|
||||
from hanzo_insights.ai.openai.openai_converter import (
|
||||
extract_openai_usage_from_chunk,
|
||||
extract_openai_content_from_chunk,
|
||||
extract_openai_tool_calls_from_chunk,
|
||||
accumulate_openai_tool_calls,
|
||||
format_openai_streaming_output,
|
||||
)
|
||||
from posthog.ai.sanitization import sanitize_openai, sanitize_openai_response
|
||||
from posthog.client import Client as PostHogClient
|
||||
from hanzo_insights.ai.sanitization import sanitize_openai, sanitize_openai_response
|
||||
from hanzo_insights.client import Client as InsightsClient
|
||||
|
||||
|
||||
class AsyncOpenAI(openai.AsyncOpenAI):
|
||||
"""
|
||||
An async wrapper around the OpenAI SDK that automatically sends LLM usage events to PostHog.
|
||||
An async wrapper around the OpenAI SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
"""
|
||||
Args:
|
||||
api_key: OpenAI API key.
|
||||
posthog_client: If provided, events will be captured via this client instead
|
||||
of the global posthog.
|
||||
insights_client: If provided, events will be captured via this client instead
|
||||
of the global hanzo_insights.
|
||||
**openai_config: Any additional keyword args to set on openai (e.g. organization="xxx").
|
||||
"""
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
|
||||
# Store original objects after parent initialization (only if they exist)
|
||||
self._original_chat = getattr(self, "chat", None)
|
||||
@@ -70,7 +70,7 @@ class AsyncOpenAI(openai.AsyncOpenAI):
|
||||
|
||||
|
||||
class WrappedResponses:
|
||||
"""Async wrapper for OpenAI responses that tracks usage in PostHog."""
|
||||
"""Async wrapper for OpenAI responses that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: AsyncOpenAI, original_responses):
|
||||
self._client = client
|
||||
@@ -83,34 +83,34 @@ class WrappedResponses:
|
||||
|
||||
async def create(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
if kwargs.get("stream", False):
|
||||
return await self._create_streaming(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return await call_llm_and_track_usage_async(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"openai",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
self._original.create,
|
||||
**kwargs,
|
||||
@@ -118,11 +118,11 @@ class WrappedResponses:
|
||||
|
||||
async def _create_streaming(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
**kwargs: Any,
|
||||
):
|
||||
start_time = time.time()
|
||||
@@ -165,11 +165,11 @@ class WrappedResponses:
|
||||
output = final_content
|
||||
|
||||
await self._capture_streaming_event(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
kwargs,
|
||||
usage_stats,
|
||||
latency,
|
||||
@@ -182,11 +182,11 @@ class WrappedResponses:
|
||||
|
||||
async def _capture_streaming_event(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
kwargs: Dict[str, Any],
|
||||
usage_stats: TokenUsage,
|
||||
latency: float,
|
||||
@@ -194,8 +194,8 @@ class WrappedResponses:
|
||||
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
|
||||
model_from_response: Optional[str] = None,
|
||||
):
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
# Use model from kwargs, fallback to model from response
|
||||
model = kwargs.get("model") or model_from_response or "unknown"
|
||||
@@ -206,12 +206,12 @@ class WrappedResponses:
|
||||
"$ai_model_parameters": get_model_params(kwargs),
|
||||
"$ai_input": with_privacy_mode(
|
||||
self._client._ph_client,
|
||||
posthog_privacy_mode,
|
||||
insights_privacy_mode,
|
||||
sanitize_openai_response(kwargs.get("input")),
|
||||
),
|
||||
"$ai_output_choices": with_privacy_mode(
|
||||
self._client._ph_client,
|
||||
posthog_privacy_mode,
|
||||
insights_privacy_mode,
|
||||
format_openai_streaming_output(output, "responses"),
|
||||
),
|
||||
"$ai_http_status": 200,
|
||||
@@ -222,9 +222,9 @@ class WrappedResponses:
|
||||
),
|
||||
"$ai_reasoning_tokens": usage_stats.get("reasoning_tokens", 0),
|
||||
"$ai_latency": latency,
|
||||
"$ai_trace_id": posthog_trace_id,
|
||||
"$ai_trace_id": insights_trace_id,
|
||||
"$ai_base_url": str(self._client.base_url),
|
||||
**(posthog_properties or {}),
|
||||
**(insights_properties or {}),
|
||||
}
|
||||
|
||||
# Add web search count if present
|
||||
@@ -239,48 +239,48 @@ class WrappedResponses:
|
||||
if available_tool_calls:
|
||||
event_properties["$ai_tools"] = available_tool_calls
|
||||
|
||||
if posthog_distinct_id is None:
|
||||
if insights_distinct_id is None:
|
||||
event_properties["$process_person_profile"] = False
|
||||
|
||||
if hasattr(self._client._ph_client, "capture"):
|
||||
self._client._ph_client.capture(
|
||||
distinct_id=posthog_distinct_id or posthog_trace_id,
|
||||
distinct_id=insights_distinct_id or insights_trace_id,
|
||||
event="$ai_generation",
|
||||
properties=event_properties,
|
||||
groups=posthog_groups,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
async def parse(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Parse structured output using OpenAI's 'responses.parse' method, but also track usage in PostHog.
|
||||
Parse structured output using OpenAI's 'responses.parse' method, but also track usage in Insights.
|
||||
|
||||
Args:
|
||||
posthog_distinct_id: Optional ID to associate with the usage event.
|
||||
posthog_trace_id: Optional trace UUID for linking events.
|
||||
posthog_properties: Optional dictionary of extra properties to include in the event.
|
||||
posthog_privacy_mode: Whether to anonymize the input and output.
|
||||
posthog_groups: Optional dictionary of groups to associate with the event.
|
||||
insights_distinct_id: Optional ID to associate with the usage event.
|
||||
insights_trace_id: Optional trace UUID for linking events.
|
||||
insights_properties: Optional dictionary of extra properties to include in the event.
|
||||
insights_privacy_mode: Whether to anonymize the input and output.
|
||||
insights_groups: Optional dictionary of groups to associate with the event.
|
||||
**kwargs: Any additional parameters for the OpenAI Responses Parse API.
|
||||
|
||||
Returns:
|
||||
The response from OpenAI's responses.parse call.
|
||||
"""
|
||||
return await call_llm_and_track_usage_async(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"openai",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
self._original.parse,
|
||||
**kwargs,
|
||||
@@ -288,7 +288,7 @@ class WrappedResponses:
|
||||
|
||||
|
||||
class WrappedChat:
|
||||
"""Async wrapper for OpenAI chat that tracks usage in PostHog."""
|
||||
"""Async wrapper for OpenAI chat that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: AsyncOpenAI, original_chat):
|
||||
self._client = client
|
||||
@@ -304,7 +304,7 @@ class WrappedChat:
|
||||
|
||||
|
||||
class WrappedCompletions:
|
||||
"""Async wrapper for OpenAI chat completions that tracks usage in PostHog."""
|
||||
"""Async wrapper for OpenAI chat completions that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: AsyncOpenAI, original_completions):
|
||||
self._client = client
|
||||
@@ -316,35 +316,35 @@ class WrappedCompletions:
|
||||
|
||||
async def create(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
# If streaming, handle streaming specifically
|
||||
if kwargs.get("stream", False):
|
||||
return await self._create_streaming(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
response = await call_llm_and_track_usage_async(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"openai",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
self._original.create,
|
||||
**kwargs,
|
||||
@@ -353,11 +353,11 @@ class WrappedCompletions:
|
||||
|
||||
async def _create_streaming(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
**kwargs: Any,
|
||||
):
|
||||
start_time = time.time()
|
||||
@@ -414,11 +414,11 @@ class WrappedCompletions:
|
||||
)
|
||||
|
||||
await self._capture_streaming_event(
|
||||
posthog_distinct_id,
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_distinct_id,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
kwargs,
|
||||
usage_stats,
|
||||
latency,
|
||||
@@ -432,11 +432,11 @@ class WrappedCompletions:
|
||||
|
||||
async def _capture_streaming_event(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str],
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_distinct_id: Optional[str],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
kwargs: Dict[str, Any],
|
||||
usage_stats: TokenUsage,
|
||||
latency: float,
|
||||
@@ -445,8 +445,8 @@ class WrappedCompletions:
|
||||
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
|
||||
model_from_response: Optional[str] = None,
|
||||
):
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
# Use model from kwargs, fallback to model from response
|
||||
model = kwargs.get("model") or model_from_response or "unknown"
|
||||
@@ -457,12 +457,12 @@ class WrappedCompletions:
|
||||
"$ai_model_parameters": get_model_params(kwargs),
|
||||
"$ai_input": with_privacy_mode(
|
||||
self._client._ph_client,
|
||||
posthog_privacy_mode,
|
||||
insights_privacy_mode,
|
||||
sanitize_openai(kwargs.get("messages")),
|
||||
),
|
||||
"$ai_output_choices": with_privacy_mode(
|
||||
self._client._ph_client,
|
||||
posthog_privacy_mode,
|
||||
insights_privacy_mode,
|
||||
format_openai_streaming_output(output, "chat", tool_calls),
|
||||
),
|
||||
"$ai_http_status": 200,
|
||||
@@ -473,9 +473,9 @@ class WrappedCompletions:
|
||||
),
|
||||
"$ai_reasoning_tokens": usage_stats.get("reasoning_tokens", 0),
|
||||
"$ai_latency": latency,
|
||||
"$ai_trace_id": posthog_trace_id,
|
||||
"$ai_trace_id": insights_trace_id,
|
||||
"$ai_base_url": str(self._client.base_url),
|
||||
**(posthog_properties or {}),
|
||||
**(insights_properties or {}),
|
||||
}
|
||||
|
||||
# Add web search count if present
|
||||
@@ -491,20 +491,20 @@ class WrappedCompletions:
|
||||
if available_tool_calls:
|
||||
event_properties["$ai_tools"] = available_tool_calls
|
||||
|
||||
if posthog_distinct_id is None:
|
||||
if insights_distinct_id is None:
|
||||
event_properties["$process_person_profile"] = False
|
||||
|
||||
if hasattr(self._client._ph_client, "capture"):
|
||||
self._client._ph_client.capture(
|
||||
distinct_id=posthog_distinct_id or posthog_trace_id,
|
||||
distinct_id=insights_distinct_id or insights_trace_id,
|
||||
event="$ai_generation",
|
||||
properties=event_properties,
|
||||
groups=posthog_groups,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
|
||||
class WrappedEmbeddings:
|
||||
"""Async wrapper for OpenAI embeddings that tracks usage in PostHog."""
|
||||
"""Async wrapper for OpenAI embeddings that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: AsyncOpenAI, original_embeddings):
|
||||
self._client = client
|
||||
@@ -517,30 +517,30 @@ class WrappedEmbeddings:
|
||||
|
||||
async def create(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Create an embedding using OpenAI's 'embeddings.create' method, but also track usage in PostHog.
|
||||
Create an embedding using OpenAI's 'embeddings.create' method, but also track usage in Insights.
|
||||
|
||||
Args:
|
||||
posthog_distinct_id: Optional ID to associate with the usage event.
|
||||
posthog_trace_id: Optional trace UUID for linking events.
|
||||
posthog_properties: Optional dictionary of extra properties to include in the event.
|
||||
posthog_privacy_mode: Whether to anonymize the input and output.
|
||||
posthog_groups: Optional dictionary of groups to associate with the event.
|
||||
insights_distinct_id: Optional ID to associate with the usage event.
|
||||
insights_trace_id: Optional trace UUID for linking events.
|
||||
insights_properties: Optional dictionary of extra properties to include in the event.
|
||||
insights_privacy_mode: Whether to anonymize the input and output.
|
||||
insights_groups: Optional dictionary of groups to associate with the event.
|
||||
**kwargs: Any additional parameters for the OpenAI Embeddings API.
|
||||
|
||||
Returns:
|
||||
The response from OpenAI's embeddings.create call.
|
||||
"""
|
||||
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
start_time = time.time()
|
||||
response = await self._original.create(**kwargs)
|
||||
@@ -563,34 +563,34 @@ class WrappedEmbeddings:
|
||||
"$ai_model": kwargs.get("model"),
|
||||
"$ai_input": with_privacy_mode(
|
||||
self._client._ph_client,
|
||||
posthog_privacy_mode,
|
||||
insights_privacy_mode,
|
||||
sanitize_openai_response(kwargs.get("input")),
|
||||
),
|
||||
"$ai_http_status": 200,
|
||||
"$ai_input_tokens": usage_stats.get("input_tokens", 0),
|
||||
"$ai_latency": latency,
|
||||
"$ai_trace_id": posthog_trace_id,
|
||||
"$ai_trace_id": insights_trace_id,
|
||||
"$ai_base_url": str(self._client.base_url),
|
||||
**(posthog_properties or {}),
|
||||
**(insights_properties or {}),
|
||||
}
|
||||
|
||||
if posthog_distinct_id is None:
|
||||
if insights_distinct_id is None:
|
||||
event_properties["$process_person_profile"] = False
|
||||
|
||||
# Send capture event for embeddings
|
||||
if hasattr(self._client._ph_client, "capture"):
|
||||
self._client._ph_client.capture(
|
||||
distinct_id=posthog_distinct_id or posthog_trace_id,
|
||||
distinct_id=insights_distinct_id or insights_trace_id,
|
||||
event="$ai_embedding",
|
||||
properties=event_properties,
|
||||
groups=posthog_groups,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class WrappedBeta:
|
||||
"""Async wrapper for OpenAI beta features that tracks usage in PostHog."""
|
||||
"""Async wrapper for OpenAI beta features that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: AsyncOpenAI, original_beta):
|
||||
self._client = client
|
||||
@@ -607,7 +607,7 @@ class WrappedBeta:
|
||||
|
||||
|
||||
class WrappedBetaChat:
|
||||
"""Async wrapper for OpenAI beta chat that tracks usage in PostHog."""
|
||||
"""Async wrapper for OpenAI beta chat that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: AsyncOpenAI, original_beta_chat):
|
||||
self._client = client
|
||||
@@ -624,7 +624,7 @@ class WrappedBetaChat:
|
||||
|
||||
|
||||
class WrappedBetaCompletions:
|
||||
"""Async wrapper for OpenAI beta chat completions that tracks usage in PostHog."""
|
||||
"""Async wrapper for OpenAI beta chat completions that tracks usage in Insights."""
|
||||
|
||||
def __init__(self, client: AsyncOpenAI, original_beta_completions):
|
||||
self._client = client
|
||||
@@ -637,21 +637,21 @@ class WrappedBetaCompletions:
|
||||
|
||||
async def parse(
|
||||
self,
|
||||
posthog_distinct_id: Optional[str] = None,
|
||||
posthog_trace_id: Optional[str] = None,
|
||||
posthog_properties: Optional[Dict[str, Any]] = None,
|
||||
posthog_privacy_mode: bool = False,
|
||||
posthog_groups: Optional[Dict[str, Any]] = None,
|
||||
insights_distinct_id: Optional[str] = None,
|
||||
insights_trace_id: Optional[str] = None,
|
||||
insights_properties: Optional[Dict[str, Any]] = None,
|
||||
insights_privacy_mode: bool = False,
|
||||
insights_groups: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
return await call_llm_and_track_usage_async(
|
||||
posthog_distinct_id,
|
||||
insights_distinct_id,
|
||||
self._client._ph_client,
|
||||
"openai",
|
||||
posthog_trace_id,
|
||||
posthog_properties,
|
||||
posthog_privacy_mode,
|
||||
posthog_groups,
|
||||
insights_trace_id,
|
||||
insights_properties,
|
||||
insights_privacy_mode,
|
||||
insights_groups,
|
||||
self._client.base_url,
|
||||
self._original.parse,
|
||||
**kwargs,
|
||||
+5
-5
@@ -2,13 +2,13 @@
|
||||
OpenAI-specific conversion utilities.
|
||||
|
||||
This module handles the conversion of OpenAI API responses and inputs
|
||||
into standardized formats for PostHog tracking. It supports both
|
||||
into standardized formats for Insights tracking. It supports both
|
||||
Chat Completions API and Responses API formats.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from posthog.ai.types import (
|
||||
from hanzo_insights.ai.types import (
|
||||
FormattedContentItem,
|
||||
FormattedFunctionCall,
|
||||
FormattedImageContent,
|
||||
@@ -16,7 +16,7 @@ from posthog.ai.types import (
|
||||
FormattedTextContent,
|
||||
TokenUsage,
|
||||
)
|
||||
from posthog.ai.utils import serialize_raw_usage
|
||||
from hanzo_insights.ai.utils import serialize_raw_usage
|
||||
|
||||
|
||||
def format_openai_response(response: Any) -> List[FormattedMessage]:
|
||||
@@ -753,8 +753,8 @@ def format_openai_streaming_input(
|
||||
api_type: Either "chat" or "responses"
|
||||
|
||||
Returns:
|
||||
Formatted input ready for PostHog tracking
|
||||
Formatted input ready for Insights tracking
|
||||
"""
|
||||
from posthog.ai.utils import merge_system_prompt
|
||||
from hanzo_insights.ai.utils import merge_system_prompt
|
||||
|
||||
return merge_system_prompt(kwargs, "openai")
|
||||
+19
-19
@@ -5,39 +5,39 @@ except ImportError:
|
||||
"Please install the Open AI SDK to use this feature: 'pip install openai'"
|
||||
)
|
||||
|
||||
from posthog.ai.openai.openai import (
|
||||
from hanzo_insights.ai.openai.openai import (
|
||||
WrappedBeta,
|
||||
WrappedChat,
|
||||
WrappedEmbeddings,
|
||||
WrappedResponses,
|
||||
)
|
||||
from posthog.ai.openai.openai_async import WrappedBeta as AsyncWrappedBeta
|
||||
from posthog.ai.openai.openai_async import WrappedChat as AsyncWrappedChat
|
||||
from posthog.ai.openai.openai_async import WrappedEmbeddings as AsyncWrappedEmbeddings
|
||||
from posthog.ai.openai.openai_async import WrappedResponses as AsyncWrappedResponses
|
||||
from hanzo_insights.ai.openai.openai_async import WrappedBeta as AsyncWrappedBeta
|
||||
from hanzo_insights.ai.openai.openai_async import WrappedChat as AsyncWrappedChat
|
||||
from hanzo_insights.ai.openai.openai_async import WrappedEmbeddings as AsyncWrappedEmbeddings
|
||||
from hanzo_insights.ai.openai.openai_async import WrappedResponses as AsyncWrappedResponses
|
||||
from typing import Optional
|
||||
|
||||
from posthog.client import Client as PostHogClient
|
||||
from posthog import setup
|
||||
from hanzo_insights.client import Client as InsightsClient
|
||||
from hanzo_insights import setup
|
||||
|
||||
|
||||
class AzureOpenAI(openai.AzureOpenAI):
|
||||
"""
|
||||
A wrapper around the Azure OpenAI SDK that automatically sends LLM usage events to PostHog.
|
||||
A wrapper around the Azure OpenAI SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
"""
|
||||
Args:
|
||||
api_key: Azure OpenAI API key.
|
||||
posthog_client: If provided, events will be captured via this client instead
|
||||
of the global posthog.
|
||||
insights_client: If provided, events will be captured via this client instead
|
||||
of the global hanzo_insights.
|
||||
**openai_config: Any additional keyword args to set on Azure OpenAI (e.g. azure_endpoint="xxx").
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
|
||||
# Store original objects after parent initialization (only if they exist)
|
||||
self._original_chat = getattr(self, "chat", None)
|
||||
@@ -61,21 +61,21 @@ class AzureOpenAI(openai.AzureOpenAI):
|
||||
|
||||
class AsyncAzureOpenAI(openai.AsyncAzureOpenAI):
|
||||
"""
|
||||
An async wrapper around the Azure OpenAI SDK that automatically sends LLM usage events to PostHog.
|
||||
An async wrapper around the Azure OpenAI SDK that automatically sends LLM usage events to Insights.
|
||||
"""
|
||||
|
||||
_ph_client: PostHogClient
|
||||
_ph_client: InsightsClient
|
||||
|
||||
def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
|
||||
def __init__(self, insights_client: Optional[InsightsClient] = None, **kwargs):
|
||||
"""
|
||||
Args:
|
||||
api_key: Azure OpenAI API key.
|
||||
posthog_client: If provided, events will be captured via this client instead
|
||||
of the global posthog.
|
||||
insights_client: If provided, events will be captured via this client instead
|
||||
of the global hanzo_insights.
|
||||
**openai_config: Any additional keyword args to set on Azure OpenAI (e.g. azure_endpoint="xxx").
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self._ph_client = posthog_client or setup()
|
||||
self._ph_client = insights_client or setup()
|
||||
|
||||
# Store original objects after parent initialization (only if they exist)
|
||||
self._original_chat = getattr(self, "chat", None)
|
||||
+12
-12
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union
|
||||
if TYPE_CHECKING:
|
||||
from agents.tracing import Trace
|
||||
|
||||
from posthog.client import Client
|
||||
from hanzo_insights.client import Client
|
||||
|
||||
try:
|
||||
import agents # noqa: F401
|
||||
@@ -14,9 +14,9 @@ except ImportError:
|
||||
"Please install the OpenAI Agents SDK to use this feature: 'pip install openai-agents'"
|
||||
)
|
||||
|
||||
from posthog.ai.openai_agents.processor import PostHogTracingProcessor
|
||||
from hanzo_insights.ai.openai_agents.processor import InsightsTracingProcessor
|
||||
|
||||
__all__ = ["PostHogTracingProcessor", "instrument"]
|
||||
__all__ = ["InsightsTracingProcessor", "instrument"]
|
||||
|
||||
|
||||
def instrument(
|
||||
@@ -25,27 +25,27 @@ def instrument(
|
||||
privacy_mode: bool = False,
|
||||
groups: Optional[Dict[str, Any]] = None,
|
||||
properties: Optional[Dict[str, Any]] = None,
|
||||
) -> PostHogTracingProcessor:
|
||||
) -> InsightsTracingProcessor:
|
||||
"""
|
||||
One-liner to instrument OpenAI Agents SDK with PostHog tracing.
|
||||
One-liner to instrument OpenAI Agents SDK with Hanzo Insights tracing.
|
||||
|
||||
This registers a PostHogTracingProcessor with the OpenAI Agents SDK,
|
||||
This registers an InsightsTracingProcessor with the OpenAI Agents SDK,
|
||||
automatically capturing traces, spans, and LLM generations.
|
||||
|
||||
Args:
|
||||
client: Optional PostHog client instance. If not provided, uses the default client.
|
||||
client: Optional Insights client instance. If not provided, uses the default client.
|
||||
distinct_id: Optional distinct ID to associate with all traces.
|
||||
Can also be a callable that takes a trace and returns a distinct ID.
|
||||
privacy_mode: If True, redacts input/output content from events.
|
||||
groups: Optional PostHog groups to associate with events.
|
||||
groups: Optional Insights groups to associate with events.
|
||||
properties: Optional additional properties to include with all events.
|
||||
|
||||
Returns:
|
||||
PostHogTracingProcessor: The registered processor instance.
|
||||
InsightsTracingProcessor: The registered processor instance.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from posthog.ai.openai_agents import instrument
|
||||
from hanzo_insights.ai.openai_agents import instrument
|
||||
|
||||
# Simple setup
|
||||
instrument(distinct_id="user@example.com")
|
||||
@@ -57,7 +57,7 @@ def instrument(
|
||||
properties={"environment": "production"}
|
||||
)
|
||||
|
||||
# Now run agents as normal - traces automatically sent to PostHog
|
||||
# Now run agents as normal - traces automatically sent to Insights
|
||||
from agents import Agent, Runner
|
||||
agent = Agent(name="Assistant", instructions="You are helpful.")
|
||||
result = Runner.run_sync(agent, "Hello!")
|
||||
@@ -65,7 +65,7 @@ def instrument(
|
||||
"""
|
||||
from agents.tracing import add_trace_processor
|
||||
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=client,
|
||||
distinct_id=distinct_id,
|
||||
privacy_mode=privacy_mode,
|
||||
+15
-15
@@ -20,10 +20,10 @@ from agents.tracing.span_data import (
|
||||
TranscriptionSpanData,
|
||||
)
|
||||
|
||||
from posthog import setup
|
||||
from posthog.client import Client
|
||||
from hanzo_insights import setup
|
||||
from hanzo_insights.client import Client
|
||||
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
|
||||
|
||||
def _ensure_serializable(obj: Any) -> Any:
|
||||
@@ -53,27 +53,27 @@ def _parse_iso_timestamp(iso_str: Optional[str]) -> Optional[float]:
|
||||
return None
|
||||
|
||||
|
||||
class PostHogTracingProcessor(TracingProcessor):
|
||||
class InsightsTracingProcessor(TracingProcessor):
|
||||
"""
|
||||
A tracing processor that sends OpenAI Agents SDK traces to PostHog.
|
||||
A tracing processor that sends OpenAI Agents SDK traces to Hanzo Insights.
|
||||
|
||||
This processor implements the TracingProcessor interface from the OpenAI Agents SDK
|
||||
and maps agent traces, spans, and generations to PostHog's LLM analytics events.
|
||||
and maps agent traces, spans, and generations to Insights LLM analytics events.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from agents import Agent, Runner
|
||||
from agents.tracing import add_trace_processor
|
||||
from posthog.ai.openai_agents import PostHogTracingProcessor
|
||||
from hanzo_insights.ai.openai_agents import InsightsTracingProcessor
|
||||
|
||||
# Create and register the processor
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
distinct_id="user@example.com",
|
||||
privacy_mode=False,
|
||||
)
|
||||
add_trace_processor(processor)
|
||||
|
||||
# Run agents as normal - traces automatically sent to PostHog
|
||||
# Run agents as normal - traces automatically sent to Insights
|
||||
agent = Agent(name="Assistant", instructions="You are helpful.")
|
||||
result = Runner.run_sync(agent, "Hello!")
|
||||
```
|
||||
@@ -88,14 +88,14 @@ class PostHogTracingProcessor(TracingProcessor):
|
||||
properties: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the PostHog tracing processor.
|
||||
Initialize the Insights tracing processor.
|
||||
|
||||
Args:
|
||||
client: Optional PostHog client instance. If not provided, uses the default client.
|
||||
client: Optional Insights client instance. If not provided, uses the default client.
|
||||
distinct_id: Either a string distinct ID or a callable that takes a Trace
|
||||
and returns a distinct ID. If not provided, uses the trace_id.
|
||||
privacy_mode: If True, redacts input/output content from events.
|
||||
groups: Optional PostHog groups to associate with all events.
|
||||
groups: Optional Insights groups to associate with all events.
|
||||
properties: Optional additional properties to include with all events.
|
||||
"""
|
||||
self._client = client or setup()
|
||||
@@ -173,7 +173,7 @@ class PostHogTracingProcessor(TracingProcessor):
|
||||
properties: Dict[str, Any],
|
||||
distinct_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Capture an event to PostHog with error handling.
|
||||
"""Capture an event to Insights with error handling.
|
||||
|
||||
Args:
|
||||
distinct_id: The resolved distinct ID. When the user didn't provide
|
||||
@@ -199,7 +199,7 @@ class PostHogTracingProcessor(TracingProcessor):
|
||||
groups=self._groups,
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(f"Failed to capture PostHog event: {e}")
|
||||
log.debug(f"Failed to capture Insights event: {e}")
|
||||
|
||||
def on_trace_start(self, trace: Trace) -> None:
|
||||
"""Called when a new trace begins. Stores metadata for spans; the $ai_trace event is emitted in on_trace_end."""
|
||||
@@ -848,7 +848,7 @@ class PostHogTracingProcessor(TracingProcessor):
|
||||
self._span_start_times.clear()
|
||||
self._trace_metadata.clear()
|
||||
|
||||
# Flush the PostHog client if possible
|
||||
# Flush the Insights client if possible
|
||||
if hasattr(self._client, "flush") and callable(self._client.flush):
|
||||
self._client.flush()
|
||||
except Exception as e:
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Prompt management for PostHog AI SDK.
|
||||
Prompt management for Hanzo Insights AI SDK.
|
||||
|
||||
Fetch and compile LLM prompts from PostHog with caching and fallback support.
|
||||
Fetch and compile LLM prompts from Insights with caching and fallback support.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -10,15 +10,16 @@ import time
|
||||
import urllib.parse
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from posthog.request import USER_AGENT, _get_session
|
||||
from posthog.utils import remove_trailing_slash
|
||||
from hanzo_insights.request import USER_AGENT, _get_session
|
||||
from hanzo_insights.utils import remove_trailing_slash
|
||||
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
|
||||
APP_ENDPOINT = "https://us.posthog.com"
|
||||
APP_ENDPOINT = "https://us.insights.hanzo.ai"
|
||||
DEFAULT_CACHE_TTL_SECONDS = 300 # 5 minutes
|
||||
|
||||
PromptVariables = Dict[str, Union[str, int, float, bool]]
|
||||
PromptCacheKey = tuple[str, Optional[int]]
|
||||
|
||||
|
||||
class CachedPrompt:
|
||||
@@ -29,6 +30,19 @@ class CachedPrompt:
|
||||
self.fetched_at = fetched_at
|
||||
|
||||
|
||||
def _cache_key(name: str, version: Optional[int]) -> PromptCacheKey:
|
||||
"""Build a cache key for latest or versioned prompt fetches."""
|
||||
return (name, version)
|
||||
|
||||
|
||||
def _prompt_reference(name: str, version: Optional[int]) -> str:
|
||||
"""Format a prompt reference for logs and errors."""
|
||||
label = f'prompt "{name}"'
|
||||
if version is not None:
|
||||
return f"{label} version {version}"
|
||||
return label
|
||||
|
||||
|
||||
def _is_prompt_api_response(data: Any) -> bool:
|
||||
"""Check if the response is a valid prompt API response."""
|
||||
return (
|
||||
@@ -40,29 +54,32 @@ def _is_prompt_api_response(data: Any) -> bool:
|
||||
|
||||
class Prompts:
|
||||
"""
|
||||
Fetch and compile LLM prompts from PostHog.
|
||||
Fetch and compile LLM prompts from Insights.
|
||||
|
||||
Can be initialized with a PostHog client or with direct options.
|
||||
Can be initialized with a Insights client or with direct options.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import Posthog
|
||||
from posthog.ai.prompts import Prompts
|
||||
from hanzo_insights import Insights
|
||||
from hanzo_insights.ai.prompts import Prompts
|
||||
|
||||
# With PostHog client
|
||||
posthog = Posthog('phc_xxx', host='https://us.posthog.com', personal_api_key='phx_xxx')
|
||||
prompts = Prompts(posthog)
|
||||
# With Insights client
|
||||
client = Insights('phc_xxx', host='https://us.insights.hanzo.ai', personal_api_key='phx_xxx')
|
||||
prompts = Prompts(client)
|
||||
|
||||
# Or with direct options (no PostHog client needed)
|
||||
# Or with direct options (no Insights client needed)
|
||||
prompts = Prompts(
|
||||
personal_api_key='phx_xxx',
|
||||
project_api_key='phc_xxx',
|
||||
host='https://us.posthog.com',
|
||||
host='https://us.insights.hanzo.ai',
|
||||
)
|
||||
|
||||
# Fetch with caching and fallback
|
||||
template = prompts.get('support-system-prompt', fallback='You are a helpful assistant.')
|
||||
|
||||
# Fetch a specific published version
|
||||
prompt_v1 = prompts.get('support-system-prompt', version=1)
|
||||
|
||||
# Compile with variables
|
||||
system_prompt = prompts.compile(template, {
|
||||
'company': 'Acme Corp',
|
||||
@@ -73,7 +90,7 @@ class Prompts:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
posthog: Optional[Any] = None,
|
||||
client: Optional[Any] = None,
|
||||
*,
|
||||
personal_api_key: Optional[str] = None,
|
||||
project_api_key: Optional[str] = None,
|
||||
@@ -84,22 +101,22 @@ class Prompts:
|
||||
Initialize Prompts.
|
||||
|
||||
Args:
|
||||
posthog: PostHog client instance (optional if personal_api_key provided)
|
||||
personal_api_key: Direct personal API key (optional if posthog provided)
|
||||
project_api_key: Direct project API key (optional if posthog provided)
|
||||
host: PostHog host (defaults to app endpoint)
|
||||
client: Insights client instance (optional if personal_api_key provided)
|
||||
personal_api_key: Direct personal API key (optional if client provided)
|
||||
project_api_key: Direct project API key (optional if client provided)
|
||||
host: Insights host (defaults to app endpoint)
|
||||
default_cache_ttl_seconds: Default cache TTL (defaults to 300)
|
||||
"""
|
||||
self._default_cache_ttl_seconds = (
|
||||
default_cache_ttl_seconds or DEFAULT_CACHE_TTL_SECONDS
|
||||
)
|
||||
self._cache: Dict[str, CachedPrompt] = {}
|
||||
self._cache: Dict[PromptCacheKey, CachedPrompt] = {}
|
||||
|
||||
if posthog is not None:
|
||||
self._personal_api_key = getattr(posthog, "personal_api_key", None) or ""
|
||||
self._project_api_key = getattr(posthog, "api_key", None) or ""
|
||||
if client is not None:
|
||||
self._personal_api_key = getattr(client, "personal_api_key", None) or ""
|
||||
self._project_api_key = getattr(client, "api_key", None) or ""
|
||||
self._host = remove_trailing_slash(
|
||||
getattr(posthog, "raw_host", None) or APP_ENDPOINT
|
||||
getattr(client, "raw_host", None) or APP_ENDPOINT
|
||||
)
|
||||
else:
|
||||
self._personal_api_key = personal_api_key or ""
|
||||
@@ -112,9 +129,10 @@ class Prompts:
|
||||
*,
|
||||
cache_ttl_seconds: Optional[int] = None,
|
||||
fallback: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Fetch a prompt by name from the PostHog API.
|
||||
Fetch a prompt by name from the Insights API.
|
||||
|
||||
Caching behavior:
|
||||
1. If cache is fresh, return cached value
|
||||
@@ -126,6 +144,8 @@ class Prompts:
|
||||
name: The name of the prompt to fetch
|
||||
cache_ttl_seconds: Cache TTL in seconds (defaults to instance default)
|
||||
fallback: Fallback prompt to use if fetch fails and no cache available
|
||||
version: Specific prompt version to fetch. If None, fetches the latest
|
||||
version
|
||||
|
||||
Returns:
|
||||
The prompt string
|
||||
@@ -138,9 +158,10 @@ class Prompts:
|
||||
if cache_ttl_seconds is not None
|
||||
else self._default_cache_ttl_seconds
|
||||
)
|
||||
cache_key = _cache_key(name, version)
|
||||
|
||||
# Check cache first
|
||||
cached = self._cache.get(name)
|
||||
cached = self._cache.get(cache_key)
|
||||
now = time.time()
|
||||
|
||||
if cached is not None:
|
||||
@@ -151,21 +172,22 @@ class Prompts:
|
||||
|
||||
# Try to fetch from API
|
||||
try:
|
||||
prompt = self._fetch_prompt_from_api(name)
|
||||
prompt = self._fetch_prompt_from_api(name, version)
|
||||
fetched_at = time.time()
|
||||
|
||||
# Update cache
|
||||
self._cache[name] = CachedPrompt(prompt=prompt, fetched_at=fetched_at)
|
||||
self._cache[cache_key] = CachedPrompt(prompt=prompt, fetched_at=fetched_at)
|
||||
|
||||
return prompt
|
||||
|
||||
except Exception as error:
|
||||
prompt_reference = _prompt_reference(name, version)
|
||||
# Fallback order:
|
||||
# 1. Return stale cache (with warning)
|
||||
if cached is not None:
|
||||
log.warning(
|
||||
'[PostHog Prompts] Failed to fetch prompt "%s", using stale cache: %s',
|
||||
name,
|
||||
"[Insights Prompts] Failed to fetch %s, using stale cache: %s",
|
||||
prompt_reference,
|
||||
error,
|
||||
)
|
||||
return cached.prompt
|
||||
@@ -173,8 +195,8 @@ class Prompts:
|
||||
# 2. Return fallback (with warning)
|
||||
if fallback is not None:
|
||||
log.warning(
|
||||
'[PostHog Prompts] Failed to fetch prompt "%s", using fallback: %s',
|
||||
name,
|
||||
"[Insights Prompts] Failed to fetch %s, using fallback: %s",
|
||||
prompt_reference,
|
||||
error,
|
||||
)
|
||||
return fallback
|
||||
@@ -207,27 +229,43 @@ class Prompts:
|
||||
|
||||
return re.sub(r"\{\{([\w.-]+)\}\}", replace_variable, prompt)
|
||||
|
||||
def clear_cache(self, name: Optional[str] = None) -> None:
|
||||
def clear_cache(
|
||||
self, name: Optional[str] = None, *, version: Optional[int] = None
|
||||
) -> None:
|
||||
"""
|
||||
Clear cached prompts.
|
||||
|
||||
Args:
|
||||
name: Specific prompt to clear. If None, clears all cached prompts.
|
||||
name: Specific prompt name to clear. If None, clears all cached prompts.
|
||||
version: Specific prompt version to clear. Requires name.
|
||||
"""
|
||||
if name is not None:
|
||||
self._cache.pop(name, None)
|
||||
else:
|
||||
if version is not None and name is None:
|
||||
raise ValueError("'version' requires 'name' to be provided")
|
||||
|
||||
if name is None:
|
||||
self._cache.clear()
|
||||
return
|
||||
|
||||
def _fetch_prompt_from_api(self, name: str) -> str:
|
||||
if version is not None:
|
||||
self._cache.pop(_cache_key(name, version), None)
|
||||
return
|
||||
|
||||
keys_to_clear = [key for key in self._cache if key[0] == name]
|
||||
for key in keys_to_clear:
|
||||
self._cache.pop(key, None)
|
||||
|
||||
def _fetch_prompt_from_api(self, name: str, version: Optional[int] = None) -> str:
|
||||
"""
|
||||
Fetch prompt from PostHog API.
|
||||
Fetch prompt from Insights API.
|
||||
|
||||
Endpoint: {host}/api/environments/@current/llm_prompts/name/{encoded_name}/?token={encoded_project_api_key}
|
||||
Endpoint:
|
||||
{host}/api/environments/@current/llm_prompts/name/{encoded_name}/
|
||||
?token={encoded_project_api_key}[&version={version}]
|
||||
Auth: Bearer {personal_api_key}
|
||||
|
||||
Args:
|
||||
name: The name of the prompt to fetch
|
||||
version: Specific prompt version to fetch. If None, fetches the latest
|
||||
|
||||
Returns:
|
||||
The prompt string
|
||||
@@ -237,18 +275,23 @@ class Prompts:
|
||||
"""
|
||||
if not self._personal_api_key:
|
||||
raise Exception(
|
||||
"[PostHog Prompts] personal_api_key is required to fetch prompts. "
|
||||
"[Insights Prompts] personal_api_key is required to fetch prompts. "
|
||||
"Please provide it when initializing the Prompts instance."
|
||||
)
|
||||
if not self._project_api_key:
|
||||
raise Exception(
|
||||
"[PostHog Prompts] project_api_key is required to fetch prompts. "
|
||||
"[Insights Prompts] project_api_key is required to fetch prompts. "
|
||||
"Please provide it when initializing the Prompts instance."
|
||||
)
|
||||
|
||||
encoded_name = urllib.parse.quote(name, safe="")
|
||||
encoded_project_api_key = urllib.parse.quote(self._project_api_key, safe="")
|
||||
url = f"{self._host}/api/environments/@current/llm_prompts/name/{encoded_name}/?token={encoded_project_api_key}"
|
||||
query_params: Dict[str, Union[str, int]] = {"token": self._project_api_key}
|
||||
if version is not None:
|
||||
query_params["version"] = version
|
||||
encoded_query = urllib.parse.urlencode(query_params)
|
||||
url = f"{self._host}/api/environments/@current/llm_prompts/name/{encoded_name}/?{encoded_query}"
|
||||
prompt_reference = _prompt_reference(name, version)
|
||||
prompt_label = prompt_reference[:1].upper() + prompt_reference[1:]
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self._personal_api_key}",
|
||||
@@ -259,28 +302,28 @@ class Prompts:
|
||||
|
||||
if not response.ok:
|
||||
if response.status_code == 404:
|
||||
raise Exception(f'[PostHog Prompts] Prompt "{name}" not found')
|
||||
raise Exception(f"[Insights Prompts] {prompt_label} not found")
|
||||
|
||||
if response.status_code == 403:
|
||||
raise Exception(
|
||||
f'[PostHog Prompts] Access denied for prompt "{name}". '
|
||||
f"[Insights Prompts] Access denied for {prompt_reference}. "
|
||||
"Check that your personal_api_key has the correct permissions and the LLM prompts feature is enabled."
|
||||
)
|
||||
|
||||
raise Exception(
|
||||
f'[PostHog Prompts] Failed to fetch prompt "{name}": HTTP {response.status_code}'
|
||||
f"[Insights Prompts] Failed to fetch {prompt_label}: HTTP {response.status_code}"
|
||||
)
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except Exception:
|
||||
raise Exception(
|
||||
f'[PostHog Prompts] Invalid response format for prompt "{name}"'
|
||||
f"[Insights Prompts] Invalid response format for {prompt_label}"
|
||||
)
|
||||
|
||||
if not _is_prompt_api_response(data):
|
||||
raise Exception(
|
||||
f'[PostHog Prompts] Invalid response format for prompt "{name}"'
|
||||
f"[Insights Prompts] Invalid response format for {prompt_label}"
|
||||
)
|
||||
|
||||
return data["prompt"]
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Common type definitions for PostHog AI SDK.
|
||||
Common type definitions for Insights AI SDK.
|
||||
|
||||
These types are used for formatting messages and responses across different AI providers
|
||||
(Anthropic, OpenAI, Gemini, etc.) to ensure consistency in tracking and data structure.
|
||||
@@ -41,10 +41,10 @@ FormattedContentItem = Union[
|
||||
|
||||
class FormattedMessage(TypedDict):
|
||||
"""
|
||||
Standardized message format for PostHog tracking.
|
||||
Standardized message format for Insights tracking.
|
||||
|
||||
Used across all providers to ensure consistent message structure
|
||||
when sending events to PostHog.
|
||||
when sending events to Insights.
|
||||
"""
|
||||
|
||||
role: str
|
||||
@@ -2,15 +2,15 @@ import time
|
||||
import uuid
|
||||
from typing import Any, Callable, Dict, List, Optional, cast
|
||||
|
||||
from posthog import get_tags, identify_context, new_context, tag
|
||||
from posthog.ai.sanitization import (
|
||||
from hanzo_insights import get_tags, identify_context, new_context, tag, contexts
|
||||
from hanzo_insights.ai.sanitization import (
|
||||
sanitize_anthropic,
|
||||
sanitize_gemini,
|
||||
sanitize_langchain,
|
||||
sanitize_openai,
|
||||
)
|
||||
from posthog.ai.types import FormattedMessage, StreamingEventData, TokenUsage
|
||||
from posthog.client import Client as PostHogClient
|
||||
from hanzo_insights.ai.types import FormattedMessage, StreamingEventData, TokenUsage
|
||||
from hanzo_insights.client import Client as InsightsClient
|
||||
|
||||
|
||||
_TOKEN_PROPERTY_KEYS = frozenset(
|
||||
@@ -26,10 +26,10 @@ _TOKEN_PROPERTY_KEYS = frozenset(
|
||||
|
||||
|
||||
def _get_tokens_source(
|
||||
sdk_tags: Dict[str, Any], posthog_properties: Optional[Dict[str, Any]]
|
||||
sdk_tags: Dict[str, Any], insights_properties: Optional[Dict[str, Any]]
|
||||
) -> str:
|
||||
if posthog_properties and any(
|
||||
key in posthog_properties for key in _TOKEN_PROPERTY_KEYS
|
||||
if insights_properties and any(
|
||||
key in insights_properties for key in _TOKEN_PROPERTY_KEYS
|
||||
):
|
||||
return "passthrough"
|
||||
return "sdk"
|
||||
@@ -40,7 +40,7 @@ def serialize_raw_usage(raw_usage: Any) -> Optional[Dict[str, Any]]:
|
||||
Convert raw provider usage objects to JSON-serializable dicts.
|
||||
|
||||
Handles Pydantic models (OpenAI/Anthropic) and protobuf-like objects (Gemini)
|
||||
with a fallback chain to ensure we never pass unserializable objects to PostHog.
|
||||
with a fallback chain to ensure we never pass unserializable objects to Insights.
|
||||
|
||||
Args:
|
||||
raw_usage: Raw usage object from provider SDK
|
||||
@@ -193,19 +193,19 @@ def get_usage(response, provider: str) -> TokenUsage:
|
||||
Delegates to provider-specific converter functions.
|
||||
"""
|
||||
if provider == "anthropic":
|
||||
from posthog.ai.anthropic.anthropic_converter import (
|
||||
from hanzo_insights.ai.anthropic.anthropic_converter import (
|
||||
extract_anthropic_usage_from_response,
|
||||
)
|
||||
|
||||
return extract_anthropic_usage_from_response(response)
|
||||
elif provider == "openai":
|
||||
from posthog.ai.openai.openai_converter import (
|
||||
from hanzo_insights.ai.openai.openai_converter import (
|
||||
extract_openai_usage_from_response,
|
||||
)
|
||||
|
||||
return extract_openai_usage_from_response(response)
|
||||
elif provider == "gemini":
|
||||
from posthog.ai.gemini.gemini_converter import (
|
||||
from hanzo_insights.ai.gemini.gemini_converter import (
|
||||
extract_gemini_usage_from_response,
|
||||
)
|
||||
|
||||
@@ -219,15 +219,15 @@ def format_response(response, provider: str):
|
||||
Format a regular (non-streaming) response.
|
||||
"""
|
||||
if provider == "anthropic":
|
||||
from posthog.ai.anthropic.anthropic_converter import format_anthropic_response
|
||||
from hanzo_insights.ai.anthropic.anthropic_converter import format_anthropic_response
|
||||
|
||||
return format_anthropic_response(response)
|
||||
elif provider == "openai":
|
||||
from posthog.ai.openai.openai_converter import format_openai_response
|
||||
from hanzo_insights.ai.openai.openai_converter import format_openai_response
|
||||
|
||||
return format_openai_response(response)
|
||||
elif provider == "gemini":
|
||||
from posthog.ai.gemini.gemini_converter import format_gemini_response
|
||||
from hanzo_insights.ai.gemini.gemini_converter import format_gemini_response
|
||||
|
||||
return format_gemini_response(response)
|
||||
return []
|
||||
@@ -238,15 +238,15 @@ def extract_available_tool_calls(provider: str, kwargs: Dict[str, Any]):
|
||||
Extract available tool calls for the given provider.
|
||||
"""
|
||||
if provider == "anthropic":
|
||||
from posthog.ai.anthropic.anthropic_converter import extract_anthropic_tools
|
||||
from hanzo_insights.ai.anthropic.anthropic_converter import extract_anthropic_tools
|
||||
|
||||
return extract_anthropic_tools(kwargs)
|
||||
elif provider == "gemini":
|
||||
from posthog.ai.gemini.gemini_converter import extract_gemini_tools
|
||||
from hanzo_insights.ai.gemini.gemini_converter import extract_gemini_tools
|
||||
|
||||
return extract_gemini_tools(kwargs)
|
||||
elif provider == "openai":
|
||||
from posthog.ai.openai.openai_converter import extract_openai_tools
|
||||
from hanzo_insights.ai.openai.openai_converter import extract_openai_tools
|
||||
|
||||
return extract_openai_tools(kwargs)
|
||||
return None
|
||||
@@ -259,19 +259,19 @@ def merge_system_prompt(
|
||||
Merge system prompts and format messages for the given provider.
|
||||
"""
|
||||
if provider == "anthropic":
|
||||
from posthog.ai.anthropic.anthropic_converter import format_anthropic_input
|
||||
from hanzo_insights.ai.anthropic.anthropic_converter import format_anthropic_input
|
||||
|
||||
messages = kwargs.get("messages") or []
|
||||
system = kwargs.get("system")
|
||||
return format_anthropic_input(messages, system)
|
||||
elif provider == "gemini":
|
||||
from posthog.ai.gemini.gemini_converter import format_gemini_input_with_system
|
||||
from hanzo_insights.ai.gemini.gemini_converter import format_gemini_input_with_system
|
||||
|
||||
contents = kwargs.get("contents", [])
|
||||
config = kwargs.get("config")
|
||||
return format_gemini_input_with_system(contents, config)
|
||||
elif provider == "openai":
|
||||
from posthog.ai.openai.openai_converter import format_openai_input
|
||||
from hanzo_insights.ai.openai.openai_converter import format_openai_input
|
||||
|
||||
# For OpenAI, handle both Chat Completions and Responses API
|
||||
messages_param = kwargs.get("messages")
|
||||
@@ -319,13 +319,13 @@ def merge_system_prompt(
|
||||
|
||||
|
||||
def call_llm_and_track_usage(
|
||||
posthog_distinct_id: Optional[str],
|
||||
ph_client: PostHogClient,
|
||||
insights_distinct_id: Optional[str],
|
||||
ph_client: InsightsClient,
|
||||
provider: str,
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
base_url: str,
|
||||
call_method: Callable[..., Any],
|
||||
**kwargs: Any,
|
||||
@@ -342,8 +342,8 @@ def call_llm_and_track_usage(
|
||||
error_params: Dict[str, Any] = {}
|
||||
|
||||
with new_context(client=ph_client, capture_exceptions=False):
|
||||
if posthog_distinct_id:
|
||||
identify_context(posthog_distinct_id)
|
||||
if insights_distinct_id:
|
||||
identify_context(insights_distinct_id)
|
||||
|
||||
try:
|
||||
response = call_method(**kwargs)
|
||||
@@ -363,8 +363,18 @@ def call_llm_and_track_usage(
|
||||
end_time = time.time()
|
||||
latency = end_time - start_time
|
||||
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
# Check if we have a real user distinct_id (from param or outer context)
|
||||
has_person_distinct_id = (
|
||||
insights_distinct_id is not None
|
||||
or contexts.get_context_distinct_id() is not None
|
||||
)
|
||||
|
||||
if not has_person_distinct_id:
|
||||
# Fall back to trace_id as distinct_id when no real user id is available.
|
||||
identify_context(insights_trace_id)
|
||||
|
||||
if response and (
|
||||
hasattr(response, "usage")
|
||||
@@ -380,19 +390,19 @@ def call_llm_and_track_usage(
|
||||
tag("$ai_model_parameters", get_model_params(kwargs))
|
||||
tag(
|
||||
"$ai_input",
|
||||
with_privacy_mode(ph_client, posthog_privacy_mode, sanitized_messages),
|
||||
with_privacy_mode(ph_client, insights_privacy_mode, sanitized_messages),
|
||||
)
|
||||
tag(
|
||||
"$ai_output_choices",
|
||||
with_privacy_mode(
|
||||
ph_client, posthog_privacy_mode, format_response(response, provider)
|
||||
ph_client, insights_privacy_mode, format_response(response, provider)
|
||||
),
|
||||
)
|
||||
tag("$ai_http_status", http_status)
|
||||
tag("$ai_input_tokens", usage.get("input_tokens", 0))
|
||||
tag("$ai_output_tokens", usage.get("output_tokens", 0))
|
||||
tag("$ai_latency", latency)
|
||||
tag("$ai_trace_id", posthog_trace_id)
|
||||
tag("$ai_trace_id", insights_trace_id)
|
||||
tag("$ai_base_url", str(base_url))
|
||||
|
||||
available_tool_calls = extract_available_tool_calls(provider, kwargs)
|
||||
@@ -421,7 +431,7 @@ def call_llm_and_track_usage(
|
||||
# Already serialized by converters
|
||||
tag("$ai_usage", raw_usage)
|
||||
|
||||
if posthog_distinct_id is None:
|
||||
if not has_person_distinct_id:
|
||||
tag("$process_person_profile", False)
|
||||
|
||||
# Process instructions for Responses API
|
||||
@@ -429,26 +439,26 @@ def call_llm_and_track_usage(
|
||||
tag(
|
||||
"$ai_instructions",
|
||||
with_privacy_mode(
|
||||
ph_client, posthog_privacy_mode, kwargs.get("instructions")
|
||||
ph_client, insights_privacy_mode, kwargs.get("instructions")
|
||||
),
|
||||
)
|
||||
|
||||
# send the event to posthog
|
||||
# send the event to Insights
|
||||
if hasattr(ph_client, "capture") and callable(ph_client.capture):
|
||||
sdk_tags = get_tags()
|
||||
merged_properties = {
|
||||
**sdk_tags,
|
||||
**(posthog_properties or {}),
|
||||
**(insights_properties or {}),
|
||||
**(error_params or {}),
|
||||
}
|
||||
merged_properties["$ai_tokens_source"] = _get_tokens_source(
|
||||
sdk_tags, posthog_properties
|
||||
sdk_tags, insights_properties
|
||||
)
|
||||
ph_client.capture(
|
||||
distinct_id=posthog_distinct_id or posthog_trace_id,
|
||||
distinct_id=contexts.get_context_distinct_id(),
|
||||
event="$ai_generation",
|
||||
properties=merged_properties,
|
||||
groups=posthog_groups,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
if error:
|
||||
@@ -458,13 +468,13 @@ def call_llm_and_track_usage(
|
||||
|
||||
|
||||
async def call_llm_and_track_usage_async(
|
||||
posthog_distinct_id: Optional[str],
|
||||
ph_client: PostHogClient,
|
||||
insights_distinct_id: Optional[str],
|
||||
ph_client: InsightsClient,
|
||||
provider: str,
|
||||
posthog_trace_id: Optional[str],
|
||||
posthog_properties: Optional[Dict[str, Any]],
|
||||
posthog_privacy_mode: bool,
|
||||
posthog_groups: Optional[Dict[str, Any]],
|
||||
insights_trace_id: Optional[str],
|
||||
insights_properties: Optional[Dict[str, Any]],
|
||||
insights_privacy_mode: bool,
|
||||
insights_groups: Optional[Dict[str, Any]],
|
||||
base_url: str,
|
||||
call_async_method: Callable[..., Any],
|
||||
**kwargs: Any,
|
||||
@@ -477,8 +487,8 @@ async def call_llm_and_track_usage_async(
|
||||
error_params: Dict[str, Any] = {}
|
||||
|
||||
with new_context(client=ph_client, capture_exceptions=False):
|
||||
if posthog_distinct_id:
|
||||
identify_context(posthog_distinct_id)
|
||||
if insights_distinct_id:
|
||||
identify_context(insights_distinct_id)
|
||||
|
||||
try:
|
||||
response = await call_async_method(**kwargs)
|
||||
@@ -498,8 +508,18 @@ async def call_llm_and_track_usage_async(
|
||||
end_time = time.time()
|
||||
latency = end_time - start_time
|
||||
|
||||
if posthog_trace_id is None:
|
||||
posthog_trace_id = str(uuid.uuid4())
|
||||
if insights_trace_id is None:
|
||||
insights_trace_id = str(uuid.uuid4())
|
||||
|
||||
# Check if we have a real user distinct_id (from param or outer context)
|
||||
has_person_distinct_id = (
|
||||
insights_distinct_id is not None
|
||||
or contexts.get_context_distinct_id() is not None
|
||||
)
|
||||
|
||||
if not has_person_distinct_id:
|
||||
# Fall back to trace_id as distinct_id when no real user id is available.
|
||||
identify_context(insights_trace_id)
|
||||
|
||||
if response and (
|
||||
hasattr(response, "usage")
|
||||
@@ -515,19 +535,19 @@ async def call_llm_and_track_usage_async(
|
||||
tag("$ai_model_parameters", get_model_params(kwargs))
|
||||
tag(
|
||||
"$ai_input",
|
||||
with_privacy_mode(ph_client, posthog_privacy_mode, sanitized_messages),
|
||||
with_privacy_mode(ph_client, insights_privacy_mode, sanitized_messages),
|
||||
)
|
||||
tag(
|
||||
"$ai_output_choices",
|
||||
with_privacy_mode(
|
||||
ph_client, posthog_privacy_mode, format_response(response, provider)
|
||||
ph_client, insights_privacy_mode, format_response(response, provider)
|
||||
),
|
||||
)
|
||||
tag("$ai_http_status", http_status)
|
||||
tag("$ai_input_tokens", usage.get("input_tokens", 0))
|
||||
tag("$ai_output_tokens", usage.get("output_tokens", 0))
|
||||
tag("$ai_latency", latency)
|
||||
tag("$ai_trace_id", posthog_trace_id)
|
||||
tag("$ai_trace_id", insights_trace_id)
|
||||
tag("$ai_base_url", str(base_url))
|
||||
|
||||
available_tool_calls = extract_available_tool_calls(provider, kwargs)
|
||||
@@ -556,7 +576,7 @@ async def call_llm_and_track_usage_async(
|
||||
# Already serialized by converters
|
||||
tag("$ai_usage", raw_usage)
|
||||
|
||||
if posthog_distinct_id is None:
|
||||
if not has_person_distinct_id:
|
||||
tag("$process_person_profile", False)
|
||||
|
||||
# Process instructions for Responses API
|
||||
@@ -564,26 +584,26 @@ async def call_llm_and_track_usage_async(
|
||||
tag(
|
||||
"$ai_instructions",
|
||||
with_privacy_mode(
|
||||
ph_client, posthog_privacy_mode, kwargs.get("instructions")
|
||||
ph_client, insights_privacy_mode, kwargs.get("instructions")
|
||||
),
|
||||
)
|
||||
|
||||
# send the event to posthog
|
||||
# send the event to Insights
|
||||
if hasattr(ph_client, "capture") and callable(ph_client.capture):
|
||||
sdk_tags = get_tags()
|
||||
merged_properties = {
|
||||
**sdk_tags,
|
||||
**(posthog_properties or {}),
|
||||
**(insights_properties or {}),
|
||||
**(error_params or {}),
|
||||
}
|
||||
merged_properties["$ai_tokens_source"] = _get_tokens_source(
|
||||
sdk_tags, posthog_properties
|
||||
sdk_tags, insights_properties
|
||||
)
|
||||
ph_client.capture(
|
||||
distinct_id=posthog_distinct_id or posthog_trace_id,
|
||||
distinct_id=contexts.get_context_distinct_id(),
|
||||
event="$ai_generation",
|
||||
properties=merged_properties,
|
||||
groups=posthog_groups,
|
||||
groups=insights_groups,
|
||||
)
|
||||
|
||||
if error:
|
||||
@@ -605,14 +625,14 @@ def sanitize_messages(data: Any, provider: str) -> Any:
|
||||
return data
|
||||
|
||||
|
||||
def with_privacy_mode(ph_client: PostHogClient, privacy_mode: bool, value: Any):
|
||||
def with_privacy_mode(ph_client: InsightsClient, privacy_mode: bool, value: Any):
|
||||
if ph_client.privacy_mode or privacy_mode:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def capture_streaming_event(
|
||||
ph_client: PostHogClient,
|
||||
ph_client: InsightsClient,
|
||||
event_data: StreamingEventData,
|
||||
):
|
||||
"""
|
||||
@@ -622,15 +642,15 @@ def capture_streaming_event(
|
||||
All provider-specific formatting should be done BEFORE calling this function.
|
||||
|
||||
The function handles:
|
||||
- Building PostHog event properties
|
||||
- Building Insights event properties
|
||||
- Extracting and adding tools based on provider
|
||||
- Applying privacy mode
|
||||
- Adding special token fields (cache, reasoning)
|
||||
- Provider-specific fields (e.g., OpenAI instructions)
|
||||
- Sending the event to PostHog
|
||||
- Sending the event to Insights
|
||||
|
||||
Args:
|
||||
ph_client: PostHog client instance
|
||||
ph_client: Insights client instance
|
||||
event_data: Standardized streaming event data containing all necessary information
|
||||
"""
|
||||
trace_id = event_data.get("trace_id") or str(uuid.uuid4())
|
||||
@@ -727,7 +747,7 @@ def capture_streaming_event(
|
||||
if event_data.get("distinct_id") is None:
|
||||
event_properties["$process_person_profile"] = False
|
||||
|
||||
# Send event to PostHog
|
||||
# Send event to Insights
|
||||
if hasattr(ph_client, "capture"):
|
||||
ph_client.capture(
|
||||
distinct_id=event_data.get("distinct_id") or trace_id,
|
||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
||||
import numbers
|
||||
from uuid import UUID
|
||||
|
||||
from posthog.types import SendFeatureFlagsOptions
|
||||
from hanzo_insights.types import SendFeatureFlagsOptions
|
||||
|
||||
ID_TYPES = Union[numbers.Number, str, UUID, int]
|
||||
|
||||
@@ -11,9 +11,9 @@ from dateutil.tz import tzutc
|
||||
from six import string_types
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from posthog.args import ID_TYPES, ExceptionArg, OptionalCaptureArgs, OptionalSetArgs
|
||||
from posthog.consumer import Consumer
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.args import ID_TYPES, ExceptionArg, OptionalCaptureArgs, OptionalSetArgs
|
||||
from hanzo_insights.consumer import Consumer
|
||||
from hanzo_insights.contexts import (
|
||||
_get_current_context,
|
||||
get_capture_exception_code_variables_context,
|
||||
get_code_variables_ignore_patterns_context,
|
||||
@@ -23,8 +23,8 @@ from posthog.contexts import (
|
||||
get_context_session_id,
|
||||
new_context,
|
||||
)
|
||||
from posthog.exception_capture import ExceptionCapture
|
||||
from posthog.exception_utils import (
|
||||
from hanzo_insights.exception_capture import ExceptionCapture
|
||||
from hanzo_insights.exception_utils import (
|
||||
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
|
||||
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
|
||||
exc_info_from_error,
|
||||
@@ -34,18 +34,18 @@ from posthog.exception_utils import (
|
||||
mark_exception_as_captured,
|
||||
try_attach_code_variables_to_frames,
|
||||
)
|
||||
from posthog.feature_flags import (
|
||||
from hanzo_insights.feature_flags import (
|
||||
InconclusiveMatchError,
|
||||
RequiresServerEvaluation,
|
||||
match_feature_flag_properties,
|
||||
resolve_bucketing_value,
|
||||
)
|
||||
from posthog.flag_definition_cache import (
|
||||
from hanzo_insights.flag_definition_cache import (
|
||||
FlagDefinitionCacheData,
|
||||
FlagDefinitionCacheProvider,
|
||||
)
|
||||
from posthog.poller import Poller
|
||||
from posthog.request import (
|
||||
from hanzo_insights.poller import Poller
|
||||
from hanzo_insights.request import (
|
||||
DEFAULT_HOST,
|
||||
APIError,
|
||||
QuotaLimitError,
|
||||
@@ -57,7 +57,7 @@ from posthog.request import (
|
||||
get,
|
||||
remote_config,
|
||||
)
|
||||
from posthog.types import (
|
||||
from hanzo_insights.types import (
|
||||
FeatureFlag,
|
||||
FeatureFlagError,
|
||||
FeatureFlagResult,
|
||||
@@ -71,7 +71,7 @@ from posthog.types import (
|
||||
to_payloads,
|
||||
to_values,
|
||||
)
|
||||
from posthog.utils import (
|
||||
from hanzo_insights.utils import (
|
||||
FlagCache,
|
||||
RedisFlagCache,
|
||||
SizeLimitedDict,
|
||||
@@ -79,7 +79,7 @@ from posthog.utils import (
|
||||
guess_timezone,
|
||||
system_context,
|
||||
)
|
||||
from posthog.version import VERSION
|
||||
from hanzo_insights.version import VERSION
|
||||
|
||||
try:
|
||||
import queue
|
||||
@@ -149,22 +149,22 @@ def no_throw(default_return=None):
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
This is the SDK reference for the PostHog Python SDK.
|
||||
This is the SDK reference for the Hanzo Insights Python SDK.
|
||||
You can learn more about example usage in the [Python SDK documentation](/docs/libraries/python).
|
||||
You can also follow [Flask](/docs/libraries/flask) and [Django](/docs/libraries/django)
|
||||
guides to integrate PostHog into your project.
|
||||
guides to integrate Insights into your project.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import Posthog
|
||||
posthog = Posthog('<ph_project_api_key>', host='<ph_client_api_host>')
|
||||
posthog.debug = True
|
||||
from hanzo_insights import Insights
|
||||
client = Insights('<ph_project_api_key>', host='<ph_client_api_host>')
|
||||
hanzo_insights.debug = True
|
||||
if settings.TEST:
|
||||
posthog.disabled = True
|
||||
hanzo_insights.disabled = True
|
||||
```
|
||||
"""
|
||||
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -202,7 +202,7 @@ class Client(object):
|
||||
in_app_modules: list[str] | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize a new PostHog client instance.
|
||||
Initialize a new Insights client instance.
|
||||
|
||||
Args:
|
||||
project_api_key: The project API key.
|
||||
@@ -211,9 +211,9 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from posthog import Posthog
|
||||
from hanzo_insights import Insights
|
||||
|
||||
posthog = Posthog('<ph_project_api_key>', host='<ph_app_host>')
|
||||
client = Insights('<ph_project_api_key>', host='<ph_app_host>')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -342,9 +342,9 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
with posthog.new_context():
|
||||
with hanzo_insights.new_context():
|
||||
identify_context('<distinct_id>')
|
||||
posthog.capture('event_name')
|
||||
hanzo_insights.capture('event_name')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -438,7 +438,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
payloads = posthog.get_feature_payloads('<distinct_id>')
|
||||
payloads = hanzo_insights.get_feature_payloads('<distinct_id>')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -480,7 +480,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
result = posthog.get_feature_flags_and_payloads('<distinct_id>')
|
||||
result = hanzo_insights.get_feature_flags_and_payloads('<distinct_id>')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -522,7 +522,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
decision = posthog.get_flags_decision('user123')
|
||||
decision = hanzo_insights.get_flags_decision('user123')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -570,7 +570,7 @@ class Client(object):
|
||||
self, event: str, **kwargs: Unpack[OptionalCaptureArgs]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Captures an event manually. [Learn about capture best practices](https://posthog.com/docs/product-analytics/capture-events)
|
||||
Captures an event manually. [Learn about capture best practices](https://insights.hanzo.ai/docs/product-analytics/capture-events)
|
||||
|
||||
Args:
|
||||
event: The event name to capture.
|
||||
@@ -585,20 +585,20 @@ class Client(object):
|
||||
Examples:
|
||||
```python
|
||||
# Anonymous event
|
||||
posthog.capture('some-anon-event')
|
||||
hanzo_insights.capture('some-anon-event')
|
||||
```
|
||||
```python
|
||||
# Context usage
|
||||
from posthog import identify_context, new_context
|
||||
from hanzo_insights import identify_context, new_context
|
||||
with new_context():
|
||||
identify_context('distinct_id_of_the_user')
|
||||
posthog.capture('user_signed_up')
|
||||
posthog.capture('user_logged_in')
|
||||
posthog.capture('some-custom-action', distinct_id='distinct_id_of_the_user')
|
||||
hanzo_insights.capture('user_signed_up')
|
||||
hanzo_insights.capture('user_logged_in')
|
||||
hanzo_insights.capture('some-custom-action', distinct_id='distinct_id_of_the_user')
|
||||
```
|
||||
```python
|
||||
# Set event properties
|
||||
posthog.capture(
|
||||
hanzo_insights.capture(
|
||||
"user_signed_up",
|
||||
distinct_id="distinct_id_of_the_user",
|
||||
properties={
|
||||
@@ -609,7 +609,7 @@ class Client(object):
|
||||
```
|
||||
```python
|
||||
# Page view event
|
||||
posthog.capture('$pageview', distinct_id="distinct_id_of_the_user", properties={'$current_url': 'https://example.com'})
|
||||
hanzo_insights.capture('$pageview', distinct_id="distinct_id_of_the_user", properties={'$current_url': 'https://example.com'})
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -769,7 +769,7 @@ class Client(object):
|
||||
Examples:
|
||||
```python
|
||||
# Set with distinct id
|
||||
posthog.set(distinct_id='user123', properties={'name': 'Max Hedgehog'})
|
||||
hanzo_insights.set(distinct_id='user123', properties={'name': 'Max Hedgehog'})
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -816,7 +816,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
posthog.set_once(distinct_id='user123', properties={'initial_signup_date': '2024-01-01'})
|
||||
hanzo_insights.set_once(distinct_id='user123', properties={'initial_signup_date': '2024-01-01'})
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -873,7 +873,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
posthog.group_identify('company', 'company_id_in_your_db', {
|
||||
hanzo_insights.group_identify('company', 'company_id_in_your_db', {
|
||||
'name': 'Awesome Inc.',
|
||||
'employees': 11
|
||||
})
|
||||
@@ -928,7 +928,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
posthog.alias(previous_id='distinct_id', distinct_id='alias_id')
|
||||
hanzo_insights.alias(previous_id='distinct_id', distinct_id='alias_id')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -978,7 +978,7 @@ class Client(object):
|
||||
# Some code that might fail
|
||||
pass
|
||||
except Exception as e:
|
||||
posthog.capture_exception(e, 'user_distinct_id', properties=additional_properties)
|
||||
hanzo_insights.capture_exception(e, 'user_distinct_id', properties=additional_properties)
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -1108,7 +1108,7 @@ class Client(object):
|
||||
|
||||
if not msg.get("properties"):
|
||||
msg["properties"] = {}
|
||||
msg["properties"]["$lib"] = "posthog-python"
|
||||
msg["properties"]["$lib"] = "insights-python"
|
||||
msg["properties"]["$lib_version"] = VERSION
|
||||
|
||||
if disable_geoip is None:
|
||||
@@ -1168,8 +1168,8 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
posthog.capture('event_name')
|
||||
posthog.flush() # Ensures the event is sent immediately
|
||||
hanzo_insights.capture('event_name')
|
||||
hanzo_insights.flush() # Ensures the event is sent immediately
|
||||
```
|
||||
"""
|
||||
queue = self.queue
|
||||
@@ -1184,7 +1184,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
posthog.join()
|
||||
hanzo_insights.join()
|
||||
```
|
||||
"""
|
||||
if self.consumers:
|
||||
@@ -1212,7 +1212,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
posthog.shutdown()
|
||||
hanzo_insights.shutdown()
|
||||
```
|
||||
"""
|
||||
self.flush()
|
||||
@@ -1285,7 +1285,7 @@ class Client(object):
|
||||
self._fetch_feature_flags_from_api()
|
||||
|
||||
def _fetch_feature_flags_from_api(self):
|
||||
"""Fetch feature flags from the PostHog API."""
|
||||
"""Fetch feature flags from the Insights API."""
|
||||
try:
|
||||
# Store old flags to detect changes
|
||||
old_flags_by_key: dict[str, dict] = self.feature_flags_by_key or {}
|
||||
@@ -1334,7 +1334,7 @@ class Client(object):
|
||||
except APIError as e:
|
||||
if e.status == 401:
|
||||
self.log.error(
|
||||
"[FEATURE FLAGS] Error loading feature flags: To use feature flags, please set a valid personal_api_key. More information: https://posthog.com/docs/api/overview"
|
||||
"[FEATURE FLAGS] Error loading feature flags: To use feature flags, please set a valid personal_api_key. More information: https://insights.hanzo.ai/docs/api/overview"
|
||||
)
|
||||
self.feature_flags = []
|
||||
self.group_type_mapping = {}
|
||||
@@ -1348,11 +1348,11 @@ class Client(object):
|
||||
status=401,
|
||||
message="You are using a write-only key with feature flags. "
|
||||
"To use feature flags, please set a personal_api_key "
|
||||
"More information: https://posthog.com/docs/api/overview",
|
||||
"More information: https://insights.hanzo.ai/docs/api/overview",
|
||||
)
|
||||
elif e.status == 402:
|
||||
self.log.warning(
|
||||
"[FEATURE FLAGS] PostHog feature flags quota limited, resetting feature flag data. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts"
|
||||
"[FEATURE FLAGS] Insights feature flags quota limited, resetting feature flag data. Learn more about billing limits at https://insights.hanzo.ai/docs/billing/limits-alerts"
|
||||
)
|
||||
# Reset all feature flag data when quota limited
|
||||
self.feature_flags = []
|
||||
@@ -1366,7 +1366,7 @@ class Client(object):
|
||||
if self.debug:
|
||||
raise APIError(
|
||||
status=402,
|
||||
message="PostHog feature flags quota limited",
|
||||
message="Insights feature flags quota limited",
|
||||
)
|
||||
else:
|
||||
self.log.error(f"[FEATURE FLAGS] Error loading feature flags: {e}")
|
||||
@@ -1385,7 +1385,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
posthog.load_feature_flags()
|
||||
hanzo_insights.load_feature_flags()
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -1520,11 +1520,11 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user')
|
||||
is_my_flag_enabled = hanzo_insights.feature_enabled('flag-key', 'distinct_id_of_your_user')
|
||||
if is_my_flag_enabled:
|
||||
# Do something differently for this user
|
||||
# Optional: fetch the payload
|
||||
matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
|
||||
matched_flag_payload = hanzo_insights.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -1620,7 +1620,13 @@ class Client(object):
|
||||
self.flag_cache.set_cached_flag(
|
||||
distinct_id, key, flag_result, self.flag_definition_version
|
||||
)
|
||||
elif not only_evaluate_locally:
|
||||
elif only_evaluate_locally:
|
||||
if self.feature_flags is None:
|
||||
self.log.warning(
|
||||
"[FEATURE FLAGS] Local evaluation called but feature flag definitions are not loaded yet. "
|
||||
"Returning None. You can call load_feature_flags() to load flags explicitly."
|
||||
)
|
||||
else:
|
||||
try:
|
||||
flag_details, request_id, evaluated_at, errors_while_computing = (
|
||||
self._get_feature_flag_details_from_server(
|
||||
@@ -1712,7 +1718,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
flag_result = posthog.get_feature_flag_result('flag-key', 'distinct_id_of_your_user')
|
||||
flag_result = hanzo_insights.get_feature_flag_result('flag-key', 'distinct_id_of_your_user')
|
||||
if flag_result and flag_result.get_value() == 'variant-key':
|
||||
# Do something differently for this user
|
||||
# Optional: fetch the payload
|
||||
@@ -1774,11 +1780,11 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user')
|
||||
enabled_variant = hanzo_insights.get_feature_flag('flag-key', 'distinct_id_of_your_user')
|
||||
if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant
|
||||
# Do something differently for this user
|
||||
# Optional: fetch the payload
|
||||
matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
|
||||
matched_flag_payload = hanzo_insights.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -1868,12 +1874,12 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user')
|
||||
is_my_flag_enabled = hanzo_insights.feature_enabled('flag-key', 'distinct_id_of_your_user')
|
||||
|
||||
if is_my_flag_enabled:
|
||||
# Do something differently for this user
|
||||
# Optional: fetch the payload
|
||||
matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
|
||||
matched_flag_payload = hanzo_insights.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -2065,7 +2071,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
posthog.get_all_flags('distinct_id_of_your_user')
|
||||
hanzo_insights.get_all_flags('distinct_id_of_your_user')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -2112,7 +2118,7 @@ class Client(object):
|
||||
|
||||
Examples:
|
||||
```python
|
||||
posthog.get_all_flags_and_payloads('distinct_id_of_your_user')
|
||||
hanzo_insights.get_all_flags_and_payloads('distinct_id_of_your_user')
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -2225,7 +2231,7 @@ class Client(object):
|
||||
"""Initialize feature flag cache for graceful degradation during service outages.
|
||||
|
||||
When enabled, the cache stores flag evaluation results and serves them as fallback
|
||||
when the PostHog API is unavailable. This ensures your application continues to
|
||||
when the Insights API is unavailable. This ensures your application continues to
|
||||
receive flag values even during outages.
|
||||
|
||||
Args:
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from posthog.request import APIError, DatetimeSerializer, batch_post
|
||||
from hanzo_insights.request import APIError, DatetimeSerializer, batch_post
|
||||
|
||||
try:
|
||||
from queue import Empty
|
||||
@@ -21,7 +21,7 @@ BATCH_SIZE_LIMIT = 5 * 1024 * 1024
|
||||
class Consumer(Thread):
|
||||
"""Consumes the messages from the client's queue."""
|
||||
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -4,7 +4,7 @@ from typing import Optional, Any, Callable, Dict, TypeVar, cast, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# To avoid circular imports
|
||||
from posthog.client import Client
|
||||
from hanzo_insights.client import Client
|
||||
|
||||
|
||||
class ContextScope:
|
||||
@@ -104,7 +104,7 @@ class ContextScope:
|
||||
|
||||
|
||||
_context_stack: contextvars.ContextVar[Optional[ContextScope]] = contextvars.ContextVar(
|
||||
"posthog_context_stack", default=None
|
||||
"insights_context_stack", default=None
|
||||
)
|
||||
|
||||
|
||||
@@ -134,32 +134,32 @@ def new_context(
|
||||
If provided, the client will be used to capture exceptions within the context.
|
||||
If not provided, the default (global) client will be used. Note that the passed
|
||||
client is only used to capture exceptions within the context - other events captured
|
||||
within the context via `Client.capture` or `posthog.capture` will still carry the context
|
||||
within the context via `Client.capture` or `hanzo_insights.capture` will still carry the context
|
||||
state (tags, identity, session id), but will be captured by the client directly used (or
|
||||
the global one, in the case of `posthog.capture`)
|
||||
the global one, in the case of `hanzo_insights.capture`)
|
||||
|
||||
Examples:
|
||||
```python
|
||||
# Inherit parent context tags
|
||||
with posthog.new_context():
|
||||
posthog.tag("request_id", "123")
|
||||
with hanzo_insights.new_context():
|
||||
hanzo_insights.tag("request_id", "123")
|
||||
# Both this event and the exception will be tagged with the context tags
|
||||
posthog.capture("event_name", {"property": "value"})
|
||||
hanzo_insights.capture("event_name", {"property": "value"})
|
||||
raise ValueError("Something went wrong")
|
||||
```
|
||||
```python
|
||||
# Start with fresh context (no inherited tags)
|
||||
with posthog.new_context(fresh=True):
|
||||
posthog.tag("request_id", "123")
|
||||
with hanzo_insights.new_context(fresh=True):
|
||||
hanzo_insights.tag("request_id", "123")
|
||||
# Both this event and the exception will be tagged with the context tags
|
||||
posthog.capture("event_name", {"property": "value"})
|
||||
hanzo_insights.capture("event_name", {"property": "value"})
|
||||
raise ValueError("Something went wrong")
|
||||
```
|
||||
|
||||
Category:
|
||||
Contexts
|
||||
"""
|
||||
from posthog import capture_exception
|
||||
from hanzo_insights import capture_exception
|
||||
|
||||
current_context = _get_current_context()
|
||||
new_context = ContextScope(current_context, fresh, capture_exceptions, client)
|
||||
@@ -189,7 +189,7 @@ def tag(key: str, value: Any) -> None:
|
||||
|
||||
Example:
|
||||
```python
|
||||
posthog.tag("user_id", "123")
|
||||
hanzo_insights.tag("user_id", "123")
|
||||
```
|
||||
|
||||
Category:
|
||||
@@ -221,7 +221,7 @@ def identify_context(distinct_id: str) -> None:
|
||||
"""
|
||||
Identify the current context with a distinct ID, associating all events captured in this or
|
||||
child contexts with the given distinct ID (unless identify_context is called again). This is overridden by
|
||||
distinct id's passed directly to posthog.capture and related methods (identify, set etc). Entering a
|
||||
distinct id's passed directly to hanzo_insights.capture and related methods (identify, set etc). Entering a
|
||||
fresh context will clear the context-level distinct ID. The distinct-id passed should be uniquely associated
|
||||
with one of your users. Events captured outside of a context, or in a context with no associated distinct
|
||||
ID, will be assigned a random UUID, and captured as "personless".
|
||||
@@ -244,7 +244,7 @@ def set_context_session(session_id: str) -> None:
|
||||
Entering a fresh context will clear the context-level session ID.
|
||||
|
||||
Args:
|
||||
session_id: The session ID to associate with the current context and its children. See https://posthog.com/docs/data/sessions
|
||||
session_id: The session ID to associate with the current context and its children. See https://insights.hanzo.ai/docs/data/sessions
|
||||
|
||||
Category:
|
||||
Contexts
|
||||
@@ -373,20 +373,20 @@ F = TypeVar("F", bound=Callable[..., Any])
|
||||
def scoped(fresh: bool = False, capture_exceptions: bool = True):
|
||||
"""
|
||||
Decorator that creates a new context for the function. Simply wraps
|
||||
the function in a with posthog.new_context(): block.
|
||||
the function in a with hanzo_insights.new_context(): block.
|
||||
|
||||
Args:
|
||||
fresh: Whether to start with a fresh context (default: False)
|
||||
capture_exceptions: Whether to capture and track exceptions with posthog error tracking (default: True)
|
||||
capture_exceptions: Whether to capture and track exceptions with Insights error tracking (default: True)
|
||||
|
||||
Example:
|
||||
@posthog.scoped()
|
||||
@hanzo_insights.scoped()
|
||||
def process_payment(payment_id):
|
||||
posthog.tag("payment_id", payment_id)
|
||||
posthog.tag("payment_method", "credit_card")
|
||||
hanzo_insights.tag("payment_id", payment_id)
|
||||
hanzo_insights.tag("payment_method", "credit_card")
|
||||
|
||||
# This event will be captured with tags
|
||||
posthog.capture("payment_started")
|
||||
hanzo_insights.capture("payment_started")
|
||||
# If this raises an exception, it will be captured with tags
|
||||
# and then re-raised
|
||||
some_risky_function()
|
||||
@@ -9,13 +9,13 @@ import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from posthog.client import Client
|
||||
from hanzo_insights.client import Client
|
||||
|
||||
|
||||
class ExceptionCapture:
|
||||
# TODO: Add client side rate limiting to prevent spamming the server with exceptions
|
||||
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
|
||||
def __init__(self, client: "Client"):
|
||||
self.client = client
|
||||
@@ -30,7 +30,7 @@ from typing import ( # noqa: F401
|
||||
cast,
|
||||
)
|
||||
|
||||
from posthog.args import ExceptionArg, ExcInfo # noqa: F401
|
||||
from hanzo_insights.args import ExceptionArg, ExcInfo # noqa: F401
|
||||
|
||||
try:
|
||||
# Python 3.11
|
||||
@@ -62,8 +62,8 @@ DEFAULT_CODE_VARIABLES_MASK_PATTERNS = [
|
||||
|
||||
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS = [r"^__.*"]
|
||||
|
||||
CODE_VARIABLES_REDACTED_VALUE = "$$_posthog_redacted_based_on_masking_rules_$$"
|
||||
CODE_VARIABLES_TOO_LONG_VALUE = "$$_posthog_value_too_long_$$"
|
||||
CODE_VARIABLES_REDACTED_VALUE = "$$_insights_redacted_based_on_masking_rules_$$"
|
||||
CODE_VARIABLES_TOO_LONG_VALUE = "$$_insights_value_too_long_$$"
|
||||
|
||||
_MAX_VALUE_LENGTH_FOR_PATTERN_MATCH = 5_000
|
||||
_MAX_COLLECTION_ITEMS_TO_SCAN = 100
|
||||
@@ -768,12 +768,12 @@ def set_in_app_in_frames(frames, in_app_exclude, in_app_include, project_root=No
|
||||
def exception_is_already_captured(error):
|
||||
# type: (ExceptionArg) -> bool
|
||||
if isinstance(error, BaseException):
|
||||
return hasattr(error, "__posthog_exception_captured")
|
||||
return hasattr(error, "__insights_exception_captured")
|
||||
# Autocaptured exceptions are passed as a tuple from our system hooks,
|
||||
# the second item is the exception value (the first is the exception type)
|
||||
elif isinstance(error, tuple) and len(error) > 1:
|
||||
return error[1] is not None and hasattr(
|
||||
error[1], "__posthog_exception_captured"
|
||||
error[1], "__insights_exception_captured"
|
||||
)
|
||||
else:
|
||||
return False # type: ignore[unreachable]
|
||||
@@ -782,14 +782,14 @@ def exception_is_already_captured(error):
|
||||
def mark_exception_as_captured(error, uuid):
|
||||
# type: (ExceptionArg, str) -> None
|
||||
if isinstance(error, BaseException):
|
||||
setattr(error, "__posthog_exception_captured", True)
|
||||
setattr(error, "__posthog_exception_uuid", uuid)
|
||||
setattr(error, "__insights_exception_captured", True)
|
||||
setattr(error, "__insights_exception_uuid", uuid)
|
||||
# Autocaptured exceptions are passed as a tuple from our system hooks,
|
||||
# the second item is the exception value (the first is the exception type)
|
||||
elif isinstance(error, tuple) and len(error) > 1:
|
||||
if error[1] is not None:
|
||||
setattr(error[1], "__posthog_exception_captured", True)
|
||||
setattr(error[1], "__posthog_exception_uuid", uuid)
|
||||
setattr(error[1], "__insights_exception_captured", True)
|
||||
setattr(error[1], "__insights_exception_uuid", uuid)
|
||||
|
||||
|
||||
def exc_info_from_error(error):
|
||||
@@ -8,16 +8,40 @@ from typing import Optional
|
||||
from dateutil import parser
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from posthog import utils
|
||||
from posthog.types import FlagValue
|
||||
from posthog.utils import convert_to_datetime_aware, is_valid_regex
|
||||
from hanzo_insights import utils
|
||||
from hanzo_insights.types import FlagValue
|
||||
from hanzo_insights.utils import convert_to_datetime_aware, is_valid_regex
|
||||
|
||||
__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)
|
||||
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
|
||||
NONE_VALUES_ALLOWED_OPERATORS = ["is_not"]
|
||||
|
||||
# All operators supported by match_property, grouped by category.
|
||||
EQUALITY_OPERATORS = ("exact", "is_not", "is_set", "is_not_set")
|
||||
STRING_OPERATORS = ("icontains", "not_icontains", "regex", "not_regex")
|
||||
NUMERIC_OPERATORS = ("gt", "gte", "lt", "lte")
|
||||
DATE_OPERATORS = ("is_date_before", "is_date_after")
|
||||
SEMVER_COMPARISON_OPERATORS = (
|
||||
"semver_eq",
|
||||
"semver_neq",
|
||||
"semver_gt",
|
||||
"semver_gte",
|
||||
"semver_lt",
|
||||
"semver_lte",
|
||||
)
|
||||
SEMVER_RANGE_OPERATORS = ("semver_tilde", "semver_caret", "semver_wildcard")
|
||||
SEMVER_OPERATORS = SEMVER_COMPARISON_OPERATORS + SEMVER_RANGE_OPERATORS
|
||||
|
||||
PROPERTY_OPERATORS = (
|
||||
EQUALITY_OPERATORS
|
||||
+ STRING_OPERATORS
|
||||
+ NUMERIC_OPERATORS
|
||||
+ DATE_OPERATORS
|
||||
+ SEMVER_OPERATORS
|
||||
)
|
||||
|
||||
|
||||
class InconclusiveMatchError(Exception):
|
||||
pass
|
||||
@@ -385,6 +409,9 @@ def match_property(property, property_values) -> bool:
|
||||
operator = property.get("operator") or "exact"
|
||||
value = property.get("value")
|
||||
|
||||
if operator not in PROPERTY_OPERATORS:
|
||||
raise InconclusiveMatchError(f"Unknown operator {operator}")
|
||||
|
||||
if key not in property_values:
|
||||
raise InconclusiveMatchError(
|
||||
"can't match properties without a given property value"
|
||||
@@ -505,7 +532,64 @@ def match_property(property, property_values) -> bool:
|
||||
"The date provided must be a string or date object"
|
||||
)
|
||||
|
||||
# if we get here, we don't know how to handle the operator
|
||||
if operator in SEMVER_OPERATORS:
|
||||
try:
|
||||
override_parsed = parse_semver(override_value)
|
||||
except (ValueError, TypeError):
|
||||
raise InconclusiveMatchError(
|
||||
f"Person property value '{override_value}' is not a valid semver"
|
||||
)
|
||||
|
||||
if operator in SEMVER_COMPARISON_OPERATORS:
|
||||
try:
|
||||
flag_parsed = parse_semver(value)
|
||||
except (ValueError, TypeError):
|
||||
raise InconclusiveMatchError(
|
||||
f"Flag semver value '{value}' is not a valid semver"
|
||||
)
|
||||
|
||||
if operator == "semver_eq":
|
||||
return override_parsed == flag_parsed
|
||||
elif operator == "semver_neq":
|
||||
return override_parsed != flag_parsed
|
||||
elif operator == "semver_gt":
|
||||
return override_parsed > flag_parsed
|
||||
elif operator == "semver_gte":
|
||||
return override_parsed >= flag_parsed
|
||||
elif operator == "semver_lt":
|
||||
return override_parsed < flag_parsed
|
||||
elif operator == "semver_lte":
|
||||
return override_parsed <= flag_parsed
|
||||
|
||||
elif operator == "semver_tilde":
|
||||
try:
|
||||
lower, upper = _tilde_bounds(str(value))
|
||||
except (ValueError, TypeError):
|
||||
raise InconclusiveMatchError(
|
||||
f"Flag semver value '{value}' is not valid for tilde operator"
|
||||
)
|
||||
return lower <= override_parsed < upper
|
||||
|
||||
elif operator == "semver_caret":
|
||||
try:
|
||||
lower, upper = _caret_bounds(str(value))
|
||||
except (ValueError, TypeError):
|
||||
raise InconclusiveMatchError(
|
||||
f"Flag semver value '{value}' is not valid for caret operator"
|
||||
)
|
||||
return lower <= override_parsed < upper
|
||||
|
||||
elif operator == "semver_wildcard":
|
||||
try:
|
||||
lower, upper = _wildcard_bounds(str(value))
|
||||
except (ValueError, TypeError):
|
||||
raise InconclusiveMatchError(
|
||||
f"Flag semver value '{value}' is not valid for wildcard operator"
|
||||
)
|
||||
return lower <= override_parsed < upper
|
||||
|
||||
# Unreachable: all operators in PROPERTY_OPERATORS are handled above,
|
||||
# and unknown operators are rejected at the top of this function.
|
||||
raise InconclusiveMatchError(f"Unknown operator {operator}")
|
||||
|
||||
|
||||
@@ -686,3 +770,75 @@ def relative_date_parse_for_feature_flag_matching(
|
||||
return parsed_dt
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def parse_semver(value: str) -> tuple:
|
||||
"""Parse a semver string into a comparable (major, minor, patch) integer tuple.
|
||||
|
||||
Matches the behavior of the sortableSemver HogQL function:
|
||||
- Handles v-prefix, whitespace, pre-release suffixes
|
||||
- Defaults missing components to 0 (e.g., 1.2 -> 1.2.0)
|
||||
Raises ValueError if parsing fails.
|
||||
"""
|
||||
text = str(value).strip().lstrip("vV")
|
||||
# Strip pre-release/build metadata suffix
|
||||
text = text.split("-")[0].split("+")[0]
|
||||
parts = text.split(".")
|
||||
|
||||
if not parts or not parts[0]:
|
||||
raise ValueError("Invalid semver format")
|
||||
|
||||
major = int(parts[0])
|
||||
minor = int(parts[1]) if len(parts) > 1 and parts[1] else 0
|
||||
patch = int(parts[2]) if len(parts) > 2 and parts[2] else 0
|
||||
|
||||
return (major, minor, patch)
|
||||
|
||||
|
||||
def _tilde_bounds(value: str) -> tuple:
|
||||
"""~1.2.3 means >=1.2.3 <1.3.0 (allows patch-level changes)."""
|
||||
major, minor, patch = parse_semver(value)
|
||||
return (major, minor, patch), (major, minor + 1, 0)
|
||||
|
||||
|
||||
def _caret_bounds(value: str) -> tuple:
|
||||
"""Caret follows semver spec:
|
||||
^1.2.3 means >=1.2.3 <2.0.0
|
||||
^0.2.3 means >=0.2.3 <0.3.0
|
||||
^0.0.3 means >=0.0.3 <0.0.4
|
||||
"""
|
||||
major, minor, patch = parse_semver(value)
|
||||
lower = (major, minor, patch)
|
||||
|
||||
if major > 0:
|
||||
upper = (major + 1, 0, 0)
|
||||
elif minor > 0:
|
||||
upper = (0, minor + 1, 0)
|
||||
else:
|
||||
upper = (0, 0, patch + 1)
|
||||
|
||||
return lower, upper
|
||||
|
||||
|
||||
def _wildcard_bounds(value: str) -> tuple:
|
||||
"""Wildcard matching:
|
||||
1.* means >=1.0.0 <2.0.0
|
||||
1.2.* means >=1.2.0 <1.3.0
|
||||
"""
|
||||
cleaned = str(value).strip().lstrip("vV").replace("*", "").rstrip(".")
|
||||
if not cleaned:
|
||||
raise ValueError("Invalid wildcard pattern")
|
||||
|
||||
parts = [p for p in cleaned.split(".") if p]
|
||||
if not parts:
|
||||
raise ValueError("Invalid wildcard pattern")
|
||||
|
||||
if len(parts) == 1:
|
||||
major = int(parts[0])
|
||||
return (major, 0, 0), (major + 1, 0, 0)
|
||||
elif len(parts) == 2:
|
||||
major, minor = int(parts[0]), int(parts[1])
|
||||
return (major, minor, 0), (major, minor + 1, 0)
|
||||
else:
|
||||
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
|
||||
return (major, minor, patch), (major, minor, patch + 1)
|
||||
@@ -9,11 +9,11 @@ functions) to share flag definitions and reduce API calls.
|
||||
|
||||
Usage:
|
||||
|
||||
from posthog import Posthog
|
||||
from posthog.flag_definition_cache import FlagDefinitionCacheProvider
|
||||
from hanzo_insights import Insights
|
||||
from hanzo_insights.flag_definition_cache import FlagDefinitionCacheProvider
|
||||
|
||||
cache = RedisFlagDefinitionCache(redis_client, "my-team")
|
||||
posthog = Posthog(
|
||||
client = Insights(
|
||||
"<project_api_key>",
|
||||
personal_api_key="<personal_api_key>",
|
||||
flag_definition_cache_provider=cache,
|
||||
@@ -63,7 +63,7 @@ class FlagDefinitionCacheProvider(Protocol):
|
||||
new definitions from the API. Store the data in your external cache
|
||||
and release any locks.
|
||||
|
||||
4. `shutdown()` - Called when the PostHog client shuts down. Release any
|
||||
4. `shutdown()` - Called when the Insights client shuts down. Release any
|
||||
distributed locks and clean up resources.
|
||||
|
||||
Error Handling:
|
||||
@@ -104,7 +104,7 @@ class FlagDefinitionCacheProvider(Protocol):
|
||||
|
||||
def on_flag_definitions_received(self, data: FlagDefinitionCacheData) -> None:
|
||||
"""
|
||||
Called after successfully receiving new flag definitions from PostHog.
|
||||
Called after successfully receiving new flag definitions from Insights.
|
||||
|
||||
Use this to store the data in your external cache and release any
|
||||
distributed locks acquired in `should_fetch_flag_definitions()`.
|
||||
@@ -117,7 +117,7 @@ class FlagDefinitionCacheProvider(Protocol):
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Called when the PostHog client shuts down.
|
||||
Called when the Insights client shuts down.
|
||||
|
||||
Use this to release any distributed locks and clean up resources.
|
||||
This method is called even if `should_fetch_flag_definitions()`
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from posthog import contexts
|
||||
from posthog.client import Client
|
||||
from hanzo_insights import contexts
|
||||
from hanzo_insights.client import Client
|
||||
|
||||
try:
|
||||
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
|
||||
@@ -21,25 +21,26 @@ if TYPE_CHECKING:
|
||||
from typing import Callable, Dict, Any, Optional, Union, Awaitable # noqa: F401
|
||||
|
||||
|
||||
class PosthogContextMiddleware:
|
||||
class InsightsContextMiddleware:
|
||||
"""Middleware to automatically track Django requests.
|
||||
|
||||
This middleware wraps all calls with a posthog context. It attempts to extract the following from the request headers:
|
||||
- Session ID, (extracted from `X-POSTHOG-SESSION-ID`)
|
||||
- Distinct ID, (extracted from `X-POSTHOG-DISTINCT-ID`)
|
||||
This middleware wraps all calls with an Insights context. It attempts to extract the following from the request headers:
|
||||
- Session ID, (extracted from `X-INSIGHTS-SESSION-ID`)
|
||||
- Distinct ID, (extracted from `X-INSIGHTS-DISTINCT-ID`)
|
||||
- Request URL as $current_url
|
||||
- Request Method as $request_method
|
||||
|
||||
The context will also auto-capture exceptions and send them to PostHog, unless you disable it by setting
|
||||
`POSTHOG_MW_CAPTURE_EXCEPTIONS` to `False` in your Django settings. The exceptions are captured using the
|
||||
global client, unless the setting `POSTHOG_MW_CLIENT` is set to a custom client instance
|
||||
The context will also auto-capture exceptions and send them to Insights, unless you disable it by setting
|
||||
`INSIGHTS_MW_CAPTURE_EXCEPTIONS` to `False` in your Django settings.
|
||||
The exceptions are captured using the global client, unless the setting `INSIGHTS_MW_CLIENT`
|
||||
is set to a custom client instance.
|
||||
|
||||
The middleware behaviour is customisable through 3 additional functions:
|
||||
- `POSTHOG_MW_EXTRA_TAGS`, which is a Callable[[HttpRequest], Dict[str, Any]] expected to return a dictionary of additional tags to be added to the context.
|
||||
- `POSTHOG_MW_REQUEST_FILTER`, which is a Callable[[HttpRequest], bool] expected to return `False` if the request should not be tracked.
|
||||
- `POSTHOG_MW_TAG_MAP`, which is a Callable[[Dict[str, Any]], Dict[str, Any]], which you can use to modify the tags before they're added to the context.
|
||||
- `INSIGHTS_MW_EXTRA_TAGS`, which is a Callable[[HttpRequest], Dict[str, Any]] expected to return a dictionary of additional tags to be added to the context.
|
||||
- `INSIGHTS_MW_REQUEST_FILTER`, which is a Callable[[HttpRequest], bool] expected to return `False` if the request should not be tracked.
|
||||
- `INSIGHTS_MW_TAG_MAP`, which is a Callable[[Dict[str, Any]], Dict[str, Any]], which you can use to modify the tags before they're added to the context.
|
||||
|
||||
You can use the `POSTHOG_MW_TAG_MAP` function to remove any default tags you don't want to capture, or override them with your own values.
|
||||
You can use the `INSIGHTS_MW_TAG_MAP` function to remove any default tags you don't want to capture, or override them with your own values.
|
||||
|
||||
Context tags are automatically included as properties on all events captured within a context, including exceptions.
|
||||
See the context documentation for more information. The extracted distinct ID and session ID, if found, are used to
|
||||
@@ -66,47 +67,48 @@ class PosthogContextMiddleware:
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
if hasattr(settings, "POSTHOG_MW_EXTRA_TAGS") and callable(
|
||||
settings.POSTHOG_MW_EXTRA_TAGS
|
||||
):
|
||||
def _get_setting(name):
|
||||
insights_name = f"INSIGHTS_MW_{name}"
|
||||
if hasattr(settings, insights_name):
|
||||
return getattr(settings, insights_name)
|
||||
return None
|
||||
|
||||
extra_tags = _get_setting("EXTRA_TAGS")
|
||||
if extra_tags and callable(extra_tags):
|
||||
self.extra_tags = cast(
|
||||
"Optional[Callable[[HttpRequest], Dict[str, Any]]]",
|
||||
settings.POSTHOG_MW_EXTRA_TAGS,
|
||||
extra_tags,
|
||||
)
|
||||
else:
|
||||
self.extra_tags = None
|
||||
|
||||
if hasattr(settings, "POSTHOG_MW_REQUEST_FILTER") and callable(
|
||||
settings.POSTHOG_MW_REQUEST_FILTER
|
||||
):
|
||||
request_filter = _get_setting("REQUEST_FILTER")
|
||||
if request_filter and callable(request_filter):
|
||||
self.request_filter = cast(
|
||||
"Optional[Callable[[HttpRequest], bool]]",
|
||||
settings.POSTHOG_MW_REQUEST_FILTER,
|
||||
request_filter,
|
||||
)
|
||||
else:
|
||||
self.request_filter = None
|
||||
|
||||
if hasattr(settings, "POSTHOG_MW_TAG_MAP") and callable(
|
||||
settings.POSTHOG_MW_TAG_MAP
|
||||
):
|
||||
tag_map = _get_setting("TAG_MAP")
|
||||
if tag_map and callable(tag_map):
|
||||
self.tag_map = cast(
|
||||
"Optional[Callable[[Dict[str, Any]], Dict[str, Any]]]",
|
||||
settings.POSTHOG_MW_TAG_MAP,
|
||||
tag_map,
|
||||
)
|
||||
else:
|
||||
self.tag_map = None
|
||||
|
||||
if hasattr(settings, "POSTHOG_MW_CAPTURE_EXCEPTIONS") and isinstance(
|
||||
settings.POSTHOG_MW_CAPTURE_EXCEPTIONS, bool
|
||||
):
|
||||
self.capture_exceptions = settings.POSTHOG_MW_CAPTURE_EXCEPTIONS
|
||||
capture_exceptions = _get_setting("CAPTURE_EXCEPTIONS")
|
||||
if isinstance(capture_exceptions, bool):
|
||||
self.capture_exceptions = capture_exceptions
|
||||
else:
|
||||
self.capture_exceptions = True
|
||||
|
||||
if hasattr(settings, "POSTHOG_MW_CLIENT") and isinstance(
|
||||
settings.POSTHOG_MW_CLIENT, Client
|
||||
):
|
||||
self.client = cast("Optional[Client]", settings.POSTHOG_MW_CLIENT)
|
||||
mw_client = _get_setting("CLIENT")
|
||||
if isinstance(mw_client, Client):
|
||||
self.client = cast("Optional[Client]", mw_client)
|
||||
else:
|
||||
self.client = None
|
||||
|
||||
@@ -125,13 +127,13 @@ class PosthogContextMiddleware:
|
||||
"""
|
||||
tags = {}
|
||||
|
||||
# Extract session ID from X-POSTHOG-SESSION-ID header
|
||||
session_id = request.headers.get("X-POSTHOG-SESSION-ID")
|
||||
# Extract session ID from X-INSIGHTS-SESSION-ID header
|
||||
session_id = request.headers.get("X-INSIGHTS-SESSION-ID")
|
||||
if session_id:
|
||||
contexts.set_context_session(session_id)
|
||||
|
||||
# Extract distinct ID from X-POSTHOG-DISTINCT-ID header or request user id
|
||||
distinct_id = request.headers.get("X-POSTHOG-DISTINCT-ID") or user_id
|
||||
# Extract distinct ID from X-INSIGHTS-DISTINCT-ID header or request user id
|
||||
distinct_id = request.headers.get("X-INSIGHTS-DISTINCT-ID") or user_id
|
||||
if distinct_id:
|
||||
contexts.identify_context(distinct_id)
|
||||
|
||||
@@ -314,6 +316,6 @@ class PosthogContextMiddleware:
|
||||
if self.client:
|
||||
self.client.capture_exception(exception)
|
||||
else:
|
||||
from posthog import capture_exception
|
||||
from hanzo_insights import capture_exception
|
||||
|
||||
capture_exception(exception)
|
||||
@@ -14,8 +14,8 @@ from requests.adapters import HTTPAdapter # type: ignore[import-untyped]
|
||||
from urllib3.connection import HTTPConnection
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
from posthog.utils import remove_trailing_slash
|
||||
from posthog.version import VERSION
|
||||
from hanzo_insights.utils import remove_trailing_slash
|
||||
from hanzo_insights.version import VERSION
|
||||
|
||||
SocketOptions = List[Tuple[int, int, Union[int, bytes]]]
|
||||
|
||||
@@ -137,7 +137,7 @@ def set_socket_options(socket_options: Optional[SocketOptions]) -> None:
|
||||
Configure socket options for all HTTP connections.
|
||||
|
||||
Example:
|
||||
from posthog import set_socket_options
|
||||
from hanzo_insights import set_socket_options
|
||||
set_socket_options([(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)])
|
||||
"""
|
||||
global _session, _flags_session, _socket_options
|
||||
@@ -159,19 +159,19 @@ def disable_connection_reuse() -> None:
|
||||
_pooling_enabled = False
|
||||
|
||||
|
||||
US_INGESTION_ENDPOINT = "https://us.i.posthog.com"
|
||||
EU_INGESTION_ENDPOINT = "https://eu.i.posthog.com"
|
||||
US_INGESTION_ENDPOINT = "https://us.i.insights.hanzo.ai"
|
||||
EU_INGESTION_ENDPOINT = "https://eu.i.insights.hanzo.ai"
|
||||
DEFAULT_HOST = US_INGESTION_ENDPOINT
|
||||
USER_AGENT = "posthog-python/" + VERSION
|
||||
USER_AGENT = "hanzo-insights-python/" + VERSION
|
||||
|
||||
|
||||
def determine_server_host(host: Optional[str]) -> str:
|
||||
"""Determines the server host to use."""
|
||||
host_or_default = host or DEFAULT_HOST
|
||||
trimmed_host = remove_trailing_slash(host_or_default)
|
||||
if trimmed_host in ("https://app.posthog.com", "https://us.posthog.com"):
|
||||
if trimmed_host in ("https://app.posthog.com", "https://us.posthog.com", "https://insights.hanzo.ai", "https://us.insights.hanzo.ai"):
|
||||
return US_INGESTION_ENDPOINT
|
||||
elif trimmed_host == "https://eu.posthog.com":
|
||||
elif trimmed_host in ("https://eu.posthog.com", "https://eu.insights.hanzo.ai"):
|
||||
return EU_INGESTION_ENDPOINT
|
||||
else:
|
||||
return host_or_default
|
||||
@@ -187,7 +187,7 @@ def post(
|
||||
**kwargs,
|
||||
) -> requests.Response:
|
||||
"""Post the `kwargs` to the API"""
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
body = kwargs
|
||||
body["sentAt"] = datetime.now(tz=tzutc()).isoformat()
|
||||
url = remove_trailing_slash(host or DEFAULT_HOST) + path
|
||||
@@ -217,7 +217,7 @@ def post(
|
||||
def _process_response(
|
||||
res: requests.Response, success_message: str, *, return_json: bool = True
|
||||
) -> Union[requests.Response, Any]:
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
if res.status_code == 200:
|
||||
log.debug(success_message)
|
||||
response = res.json() if return_json else res
|
||||
@@ -231,7 +231,7 @@ def _process_response(
|
||||
and "feature_flags" in response["quotaLimited"]
|
||||
):
|
||||
log.warning(
|
||||
"[FEATURE FLAGS] PostHog feature flags quota limited, resetting feature flag data. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts"
|
||||
"[FEATURE FLAGS] Feature flags quota limited, resetting feature flag data. Learn more about billing limits at https://insights.hanzo.ai/docs/billing/limits-alerts"
|
||||
)
|
||||
raise QuotaLimitError(res.status_code, "Feature flags quota limited")
|
||||
return response
|
||||
@@ -341,7 +341,7 @@ def get(
|
||||
- not_modified=True and data=None if server returns 304
|
||||
- not_modified=False and data=response if server returns 200
|
||||
"""
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
full_url = remove_trailing_slash(host or DEFAULT_HOST) + url
|
||||
headers = {"Authorization": "Bearer %s" % api_key, "User-Agent": USER_AGENT}
|
||||
|
||||
@@ -375,7 +375,7 @@ class APIError(Exception):
|
||||
self.retry_after = retry_after
|
||||
|
||||
def __str__(self):
|
||||
msg = "[PostHog] {0} ({1})"
|
||||
msg = "[Insights] {0} ({1})"
|
||||
return msg.format(self.message, self.status)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import unittest
|
||||
|
||||
def all_names():
|
||||
for _, modname, _ in pkgutil.iter_modules(__path__):
|
||||
yield "posthog.test." + modname
|
||||
yield "hanzo_insights.test." + modname
|
||||
|
||||
|
||||
def all():
|
||||
+147
-49
@@ -3,10 +3,12 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hanzo_insights import identify_context, new_context
|
||||
|
||||
try:
|
||||
from anthropic.types import Message, Usage
|
||||
|
||||
from posthog.ai.anthropic import Anthropic, AsyncAnthropic
|
||||
from hanzo_insights.ai.anthropic import Anthropic, AsyncAnthropic
|
||||
|
||||
ANTHROPIC_AVAILABLE = True
|
||||
except ImportError:
|
||||
@@ -103,7 +105,7 @@ class MockDelta:
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
with patch("posthog.client.Client") as mock_client:
|
||||
with patch("hanzo_insights.client.Client") as mock_client:
|
||||
mock_client.privacy_mode = False
|
||||
yield mock_client
|
||||
|
||||
@@ -277,12 +279,12 @@ def test_basic_completion(mock_client, mock_anthropic_response):
|
||||
with patch(
|
||||
"anthropic.resources.Messages.create", return_value=mock_anthropic_response
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_anthropic_response
|
||||
@@ -323,12 +325,12 @@ def test_tokens_source_passthrough(mock_client, mock_anthropic_response):
|
||||
with patch(
|
||||
"anthropic.resources.Messages.create", return_value=mock_anthropic_response
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"$ai_input_tokens": 99999},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"$ai_input_tokens": 99999},
|
||||
)
|
||||
|
||||
props = mock_client.capture.call_args[1]["properties"]
|
||||
@@ -340,12 +342,12 @@ def test_groups(mock_client, mock_anthropic_response):
|
||||
with patch(
|
||||
"anthropic.resources.Messages.create", return_value=mock_anthropic_response
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_groups={"company": "test_company"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_groups={"company": "test_company"},
|
||||
)
|
||||
|
||||
assert response == mock_anthropic_response
|
||||
@@ -359,12 +361,12 @@ def test_privacy_mode_local(mock_client, mock_anthropic_response):
|
||||
with patch(
|
||||
"anthropic.resources.Messages.create", return_value=mock_anthropic_response
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_privacy_mode=True,
|
||||
insights_distinct_id="test-id",
|
||||
insights_privacy_mode=True,
|
||||
)
|
||||
|
||||
assert response == mock_anthropic_response
|
||||
@@ -381,12 +383,12 @@ def test_privacy_mode_global(mock_client, mock_anthropic_response):
|
||||
"anthropic.resources.Messages.create", return_value=mock_anthropic_response
|
||||
):
|
||||
mock_client.privacy_mode = True
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_privacy_mode=False,
|
||||
insights_distinct_id="test-id",
|
||||
insights_privacy_mode=False,
|
||||
)
|
||||
|
||||
assert response == mock_anthropic_response
|
||||
@@ -405,14 +407,14 @@ def test_basic_integration(mock_client):
|
||||
"anthropic.resources.Messages.create",
|
||||
return_value=create_mock_response(),
|
||||
):
|
||||
client = Anthropic(posthog_client=mock_client)
|
||||
client = Anthropic(insights_client=mock_client)
|
||||
client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Foo"}],
|
||||
max_tokens=1,
|
||||
temperature=0,
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
system="You must always answer with 'Bar'.",
|
||||
)
|
||||
|
||||
@@ -450,7 +452,7 @@ async def test_basic_async_integration(mock_client):
|
||||
"anthropic.resources.messages.AsyncMessages.create",
|
||||
side_effect=mock_async_create,
|
||||
):
|
||||
client = AsyncAnthropic(posthog_client=mock_client)
|
||||
client = AsyncAnthropic(insights_client=mock_client)
|
||||
await client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[
|
||||
@@ -458,8 +460,8 @@ async def test_basic_async_integration(mock_client):
|
||||
],
|
||||
max_tokens=1,
|
||||
temperature=0,
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert mock_client.capture.call_count == 1
|
||||
@@ -511,7 +513,7 @@ async def test_async_streaming_system_prompt(mock_client):
|
||||
"anthropic.resources.messages.AsyncMessages.create",
|
||||
side_effect=async_create_wrapper,
|
||||
):
|
||||
client = AsyncAnthropic(posthog_client=mock_client)
|
||||
client = AsyncAnthropic(insights_client=mock_client)
|
||||
response = await client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
system="You must always answer with 'Bar'.",
|
||||
@@ -539,7 +541,7 @@ def test_error(mock_client, mock_anthropic_response):
|
||||
with patch(
|
||||
"anthropic.resources.Messages.create", side_effect=Exception("Test error")
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
with pytest.raises(Exception):
|
||||
client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
@@ -559,12 +561,12 @@ def test_cached_tokens(mock_client, mock_anthropic_response_with_cached_tokens):
|
||||
"anthropic.resources.Messages.create",
|
||||
return_value=mock_anthropic_response_with_cached_tokens,
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_anthropic_response_with_cached_tokens
|
||||
@@ -598,7 +600,7 @@ def test_tool_definition(mock_client, mock_anthropic_response):
|
||||
"anthropic.resources.Messages.create",
|
||||
return_value=mock_anthropic_response,
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
tools = [
|
||||
{
|
||||
@@ -623,8 +625,8 @@ def test_tool_definition(mock_client, mock_anthropic_response):
|
||||
temperature=0.7,
|
||||
tools=tools,
|
||||
messages=[{"role": "user", "content": "hey"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_anthropic_response
|
||||
@@ -660,7 +662,7 @@ def test_tool_calls_in_output_choices(
|
||||
"anthropic.resources.Messages.create",
|
||||
return_value=mock_anthropic_response_with_tool_calls,
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
max_tokens=200,
|
||||
@@ -678,7 +680,7 @@ def test_tool_calls_in_output_choices(
|
||||
},
|
||||
}
|
||||
],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_anthropic_response_with_tool_calls
|
||||
@@ -722,7 +724,7 @@ def test_tool_calls_only_no_content(
|
||||
"anthropic.resources.Messages.create",
|
||||
return_value=mock_anthropic_response_tool_calls_only,
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
max_tokens=200,
|
||||
@@ -741,7 +743,7 @@ def test_tool_calls_only_no_content(
|
||||
},
|
||||
}
|
||||
],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_anthropic_response_tool_calls_only
|
||||
@@ -788,7 +790,7 @@ def test_async_tool_calls_in_output_choices(
|
||||
"anthropic.resources.AsyncMessages.create",
|
||||
side_effect=mock_async_create,
|
||||
):
|
||||
async_client = AsyncAnthropic(api_key="test-key", posthog_client=mock_client)
|
||||
async_client = AsyncAnthropic(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
async def run_test():
|
||||
return await async_client.messages.create(
|
||||
@@ -808,7 +810,7 @@ def test_async_tool_calls_in_output_choices(
|
||||
},
|
||||
}
|
||||
],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
response = asyncio.run(run_test())
|
||||
@@ -853,7 +855,7 @@ def test_streaming_with_tool_calls(mock_client, mock_anthropic_stream_with_tools
|
||||
"anthropic.resources.Messages.create",
|
||||
return_value=mock_anthropic_stream_with_tools,
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
system="You are a helpful weather assistant.",
|
||||
@@ -875,7 +877,7 @@ def test_streaming_with_tool_calls(mock_client, mock_anthropic_stream_with_tools
|
||||
}
|
||||
],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the stream - this triggers the finally block synchronously
|
||||
@@ -975,7 +977,7 @@ def test_async_streaming_with_tool_calls(mock_client, mock_anthropic_stream_with
|
||||
"anthropic.resources.AsyncMessages.create",
|
||||
side_effect=mock_async_create,
|
||||
):
|
||||
async_client = AsyncAnthropic(api_key="test-key", posthog_client=mock_client)
|
||||
async_client = AsyncAnthropic(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
async def run_test():
|
||||
response = await async_client.messages.create(
|
||||
@@ -999,7 +1001,7 @@ def test_async_streaming_with_tool_calls(mock_client, mock_anthropic_stream_with
|
||||
}
|
||||
],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the async stream
|
||||
@@ -1099,11 +1101,11 @@ def test_web_search_count(mock_client):
|
||||
mock_response = MockResponseWithWebSearch()
|
||||
|
||||
with patch("anthropic.resources.Messages.create", return_value=mock_response):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Search for recent news"}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -1177,12 +1179,12 @@ def test_streaming_with_web_search(mock_client, mock_anthropic_stream_with_web_s
|
||||
"anthropic.resources.Messages.create",
|
||||
return_value=mock_anthropic_stream_with_web_search,
|
||||
):
|
||||
client = Anthropic(api_key="test-key", posthog_client=mock_client)
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
response = client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Search for recent news"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the stream - this triggers the finally block synchronously
|
||||
@@ -1232,13 +1234,13 @@ def test_async_with_web_search(mock_client):
|
||||
"anthropic.resources.AsyncMessages.create",
|
||||
side_effect=mock_async_create,
|
||||
):
|
||||
async_client = AsyncAnthropic(api_key="test-key", posthog_client=mock_client)
|
||||
async_client = AsyncAnthropic(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
async def run_test():
|
||||
response = await async_client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Search for recent news"}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
return response
|
||||
|
||||
@@ -1276,14 +1278,14 @@ def test_async_streaming_with_web_search(
|
||||
"anthropic.resources.AsyncMessages.create",
|
||||
side_effect=mock_async_create,
|
||||
):
|
||||
async_client = AsyncAnthropic(api_key="test-key", posthog_client=mock_client)
|
||||
async_client = AsyncAnthropic(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
async def run_test():
|
||||
response = await async_client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Search for recent news"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the async stream
|
||||
@@ -1302,3 +1304,99 @@ def test_async_streaming_with_web_search(
|
||||
assert props["$ai_web_search_count"] == 2
|
||||
assert props["$ai_input_tokens"] == 50
|
||||
assert props["$ai_output_tokens"] == 25
|
||||
|
||||
|
||||
# =======================
|
||||
# Distinct ID Context Tests
|
||||
# =======================
|
||||
|
||||
|
||||
def test_no_distinct_id_uses_trace_id_and_personless(
|
||||
mock_client, mock_anthropic_response
|
||||
):
|
||||
"""When no distinct_id is provided and no outer context, trace_id is used and event is personless."""
|
||||
with patch(
|
||||
"anthropic.resources.Messages.create", return_value=mock_anthropic_response
|
||||
):
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
insights_trace_id="trace-123",
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
|
||||
assert call_args["distinct_id"] == "trace-123"
|
||||
assert props["$process_person_profile"] is False
|
||||
|
||||
|
||||
def test_explicit_distinct_id_creates_person_profile(
|
||||
mock_client, mock_anthropic_response
|
||||
):
|
||||
"""When insights_distinct_id is explicitly passed, it is used and event is not personless."""
|
||||
with patch(
|
||||
"anthropic.resources.Messages.create", return_value=mock_anthropic_response
|
||||
):
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
insights_distinct_id="user-123",
|
||||
insights_trace_id="trace-123",
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
|
||||
assert call_args["distinct_id"] == "user-123"
|
||||
assert (
|
||||
"$process_person_profile" not in props
|
||||
or props["$process_person_profile"] is not False
|
||||
)
|
||||
|
||||
|
||||
def test_outer_context_distinct_id_is_used(mock_client, mock_anthropic_response):
|
||||
"""When an outer context has a distinct_id, it should be used instead of trace_id."""
|
||||
with patch(
|
||||
"anthropic.resources.Messages.create", return_value=mock_anthropic_response
|
||||
):
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
with new_context():
|
||||
identify_context("outer-user-456")
|
||||
client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
insights_trace_id="trace-123",
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
|
||||
assert call_args["distinct_id"] == "outer-user-456"
|
||||
assert (
|
||||
"$process_person_profile" not in props
|
||||
or props["$process_person_profile"] is not False
|
||||
)
|
||||
|
||||
|
||||
def test_explicit_distinct_id_overrides_outer_context(
|
||||
mock_client, mock_anthropic_response
|
||||
):
|
||||
"""When both outer context and explicit insights_distinct_id are set, explicit wins."""
|
||||
with patch(
|
||||
"anthropic.resources.Messages.create", return_value=mock_anthropic_response
|
||||
):
|
||||
client = Anthropic(api_key="test-key", insights_client=mock_client)
|
||||
with new_context():
|
||||
identify_context("outer-user-456")
|
||||
client.messages.create(
|
||||
model="claude-3-opus-20240229",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
insights_distinct_id="explicit-user-789",
|
||||
insights_trace_id="trace-123",
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
assert call_args["distinct_id"] == "explicit-user-789"
|
||||
+64
-64
@@ -6,7 +6,7 @@ import pytest
|
||||
try:
|
||||
from google import genai as google_genai
|
||||
|
||||
from posthog.ai.gemini import Client
|
||||
from hanzo_insights.ai.gemini import Client
|
||||
|
||||
GEMINI_AVAILABLE = True
|
||||
except ImportError:
|
||||
@@ -19,7 +19,7 @@ pytestmark = pytest.mark.skipif(
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
with patch("posthog.client.Client") as mock_client:
|
||||
with patch("hanzo_insights.client.Client") as mock_client:
|
||||
mock_client.privacy_mode = False
|
||||
yield mock_client
|
||||
|
||||
@@ -172,13 +172,13 @@ def test_new_client_basic_generation(
|
||||
"""Test the new Client/Models API structure"""
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_gemini_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Tell me a fun fact about hedgehogs"],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_gemini_response
|
||||
@@ -239,13 +239,13 @@ def test_new_client_streaming_with_generate_content_stream(
|
||||
mock_streaming_response()
|
||||
)
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = client.models.generate_content_stream(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Write a short story"],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"feature": "streaming"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"feature": "streaming"},
|
||||
)
|
||||
|
||||
chunks = list(response)
|
||||
@@ -298,7 +298,7 @@ def test_new_client_streaming_with_tools(mock_client, mock_google_genai_client):
|
||||
mock_streaming_response()
|
||||
)
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Create mock tools configuration
|
||||
mock_tool = MagicMock()
|
||||
@@ -326,8 +326,8 @@ def test_new_client_streaming_with_tools(mock_client, mock_google_genai_client):
|
||||
model="gemini-2.0-flash",
|
||||
contents=["What's the weather in SF?"],
|
||||
config=mock_config,
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"feature": "streaming_with_tools"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"feature": "streaming_with_tools"},
|
||||
)
|
||||
|
||||
chunks = list(response)
|
||||
@@ -357,13 +357,13 @@ def test_new_client_groups(mock_client, mock_google_genai_client, mock_gemini_re
|
||||
"""Test groups functionality with new Client API"""
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_gemini_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_groups={"company": "company_123"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_groups={"company": "company_123"},
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -376,13 +376,13 @@ def test_new_client_privacy_mode_local(
|
||||
"""Test local privacy mode with new Client API"""
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_gemini_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_privacy_mode=True,
|
||||
insights_distinct_id="test-id",
|
||||
insights_privacy_mode=True,
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -399,12 +399,12 @@ def test_new_client_privacy_mode_global(
|
||||
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_gemini_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -419,11 +419,11 @@ def test_new_client_different_input_formats(
|
||||
"""Test different input formats with new Client API"""
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_gemini_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Test string input
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash", contents="Hello", posthog_distinct_id="test-id"
|
||||
model="gemini-2.0-flash", contents="Hello", insights_distinct_id="test-id"
|
||||
)
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
@@ -434,7 +434,7 @@ def test_new_client_different_input_formats(
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=[{"role": "user", "parts": [{"text": "hey"}]}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
@@ -447,7 +447,7 @@ def test_new_client_different_input_formats(
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=[{"role": "user", "parts": [{"text": "Hello "}, {"text": "world"}]}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
@@ -464,7 +464,7 @@ def test_new_client_different_input_formats(
|
||||
# Test list input with string
|
||||
mock_client.capture.reset_mock()
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash", contents=["List item"], posthog_distinct_id="test-id"
|
||||
model="gemini-2.0-flash", contents=["List item"], insights_distinct_id="test-id"
|
||||
)
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
@@ -477,12 +477,12 @@ def test_new_client_model_parameters(
|
||||
"""Test model parameters with new Client API"""
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_gemini_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
temperature=0.7,
|
||||
max_tokens=100,
|
||||
)
|
||||
@@ -496,16 +496,16 @@ def test_new_client_model_parameters(
|
||||
def test_new_client_default_settings(
|
||||
mock_client, mock_google_genai_client, mock_gemini_response
|
||||
):
|
||||
"""Test client with default PostHog settings"""
|
||||
"""Test client with default Insights settings"""
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_gemini_response
|
||||
|
||||
client = Client(
|
||||
api_key="test-key",
|
||||
posthog_client=mock_client,
|
||||
posthog_distinct_id="default_user",
|
||||
posthog_properties={"team": "ai"},
|
||||
posthog_privacy_mode=False,
|
||||
posthog_groups={"company": "acme_corp"},
|
||||
insights_client=mock_client,
|
||||
insights_distinct_id="default_user",
|
||||
insights_properties={"team": "ai"},
|
||||
insights_privacy_mode=False,
|
||||
insights_groups={"company": "acme_corp"},
|
||||
)
|
||||
|
||||
# Call without overriding defaults
|
||||
@@ -527,21 +527,21 @@ def test_new_client_override_defaults(
|
||||
|
||||
client = Client(
|
||||
api_key="test-key",
|
||||
posthog_client=mock_client,
|
||||
posthog_distinct_id="default_user",
|
||||
posthog_properties={"team": "ai"},
|
||||
posthog_privacy_mode=False,
|
||||
posthog_groups={"company": "acme_corp"},
|
||||
insights_client=mock_client,
|
||||
insights_distinct_id="default_user",
|
||||
insights_properties={"team": "ai"},
|
||||
insights_privacy_mode=False,
|
||||
insights_groups={"company": "acme_corp"},
|
||||
)
|
||||
|
||||
# Override defaults in call
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="specific_user",
|
||||
posthog_properties={"feature": "chat", "urgent": True},
|
||||
posthog_privacy_mode=True,
|
||||
posthog_groups={"organization": "special_org"},
|
||||
insights_distinct_id="specific_user",
|
||||
insights_properties={"feature": "chat", "urgent": True},
|
||||
insights_privacy_mode=True,
|
||||
insights_groups={"organization": "special_org"},
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -577,7 +577,7 @@ def test_vertex_ai_parameters_passed_through(
|
||||
location="us-central1",
|
||||
debug_config=mock_debug_config,
|
||||
http_options=mock_http_options,
|
||||
posthog_client=mock_client,
|
||||
insights_client=mock_client,
|
||||
)
|
||||
|
||||
# Verify genai.Client was called with correct parameters
|
||||
@@ -597,7 +597,7 @@ def test_api_key_mode(mock_client, mock_google_genai_client):
|
||||
# Create client with just API key (traditional mode)
|
||||
Client(
|
||||
api_key="test-api-key",
|
||||
posthog_client=mock_client,
|
||||
insights_client=mock_client,
|
||||
)
|
||||
|
||||
# Verify genai.Client was called with only api_key
|
||||
@@ -618,7 +618,7 @@ def test_vertex_ai_mode_with_optional_api_key(
|
||||
api_key="test-api-key",
|
||||
credentials=mock_credentials,
|
||||
project="test-project",
|
||||
posthog_client=mock_client,
|
||||
insights_client=mock_client,
|
||||
)
|
||||
|
||||
# Verify genai.Client was called with both Vertex AI params and API key
|
||||
@@ -634,7 +634,7 @@ def test_tool_use_response(mock_client, mock_google_genai_client, mock_gemini_re
|
||||
"""Test that tools defined in config are captured in $ai_tools property"""
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_gemini_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Create mock tools configuration
|
||||
mock_tool = MagicMock()
|
||||
@@ -664,8 +664,8 @@ def test_tool_use_response(mock_client, mock_google_genai_client, mock_gemini_re
|
||||
model="gemini-2.5-flash",
|
||||
contents=["hey"],
|
||||
config=mock_config,
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_gemini_response
|
||||
@@ -702,12 +702,12 @@ def test_function_calls_in_output_choices(
|
||||
mock_gemini_response_with_function_calls
|
||||
)
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents=["What's the weather in San Francisco?"],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_gemini_response_with_function_calls
|
||||
@@ -751,12 +751,12 @@ def test_function_calls_only_no_content(
|
||||
mock_gemini_response_function_calls_only
|
||||
)
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents=["Get weather for New York"],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_gemini_response_function_calls_only
|
||||
@@ -810,12 +810,12 @@ def test_cache_and_reasoning_tokens(mock_client, mock_google_genai_client):
|
||||
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-pro",
|
||||
contents="Test with cache",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -869,19 +869,19 @@ def test_streaming_cache_and_reasoning_tokens(mock_client, mock_google_genai_cli
|
||||
mock_stream = iter([chunk1, chunk2])
|
||||
mock_google_genai_client.models.generate_content_stream.return_value = mock_stream
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = client.models.generate_content_stream(
|
||||
model="gemini-2.5-pro",
|
||||
contents="Test streaming with cache",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the stream
|
||||
result = list(response)
|
||||
assert len(result) == 2
|
||||
|
||||
# Check PostHog capture was called
|
||||
# Check Insights capture was called
|
||||
assert mock_client.capture.call_count == 1
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -946,11 +946,11 @@ def test_web_search_grounding(mock_client, mock_google_genai_client):
|
||||
# Mock the generate_content method
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents="What's the latest news?",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -1015,12 +1015,12 @@ def test_streaming_with_web_search(mock_client, mock_google_genai_client):
|
||||
mock_streaming_response()
|
||||
)
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = client.models.generate_content_stream(
|
||||
model="gemini-2.5-flash",
|
||||
contents="What's the latest news?",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
chunks = list(response)
|
||||
@@ -1080,12 +1080,12 @@ def test_empty_grounding_metadata_no_web_search(mock_client, mock_google_genai_c
|
||||
# Mock the generate_content method
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents="Hello",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -1143,12 +1143,12 @@ def test_empty_array_grounding_metadata_no_web_search(
|
||||
# Mock the generate_content method
|
||||
mock_google_genai_client.models.generate_content.return_value = mock_response
|
||||
|
||||
client = Client(api_key="test-key", posthog_client=mock_client)
|
||||
client = Client(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents="What can you do?",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
+54
-54
@@ -5,7 +5,7 @@ import pytest
|
||||
try:
|
||||
from google import genai as google_genai
|
||||
|
||||
from posthog.ai.gemini import AsyncClient
|
||||
from hanzo_insights.ai.gemini import AsyncClient
|
||||
|
||||
GEMINI_AVAILABLE = True
|
||||
except ImportError:
|
||||
@@ -21,7 +21,7 @@ pytestmark = [
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
with patch("posthog.client.Client") as mock_client:
|
||||
with patch("hanzo_insights.client.Client") as mock_client:
|
||||
mock_client.privacy_mode = False
|
||||
yield mock_client
|
||||
|
||||
@@ -121,13 +121,13 @@ async def test_async_client_basic_generation(
|
||||
return_value=mock_gemini_response
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = await client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Tell me a fun fact about hedgehogs"],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_gemini_response
|
||||
@@ -178,13 +178,13 @@ async def test_async_client_streaming_with_generate_content_stream(
|
||||
return_value=mock_streaming_response()
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = await client.models.generate_content_stream(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Write a short story"],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"feature": "streaming"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"feature": "streaming"},
|
||||
)
|
||||
|
||||
chunks = []
|
||||
@@ -239,7 +239,7 @@ async def test_async_client_streaming_with_tools(mock_client, mock_google_genai_
|
||||
return_value=mock_streaming_response()
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Create mock tools configuration
|
||||
mock_tool = MagicMock()
|
||||
@@ -267,8 +267,8 @@ async def test_async_client_streaming_with_tools(mock_client, mock_google_genai_
|
||||
model="gemini-2.0-flash",
|
||||
contents=["What's the weather in SF?"],
|
||||
config=mock_config,
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"feature": "streaming_with_tools"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"feature": "streaming_with_tools"},
|
||||
)
|
||||
|
||||
chunks = []
|
||||
@@ -305,13 +305,13 @@ async def test_async_client_groups(
|
||||
return_value=mock_gemini_response
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
await client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_groups={"company": "company_123"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_groups={"company": "company_123"},
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -326,13 +326,13 @@ async def test_async_client_privacy_mode_local(
|
||||
return_value=mock_gemini_response
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
await client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_privacy_mode=True,
|
||||
insights_distinct_id="test-id",
|
||||
insights_privacy_mode=True,
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -351,12 +351,12 @@ async def test_async_client_privacy_mode_global(
|
||||
return_value=mock_gemini_response
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
await client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -373,11 +373,11 @@ async def test_async_client_different_input_formats(
|
||||
return_value=mock_gemini_response
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Test string input
|
||||
await client.models.generate_content(
|
||||
model="gemini-2.0-flash", contents="Hello", posthog_distinct_id="test-id"
|
||||
model="gemini-2.0-flash", contents="Hello", insights_distinct_id="test-id"
|
||||
)
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
@@ -388,7 +388,7 @@ async def test_async_client_different_input_formats(
|
||||
await client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=[{"role": "user", "parts": [{"text": "hey"}]}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
@@ -401,7 +401,7 @@ async def test_async_client_different_input_formats(
|
||||
await client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=[{"role": "user", "parts": [{"text": "Hello "}, {"text": "world"}]}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
@@ -418,7 +418,7 @@ async def test_async_client_different_input_formats(
|
||||
# Test list input with string
|
||||
mock_client.capture.reset_mock()
|
||||
await client.models.generate_content(
|
||||
model="gemini-2.0-flash", contents=["List item"], posthog_distinct_id="test-id"
|
||||
model="gemini-2.0-flash", contents=["List item"], insights_distinct_id="test-id"
|
||||
)
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
props = call_args["properties"]
|
||||
@@ -433,12 +433,12 @@ async def test_async_client_model_parameters(
|
||||
return_value=mock_gemini_response
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
await client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
temperature=0.7,
|
||||
max_tokens=100,
|
||||
)
|
||||
@@ -452,18 +452,18 @@ async def test_async_client_model_parameters(
|
||||
async def test_async_client_default_settings(
|
||||
mock_client, mock_google_genai_client, mock_gemini_response
|
||||
):
|
||||
"""Test async client with default PostHog settings"""
|
||||
"""Test async client with default Insights settings"""
|
||||
mock_google_genai_client.aio.models.generate_content = AsyncMock(
|
||||
return_value=mock_gemini_response
|
||||
)
|
||||
|
||||
client = AsyncClient(
|
||||
api_key="test-key",
|
||||
posthog_client=mock_client,
|
||||
posthog_distinct_id="default_user",
|
||||
posthog_properties={"team": "ai"},
|
||||
posthog_privacy_mode=False,
|
||||
posthog_groups={"company": "acme_corp"},
|
||||
insights_client=mock_client,
|
||||
insights_distinct_id="default_user",
|
||||
insights_properties={"team": "ai"},
|
||||
insights_privacy_mode=False,
|
||||
insights_groups={"company": "acme_corp"},
|
||||
)
|
||||
|
||||
# Call without overriding defaults
|
||||
@@ -487,21 +487,21 @@ async def test_async_client_override_defaults(
|
||||
|
||||
client = AsyncClient(
|
||||
api_key="test-key",
|
||||
posthog_client=mock_client,
|
||||
posthog_distinct_id="default_user",
|
||||
posthog_properties={"team": "ai"},
|
||||
posthog_privacy_mode=False,
|
||||
posthog_groups={"company": "acme_corp"},
|
||||
insights_client=mock_client,
|
||||
insights_distinct_id="default_user",
|
||||
insights_properties={"team": "ai"},
|
||||
insights_privacy_mode=False,
|
||||
insights_groups={"company": "acme_corp"},
|
||||
)
|
||||
|
||||
# Override defaults in call
|
||||
await client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=["Hello"],
|
||||
posthog_distinct_id="specific_user",
|
||||
posthog_properties={"feature": "chat", "urgent": True},
|
||||
posthog_privacy_mode=True,
|
||||
posthog_groups={"organization": "special_org"},
|
||||
insights_distinct_id="specific_user",
|
||||
insights_properties={"feature": "chat", "urgent": True},
|
||||
insights_privacy_mode=True,
|
||||
insights_groups={"organization": "special_org"},
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -539,7 +539,7 @@ async def test_async_vertex_ai_parameters_passed_through(
|
||||
location="us-central1",
|
||||
debug_config=mock_debug_config,
|
||||
http_options=mock_http_options,
|
||||
posthog_client=mock_client,
|
||||
insights_client=mock_client,
|
||||
)
|
||||
|
||||
# Verify genai.Client was called with correct parameters
|
||||
@@ -559,7 +559,7 @@ async def test_async_api_key_mode(mock_client, mock_google_genai_client):
|
||||
# Create async client with just API key (traditional mode)
|
||||
AsyncClient(
|
||||
api_key="test-api-key",
|
||||
posthog_client=mock_client,
|
||||
insights_client=mock_client,
|
||||
)
|
||||
|
||||
# Verify genai.Client was called with only api_key
|
||||
@@ -574,12 +574,12 @@ async def test_async_function_calls_in_output_choices(
|
||||
return_value=mock_gemini_response_with_function_calls
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = await client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents=["What's the weather in San Francisco?"],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_gemini_response_with_function_calls
|
||||
@@ -637,12 +637,12 @@ async def test_async_cache_and_reasoning_tokens(mock_client, mock_google_genai_c
|
||||
return_value=mock_response
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = await client.models.generate_content(
|
||||
model="gemini-2.5-pro",
|
||||
contents="Test with cache",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -689,12 +689,12 @@ async def test_async_streaming_cache_and_reasoning_tokens(
|
||||
return_value=mock_streaming_response()
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = await client.models.generate_content_stream(
|
||||
model="gemini-2.5-pro",
|
||||
contents="Test streaming with cache",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the stream
|
||||
@@ -704,7 +704,7 @@ async def test_async_streaming_cache_and_reasoning_tokens(
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
# Check PostHog capture was called
|
||||
# Check Insights capture was called
|
||||
assert mock_client.capture.call_count == 1
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -761,11 +761,11 @@ async def test_async_web_search_grounding(mock_client, mock_google_genai_client)
|
||||
return_value=mock_response
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
response = await client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents="What's the latest news?",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -829,12 +829,12 @@ async def test_async_streaming_with_web_search(mock_client, mock_google_genai_cl
|
||||
return_value=mock_streaming_response()
|
||||
)
|
||||
|
||||
client = AsyncClient(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncClient(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = await client.models.generate_content_stream(
|
||||
model="gemini-2.5-flash",
|
||||
contents="What's the latest news?",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
chunks = []
|
||||
+28
-28
@@ -21,8 +21,8 @@ try:
|
||||
from langgraph.graph.state import END, START, StateGraph
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
from posthog.ai.langchain import CallbackHandler
|
||||
from posthog.ai.langchain.callbacks import GenerationMetadata, SpanMetadata
|
||||
from hanzo_insights.ai.langchain import CallbackHandler
|
||||
from hanzo_insights.ai.langchain.callbacks import GenerationMetadata, SpanMetadata
|
||||
|
||||
LANGCHAIN_AVAILABLE = True
|
||||
except ImportError:
|
||||
@@ -53,9 +53,9 @@ ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_client():
|
||||
with patch("posthog.client.Client") as mock_client:
|
||||
with patch("hanzo_insights.client.Client") as mock_client:
|
||||
mock_client.privacy_mode = False
|
||||
logging.getLogger("posthog").setLevel(logging.DEBUG)
|
||||
logging.getLogger("hanzo_insights").setLevel(logging.DEBUG)
|
||||
yield mock_client
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ def test_metadata_capture(mock_client):
|
||||
run_id,
|
||||
messages=[{"role": "user", "content": "Who won the world series in 2020?"}],
|
||||
invocation_params={"temperature": 0.5},
|
||||
metadata={"ls_model_name": "hog-mini", "ls_provider": "posthog"},
|
||||
metadata={"ls_model_name": "hog-mini", "ls_provider": "hanzo_insights"},
|
||||
name="test",
|
||||
)
|
||||
expected = GenerationMetadata(
|
||||
@@ -109,11 +109,11 @@ def test_metadata_capture(mock_client):
|
||||
input=[{"role": "user", "content": "Who won the world series in 2020?"}],
|
||||
start_time=1234567890,
|
||||
model_params={"temperature": 0.5},
|
||||
provider="posthog",
|
||||
provider="hanzo_insights",
|
||||
base_url="https://us.posthog.com",
|
||||
name="test",
|
||||
end_time=None,
|
||||
posthog_properties=None,
|
||||
insights_properties=None,
|
||||
)
|
||||
assert callbacks._runs[run_id] == expected
|
||||
with patch("time.time", return_value=1234567891):
|
||||
@@ -1049,7 +1049,7 @@ def test_base_url_retrieval(mock_client):
|
||||
prompt = ChatPromptTemplate.from_messages([("user", "Foo")])
|
||||
chain = prompt | ChatOpenAI(
|
||||
api_key="test",
|
||||
model="posthog-mini",
|
||||
model="insights-mini",
|
||||
base_url="https://test.posthog.com",
|
||||
)
|
||||
callbacks = CallbackHandler(mock_client)
|
||||
@@ -1257,7 +1257,7 @@ def test_metadata_tools(mock_client):
|
||||
run_id,
|
||||
messages=[{"role": "user", "content": "What's the weather like in SF?"}],
|
||||
invocation_params={"temperature": 0.5, "tools": tools},
|
||||
metadata={"ls_model_name": "hog-mini", "ls_provider": "posthog"},
|
||||
metadata={"ls_model_name": "hog-mini", "ls_provider": "hanzo_insights"},
|
||||
name="test",
|
||||
)
|
||||
expected = GenerationMetadata(
|
||||
@@ -1265,12 +1265,12 @@ def test_metadata_tools(mock_client):
|
||||
input=[{"role": "user", "content": "What's the weather like in SF?"}],
|
||||
start_time=1234567890,
|
||||
model_params={"temperature": 0.5},
|
||||
provider="posthog",
|
||||
provider="hanzo_insights",
|
||||
base_url="https://us.posthog.com",
|
||||
name="test",
|
||||
tools=tools,
|
||||
end_time=None,
|
||||
posthog_properties=None,
|
||||
insights_properties=None,
|
||||
)
|
||||
assert callbacks._runs[run_id] == expected
|
||||
with patch("time.time", return_value=1234567891):
|
||||
@@ -1867,8 +1867,8 @@ def test_openai_reasoning_tokens_o4_mini(mock_client):
|
||||
|
||||
|
||||
def test_callback_handler_without_client():
|
||||
"""Test that CallbackHandler works properly when no PostHog client is passed."""
|
||||
with patch("posthog.ai.langchain.callbacks.setup") as mock_setup:
|
||||
"""Test that CallbackHandler works properly when no Insights client is passed."""
|
||||
with patch("hanzo_insights.ai.langchain.callbacks.setup") as mock_setup:
|
||||
mock_client = mock_setup.return_value
|
||||
|
||||
callbacks = CallbackHandler()
|
||||
@@ -1894,7 +1894,7 @@ def test_callback_handler_without_client():
|
||||
|
||||
def test_convert_message_to_dict_tool_calls():
|
||||
"""Test that _convert_message_to_dict properly converts tool calls in AIMessage."""
|
||||
from posthog.ai.langchain.callbacks import _convert_message_to_dict
|
||||
from hanzo_insights.ai.langchain.callbacks import _convert_message_to_dict
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.messages.tool import ToolCall
|
||||
|
||||
@@ -1984,7 +1984,7 @@ def test_tool_definition(mock_client):
|
||||
assert run == expected
|
||||
assert callbacks._runs == {}
|
||||
|
||||
# Now test that the tools are properly captured in the PostHog event
|
||||
# Now test that the tools are properly captured in the Insights event
|
||||
mock_response = MagicMock()
|
||||
mock_response.generations = [[MagicMock()]]
|
||||
|
||||
@@ -2236,7 +2236,7 @@ def test_agent_action_and_finish_imports():
|
||||
from langchain.schema.agent import AgentAction, AgentFinish # type: ignore
|
||||
|
||||
# Verify they're available in the callbacks module
|
||||
from posthog.ai.langchain.callbacks import CallbackHandler
|
||||
from hanzo_insights.ai.langchain.callbacks import CallbackHandler
|
||||
|
||||
# Test on_agent_action with mock data
|
||||
mock_client = MagicMock()
|
||||
@@ -2266,8 +2266,8 @@ def test_agent_action_and_finish_imports():
|
||||
assert call_args["event"] == "$ai_span"
|
||||
|
||||
|
||||
def test_posthog_properties_field_in_generation_metadata(mock_client):
|
||||
"""Test that posthog_properties is properly stored in GenerationMetadata."""
|
||||
def test_insights_properties_field_in_generation_metadata(mock_client):
|
||||
"""Test that insights_properties is properly stored in GenerationMetadata."""
|
||||
callbacks = CallbackHandler(mock_client)
|
||||
run_id = uuid.uuid4()
|
||||
|
||||
@@ -2281,7 +2281,7 @@ def test_posthog_properties_field_in_generation_metadata(mock_client):
|
||||
metadata={
|
||||
"ls_model_name": "gpt-4o",
|
||||
"ls_provider": "openai",
|
||||
"posthog_properties": {"$ai_billable": True},
|
||||
"insights_properties": {"$ai_billable": True},
|
||||
},
|
||||
name="test",
|
||||
)
|
||||
@@ -2294,11 +2294,11 @@ def test_posthog_properties_field_in_generation_metadata(mock_client):
|
||||
provider="openai",
|
||||
base_url="https://api.openai.com",
|
||||
name="test",
|
||||
posthog_properties={"$ai_billable": True},
|
||||
insights_properties={"$ai_billable": True},
|
||||
end_time=None,
|
||||
)
|
||||
assert callbacks._runs[run_id] == expected
|
||||
assert callbacks._runs[run_id].posthog_properties == {"$ai_billable": True}
|
||||
assert callbacks._runs[run_id].insights_properties == {"$ai_billable": True}
|
||||
|
||||
callbacks._pop_run_metadata(run_id)
|
||||
|
||||
@@ -2313,15 +2313,15 @@ def test_posthog_properties_field_in_generation_metadata(mock_client):
|
||||
metadata={
|
||||
"ls_model_name": "gpt-4o",
|
||||
"ls_provider": "openai",
|
||||
"posthog_properties": {"$ai_billable": False},
|
||||
"insights_properties": {"$ai_billable": False},
|
||||
},
|
||||
name="test",
|
||||
)
|
||||
|
||||
assert callbacks._runs[run_id2].posthog_properties == {"$ai_billable": False}
|
||||
assert callbacks._runs[run_id2].insights_properties == {"$ai_billable": False}
|
||||
callbacks._pop_run_metadata(run_id2)
|
||||
|
||||
# Test when posthog_properties not provided
|
||||
# Test when insights_properties not provided
|
||||
run_id3 = uuid.uuid4()
|
||||
with patch("time.time", return_value=1234567890):
|
||||
callbacks._set_llm_metadata(
|
||||
@@ -2333,7 +2333,7 @@ def test_posthog_properties_field_in_generation_metadata(mock_client):
|
||||
name="test",
|
||||
)
|
||||
|
||||
assert callbacks._runs[run_id3].posthog_properties is None
|
||||
assert callbacks._runs[run_id3].insights_properties is None
|
||||
|
||||
|
||||
def test_billable_property_in_generation_event(mock_client):
|
||||
@@ -2349,7 +2349,7 @@ def test_billable_property_in_generation_event(mock_client):
|
||||
run_id,
|
||||
messages=[{"role": "user", "content": "Test"}],
|
||||
metadata={
|
||||
"posthog_properties": {"$ai_billable": True},
|
||||
"insights_properties": {"$ai_billable": True},
|
||||
"ls_model_name": "test-model",
|
||||
},
|
||||
invocation_params={},
|
||||
@@ -2412,12 +2412,12 @@ def test_billable_with_real_chain(mock_client):
|
||||
metadata={
|
||||
"ls_model_name": "fake-model",
|
||||
"ls_provider": "fake",
|
||||
"posthog_properties": {"$ai_billable": True},
|
||||
"insights_properties": {"$ai_billable": True},
|
||||
},
|
||||
invocation_params={"temperature": 0.7},
|
||||
)
|
||||
|
||||
assert callbacks._runs[run_id].posthog_properties == {"$ai_billable": True}
|
||||
assert callbacks._runs[run_id].insights_properties == {"$ai_billable": True}
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.generations = [[MagicMock()]]
|
||||
+84
-84
@@ -34,8 +34,8 @@ try:
|
||||
ParsedResponseOutputText,
|
||||
)
|
||||
|
||||
from posthog.ai.openai import OpenAI
|
||||
from posthog.ai.openai.openai_async import AsyncOpenAI
|
||||
from hanzo_insights.ai.openai import OpenAI
|
||||
from hanzo_insights.ai.openai.openai_async import AsyncOpenAI
|
||||
|
||||
OPENAI_AVAILABLE = True
|
||||
except ImportError:
|
||||
@@ -49,7 +49,7 @@ pytestmark = pytest.mark.skipif(
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
with patch("posthog.client.Client") as mock_client:
|
||||
with patch("hanzo_insights.client.Client") as mock_client:
|
||||
mock_client.privacy_mode = False
|
||||
yield mock_client
|
||||
|
||||
@@ -467,12 +467,12 @@ def test_basic_completion(mock_client, mock_openai_response):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_openai_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_openai_response
|
||||
@@ -513,12 +513,12 @@ def test_embeddings(mock_client, mock_embedding_response):
|
||||
"openai.resources.embeddings.Embeddings.create",
|
||||
return_value=mock_embedding_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.embeddings.create(
|
||||
model="text-embedding-3-small",
|
||||
input="Hello world",
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_embedding_response
|
||||
@@ -543,12 +543,12 @@ def test_groups(mock_client, mock_openai_response):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_openai_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_groups={"company": "test_company"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_groups={"company": "test_company"},
|
||||
)
|
||||
|
||||
assert response == mock_openai_response
|
||||
@@ -564,12 +564,12 @@ def test_privacy_mode_local(mock_client, mock_openai_response):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_openai_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_privacy_mode=True,
|
||||
insights_distinct_id="test-id",
|
||||
insights_privacy_mode=True,
|
||||
)
|
||||
|
||||
assert response == mock_openai_response
|
||||
@@ -587,12 +587,12 @@ def test_privacy_mode_global(mock_client, mock_openai_response):
|
||||
return_value=mock_openai_response,
|
||||
):
|
||||
mock_client.privacy_mode = True
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_privacy_mode=False,
|
||||
insights_distinct_id="test-id",
|
||||
insights_privacy_mode=False,
|
||||
)
|
||||
|
||||
assert response == mock_openai_response
|
||||
@@ -609,7 +609,7 @@ def test_error(mock_client, mock_openai_response):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
side_effect=Exception("Test error"),
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
with pytest.raises(Exception):
|
||||
client.chat.completions.create(
|
||||
model="gpt-4", messages=[{"role": "user", "content": "Hello"}]
|
||||
@@ -628,12 +628,12 @@ def test_cached_tokens(mock_client, mock_openai_response_with_cached_tokens):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_openai_response_with_cached_tokens,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_openai_response_with_cached_tokens
|
||||
@@ -666,7 +666,7 @@ def test_tool_calls(mock_client, mock_openai_response_with_tool_calls):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_openai_response_with_tool_calls,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[
|
||||
@@ -682,7 +682,7 @@ def test_tool_calls(mock_client, mock_openai_response_with_tool_calls):
|
||||
},
|
||||
}
|
||||
],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_openai_response_with_tool_calls
|
||||
@@ -739,7 +739,7 @@ def test_tool_calls_only_no_content(mock_client, mock_openai_response_tool_calls
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_openai_response_tool_calls_only,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Get weather for New York"}],
|
||||
@@ -753,7 +753,7 @@ def test_tool_calls_only_no_content(mock_client, mock_openai_response_tool_calls
|
||||
},
|
||||
}
|
||||
],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_openai_response_tool_calls_only
|
||||
@@ -793,7 +793,7 @@ def test_responses_api_tool_calls(mock_client, mock_responses_api_with_tool_call
|
||||
"openai.resources.responses.Responses.create",
|
||||
return_value=mock_responses_api_with_tool_calls,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.responses.create(
|
||||
model="gpt-4o-mini",
|
||||
input=[{"role": "user", "content": "What's the weather in Chicago?"}],
|
||||
@@ -808,7 +808,7 @@ def test_responses_api_tool_calls(mock_client, mock_responses_api_with_tool_call
|
||||
},
|
||||
}
|
||||
],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_responses_api_with_tool_calls
|
||||
@@ -851,7 +851,7 @@ def test_streaming_with_tool_calls(mock_client, streaming_tool_call_chunks):
|
||||
# Set up the mock to return our chunks when iterated
|
||||
mock_create.return_value = streaming_tool_call_chunks
|
||||
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Call the streaming method
|
||||
response_generator = client.chat.completions.create(
|
||||
@@ -870,7 +870,7 @@ def test_streaming_with_tool_calls(mock_client, streaming_tool_call_chunks):
|
||||
}
|
||||
],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the generator to trigger the event capture
|
||||
@@ -949,12 +949,12 @@ def test_responses_api(mock_client, mock_openai_response_with_responses_api):
|
||||
"openai.resources.responses.Responses.create",
|
||||
return_value=mock_openai_response_with_responses_api,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.responses.create(
|
||||
model="gpt-4o-mini",
|
||||
input="Hello",
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
assert response == mock_openai_response_with_responses_api
|
||||
assert mock_client.capture.call_count == 1
|
||||
@@ -986,7 +986,7 @@ def test_responses_parse(mock_client, mock_parsed_response):
|
||||
"openai.resources.responses.Responses.parse",
|
||||
return_value=mock_parsed_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.responses.parse(
|
||||
model="gpt-4o-2024-08-06",
|
||||
input=[
|
||||
@@ -1016,8 +1016,8 @@ def test_responses_parse(mock_client, mock_parsed_response):
|
||||
},
|
||||
}
|
||||
},
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_parsed_response
|
||||
@@ -1101,15 +1101,15 @@ def test_responses_api_streaming_with_tokens(mock_client):
|
||||
"openai.resources.responses.Responses.create",
|
||||
side_effect=mock_streaming_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Consume the streaming response
|
||||
response = client.responses.create(
|
||||
model="gpt-4o-mini",
|
||||
input=[{"role": "user", "content": "Test message"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"test": "streaming"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"test": "streaming"},
|
||||
)
|
||||
|
||||
# Consume all chunks
|
||||
@@ -1153,7 +1153,7 @@ async def test_async_chat_streaming_with_tool_calls(
|
||||
with patch(
|
||||
"openai.resources.chat.completions.AsyncCompletions.create", new=mock_create
|
||||
):
|
||||
client = AsyncOpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncOpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response_stream = await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
@@ -1171,7 +1171,7 @@ async def test_async_chat_streaming_with_tool_calls(
|
||||
}
|
||||
],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
chunks = []
|
||||
@@ -1239,14 +1239,14 @@ async def test_async_responses_streaming_with_tokens(mock_client):
|
||||
return chunk_iterable()
|
||||
|
||||
with patch("openai.resources.responses.AsyncResponses.create", new=mock_create):
|
||||
client = AsyncOpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncOpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response_stream = await client.responses.create(
|
||||
model="gpt-4o-mini",
|
||||
input=[{"role": "user", "content": "Test message"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"test": "streaming"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"test": "streaming"},
|
||||
)
|
||||
|
||||
async for _ in response_stream:
|
||||
@@ -1274,13 +1274,13 @@ async def test_async_embeddings_create(mock_client, mock_embedding_response):
|
||||
mock_create = AsyncMock(return_value=mock_embedding_response)
|
||||
|
||||
with patch("openai.resources.embeddings.AsyncEmbeddings.create", new=mock_create):
|
||||
client = AsyncOpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncOpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = await client.embeddings.create(
|
||||
model="text-embedding-3-small",
|
||||
input="Hello world",
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_embedding_response
|
||||
@@ -1304,7 +1304,7 @@ def test_tool_definition(mock_client, mock_openai_response):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_openai_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Define tools to be passed to the create function
|
||||
tools = [
|
||||
@@ -1331,8 +1331,8 @@ def test_tool_definition(mock_client, mock_openai_response):
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": "hey"}],
|
||||
tools=tools,
|
||||
posthog_distinct_id="test-id",
|
||||
posthog_properties={"foo": "bar"},
|
||||
insights_distinct_id="test-id",
|
||||
insights_properties={"foo": "bar"},
|
||||
)
|
||||
|
||||
assert response == mock_openai_response
|
||||
@@ -1392,11 +1392,11 @@ def test_web_search_perplexity_style(mock_client):
|
||||
mock_response = MockResponseWithAnnotations()
|
||||
|
||||
with patch("openai.resources.chat.Completions.create", return_value=mock_response):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4-turbo",
|
||||
messages=[{"role": "user", "content": "What's happening in tech?"}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -1439,19 +1439,19 @@ def test_web_search_responses_api(mock_client):
|
||||
"openai.resources.responses.Responses.create", return_value=mock_response
|
||||
):
|
||||
# Manually call the tracking since we're testing the converter logic
|
||||
from posthog.ai.utils import call_llm_and_track_usage
|
||||
from hanzo_insights.ai.utils import call_llm_and_track_usage
|
||||
|
||||
def mock_create_call(**kwargs):
|
||||
return mock_response
|
||||
|
||||
result = call_llm_and_track_usage(
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
ph_client=mock_client,
|
||||
provider="openai",
|
||||
posthog_trace_id=None,
|
||||
posthog_properties=None,
|
||||
posthog_privacy_mode=False,
|
||||
posthog_groups=None,
|
||||
insights_trace_id=None,
|
||||
insights_properties=None,
|
||||
insights_privacy_mode=False,
|
||||
insights_groups=None,
|
||||
base_url="https://api.openai.com/v1",
|
||||
call_method=mock_create_call,
|
||||
model="gpt-4o",
|
||||
@@ -1533,12 +1533,12 @@ def test_streaming_with_web_search(mock_client, streaming_web_search_chunks):
|
||||
with patch("openai.resources.chat.completions.Completions.create") as mock_create:
|
||||
mock_create.return_value = streaming_web_search_chunks
|
||||
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response_generator = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Search for recent news"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the generator to trigger the event capture
|
||||
@@ -1569,12 +1569,12 @@ def test_streaming_with_web_search_on_non_usage_chunk(
|
||||
with patch("openai.resources.chat.completions.Completions.create") as mock_create:
|
||||
mock_create.return_value = streaming_web_search_chunks
|
||||
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
response_generator = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Search for recent news"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the generator to trigger the event capture
|
||||
@@ -1629,12 +1629,12 @@ async def test_async_chat_with_web_search(mock_client):
|
||||
with patch(
|
||||
"openai.resources.chat.completions.AsyncCompletions.create", new=mock_create
|
||||
):
|
||||
client = AsyncOpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncOpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Search for recent news"}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -1672,13 +1672,13 @@ async def test_async_chat_streaming_with_web_search(
|
||||
with patch(
|
||||
"openai.resources.chat.completions.AsyncCompletions.create", new=mock_create
|
||||
):
|
||||
client = AsyncOpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncOpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response_stream = await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Search for recent news"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
chunks = []
|
||||
@@ -1742,13 +1742,13 @@ def test_streaming_chat_extracts_model_from_chunk_when_not_in_kwargs(mock_client
|
||||
with patch("openai.resources.chat.completions.Completions.create") as mock_create:
|
||||
mock_create.return_value = chunks
|
||||
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Note: NOT passing model in kwargs - simulates stored prompt usage
|
||||
response_generator = client.chat.completions.create(
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
# Consume the generator
|
||||
@@ -1788,13 +1788,13 @@ def test_streaming_chat_prefers_kwargs_model_over_chunk_model(mock_client):
|
||||
with patch("openai.resources.chat.completions.Completions.create") as mock_create:
|
||||
mock_create.return_value = chunks
|
||||
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response_generator = client.chat.completions.create(
|
||||
model="gpt-4o-from-kwargs", # Explicitly passed model
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
list(response_generator)
|
||||
@@ -1839,13 +1839,13 @@ def test_streaming_responses_api_extracts_model_from_response_object(mock_client
|
||||
with patch("openai.resources.responses.Responses.create") as mock_create:
|
||||
mock_create.return_value = iter(chunks)
|
||||
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Note: NOT passing model - simulates stored prompt
|
||||
response_generator = client.responses.create(
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
list(response_generator)
|
||||
@@ -1886,12 +1886,12 @@ def test_non_streaming_extracts_model_from_response(mock_client):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Note: NOT passing model in kwargs
|
||||
response = client.chat.completions.create(
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -1948,12 +1948,12 @@ def test_non_streaming_responses_api_extracts_model_from_response(mock_client):
|
||||
"openai.resources.responses.Responses.create",
|
||||
return_value=mock_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Note: NOT passing model in kwargs
|
||||
response = client.responses.create(
|
||||
input="Hello",
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
assert response == mock_response
|
||||
@@ -1995,12 +1995,12 @@ def test_non_streaming_returns_none_when_no_model(mock_client):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_response,
|
||||
):
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Note: NOT passing model in kwargs and response has no model
|
||||
client.chat.completions.create(
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
call_args = mock_client.capture.call_args[1]
|
||||
@@ -2032,12 +2032,12 @@ def test_streaming_falls_back_to_unknown_when_no_model(mock_client):
|
||||
with patch("openai.resources.chat.completions.Completions.create") as mock_create:
|
||||
mock_create.return_value = [chunk]
|
||||
|
||||
client = OpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = OpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response_generator = client.chat.completions.create(
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
list(response_generator)
|
||||
@@ -2083,13 +2083,13 @@ async def test_async_streaming_chat_extracts_model_from_chunk(mock_client):
|
||||
with patch(
|
||||
"openai.resources.chat.completions.AsyncCompletions.create", new=mock_create
|
||||
):
|
||||
client = AsyncOpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncOpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
# Note: NOT passing model
|
||||
response_stream = await client.chat.completions.create(
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
async for _ in response_stream:
|
||||
@@ -2137,12 +2137,12 @@ async def test_async_streaming_responses_extracts_model_from_response(mock_clien
|
||||
return chunk_iterable()
|
||||
|
||||
with patch("openai.resources.responses.AsyncResponses.create", new=mock_create):
|
||||
client = AsyncOpenAI(api_key="test-key", posthog_client=mock_client)
|
||||
client = AsyncOpenAI(api_key="test-key", insights_client=mock_client)
|
||||
|
||||
response_stream = await client.responses.create(
|
||||
input=[{"role": "user", "content": "Hello"}],
|
||||
stream=True,
|
||||
posthog_distinct_id="test-id",
|
||||
insights_distinct_id="test-id",
|
||||
)
|
||||
|
||||
async for _ in response_stream:
|
||||
+17
-17
@@ -16,7 +16,7 @@ try:
|
||||
TranscriptionSpanData,
|
||||
)
|
||||
|
||||
from posthog.ai.openai_agents import PostHogTracingProcessor, instrument
|
||||
from hanzo_insights.ai.openai_agents import InsightsTracingProcessor, instrument
|
||||
|
||||
OPENAI_AGENTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
@@ -33,13 +33,13 @@ pytestmark = pytest.mark.skipif(
|
||||
def mock_client():
|
||||
client = MagicMock()
|
||||
client.privacy_mode = False
|
||||
logging.getLogger("posthog").setLevel(logging.DEBUG)
|
||||
logging.getLogger("hanzo_insights").setLevel(logging.DEBUG)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def processor(mock_client):
|
||||
return PostHogTracingProcessor(
|
||||
return InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id="test-user",
|
||||
privacy_mode=False,
|
||||
@@ -68,12 +68,12 @@ def mock_span():
|
||||
return span
|
||||
|
||||
|
||||
class TestPostHogTracingProcessor:
|
||||
"""Tests for the PostHogTracingProcessor class."""
|
||||
class TestInsightsTracingProcessor:
|
||||
"""Tests for the InsightsTracingProcessor class."""
|
||||
|
||||
def test_initialization(self, mock_client):
|
||||
"""Test processor initializes correctly."""
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id="user@example.com",
|
||||
privacy_mode=True,
|
||||
@@ -93,7 +93,7 @@ class TestPostHogTracingProcessor:
|
||||
def resolver(trace):
|
||||
return trace.metadata.get("user_id", "default")
|
||||
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id=resolver,
|
||||
)
|
||||
@@ -127,7 +127,7 @@ class TestPostHogTracingProcessor:
|
||||
|
||||
def test_personless_mode_when_no_distinct_id(self, mock_client, mock_trace):
|
||||
"""Test that trace events use personless mode when no distinct_id is provided."""
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
)
|
||||
|
||||
@@ -143,7 +143,7 @@ class TestPostHogTracingProcessor:
|
||||
self, mock_client, mock_trace, mock_span
|
||||
):
|
||||
"""Test that span events use personless mode when no distinct_id is provided."""
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
)
|
||||
|
||||
@@ -168,7 +168,7 @@ class TestPostHogTracingProcessor:
|
||||
def resolver(trace):
|
||||
return None # Simulate no user ID available
|
||||
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id=resolver,
|
||||
)
|
||||
@@ -188,7 +188,7 @@ class TestPostHogTracingProcessor:
|
||||
|
||||
def test_person_profile_when_distinct_id_provided(self, mock_client, mock_trace):
|
||||
"""Test that events create person profiles when distinct_id is provided."""
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id="real-user",
|
||||
)
|
||||
@@ -375,7 +375,7 @@ class TestPostHogTracingProcessor:
|
||||
|
||||
def test_privacy_mode_redacts_content(self, mock_client, mock_span):
|
||||
"""Test that privacy_mode redacts input/output content."""
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id="test-user",
|
||||
privacy_mode=True,
|
||||
@@ -636,7 +636,7 @@ class TestPostHogTracingProcessor:
|
||||
|
||||
def test_groups_included_in_events(self, mock_client, mock_trace, mock_span):
|
||||
"""Test that groups are included in captured events."""
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id="test-user",
|
||||
groups={"company": "acme", "team": "engineering"},
|
||||
@@ -650,7 +650,7 @@ class TestPostHogTracingProcessor:
|
||||
|
||||
def test_additional_properties_included(self, mock_client, mock_trace):
|
||||
"""Test that additional properties are included in events."""
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id="test-user",
|
||||
properties={"environment": "production", "version": "1.0"},
|
||||
@@ -734,7 +734,7 @@ class TestPostHogTracingProcessor:
|
||||
def resolver(trace):
|
||||
return f"user-{trace.name}"
|
||||
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id=resolver,
|
||||
)
|
||||
@@ -755,7 +755,7 @@ class TestPostHogTracingProcessor:
|
||||
|
||||
def test_eviction_of_stale_entries(self, mock_client):
|
||||
"""Test that stale entries are evicted when max is exceeded."""
|
||||
processor = PostHogTracingProcessor(
|
||||
processor = InsightsTracingProcessor(
|
||||
client=mock_client,
|
||||
distinct_id="test-user",
|
||||
)
|
||||
@@ -785,7 +785,7 @@ class TestInstrumentHelper:
|
||||
)
|
||||
|
||||
mock_add.assert_called_once_with(processor)
|
||||
assert isinstance(processor, PostHogTracingProcessor)
|
||||
assert isinstance(processor, InsightsTracingProcessor)
|
||||
|
||||
def test_instrument_with_privacy_mode(self, mock_client):
|
||||
"""Test instrument() respects privacy_mode."""
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from posthog.ai.prompts import Prompts
|
||||
from hanzo_insights.ai.prompts import Prompts
|
||||
|
||||
|
||||
class MockResponse:
|
||||
@@ -32,13 +32,13 @@ class TestPrompts(unittest.TestCase):
|
||||
"deleted": False,
|
||||
}
|
||||
|
||||
def create_mock_posthog(
|
||||
def create_mock_client(
|
||||
self,
|
||||
personal_api_key="phx_test_key",
|
||||
project_api_key="phc_test_key",
|
||||
host="https://us.posthog.com",
|
||||
host="https://us.insights.hanzo.ai",
|
||||
):
|
||||
"""Create a mock PostHog client."""
|
||||
"""Create a mock Insights client."""
|
||||
mock = MagicMock()
|
||||
mock.personal_api_key = personal_api_key
|
||||
mock.api_key = project_api_key
|
||||
@@ -49,14 +49,14 @@ class TestPrompts(unittest.TestCase):
|
||||
class TestPromptsGet(TestPrompts):
|
||||
"""Tests for the Prompts.get() method."""
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_successfully_fetch_a_prompt(self, mock_get_session):
|
||||
"""Should successfully fetch a prompt."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.get("test-prompt")
|
||||
|
||||
@@ -65,23 +65,47 @@ class TestPromptsGet(TestPrompts):
|
||||
call_args = mock_get.call_args
|
||||
self.assertEqual(
|
||||
call_args[0][0],
|
||||
"https://us.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key",
|
||||
"https://us.insights.hanzo.ai/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key",
|
||||
)
|
||||
self.assertIn("Authorization", call_args[1]["headers"])
|
||||
self.assertEqual(
|
||||
call_args[1]["headers"]["Authorization"], "Bearer phx_test_key"
|
||||
)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("posthog.ai.prompts.time.time")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_successfully_fetch_a_specific_prompt_version(self, mock_get_session):
|
||||
"""Should successfully fetch a specific prompt version."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
versioned_prompt_response = {
|
||||
**self.mock_prompt_response,
|
||||
"prompt": "Prompt version 1",
|
||||
"version": 1,
|
||||
}
|
||||
mock_get.return_value = MockResponse(json_data=versioned_prompt_response)
|
||||
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.get("test-prompt", version=1)
|
||||
|
||||
self.assertEqual(result, versioned_prompt_response["prompt"])
|
||||
mock_get.assert_called_once()
|
||||
call_args = mock_get.call_args
|
||||
self.assertEqual(
|
||||
call_args[0][0],
|
||||
"https://us.insights.hanzo.ai/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key&version=1",
|
||||
)
|
||||
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts.time.time")
|
||||
def test_return_cached_prompt_when_fresh(self, mock_time, mock_get_session):
|
||||
"""Should return cached prompt when fresh (no API call)."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
||||
mock_time.return_value = 1000.0
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
# First call - fetches from API
|
||||
result1 = prompts.get("test-prompt", cache_ttl_seconds=300)
|
||||
@@ -96,8 +120,43 @@ class TestPromptsGet(TestPrompts):
|
||||
self.assertEqual(result2, self.mock_prompt_response["prompt"])
|
||||
self.assertEqual(mock_get.call_count, 1) # No additional fetch
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("posthog.ai.prompts.time.time")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_cache_latest_and_versioned_prompts_separately(self, mock_get_session):
|
||||
"""Should cache latest and historical prompt versions separately."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
latest_prompt_response = {
|
||||
**self.mock_prompt_response,
|
||||
"prompt": "Latest prompt",
|
||||
"version": 2,
|
||||
}
|
||||
versioned_prompt_response = {
|
||||
**self.mock_prompt_response,
|
||||
"prompt": "Prompt version 1",
|
||||
"version": 1,
|
||||
}
|
||||
|
||||
mock_get.side_effect = [
|
||||
MockResponse(json_data=latest_prompt_response),
|
||||
MockResponse(json_data=versioned_prompt_response),
|
||||
]
|
||||
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
self.assertEqual(prompts.get("test-prompt"), latest_prompt_response["prompt"])
|
||||
self.assertEqual(
|
||||
prompts.get("test-prompt", version=1),
|
||||
versioned_prompt_response["prompt"],
|
||||
)
|
||||
self.assertEqual(prompts.get("test-prompt"), latest_prompt_response["prompt"])
|
||||
self.assertEqual(
|
||||
prompts.get("test-prompt", version=1),
|
||||
versioned_prompt_response["prompt"],
|
||||
)
|
||||
self.assertEqual(mock_get.call_count, 2)
|
||||
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts.time.time")
|
||||
def test_refetch_when_cache_is_stale(self, mock_time, mock_get_session):
|
||||
"""Should refetch when cache is stale."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
@@ -112,8 +171,8 @@ class TestPromptsGet(TestPrompts):
|
||||
]
|
||||
mock_time.return_value = 1000.0
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
# First call - fetches from API
|
||||
result1 = prompts.get("test-prompt", cache_ttl_seconds=60)
|
||||
@@ -128,9 +187,9 @@ class TestPromptsGet(TestPrompts):
|
||||
self.assertEqual(result2, updated_prompt_response["prompt"])
|
||||
self.assertEqual(mock_get.call_count, 2)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("posthog.ai.prompts.time.time")
|
||||
@patch("posthog.ai.prompts.log")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts.time.time")
|
||||
@patch("hanzo_insights.ai.prompts.log")
|
||||
def test_use_stale_cache_on_fetch_failure_with_warning(
|
||||
self, mock_log, mock_time, mock_get_session
|
||||
):
|
||||
@@ -142,8 +201,8 @@ class TestPromptsGet(TestPrompts):
|
||||
]
|
||||
mock_time.return_value = 1000.0
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
# First call - populates cache
|
||||
result1 = prompts.get("test-prompt", cache_ttl_seconds=60)
|
||||
@@ -161,8 +220,8 @@ class TestPromptsGet(TestPrompts):
|
||||
warning_call = mock_log.warning.call_args
|
||||
self.assertIn("using stale cache", warning_call[0][0])
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("posthog.ai.prompts.log")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts.log")
|
||||
def test_use_fallback_when_no_cache_and_fetch_fails_with_warning(
|
||||
self, mock_log, mock_get_session
|
||||
):
|
||||
@@ -170,8 +229,8 @@ class TestPromptsGet(TestPrompts):
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.side_effect = Exception("Network error")
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
fallback = "Default system prompt."
|
||||
result = prompts.get("test-prompt", fallback=fallback)
|
||||
@@ -183,42 +242,59 @@ class TestPromptsGet(TestPrompts):
|
||||
warning_call = mock_log.warning.call_args
|
||||
self.assertIn("using fallback", warning_call[0][0])
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_throw_when_no_cache_no_fallback_and_fetch_fails(self, mock_get_session):
|
||||
"""Should throw when no cache, no fallback, and fetch fails."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.side_effect = Exception("Network error")
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
prompts.get("test-prompt")
|
||||
|
||||
self.assertIn("Network error", str(context.exception))
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_handle_404_response(self, mock_get_session):
|
||||
"""Should handle 404 response."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(status_code=404, ok=False)
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
prompts.get("nonexistent-prompt")
|
||||
|
||||
self.assertIn('Prompt "nonexistent-prompt" not found', str(context.exception))
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_handle_404_response_for_specific_prompt_version(self, mock_get_session):
|
||||
"""Should handle 404 response for a specific prompt version."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(status_code=404, ok=False)
|
||||
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
prompts.get("nonexistent-prompt", version=3)
|
||||
|
||||
self.assertIn(
|
||||
'Prompt "nonexistent-prompt" version 3 not found',
|
||||
str(context.exception),
|
||||
)
|
||||
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_handle_403_response(self, mock_get_session):
|
||||
"""Should handle 403 response."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(status_code=403, ok=False)
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
prompts.get("restricted-prompt")
|
||||
@@ -229,8 +305,8 @@ class TestPromptsGet(TestPrompts):
|
||||
|
||||
def test_throw_when_no_personal_api_key_configured(self):
|
||||
"""Should throw when no personal_api_key is configured."""
|
||||
posthog = self.create_mock_posthog(personal_api_key=None)
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client(personal_api_key=None)
|
||||
prompts = Prompts(client)
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
prompts.get("test-prompt")
|
||||
@@ -241,8 +317,8 @@ class TestPromptsGet(TestPrompts):
|
||||
|
||||
def test_throw_when_no_project_api_key_configured(self):
|
||||
"""Should throw when no project_api_key is configured."""
|
||||
posthog = self.create_mock_posthog(project_api_key=None)
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client(project_api_key=None)
|
||||
prompts = Prompts(client)
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
prompts.get("test-prompt")
|
||||
@@ -251,49 +327,49 @@ class TestPromptsGet(TestPrompts):
|
||||
"project_api_key is required to fetch prompts", str(context.exception)
|
||||
)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_throw_when_api_returns_invalid_response_format(self, mock_get_session):
|
||||
"""Should throw when API returns invalid response format."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(json_data={"invalid": "response"})
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
prompts.get("test-prompt")
|
||||
|
||||
self.assertIn("Invalid response format", str(context.exception))
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
def test_use_custom_host_from_posthog_options(self, mock_get_session):
|
||||
"""Should use custom host from PostHog options."""
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_use_custom_host_from_insights_options(self, mock_get_session):
|
||||
"""Should use custom host from Insights options."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
||||
|
||||
posthog = self.create_mock_posthog(host="https://eu.posthog.com")
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client(host="https://eu.insights.hanzo.ai")
|
||||
prompts = Prompts(client)
|
||||
|
||||
prompts.get("test-prompt")
|
||||
|
||||
call_args = mock_get.call_args
|
||||
self.assertTrue(
|
||||
call_args[0][0].startswith(
|
||||
"https://eu.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key"
|
||||
"https://eu.insights.hanzo.ai/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key"
|
||||
),
|
||||
f"Expected URL to start with 'https://eu.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key', got {call_args[0][0]}",
|
||||
f"Expected URL to start with 'https://eu.insights.hanzo.ai/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key', got {call_args[0][0]}",
|
||||
)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("posthog.ai.prompts.time.time")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts.time.time")
|
||||
def test_use_default_cache_ttl_5_minutes(self, mock_time, mock_get_session):
|
||||
"""Should use default cache TTL (5 minutes) when not specified."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
||||
mock_time.return_value = 1000.0
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
# First call
|
||||
prompts.get("test-prompt")
|
||||
@@ -313,8 +389,8 @@ class TestPromptsGet(TestPrompts):
|
||||
prompts.get("test-prompt")
|
||||
self.assertEqual(mock_get.call_count, 2)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("posthog.ai.prompts.time.time")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts.time.time")
|
||||
def test_use_custom_default_cache_ttl_from_constructor(
|
||||
self, mock_time, mock_get_session
|
||||
):
|
||||
@@ -323,8 +399,8 @@ class TestPromptsGet(TestPrompts):
|
||||
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
||||
mock_time.return_value = 1000.0
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog, default_cache_ttl_seconds=60)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client, default_cache_ttl_seconds=60)
|
||||
|
||||
# First call
|
||||
prompts.get("test-prompt")
|
||||
@@ -337,26 +413,26 @@ class TestPromptsGet(TestPrompts):
|
||||
prompts.get("test-prompt")
|
||||
self.assertEqual(mock_get.call_count, 2)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_url_encode_prompt_names_with_special_characters(self, mock_get_session):
|
||||
"""Should URL-encode prompt names with special characters."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
prompts.get("prompt with spaces/and/slashes")
|
||||
|
||||
call_args = mock_get.call_args
|
||||
self.assertEqual(
|
||||
call_args[0][0],
|
||||
"https://us.posthog.com/api/environments/@current/llm_prompts/name/prompt%20with%20spaces%2Fand%2Fslashes/?token=phc_test_key",
|
||||
"https://us.insights.hanzo.ai/api/environments/@current/llm_prompts/name/prompt%20with%20spaces%2Fand%2Fslashes/?token=phc_test_key",
|
||||
)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
def test_work_with_direct_options_no_posthog_client(self, mock_get_session):
|
||||
"""Should work with direct options (no PostHog client)."""
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_work_with_direct_options_no_insights_client(self, mock_get_session):
|
||||
"""Should work with direct options (no Insights client)."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
||||
|
||||
@@ -370,13 +446,13 @@ class TestPromptsGet(TestPrompts):
|
||||
call_args = mock_get.call_args
|
||||
self.assertEqual(
|
||||
call_args[0][0],
|
||||
"https://us.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_direct_key",
|
||||
"https://us.insights.hanzo.ai/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_direct_key",
|
||||
)
|
||||
self.assertEqual(
|
||||
call_args[1]["headers"]["Authorization"], "Bearer phx_direct_key"
|
||||
)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_use_custom_host_from_direct_options(self, mock_get_session):
|
||||
"""Should use custom host from direct options."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
@@ -385,7 +461,7 @@ class TestPromptsGet(TestPrompts):
|
||||
prompts = Prompts(
|
||||
personal_api_key="phx_direct_key",
|
||||
project_api_key="phc_direct_key",
|
||||
host="https://eu.posthog.com",
|
||||
host="https://eu.insights.hanzo.ai",
|
||||
)
|
||||
|
||||
prompts.get("test-prompt")
|
||||
@@ -393,11 +469,11 @@ class TestPromptsGet(TestPrompts):
|
||||
call_args = mock_get.call_args
|
||||
self.assertEqual(
|
||||
call_args[0][0],
|
||||
"https://eu.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_direct_key",
|
||||
"https://eu.insights.hanzo.ai/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_direct_key",
|
||||
)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("posthog.ai.prompts.time.time")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts.time.time")
|
||||
def test_use_custom_default_cache_ttl_from_direct_options(
|
||||
self, mock_time, mock_get_session
|
||||
):
|
||||
@@ -429,8 +505,8 @@ class TestPromptsCompile(TestPrompts):
|
||||
|
||||
def test_replace_a_single_variable(self):
|
||||
"""Should replace a single variable."""
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.compile("Hello, {{name}}!", {"name": "World"})
|
||||
|
||||
@@ -438,8 +514,8 @@ class TestPromptsCompile(TestPrompts):
|
||||
|
||||
def test_replace_multiple_variables(self):
|
||||
"""Should replace multiple variables."""
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.compile(
|
||||
"Hello, {{name}}! Welcome to {{company}}. Your tier is {{tier}}.",
|
||||
@@ -452,8 +528,8 @@ class TestPromptsCompile(TestPrompts):
|
||||
|
||||
def test_handle_numbers(self):
|
||||
"""Should handle numbers."""
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.compile("You have {{count}} items.", {"count": 42})
|
||||
|
||||
@@ -461,8 +537,8 @@ class TestPromptsCompile(TestPrompts):
|
||||
|
||||
def test_handle_booleans(self):
|
||||
"""Should handle booleans."""
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.compile("Feature enabled: {{enabled}}", {"enabled": True})
|
||||
|
||||
@@ -470,8 +546,8 @@ class TestPromptsCompile(TestPrompts):
|
||||
|
||||
def test_leave_unmatched_variables_unchanged(self):
|
||||
"""Should leave unmatched variables unchanged."""
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.compile(
|
||||
"Hello, {{name}}! Your {{unknown}} is ready.", {"name": "World"}
|
||||
@@ -481,8 +557,8 @@ class TestPromptsCompile(TestPrompts):
|
||||
|
||||
def test_handle_prompts_with_no_variables(self):
|
||||
"""Should handle prompts with no variables."""
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.compile("You are a helpful assistant.", {})
|
||||
|
||||
@@ -490,8 +566,8 @@ class TestPromptsCompile(TestPrompts):
|
||||
|
||||
def test_handle_empty_variables_dict(self):
|
||||
"""Should handle empty variables dict."""
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.compile("Hello, {{name}}!", {})
|
||||
|
||||
@@ -499,8 +575,8 @@ class TestPromptsCompile(TestPrompts):
|
||||
|
||||
def test_handle_multiple_occurrences_of_same_variable(self):
|
||||
"""Should handle multiple occurrences of the same variable."""
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
result = prompts.compile(
|
||||
"Hello, {{name}}! Goodbye, {{name}}!", {"name": "World"}
|
||||
@@ -542,7 +618,17 @@ class TestPromptsCompile(TestPrompts):
|
||||
class TestPromptsClearCache(TestPrompts):
|
||||
"""Tests for the Prompts.clear_cache() method."""
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
def test_clear_cache_with_version_and_no_name_raises_value_error(self):
|
||||
"""Should enforce that versioned cache clearing requires a prompt name."""
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
prompts.clear_cache(version=1)
|
||||
|
||||
self.assertIn("requires 'name'", str(context.exception))
|
||||
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_clear_a_specific_prompt_from_cache(self, mock_get_session):
|
||||
"""Should clear a specific prompt from cache."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
@@ -554,8 +640,8 @@ class TestPromptsClearCache(TestPrompts):
|
||||
MockResponse(json_data=self.mock_prompt_response),
|
||||
]
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
# Populate cache with two prompts
|
||||
prompts.get("test-prompt")
|
||||
@@ -573,7 +659,78 @@ class TestPromptsClearCache(TestPrompts):
|
||||
prompts.get("other-prompt")
|
||||
self.assertEqual(mock_get.call_count, 3)
|
||||
|
||||
@patch("posthog.ai.prompts._get_session")
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_clear_a_specific_prompt_version_from_cache(self, mock_get_session):
|
||||
"""Should clear only the requested prompt version from cache."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
latest_prompt_response = {
|
||||
**self.mock_prompt_response,
|
||||
"prompt": "Latest prompt",
|
||||
"version": 2,
|
||||
}
|
||||
versioned_prompt_response = {
|
||||
**self.mock_prompt_response,
|
||||
"prompt": "Prompt version 1",
|
||||
"version": 1,
|
||||
}
|
||||
|
||||
mock_get.side_effect = [
|
||||
MockResponse(json_data=latest_prompt_response),
|
||||
MockResponse(json_data=versioned_prompt_response),
|
||||
MockResponse(json_data=versioned_prompt_response),
|
||||
]
|
||||
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
prompts.get("test-prompt")
|
||||
prompts.get("test-prompt", version=1)
|
||||
self.assertEqual(mock_get.call_count, 2)
|
||||
|
||||
prompts.clear_cache("test-prompt", version=1)
|
||||
|
||||
prompts.get("test-prompt")
|
||||
self.assertEqual(mock_get.call_count, 2)
|
||||
|
||||
prompts.get("test-prompt", version=1)
|
||||
self.assertEqual(mock_get.call_count, 3)
|
||||
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_clear_a_prompt_name_clears_all_cached_versions(self, mock_get_session):
|
||||
"""Should clear latest and versioned cache entries for the same prompt name."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
latest_prompt_response = {
|
||||
**self.mock_prompt_response,
|
||||
"prompt": "Latest prompt",
|
||||
"version": 2,
|
||||
}
|
||||
versioned_prompt_response = {
|
||||
**self.mock_prompt_response,
|
||||
"prompt": "Prompt version 1",
|
||||
"version": 1,
|
||||
}
|
||||
|
||||
mock_get.side_effect = [
|
||||
MockResponse(json_data=latest_prompt_response),
|
||||
MockResponse(json_data=versioned_prompt_response),
|
||||
MockResponse(json_data=latest_prompt_response),
|
||||
MockResponse(json_data=versioned_prompt_response),
|
||||
]
|
||||
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
prompts.get("test-prompt")
|
||||
prompts.get("test-prompt", version=1)
|
||||
self.assertEqual(mock_get.call_count, 2)
|
||||
|
||||
prompts.clear_cache("test-prompt")
|
||||
|
||||
prompts.get("test-prompt")
|
||||
prompts.get("test-prompt", version=1)
|
||||
self.assertEqual(mock_get.call_count, 4)
|
||||
|
||||
@patch("hanzo_insights.ai.prompts._get_session")
|
||||
def test_clear_all_prompts_from_cache(self, mock_get_session):
|
||||
"""Should clear all prompts from cache when no name is provided."""
|
||||
mock_get = mock_get_session.return_value.get
|
||||
@@ -586,8 +743,8 @@ class TestPromptsClearCache(TestPrompts):
|
||||
MockResponse(json_data=other_prompt_response),
|
||||
]
|
||||
|
||||
posthog = self.create_mock_posthog()
|
||||
prompts = Prompts(posthog)
|
||||
client = self.create_mock_client()
|
||||
prompts = Prompts(client)
|
||||
|
||||
# Populate cache with two prompts
|
||||
prompts.get("test-prompt")
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from posthog.ai.sanitization import (
|
||||
from hanzo_insights.ai.sanitization import (
|
||||
redact_base64_data_url,
|
||||
sanitize_openai,
|
||||
sanitize_openai_response,
|
||||
+24
-24
@@ -13,8 +13,8 @@ import time
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from posthog.client import Client
|
||||
from posthog.test.test_utils import FAKE_TEST_API_KEY
|
||||
from hanzo_insights.client import Client
|
||||
from hanzo_insights.test.test_utils import FAKE_TEST_API_KEY
|
||||
|
||||
|
||||
class TestSystemPromptCapture(unittest.TestCase):
|
||||
@@ -26,7 +26,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
self.test_user_message = "Hello, how are you?"
|
||||
self.test_response = "I'm doing well, thank you!"
|
||||
|
||||
# Create mock PostHog client
|
||||
# Create mock Insights client
|
||||
self.client = Client(FAKE_TEST_API_KEY)
|
||||
self.client._enqueue = MagicMock()
|
||||
self.client.privacy_mode = False
|
||||
@@ -61,7 +61,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
from openai.types.chat.chat_completion import Choice
|
||||
from openai.types.completion_usage import CompletionUsage
|
||||
|
||||
from posthog.ai.openai import OpenAI
|
||||
from hanzo_insights.ai.openai import OpenAI
|
||||
except ImportError:
|
||||
self.skipTest("OpenAI package not available")
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_response,
|
||||
):
|
||||
client = OpenAI(posthog_client=self.client, api_key="test")
|
||||
client = OpenAI(insights_client=self.client, api_key="test")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": self.test_system_prompt},
|
||||
@@ -96,7 +96,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
]
|
||||
|
||||
client.chat.completions.create(
|
||||
model="gpt-4", messages=messages, posthog_distinct_id="test-user"
|
||||
model="gpt-4", messages=messages, insights_distinct_id="test-user"
|
||||
)
|
||||
|
||||
self.assertEqual(len(self.client._enqueue.call_args_list), 1)
|
||||
@@ -110,7 +110,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
from openai.types.chat.chat_completion import Choice
|
||||
from openai.types.completion_usage import CompletionUsage
|
||||
|
||||
from posthog.ai.openai import OpenAI
|
||||
from hanzo_insights.ai.openai import OpenAI
|
||||
except ImportError:
|
||||
self.skipTest("OpenAI package not available")
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=mock_response,
|
||||
):
|
||||
client = OpenAI(posthog_client=self.client, api_key="test")
|
||||
client = OpenAI(insights_client=self.client, api_key="test")
|
||||
|
||||
messages = [{"role": "user", "content": self.test_user_message}]
|
||||
|
||||
@@ -145,7 +145,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
model="gpt-4",
|
||||
messages=messages,
|
||||
system=self.test_system_prompt,
|
||||
posthog_distinct_id="test-user",
|
||||
insights_distinct_id="test-user",
|
||||
)
|
||||
|
||||
self.assertEqual(len(self.client._enqueue.call_args_list), 1)
|
||||
@@ -162,7 +162,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
from openai.types.chat.chat_completion_chunk import Choice as ChoiceChunk
|
||||
from openai.types.completion_usage import CompletionUsage
|
||||
|
||||
from posthog.ai.openai import OpenAI
|
||||
from hanzo_insights.ai.openai import OpenAI
|
||||
except ImportError:
|
||||
self.skipTest("OpenAI package not available")
|
||||
|
||||
@@ -201,7 +201,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
"openai.resources.chat.completions.Completions.create",
|
||||
return_value=[chunk1, chunk2],
|
||||
):
|
||||
client = OpenAI(posthog_client=self.client, api_key="test")
|
||||
client = OpenAI(insights_client=self.client, api_key="test")
|
||||
|
||||
messages = [{"role": "user", "content": self.test_user_message}]
|
||||
|
||||
@@ -210,7 +210,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
messages=messages,
|
||||
system=self.test_system_prompt,
|
||||
stream=True,
|
||||
posthog_distinct_id="test-user",
|
||||
insights_distinct_id="test-user",
|
||||
)
|
||||
|
||||
list(response_generator) # Consume generator
|
||||
@@ -223,7 +223,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
def test_anthropic_messages_array_system_prompt(self):
|
||||
"""Test Anthropic with system prompt in messages array."""
|
||||
try:
|
||||
from posthog.ai.anthropic import Anthropic
|
||||
from hanzo_insights.ai.anthropic import Anthropic
|
||||
except ImportError:
|
||||
self.skipTest("Anthropic package not available")
|
||||
|
||||
@@ -235,7 +235,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
mock_response.usage.cache_creation_input_tokens = None
|
||||
mock_create.return_value = mock_response
|
||||
|
||||
client = Anthropic(posthog_client=self.client, api_key="test")
|
||||
client = Anthropic(insights_client=self.client, api_key="test")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": self.test_system_prompt},
|
||||
@@ -245,7 +245,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
client.messages.create(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
messages=messages,
|
||||
posthog_distinct_id="test-user",
|
||||
insights_distinct_id="test-user",
|
||||
)
|
||||
|
||||
self.assertEqual(len(self.client._enqueue.call_args_list), 1)
|
||||
@@ -255,7 +255,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
def test_anthropic_separate_system_parameter(self):
|
||||
"""Test Anthropic with system prompt as separate parameter."""
|
||||
try:
|
||||
from posthog.ai.anthropic import Anthropic
|
||||
from hanzo_insights.ai.anthropic import Anthropic
|
||||
except ImportError:
|
||||
self.skipTest("Anthropic package not available")
|
||||
|
||||
@@ -267,7 +267,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
mock_response.usage.cache_creation_input_tokens = None
|
||||
mock_create.return_value = mock_response
|
||||
|
||||
client = Anthropic(posthog_client=self.client, api_key="test")
|
||||
client = Anthropic(insights_client=self.client, api_key="test")
|
||||
|
||||
messages = [{"role": "user", "content": self.test_user_message}]
|
||||
|
||||
@@ -275,7 +275,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
messages=messages,
|
||||
system=self.test_system_prompt,
|
||||
posthog_distinct_id="test-user",
|
||||
insights_distinct_id="test-user",
|
||||
)
|
||||
|
||||
self.assertEqual(len(self.client._enqueue.call_args_list), 1)
|
||||
@@ -286,7 +286,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
def test_gemini_contents_array_system_prompt(self):
|
||||
"""Test Gemini with system prompt in contents array."""
|
||||
try:
|
||||
from posthog.ai.gemini import Client
|
||||
from hanzo_insights.ai.gemini import Client
|
||||
except ImportError:
|
||||
self.skipTest("Gemini package not available")
|
||||
|
||||
@@ -306,7 +306,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
mock_client_instance.models = mock_models_instance
|
||||
mock_genai_class.return_value = mock_client_instance
|
||||
|
||||
client = Client(posthog_client=self.client, api_key="test")
|
||||
client = Client(insights_client=self.client, api_key="test")
|
||||
|
||||
contents = [
|
||||
{"role": "system", "content": self.test_system_prompt},
|
||||
@@ -316,7 +316,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=contents,
|
||||
posthog_distinct_id="test-user",
|
||||
insights_distinct_id="test-user",
|
||||
)
|
||||
|
||||
self.assertEqual(len(self.client._enqueue.call_args_list), 1)
|
||||
@@ -326,7 +326,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
def test_gemini_system_instruction_parameter(self):
|
||||
"""Test Gemini with system_instruction in config parameter."""
|
||||
try:
|
||||
from posthog.ai.gemini import Client
|
||||
from hanzo_insights.ai.gemini import Client
|
||||
except ImportError:
|
||||
self.skipTest("Gemini package not available")
|
||||
|
||||
@@ -346,7 +346,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
mock_client_instance.models = mock_models_instance
|
||||
mock_genai_class.return_value = mock_client_instance
|
||||
|
||||
client = Client(posthog_client=self.client, api_key="test")
|
||||
client = Client(insights_client=self.client, api_key="test")
|
||||
|
||||
contents = [{"role": "user", "content": self.test_user_message}]
|
||||
config = {"system_instruction": self.test_system_prompt}
|
||||
@@ -355,7 +355,7 @@ class TestSystemPromptCapture(unittest.TestCase):
|
||||
model="gemini-2.0-flash",
|
||||
contents=contents,
|
||||
config=config,
|
||||
posthog_distinct_id="test-user",
|
||||
insights_distinct_id="test-user",
|
||||
)
|
||||
|
||||
self.assertEqual(len(self.client._enqueue.call_args_list), 1)
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
from parameterized import parameterized
|
||||
|
||||
from posthog.ai.utils import _get_tokens_source
|
||||
from hanzo_insights.ai.utils import _get_tokens_source
|
||||
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
("no_posthog_properties", {"$ai_input_tokens": 100}, None, "sdk"),
|
||||
("empty_posthog_properties", {"$ai_input_tokens": 100}, {}, "sdk"),
|
||||
("no_insights_properties", {"$ai_input_tokens": 100}, None, "sdk"),
|
||||
("empty_insights_properties", {"$ai_input_tokens": 100}, {}, "sdk"),
|
||||
(
|
||||
"unrelated_posthog_properties",
|
||||
"unrelated_insights_properties",
|
||||
{"$ai_input_tokens": 100},
|
||||
{"foo": "bar"},
|
||||
"sdk",
|
||||
@@ -57,6 +57,6 @@ from posthog.ai.utils import _get_tokens_source
|
||||
),
|
||||
]
|
||||
)
|
||||
def test_get_tokens_source(name, sdk_tags, posthog_properties, expected):
|
||||
result = _get_tokens_source(sdk_tags, posthog_properties)
|
||||
def test_get_tokens_source(name, sdk_tags, insights_properties, expected):
|
||||
result = _get_tokens_source(sdk_tags, insights_properties)
|
||||
assert result == expected
|
||||
+52
-52
@@ -1,4 +1,4 @@
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
new_context,
|
||||
get_context_session_id,
|
||||
get_context_distinct_id,
|
||||
@@ -20,7 +20,7 @@ if not settings.configured:
|
||||
)
|
||||
django.setup()
|
||||
|
||||
from posthog.integrations.django import PosthogContextMiddleware
|
||||
from hanzo_insights.integrations.django import InsightsContextMiddleware
|
||||
|
||||
|
||||
class MockRequest:
|
||||
@@ -45,7 +45,7 @@ class MockRequest:
|
||||
return f"{scheme}://{self._host}{self.path}"
|
||||
|
||||
|
||||
class TestPosthogContextMiddleware(unittest.TestCase):
|
||||
class TestInsightsContextMiddleware(unittest.TestCase):
|
||||
def create_middleware(
|
||||
self,
|
||||
extra_tags=None,
|
||||
@@ -60,24 +60,24 @@ class TestPosthogContextMiddleware(unittest.TestCase):
|
||||
|
||||
with patch("django.conf.settings") as mock_settings:
|
||||
# Configure mock settings
|
||||
mock_settings.POSTHOG_MW_EXTRA_TAGS = extra_tags
|
||||
mock_settings.POSTHOG_MW_REQUEST_FILTER = request_filter
|
||||
mock_settings.POSTHOG_MW_TAG_MAP = tag_map
|
||||
mock_settings.POSTHOG_MW_CAPTURE_EXCEPTIONS = capture_exceptions
|
||||
mock_settings.POSTHOG_MW_CLIENT = None
|
||||
mock_settings.INSIGHTS_MW_EXTRA_TAGS = extra_tags
|
||||
mock_settings.INSIGHTS_MW_REQUEST_FILTER = request_filter
|
||||
mock_settings.INSIGHTS_MW_TAG_MAP = tag_map
|
||||
mock_settings.INSIGHTS_MW_CAPTURE_EXCEPTIONS = capture_exceptions
|
||||
mock_settings.INSIGHTS_MW_CLIENT = None
|
||||
|
||||
# Make hasattr work correctly
|
||||
def mock_hasattr(obj, name):
|
||||
return name in [
|
||||
"POSTHOG_MW_EXTRA_TAGS",
|
||||
"POSTHOG_MW_REQUEST_FILTER",
|
||||
"POSTHOG_MW_TAG_MAP",
|
||||
"POSTHOG_MW_CAPTURE_EXCEPTIONS",
|
||||
"POSTHOG_MW_CLIENT",
|
||||
"INSIGHTS_MW_EXTRA_TAGS",
|
||||
"INSIGHTS_MW_REQUEST_FILTER",
|
||||
"INSIGHTS_MW_TAG_MAP",
|
||||
"INSIGHTS_MW_CAPTURE_EXCEPTIONS",
|
||||
"INSIGHTS_MW_CLIENT",
|
||||
]
|
||||
|
||||
with patch("builtins.hasattr", side_effect=mock_hasattr):
|
||||
middleware = PosthogContextMiddleware(get_response)
|
||||
middleware = InsightsContextMiddleware(get_response)
|
||||
|
||||
return middleware
|
||||
|
||||
@@ -87,8 +87,8 @@ class TestPosthogContextMiddleware(unittest.TestCase):
|
||||
middleware = self.create_middleware()
|
||||
request = MockRequest(
|
||||
headers={
|
||||
"X-POSTHOG-SESSION-ID": "session-123",
|
||||
"X-POSTHOG-DISTINCT-ID": "user-456",
|
||||
"X-INSIGHTS-SESSION-ID": "session-123",
|
||||
"X-INSIGHTS-DISTINCT-ID": "user-456",
|
||||
},
|
||||
method="POST",
|
||||
path="/api/test",
|
||||
@@ -104,7 +104,7 @@ class TestPosthogContextMiddleware(unittest.TestCase):
|
||||
self.assertEqual(tags["$request_method"], "POST")
|
||||
|
||||
def test_extract_tags_missing_headers(self):
|
||||
"""Test tag extraction when PostHog headers are missing"""
|
||||
"""Test tag extraction when Insights headers are missing"""
|
||||
|
||||
with new_context():
|
||||
middleware = self.create_middleware()
|
||||
@@ -118,12 +118,12 @@ class TestPosthogContextMiddleware(unittest.TestCase):
|
||||
self.assertEqual(tags["$request_method"], "GET")
|
||||
|
||||
def test_extract_tags_partial_headers(self):
|
||||
"""Test tag extraction with only some PostHog headers present"""
|
||||
"""Test tag extraction with only some Insights headers present"""
|
||||
|
||||
with new_context():
|
||||
middleware = self.create_middleware()
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "session-only"}, method="PUT"
|
||||
headers={"X-INSIGHTS-SESSION-ID": "session-only"}, method="PUT"
|
||||
)
|
||||
|
||||
tags = middleware.extract_tags(request)
|
||||
@@ -141,7 +141,7 @@ class TestPosthogContextMiddleware(unittest.TestCase):
|
||||
with new_context():
|
||||
middleware = self.create_middleware(extra_tags=extra_tags_func)
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "session-123"}, method="GET"
|
||||
headers={"X-INSIGHTS-SESSION-ID": "session-123"}, method="GET"
|
||||
)
|
||||
|
||||
tags = middleware.extract_tags(request)
|
||||
@@ -167,7 +167,7 @@ class TestPosthogContextMiddleware(unittest.TestCase):
|
||||
tag_map=tag_map_func, extra_tags=extra_tags_func
|
||||
)
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "session-123"}, method="GET"
|
||||
headers={"X-INSIGHTS-SESSION-ID": "session-123"}, method="GET"
|
||||
)
|
||||
|
||||
tags = middleware.extract_tags(request)
|
||||
@@ -230,7 +230,7 @@ class TestPosthogContextMiddleware(unittest.TestCase):
|
||||
middleware.client = mock_client
|
||||
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-DISTINCT-ID": "test-user"},
|
||||
headers={"X-INSIGHTS-DISTINCT-ID": "test-user"},
|
||||
method="POST",
|
||||
path="/api/endpoint",
|
||||
)
|
||||
@@ -282,7 +282,7 @@ class TestPosthogContextMiddleware(unittest.TestCase):
|
||||
mock_client.capture_exception.assert_not_called()
|
||||
|
||||
|
||||
class TestPosthogContextMiddlewareSync(unittest.TestCase):
|
||||
class TestInsightsContextMiddlewareSync(unittest.TestCase):
|
||||
"""Test synchronous middleware behavior"""
|
||||
|
||||
def test_sync_middleware_call(self):
|
||||
@@ -291,13 +291,13 @@ class TestPosthogContextMiddlewareSync(unittest.TestCase):
|
||||
get_response = Mock(return_value=mock_response)
|
||||
|
||||
# Create middleware with sync get_response
|
||||
middleware = PosthogContextMiddleware(get_response)
|
||||
middleware = InsightsContextMiddleware(get_response)
|
||||
|
||||
# Verify sync mode detected
|
||||
self.assertFalse(middleware._is_coroutine)
|
||||
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "test-session"},
|
||||
headers={"X-INSIGHTS-SESSION-ID": "test-session"},
|
||||
method="GET",
|
||||
path="/test",
|
||||
)
|
||||
@@ -318,7 +318,7 @@ class TestPosthogContextMiddlewareSync(unittest.TestCase):
|
||||
def request_filter(req):
|
||||
return False
|
||||
|
||||
middleware = PosthogContextMiddleware.__new__(PosthogContextMiddleware)
|
||||
middleware = InsightsContextMiddleware.__new__(InsightsContextMiddleware)
|
||||
middleware.get_response = get_response
|
||||
middleware._is_coroutine = False
|
||||
middleware.request_filter = request_filter
|
||||
@@ -351,7 +351,7 @@ class TestPosthogContextMiddlewareSync(unittest.TestCase):
|
||||
mock_client = Mock()
|
||||
get_response = Mock(return_value=Mock(status_code=500))
|
||||
|
||||
middleware = PosthogContextMiddleware(get_response)
|
||||
middleware = InsightsContextMiddleware(get_response)
|
||||
middleware.client = mock_client
|
||||
|
||||
def get_response_simulating_django(request):
|
||||
@@ -380,7 +380,7 @@ class TestPosthogContextMiddlewareSync(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
class TestInsightsContextMiddlewareAsync(unittest.TestCase):
|
||||
"""Test asynchronous middleware behavior"""
|
||||
|
||||
def test_async_middleware_detection(self):
|
||||
@@ -389,7 +389,7 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
async def async_get_response(request):
|
||||
return Mock()
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
|
||||
# Verify async mode detected
|
||||
self.assertTrue(middleware._is_coroutine)
|
||||
@@ -403,10 +403,10 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
async def async_get_response(request):
|
||||
return mock_response
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "async-session"},
|
||||
headers={"X-INSIGHTS-SESSION-ID": "async-session"},
|
||||
method="POST",
|
||||
path="/async-test",
|
||||
)
|
||||
@@ -434,7 +434,7 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
return mock_response
|
||||
|
||||
# Properly initialize middleware
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
# Override request filter after initialization
|
||||
middleware.request_filter = lambda req: False
|
||||
|
||||
@@ -459,10 +459,10 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
self.assertEqual(session_id, "async-session-123")
|
||||
return mock_response
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "async-session-123"},
|
||||
headers={"X-INSIGHTS-SESSION-ID": "async-session-123"},
|
||||
method="GET",
|
||||
)
|
||||
|
||||
@@ -483,7 +483,7 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
raise ValueError("Async test exception")
|
||||
|
||||
# Properly initialize middleware
|
||||
middleware = PosthogContextMiddleware(raise_exception)
|
||||
middleware = InsightsContextMiddleware(raise_exception)
|
||||
middleware.client = mock_client # Override with mock client
|
||||
|
||||
request = MockRequest()
|
||||
@@ -525,11 +525,11 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
self.assertEqual(distinct_id, "123")
|
||||
return mock_response
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
middleware.client = Mock()
|
||||
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "test-session"}, method="GET"
|
||||
headers={"X-INSIGHTS-SESSION-ID": "test-session"}, method="GET"
|
||||
)
|
||||
|
||||
# Mock auser() to return authenticated user
|
||||
@@ -561,11 +561,11 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
self.assertIsNone(distinct_id)
|
||||
return mock_response
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
middleware.client = Mock()
|
||||
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "test-session"}, method="GET"
|
||||
headers={"X-INSIGHTS-SESSION-ID": "test-session"}, method="GET"
|
||||
)
|
||||
|
||||
async def mock_auser():
|
||||
@@ -591,12 +591,12 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
async def async_get_response(request):
|
||||
return mock_response
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
middleware.client = Mock()
|
||||
|
||||
# Request without auser method (no auth middleware)
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "test-session"}, method="GET"
|
||||
headers={"X-INSIGHTS-SESSION-ID": "test-session"}, method="GET"
|
||||
)
|
||||
|
||||
with new_context():
|
||||
@@ -621,12 +621,12 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
async def async_get_response(request):
|
||||
return mock_response
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
middleware.extra_tags = extra_tags_callback
|
||||
middleware.client = Mock()
|
||||
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "test-session"}, method="GET"
|
||||
headers={"X-INSIGHTS-SESSION-ID": "test-session"}, method="GET"
|
||||
)
|
||||
|
||||
# Mock auser for no user
|
||||
@@ -658,12 +658,12 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
async def async_get_response(request):
|
||||
return mock_response
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
middleware.tag_map = tag_map_callback
|
||||
middleware.client = Mock()
|
||||
|
||||
request = MockRequest(
|
||||
headers={"X-POSTHOG-SESSION-ID": "test-session"}, method="GET"
|
||||
headers={"X-INSIGHTS-SESSION-ID": "test-session"}, method="GET"
|
||||
)
|
||||
|
||||
# Mock auser for no user
|
||||
@@ -699,12 +699,12 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
self.assertEqual(session_id, "async-sess-123")
|
||||
return mock_response
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
middleware.client = Mock()
|
||||
|
||||
request = MockRequest(
|
||||
headers={
|
||||
"X-POSTHOG-SESSION-ID": "async-sess-123",
|
||||
"X-INSIGHTS-SESSION-ID": "async-sess-123",
|
||||
"X-Forwarded-For": "192.168.1.1",
|
||||
"User-Agent": "TestAgent/1.0",
|
||||
},
|
||||
@@ -725,13 +725,13 @@ class TestPosthogContextMiddlewareAsync(unittest.TestCase):
|
||||
asyncio.run(run_test())
|
||||
|
||||
|
||||
class TestPosthogContextMiddlewareHybrid(unittest.TestCase):
|
||||
class TestInsightsContextMiddlewareHybrid(unittest.TestCase):
|
||||
"""Test hybrid middleware behavior with mixed sync/async chains"""
|
||||
|
||||
def test_hybrid_flags_set(self):
|
||||
"""Test that both capability flags are set"""
|
||||
self.assertTrue(PosthogContextMiddleware.sync_capable)
|
||||
self.assertTrue(PosthogContextMiddleware.async_capable)
|
||||
self.assertTrue(InsightsContextMiddleware.sync_capable)
|
||||
self.assertTrue(InsightsContextMiddleware.async_capable)
|
||||
|
||||
def test_sync_to_async_routing(self):
|
||||
"""Test that __call__ routes to __acall__ when async"""
|
||||
@@ -740,7 +740,7 @@ class TestPosthogContextMiddlewareHybrid(unittest.TestCase):
|
||||
async def async_get_response(request):
|
||||
return Mock()
|
||||
|
||||
middleware = PosthogContextMiddleware(async_get_response)
|
||||
middleware = InsightsContextMiddleware(async_get_response)
|
||||
|
||||
# Verify routing happens
|
||||
request = MockRequest()
|
||||
@@ -759,7 +759,7 @@ class TestPosthogContextMiddlewareHybrid(unittest.TestCase):
|
||||
def sync_get_response(request):
|
||||
return mock_response
|
||||
|
||||
middleware = PosthogContextMiddleware(sync_get_response)
|
||||
middleware = InsightsContextMiddleware(sync_get_response)
|
||||
|
||||
request = MockRequest()
|
||||
result = middleware(request)
|
||||
@@ -2,16 +2,16 @@ import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from posthog.client import Client
|
||||
from posthog.test.test_utils import FAKE_TEST_API_KEY
|
||||
from hanzo_insights.client import Client
|
||||
from hanzo_insights.test.test_utils import FAKE_TEST_API_KEY
|
||||
|
||||
|
||||
class TestClient(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# This ensures no real HTTP POST requests are made
|
||||
cls.client_post_patcher = mock.patch("posthog.client.batch_post")
|
||||
cls.consumer_post_patcher = mock.patch("posthog.consumer.batch_post")
|
||||
cls.client_post_patcher = mock.patch("hanzo_insights.client.batch_post")
|
||||
cls.consumer_post_patcher = mock.patch("hanzo_insights.consumer.batch_post")
|
||||
cls.client_post_patcher.start()
|
||||
cls.consumer_post_patcher.start()
|
||||
|
||||
@@ -40,7 +40,7 @@ class TestClient(unittest.TestCase):
|
||||
event["properties"]["processed_by_before_send"] = True
|
||||
return event
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -73,7 +73,7 @@ class TestClient(unittest.TestCase):
|
||||
return None
|
||||
return event
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -101,7 +101,7 @@ class TestClient(unittest.TestCase):
|
||||
def buggy_before_send(event):
|
||||
raise ValueError("Oops!")
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -128,7 +128,7 @@ class TestClient(unittest.TestCase):
|
||||
event["properties"]["marked"] = True
|
||||
return event
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -153,7 +153,7 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
def test_before_send_callback_disabled_when_none(self):
|
||||
"""Test that client works normally when before_send is None."""
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -189,7 +189,7 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
return event
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -7,21 +7,21 @@ import mock
|
||||
import six
|
||||
from parameterized import parameterized
|
||||
|
||||
from posthog.client import Client
|
||||
from posthog.contexts import get_context_session_id, new_context, set_context_session
|
||||
from posthog.request import APIError, GetResponse
|
||||
from posthog.test.test_utils import FAKE_TEST_API_KEY
|
||||
from posthog.types import FeatureFlag, LegacyFlagMetadata
|
||||
from posthog.version import VERSION
|
||||
from posthog.contexts import tag
|
||||
from hanzo_insights.client import Client
|
||||
from hanzo_insights.contexts import get_context_session_id, new_context, set_context_session
|
||||
from hanzo_insights.request import APIError, GetResponse
|
||||
from hanzo_insights.test.test_utils import FAKE_TEST_API_KEY
|
||||
from hanzo_insights.types import FeatureFlag, LegacyFlagMetadata
|
||||
from hanzo_insights.version import VERSION
|
||||
from hanzo_insights.contexts import tag
|
||||
|
||||
|
||||
class TestClient(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# This ensures no real HTTP POST requests are made
|
||||
cls.client_post_patcher = mock.patch("posthog.client.batch_post")
|
||||
cls.consumer_post_patcher = mock.patch("posthog.consumer.batch_post")
|
||||
cls.client_post_patcher = mock.patch("hanzo_insights.client.batch_post")
|
||||
cls.consumer_post_patcher = mock.patch("hanzo_insights.consumer.batch_post")
|
||||
cls.client_post_patcher.start()
|
||||
cls.consumer_post_patcher.start()
|
||||
|
||||
@@ -46,7 +46,7 @@ class TestClient(unittest.TestCase):
|
||||
self.client.flush()
|
||||
|
||||
def test_basic_capture(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.capture("python test event", distinct_id="distinct_id")
|
||||
self.assertIsNotNone(msg_uuid)
|
||||
@@ -61,7 +61,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertIsNotNone(msg.get("uuid"))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
# these will change between platforms so just asssert on presence here
|
||||
assert msg["properties"]["$python_runtime"] == mock.ANY
|
||||
@@ -70,7 +70,7 @@ class TestClient(unittest.TestCase):
|
||||
assert msg["properties"]["$os_version"] == mock.ANY
|
||||
|
||||
def test_basic_capture_with_uuid(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
uuid = str(uuid4())
|
||||
msg_uuid = client.capture(
|
||||
@@ -88,11 +88,11 @@ class TestClient(unittest.TestCase):
|
||||
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"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
|
||||
def test_basic_capture_with_project_api_key(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
project_api_key=FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -111,11 +111,11 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["event"], "python test event")
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
|
||||
def test_basic_super_properties(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
super_properties={"source": "repo-name"},
|
||||
@@ -230,7 +230,7 @@ class TestClient(unittest.TestCase):
|
||||
capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][
|
||||
"frames"
|
||||
][0]["filename"],
|
||||
"posthog/test/test_client.py",
|
||||
"hanzo_insights/test/test_client.py",
|
||||
)
|
||||
self.assertEqual(
|
||||
capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][
|
||||
@@ -242,7 +242,7 @@ class TestClient(unittest.TestCase):
|
||||
capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][
|
||||
"frames"
|
||||
][0]["module"],
|
||||
"posthog.test.test_client",
|
||||
"hanzo_insights.test.test_client",
|
||||
)
|
||||
self.assertEqual(
|
||||
capture_call[1]["properties"]["$exception_list"][0]["stacktrace"][
|
||||
@@ -253,31 +253,31 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
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:
|
||||
with self.assertLogs("hanzo_insights", level="WARNING") as logs:
|
||||
client = self.client
|
||||
client.capture_exception(None)
|
||||
|
||||
self.assertFalse(patch_capture.called)
|
||||
self.assertEqual(
|
||||
logs.output[0],
|
||||
"WARNING:posthog:No exception information available",
|
||||
"WARNING:hanzo_insights:No exception information available",
|
||||
)
|
||||
|
||||
def test_capture_exception_logs_when_enabled(self):
|
||||
client = Client(FAKE_TEST_API_KEY, log_captured_exceptions=True)
|
||||
with self.assertLogs("posthog", level="ERROR") as logs:
|
||||
with self.assertLogs("hanzo_insights", level="ERROR") as logs:
|
||||
client.capture_exception(
|
||||
Exception("test exception"), distinct_id="distinct_id"
|
||||
)
|
||||
self.assertEqual(
|
||||
logs.output[0], "ERROR:posthog:test exception\nNoneType: None"
|
||||
logs.output[0], "ERROR:hanzo_insights:test exception\nNoneType: None"
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_basic_capture_with_feature_flags(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -299,7 +299,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertIsNotNone(msg.get("uuid"))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertEqual(
|
||||
msg["properties"]["$feature/beta-feature"], "random-variant"
|
||||
@@ -310,7 +310,7 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertEqual(patch_flags.call_count, 1)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_basic_capture_with_locally_evaluated_feature_flags(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
|
||||
|
||||
@@ -400,7 +400,7 @@ class TestClient(unittest.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -424,7 +424,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertIsNotNone(msg.get("uuid"))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertEqual(
|
||||
msg["properties"]["$feature/beta-feature-local"], "third-variant"
|
||||
@@ -438,7 +438,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
# test that flags are not evaluated without local evaluation
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -460,7 +460,7 @@ class TestClient(unittest.TestCase):
|
||||
assert "$feature/false-flag" not in msg["properties"]
|
||||
assert "$active_feature_flags" not in msg["properties"]
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_load_feature_flags_quota_limited(self, patch_get):
|
||||
mock_response = {
|
||||
"type": "quota_limited",
|
||||
@@ -470,21 +470,21 @@ class TestClient(unittest.TestCase):
|
||||
patch_get.side_effect = APIError(402, mock_response["detail"])
|
||||
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
with self.assertLogs("posthog", level="WARNING") as logs:
|
||||
with self.assertLogs("hanzo_insights", level="WARNING") as logs:
|
||||
client._load_feature_flags()
|
||||
|
||||
self.assertEqual(client.feature_flags, [])
|
||||
self.assertEqual(client.feature_flags_by_key, {})
|
||||
self.assertEqual(client.group_type_mapping, {})
|
||||
self.assertEqual(client.cohorts, {})
|
||||
self.assertIn("PostHog feature flags quota limited", logs.output[0])
|
||||
self.assertIn("Insights feature flags quota limited", logs.output[0])
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_load_feature_flags_unauthorized(self, patch_get):
|
||||
patch_get.side_effect = APIError(401, "Unauthorized")
|
||||
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
with self.assertLogs("posthog", level="ERROR") as logs:
|
||||
with self.assertLogs("hanzo_insights", level="ERROR") as logs:
|
||||
client._load_feature_flags()
|
||||
|
||||
self.assertEqual(client.feature_flags, [])
|
||||
@@ -493,7 +493,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(client.cohorts, {})
|
||||
self.assertIn("please set a valid personal_api_key", logs.output[0])
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_dont_override_capture_with_local_flags(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
|
||||
|
||||
@@ -568,7 +568,7 @@ class TestClient(unittest.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -595,7 +595,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertIsNotNone(msg.get("uuid"))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertEqual(
|
||||
msg["properties"]["$feature/beta-feature-local"], "my-custom-variant"
|
||||
@@ -608,7 +608,7 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_basic_capture_with_feature_flags_returns_active_only(self, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {
|
||||
@@ -618,7 +618,7 @@ class TestClient(unittest.TestCase):
|
||||
}
|
||||
}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -641,7 +641,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertIsNotNone(msg.get("uuid"))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertTrue(msg["properties"]["$geoip_disable"])
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertEqual(
|
||||
msg["properties"]["$feature/beta-feature"], "random-variant"
|
||||
@@ -655,7 +655,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 1)
|
||||
patch_flags.assert_called_with(
|
||||
"random_key",
|
||||
"https://us.i.posthog.com",
|
||||
"https://us.i.insights.hanzo.ai",
|
||||
timeout=3,
|
||||
distinct_id="distinct_id",
|
||||
groups={},
|
||||
@@ -665,7 +665,7 @@ class TestClient(unittest.TestCase):
|
||||
device_id=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_basic_capture_with_feature_flags_and_disable_geoip_returns_correctly(
|
||||
self, patch_flags
|
||||
):
|
||||
@@ -677,7 +677,7 @@ class TestClient(unittest.TestCase):
|
||||
}
|
||||
}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
host="https://app.posthog.com",
|
||||
@@ -706,7 +706,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertIsNotNone(msg.get("uuid"))
|
||||
self.assertTrue("$geoip_disable" not in msg["properties"])
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertEqual(
|
||||
msg["properties"]["$feature/beta-feature"], "random-variant"
|
||||
@@ -720,7 +720,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 1)
|
||||
patch_flags.assert_called_with(
|
||||
"random_key",
|
||||
"https://us.i.posthog.com",
|
||||
"https://us.i.insights.hanzo.ai",
|
||||
timeout=12,
|
||||
distinct_id="distinct_id",
|
||||
groups={},
|
||||
@@ -730,13 +730,13 @@ class TestClient(unittest.TestCase):
|
||||
device_id=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_basic_capture_with_feature_flags_switched_off_doesnt_send_them(
|
||||
self, patch_flags
|
||||
):
|
||||
patch_flags.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -758,14 +758,14 @@ class TestClient(unittest.TestCase):
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertIsNotNone(msg.get("uuid"))
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertTrue("$feature/beta-feature" not in msg["properties"])
|
||||
self.assertTrue("$active_feature_flags" not in msg["properties"])
|
||||
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_with_send_feature_flags_false_and_local_evaluation_doesnt_send_flags(
|
||||
self, patch_flags
|
||||
):
|
||||
@@ -814,7 +814,7 @@ class TestClient(unittest.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -847,7 +847,7 @@ class TestClient(unittest.TestCase):
|
||||
# CRITICAL: Verify the /flags API was NOT called
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_with_send_feature_flags_true_and_local_evaluation_uses_local_flags(
|
||||
self, patch_flags
|
||||
):
|
||||
@@ -896,7 +896,7 @@ class TestClient(unittest.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -937,12 +937,12 @@ class TestClient(unittest.TestCase):
|
||||
# CRITICAL: Verify the /flags API was NOT called
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_with_send_feature_flags_options_only_evaluate_locally_true(
|
||||
self, patch_flags
|
||||
):
|
||||
"""Test that SendFeatureFlagsOptions with only_evaluate_locally=True uses local evaluation"""
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -990,14 +990,14 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["properties"]["$feature/local-flag"], True)
|
||||
self.assertEqual(msg["properties"]["$active_feature_flags"], ["local-flag"])
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_with_send_feature_flags_options_only_evaluate_locally_false(
|
||||
self, patch_flags
|
||||
):
|
||||
"""Test that SendFeatureFlagsOptions with only_evaluate_locally=False forces remote evaluation"""
|
||||
patch_flags.return_value = {"featureFlags": {"remote-flag": "remote-value"}}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -1036,14 +1036,14 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertEqual(msg["properties"]["$feature/remote-flag"], "remote-value")
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_with_send_feature_flags_options_default_behavior(
|
||||
self, patch_flags
|
||||
):
|
||||
"""Test that SendFeatureFlagsOptions without only_evaluate_locally defaults to remote evaluation"""
|
||||
patch_flags.return_value = {"featureFlags": {"default-flag": "default-value"}}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -1076,12 +1076,12 @@ class TestClient(unittest.TestCase):
|
||||
msg["properties"]["$feature/default-flag"], "default-value"
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_exception_with_send_feature_flags_options(self, patch_flags):
|
||||
"""Test that capture_exception also supports SendFeatureFlagsOptions"""
|
||||
patch_flags.return_value = {"featureFlags": {"exception-flag": True}}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -1120,7 +1120,7 @@ class TestClient(unittest.TestCase):
|
||||
def test_stringifies_distinct_id(self):
|
||||
# A large number that loses precision in node:
|
||||
# node -e "console.log(157963456373623802 + 1)" > 157963456373623800
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.capture(
|
||||
"python test event", distinct_id=157963456373623802
|
||||
@@ -1136,7 +1136,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["distinct_id"], "157963456373623802")
|
||||
|
||||
def test_advanced_capture(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.capture(
|
||||
"python test event",
|
||||
@@ -1156,14 +1156,14 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
||||
self.assertEqual(msg["properties"]["property"], "value")
|
||||
self.assertEqual(msg["event"], "python test event")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertEqual(msg["uuid"], "new-uuid")
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertTrue("$groups" not in msg["properties"])
|
||||
|
||||
def test_groups_capture(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.capture(
|
||||
"test_event",
|
||||
@@ -1184,7 +1184,7 @@ class TestClient(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_basic_set(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.set(
|
||||
distinct_id="distinct_id", properties={"trait": "value"}
|
||||
@@ -1203,7 +1203,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_advanced_set(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.set(
|
||||
distinct_id="distinct_id",
|
||||
@@ -1221,14 +1221,14 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
||||
self.assertEqual(msg["$set"]["trait"], "value")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertEqual(msg["uuid"], "new-uuid")
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_basic_set_once(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.set_once(
|
||||
distinct_id="distinct_id", properties={"trait": "value"}
|
||||
@@ -1247,7 +1247,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_advanced_set_once(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.set_once(
|
||||
distinct_id="distinct_id",
|
||||
@@ -1265,14 +1265,14 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
||||
self.assertEqual(msg["$set_once"]["trait"], "value")
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
self.assertTrue(isinstance(msg["timestamp"], str))
|
||||
self.assertEqual(msg["uuid"], "new-uuid")
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
|
||||
def test_basic_group_identify(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.group_identify("organization", "id:5")
|
||||
|
||||
@@ -1290,7 +1290,7 @@ class TestClient(unittest.TestCase):
|
||||
"$group_type": "organization",
|
||||
"$group_key": "id:5",
|
||||
"$group_set": {},
|
||||
"$lib": "posthog-python",
|
||||
"$lib": "insights-python",
|
||||
"$lib_version": VERSION,
|
||||
"$geoip_disable": True,
|
||||
},
|
||||
@@ -1299,7 +1299,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertIsNotNone(msg.get("uuid"))
|
||||
|
||||
def test_basic_group_identify_with_distinct_id(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.group_identify(
|
||||
"organization", "id:5", distinct_id="distinct_id"
|
||||
@@ -1319,7 +1319,7 @@ class TestClient(unittest.TestCase):
|
||||
"$group_type": "organization",
|
||||
"$group_key": "id:5",
|
||||
"$group_set": {},
|
||||
"$lib": "posthog-python",
|
||||
"$lib": "insights-python",
|
||||
"$lib_version": VERSION,
|
||||
"$geoip_disable": True,
|
||||
},
|
||||
@@ -1328,7 +1328,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertIsNotNone(msg.get("uuid"))
|
||||
|
||||
def test_advanced_group_identify(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.group_identify(
|
||||
"organization",
|
||||
@@ -1352,7 +1352,7 @@ class TestClient(unittest.TestCase):
|
||||
"$group_type": "organization",
|
||||
"$group_key": "id:5",
|
||||
"$group_set": {"trait": "value"},
|
||||
"$lib": "posthog-python",
|
||||
"$lib": "insights-python",
|
||||
"$lib_version": VERSION,
|
||||
"$geoip_disable": True,
|
||||
},
|
||||
@@ -1360,7 +1360,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
||||
|
||||
def test_advanced_group_identify_with_distinct_id(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.group_identify(
|
||||
"organization",
|
||||
@@ -1387,7 +1387,7 @@ class TestClient(unittest.TestCase):
|
||||
"$group_type": "organization",
|
||||
"$group_key": "id:5",
|
||||
"$group_set": {"trait": "value"},
|
||||
"$lib": "posthog-python",
|
||||
"$lib": "insights-python",
|
||||
"$lib_version": VERSION,
|
||||
"$geoip_disable": True,
|
||||
},
|
||||
@@ -1395,7 +1395,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00")
|
||||
|
||||
def test_basic_alias(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
msg_uuid = client.alias("previousId", "distinct_id")
|
||||
self.assertIsNotNone(msg_uuid)
|
||||
@@ -1435,7 +1435,7 @@ class TestClient(unittest.TestCase):
|
||||
def test_capture_with_session_id_variations(
|
||||
self, test_name, session_id, additional_properties, expected_properties
|
||||
):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
|
||||
properties = {"$session_id": session_id, **additional_properties}
|
||||
@@ -1454,7 +1454,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["event"], "python test event")
|
||||
self.assertEqual(msg["distinct_id"], "distinct_id")
|
||||
self.assertEqual(msg["properties"]["$session_id"], session_id)
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
|
||||
# Check additional expected properties
|
||||
@@ -1462,7 +1462,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["properties"][key], value)
|
||||
|
||||
def test_session_id_preserved_with_groups(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
session_id = "group-session-101"
|
||||
|
||||
@@ -1487,7 +1487,7 @@ class TestClient(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_session_id_with_anonymous_event(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
session_id = "anonymous-session-202"
|
||||
|
||||
@@ -1584,7 +1584,7 @@ class TestClient(unittest.TestCase):
|
||||
additional_properties,
|
||||
expected_additional_properties,
|
||||
):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
|
||||
properties = {"$session_id": session_id, **additional_properties}
|
||||
@@ -1607,7 +1607,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["properties"][key], value)
|
||||
|
||||
# Verify system properties are still added
|
||||
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
|
||||
self.assertEqual(msg["properties"]["$lib"], "insights-python")
|
||||
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
|
||||
|
||||
@parameterized.expand(
|
||||
@@ -1651,7 +1651,7 @@ class TestClient(unittest.TestCase):
|
||||
expected_session_id,
|
||||
expected_super_props,
|
||||
):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY, super_properties=super_properties, sync_mode=True
|
||||
)
|
||||
@@ -1704,7 +1704,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertFalse(consumer.is_alive())
|
||||
|
||||
def test_synchronous(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, sync_mode=True)
|
||||
|
||||
msg_uuid = client.capture("test event", distinct_id="distinct_id")
|
||||
@@ -1758,7 +1758,7 @@ class TestClient(unittest.TestCase):
|
||||
# the post function should be called 2 times, with a batch size of 10
|
||||
# each time.
|
||||
with mock.patch(
|
||||
"posthog.consumer.batch_post", side_effect=mock_post_fn
|
||||
"hanzo_insights.consumer.batch_post", side_effect=mock_post_fn
|
||||
) as mock_post:
|
||||
for _ in range(20):
|
||||
client.capture(
|
||||
@@ -1784,7 +1784,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertIsNone(msg_uuid)
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_disabled_with_feature_flags(self, patch_flags):
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, disabled=True)
|
||||
|
||||
@@ -1812,7 +1812,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertTrue(client.queue.empty())
|
||||
|
||||
def test_enabled_to_disabled(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -1836,7 +1836,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertFalse(self.failed)
|
||||
|
||||
def test_disable_geoip_default_on_events(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -1853,7 +1853,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(capture_msg["properties"]["$geoip_disable"], True)
|
||||
|
||||
def test_disable_geoip_override_on_events(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -1889,7 +1889,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual("$geoip_disable" not in identify_msg["properties"], True)
|
||||
|
||||
def test_disable_geoip_method_overrides_init_on_events(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -1907,7 +1907,7 @@ class TestClient(unittest.TestCase):
|
||||
msg = batch_data[0]
|
||||
self.assertTrue("$geoip_disable" not in msg["properties"])
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_disable_geoip_default_on_decide(self, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {
|
||||
@@ -1920,7 +1920,7 @@ class TestClient(unittest.TestCase):
|
||||
client.get_feature_flag("random_key", "some_id", disable_geoip=True)
|
||||
patch_flags.assert_called_with(
|
||||
"random_key",
|
||||
"https://us.i.posthog.com",
|
||||
"https://us.i.insights.hanzo.ai",
|
||||
timeout=3,
|
||||
distinct_id="some_id",
|
||||
groups={},
|
||||
@@ -1936,7 +1936,7 @@ class TestClient(unittest.TestCase):
|
||||
)
|
||||
patch_flags.assert_called_with(
|
||||
"random_key",
|
||||
"https://us.i.posthog.com",
|
||||
"https://us.i.insights.hanzo.ai",
|
||||
timeout=3,
|
||||
distinct_id="feature_enabled_distinct_id",
|
||||
groups={},
|
||||
@@ -1950,7 +1950,7 @@ class TestClient(unittest.TestCase):
|
||||
client.get_all_flags_and_payloads("all_flags_payloads_id")
|
||||
patch_flags.assert_called_with(
|
||||
"random_key",
|
||||
"https://us.i.posthog.com",
|
||||
"https://us.i.insights.hanzo.ai",
|
||||
timeout=3,
|
||||
distinct_id="all_flags_payloads_id",
|
||||
groups={},
|
||||
@@ -1960,8 +1960,8 @@ class TestClient(unittest.TestCase):
|
||||
device_id=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_call_identify_fails(self, patch_get, patch_poller):
|
||||
def raise_effect():
|
||||
raise Exception("http exception")
|
||||
@@ -1972,7 +1972,7 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertFalse(client.feature_enabled("example", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_default_properties_get_added_properly(self, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {
|
||||
@@ -2080,7 +2080,7 @@ class TestClient(unittest.TestCase):
|
||||
("get_flags_decision", ["some_id"], {}, None),
|
||||
]
|
||||
)
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_device_id_is_passed_to_flags_request(
|
||||
self,
|
||||
method,
|
||||
@@ -2107,13 +2107,13 @@ class TestClient(unittest.TestCase):
|
||||
expected_call["flag_keys_to_evaluate"] = expected_flag_keys
|
||||
|
||||
patch_flags.assert_called_with(
|
||||
"random_key", "https://us.i.posthog.com", timeout=3, **expected_call
|
||||
"random_key", "https://us.i.insights.hanzo.ai", timeout=3, **expected_call
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_device_id_from_context_is_used_in_flags_request(self, patch_flags):
|
||||
"""Test that device_id from context is used in flags request when not explicitly provided."""
|
||||
from posthog.contexts import new_context, set_context_device_id
|
||||
from hanzo_insights.contexts import new_context, set_context_device_id
|
||||
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {
|
||||
@@ -2131,7 +2131,7 @@ class TestClient(unittest.TestCase):
|
||||
client.get_feature_flag("random_key", "some_id")
|
||||
patch_flags.assert_called_with(
|
||||
"random_key",
|
||||
"https://us.i.posthog.com",
|
||||
"https://us.i.insights.hanzo.ai",
|
||||
timeout=3,
|
||||
distinct_id="some_id",
|
||||
groups={},
|
||||
@@ -2151,7 +2151,7 @@ class TestClient(unittest.TestCase):
|
||||
)
|
||||
patch_flags.assert_called_with(
|
||||
"random_key",
|
||||
"https://us.i.posthog.com",
|
||||
"https://us.i.insights.hanzo.ai",
|
||||
timeout=3,
|
||||
distinct_id="some_id",
|
||||
groups={},
|
||||
@@ -2217,8 +2217,8 @@ class TestClient(unittest.TestCase):
|
||||
distro_info,
|
||||
):
|
||||
"""Test that we can mock platform and sys for testing system_context"""
|
||||
with mock.patch("posthog.utils.platform") as mock_platform:
|
||||
with mock.patch("posthog.utils.sys") as mock_sys:
|
||||
with mock.patch("hanzo_insights.utils.platform") as mock_platform:
|
||||
with mock.patch("hanzo_insights.utils.sys") as mock_sys:
|
||||
# Set up common mocks
|
||||
mock_platform.python_implementation.return_value = expected_runtime
|
||||
mock_sys.version_info = version_info
|
||||
@@ -2234,15 +2234,15 @@ class TestClient(unittest.TestCase):
|
||||
if sys_platform == "linux":
|
||||
# Directly patch the get_os_info function to return our expected values
|
||||
with mock.patch(
|
||||
"posthog.utils.get_os_info",
|
||||
"hanzo_insights.utils.get_os_info",
|
||||
return_value=(expected_os, expected_os_version),
|
||||
):
|
||||
from posthog.utils import system_context
|
||||
from hanzo_insights.utils import system_context
|
||||
|
||||
context = system_context()
|
||||
else:
|
||||
# Get system context for non-Linux platforms
|
||||
from posthog.utils import system_context
|
||||
from hanzo_insights.utils import system_context
|
||||
|
||||
context = system_context()
|
||||
|
||||
@@ -2256,7 +2256,7 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
assert context == expected_context
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_decide_returns_normalized_decide_response(self, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {
|
||||
@@ -2311,7 +2311,7 @@ class TestClient(unittest.TestCase):
|
||||
}
|
||||
|
||||
def test_set_context_session_with_capture(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
with new_context():
|
||||
set_context_session("context-session-123")
|
||||
@@ -2334,7 +2334,7 @@ class TestClient(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_set_context_session_with_page_explicit_properties(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
with new_context():
|
||||
set_context_session("page-explicit-session-789")
|
||||
@@ -2360,9 +2360,9 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
def test_set_context_session_override_in_capture(self):
|
||||
"""Test that explicit session ID overrides context session ID in capture"""
|
||||
from posthog.contexts import new_context, set_context_session
|
||||
from hanzo_insights.contexts import new_context, set_context_session
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
with new_context():
|
||||
set_context_session("context-session-override")
|
||||
@@ -2387,8 +2387,8 @@ class TestClient(unittest.TestCase):
|
||||
msg["properties"]["$session_id"], "explicit-session-override"
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_enable_local_evaluation_false_disables_poller(
|
||||
self, patch_get, patch_poller
|
||||
):
|
||||
@@ -2425,8 +2425,8 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(len(client.feature_flags), 1)
|
||||
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_enable_local_evaluation_true_starts_poller(self, patch_get, patch_poller):
|
||||
"""Test that when enable_local_evaluation=True (default), the poller is started"""
|
||||
patch_get.return_value = GetResponse(
|
||||
@@ -2460,7 +2460,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(len(client.feature_flags), 1)
|
||||
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
|
||||
|
||||
@mock.patch("posthog.client.remote_config")
|
||||
@mock.patch("hanzo_insights.client.remote_config")
|
||||
def test_get_remote_config_payload_works_without_poller(self, patch_remote_config):
|
||||
"""Test that get_remote_config_payload works without local evaluation enabled"""
|
||||
patch_remote_config.return_value = {"test": "payload"}
|
||||
@@ -2572,7 +2572,7 @@ class TestClient(unittest.TestCase):
|
||||
client._parse_send_feature_flags(None)
|
||||
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_with_send_feature_flags_flag_keys_filter(self, patch_flags):
|
||||
"""Test that SendFeatureFlagsOptions with flag_keys_filter only evaluates specified flags"""
|
||||
# When flag_keys_to_evaluate is provided, the API should only return the requested flags
|
||||
@@ -2583,7 +2583,7 @@ class TestClient(unittest.TestCase):
|
||||
}
|
||||
}
|
||||
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(
|
||||
FAKE_TEST_API_KEY,
|
||||
on_error=self.set_fail,
|
||||
@@ -2619,7 +2619,7 @@ class TestClient(unittest.TestCase):
|
||||
# flag2 should not be included since it wasn't requested
|
||||
self.assertNotIn("$feature/flag2", msg["properties"])
|
||||
|
||||
@mock.patch("posthog.client.batch_post")
|
||||
@mock.patch("hanzo_insights.client.batch_post")
|
||||
def test_get_feature_flag_result_with_empty_string_payload(self, patch_batch_post):
|
||||
"""Test that get_feature_flag_result returns a FeatureFlagResult when payload is empty string"""
|
||||
client = Client(
|
||||
@@ -2670,7 +2670,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(result.get_value(), "empty-variant")
|
||||
self.assertEqual(result.payload, "") # Should be empty string, not None
|
||||
|
||||
@mock.patch("posthog.client.batch_post")
|
||||
@mock.patch("hanzo_insights.client.batch_post")
|
||||
def test_get_all_flags_and_payloads_with_empty_string(self, patch_batch_post):
|
||||
"""Test that get_all_flags_and_payloads includes flags with empty string payloads"""
|
||||
client = Client(
|
||||
@@ -2727,7 +2727,7 @@ class TestClient(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_context_tags_added(self):
|
||||
with mock.patch("posthog.client.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.client.batch_post") as mock_post:
|
||||
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
||||
|
||||
with new_context():
|
||||
@@ -2739,7 +2739,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(msg["properties"]["$context_tags"], ["random_tag"])
|
||||
|
||||
@mock.patch(
|
||||
"posthog.client.Client._enqueue", side_effect=Exception("Unexpected error")
|
||||
"hanzo_insights.client.Client._enqueue", side_effect=Exception("Unexpected error")
|
||||
)
|
||||
def test_methods_handle_exceptions(self, mock_enqueue):
|
||||
"""Test that all decorated methods handle exceptions gracefully."""
|
||||
@@ -2760,7 +2760,7 @@ class TestClient(unittest.TestCase):
|
||||
self.assertEqual(result, None)
|
||||
|
||||
@mock.patch(
|
||||
"posthog.client.Client._enqueue", side_effect=Exception("Expected error")
|
||||
"hanzo_insights.client.Client._enqueue", side_effect=Exception("Expected error")
|
||||
)
|
||||
def test_debug_flag_re_raises_exceptions(self, mock_enqueue):
|
||||
"""Test that methods re-raise exceptions when debug=True."""
|
||||
@@ -11,9 +11,9 @@ try:
|
||||
except ImportError:
|
||||
from Queue import Queue
|
||||
|
||||
from posthog.consumer import MAX_MSG_SIZE, Consumer
|
||||
from posthog.request import APIError
|
||||
from posthog.test.test_utils import TEST_API_KEY
|
||||
from hanzo_insights.consumer import MAX_MSG_SIZE, Consumer
|
||||
from hanzo_insights.request import APIError
|
||||
from hanzo_insights.test.test_utils import TEST_API_KEY
|
||||
|
||||
|
||||
def _track_event(event_name: str = "python event") -> dict[str, str]:
|
||||
@@ -60,7 +60,7 @@ class TestConsumer(unittest.TestCase):
|
||||
q = Queue()
|
||||
flush_interval = 0.3
|
||||
consumer = Consumer(q, TEST_API_KEY, flush_at=10, flush_interval=flush_interval)
|
||||
with mock.patch("posthog.consumer.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.consumer.batch_post") as mock_post:
|
||||
consumer.start()
|
||||
for i in range(3):
|
||||
q.put(_track_event("python event %d" % i))
|
||||
@@ -76,7 +76,7 @@ class TestConsumer(unittest.TestCase):
|
||||
consumer = Consumer(
|
||||
q, TEST_API_KEY, flush_at=flush_at, flush_interval=flush_interval
|
||||
)
|
||||
with mock.patch("posthog.consumer.batch_post") as mock_post:
|
||||
with mock.patch("hanzo_insights.consumer.batch_post") as mock_post:
|
||||
consumer.start()
|
||||
for i in range(flush_at * 2):
|
||||
q.put(_track_event("python event %d" % i))
|
||||
@@ -99,7 +99,7 @@ class TestConsumer(unittest.TestCase):
|
||||
|
||||
consumer = Consumer(None, TEST_API_KEY, retries=retries)
|
||||
with mock.patch(
|
||||
"posthog.consumer.batch_post", mock.Mock(side_effect=mock_post)
|
||||
"hanzo_insights.consumer.batch_post", mock.Mock(side_effect=mock_post)
|
||||
):
|
||||
if exception_count <= retries:
|
||||
consumer.request([_track_event()])
|
||||
@@ -159,7 +159,7 @@ class TestConsumer(unittest.TestCase):
|
||||
return res
|
||||
|
||||
with mock.patch(
|
||||
"posthog.request._session.post", side_effect=mock_post_fn
|
||||
"hanzo_insights.request._session.post", side_effect=mock_post_fn
|
||||
) as mock_post:
|
||||
consumer.start()
|
||||
for _ in range(0, n_msgs + 2):
|
||||
@@ -178,8 +178,8 @@ class TestConsumer(unittest.TestCase):
|
||||
|
||||
consumer = Consumer(None, TEST_API_KEY, retries=3)
|
||||
with (
|
||||
mock.patch("posthog.consumer.batch_post", side_effect=mock_post),
|
||||
mock.patch("posthog.consumer.time.sleep") as mock_sleep,
|
||||
mock.patch("hanzo_insights.consumer.batch_post", side_effect=mock_post),
|
||||
mock.patch("hanzo_insights.consumer.time.sleep") as mock_sleep,
|
||||
):
|
||||
consumer.request([_track_event()])
|
||||
mock_sleep.assert_called_once_with(5.0)
|
||||
@@ -195,8 +195,8 @@ class TestConsumer(unittest.TestCase):
|
||||
|
||||
consumer = Consumer(None, TEST_API_KEY, retries=3)
|
||||
with (
|
||||
mock.patch("posthog.consumer.batch_post", side_effect=mock_post),
|
||||
mock.patch("posthog.consumer.time.sleep") as mock_sleep,
|
||||
mock.patch("hanzo_insights.consumer.batch_post", side_effect=mock_post),
|
||||
mock.patch("hanzo_insights.consumer.time.sleep") as mock_sleep,
|
||||
):
|
||||
consumer.request([_track_event()])
|
||||
self.assertEqual(
|
||||
@@ -218,8 +218,8 @@ class TestConsumer(unittest.TestCase):
|
||||
|
||||
consumer = Consumer(None, TEST_API_KEY, retries=3)
|
||||
with (
|
||||
mock.patch("posthog.consumer.batch_post", side_effect=mock_post),
|
||||
mock.patch("posthog.consumer.time.sleep"),
|
||||
mock.patch("hanzo_insights.consumer.batch_post", side_effect=mock_post),
|
||||
mock.patch("hanzo_insights.consumer.time.sleep"),
|
||||
):
|
||||
consumer.request([_track_event()])
|
||||
self.assertEqual(call_count[0], 2)
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from posthog.contexts import (
|
||||
from hanzo_insights.contexts import (
|
||||
get_tags,
|
||||
new_context,
|
||||
scoped,
|
||||
@@ -66,7 +66,7 @@ class TestContexts(unittest.TestCase):
|
||||
# Back to level 1
|
||||
assert get_tags() == {"level1": "value1"}
|
||||
|
||||
@patch("posthog.capture_exception")
|
||||
@patch("hanzo_insights.capture_exception")
|
||||
def test_scoped_decorator_success(self, mock_capture):
|
||||
@scoped()
|
||||
def successful_function(x, y):
|
||||
@@ -85,7 +85,7 @@ class TestContexts(unittest.TestCase):
|
||||
# Context should be cleared after function execution
|
||||
assert get_tags() == {}
|
||||
|
||||
@patch("posthog.capture_exception")
|
||||
@patch("hanzo_insights.capture_exception")
|
||||
def test_scoped_decorator_exception(self, mock_capture):
|
||||
test_exception = ValueError("Test exception")
|
||||
|
||||
@@ -111,7 +111,7 @@ class TestContexts(unittest.TestCase):
|
||||
# Context should be cleared after function execution
|
||||
assert get_tags() == {}
|
||||
|
||||
@patch("posthog.capture_exception")
|
||||
@patch("hanzo_insights.capture_exception")
|
||||
def test_new_context_exception_handling(self, mock_capture):
|
||||
test_exception = RuntimeError("Context exception")
|
||||
|
||||
+53
-53
@@ -10,8 +10,8 @@ def test_excepthook(tmpdir):
|
||||
app.write(
|
||||
dedent(
|
||||
"""
|
||||
from posthog import Posthog
|
||||
posthog = Posthog('phc_x', host='https://eu.i.posthog.com', enable_exception_autocapture=True, debug=True, on_error=lambda e, batch: print('error handling batch: ', e, batch))
|
||||
from hanzo_insights import Insights
|
||||
client = Insights('phc_x', host='https://eu.i.insights.hanzo.ai', enable_exception_autocapture=True, debug=True, on_error=lambda e, batch: print('error handling batch: ', e, batch))
|
||||
|
||||
# frame_value = "LOL"
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_excepthook(tmpdir):
|
||||
|
||||
assert b"ZeroDivisionError" in output
|
||||
assert b"LOL" in output
|
||||
assert b"DEBUG:posthog:data uploaded successfully" in output
|
||||
assert b"DEBUG:hanzo_insights:data uploaded successfully" in output
|
||||
assert (
|
||||
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
|
||||
@@ -40,14 +40,14 @@ def test_code_variables_capture(tmpdir):
|
||||
dedent(
|
||||
"""
|
||||
import os
|
||||
from posthog import Posthog
|
||||
from hanzo_insights import Insights
|
||||
|
||||
class UnserializableObject:
|
||||
pass
|
||||
|
||||
posthog = Posthog(
|
||||
client = Insights(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
host='https://eu.i.insights.hanzo.ai',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=True,
|
||||
@@ -118,29 +118,29 @@ def test_code_variables_capture(tmpdir):
|
||||
assert b"'my_bool': 'True'" in output
|
||||
assert b'"my_dict": "{\\"name\\": \\"test\\", \\"value\\": 123}"' in output
|
||||
assert (
|
||||
b'{\\"safe_key\\": \\"safe_value\\", \\"password\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"other_key\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\"}'
|
||||
b'{\\"safe_key\\": \\"safe_value\\", \\"password\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"other_key\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\"}'
|
||||
in output
|
||||
)
|
||||
assert (
|
||||
b'{\\"level1\\": {\\"level2\\": {\\"api_key\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"data\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"safe\\": \\"visible\\"}}}'
|
||||
b'{\\"level1\\": {\\"level2\\": {\\"api_key\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"data\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"safe\\": \\"visible\\"}}}'
|
||||
in output
|
||||
)
|
||||
assert (
|
||||
b'[\\"safe_item\\", \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"another_safe\\"]'
|
||||
b'[\\"safe_item\\", \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"another_safe\\"]'
|
||||
in output
|
||||
)
|
||||
assert (
|
||||
b'[\\"tuple_safe\\", \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"tuple_also_safe\\"]'
|
||||
b'[\\"tuple_safe\\", \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"tuple_also_safe\\"]'
|
||||
in output
|
||||
)
|
||||
assert (
|
||||
b'[{\\"id\\": 1, \\"password\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\"}, {\\"id\\": 2, \\"value\\": \\"safe_value\\"}]'
|
||||
b'[{\\"id\\": 1, \\"password\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\"}, {\\"id\\": 2, \\"value\\": \\"safe_value\\"}]'
|
||||
in output
|
||||
)
|
||||
assert b"<__main__.UnserializableObject object at" in output
|
||||
assert b"'my_password': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
|
||||
assert b"'my_password': '$$_insights_redacted_based_on_masking_rules_$$'" in output
|
||||
assert (
|
||||
b"'my_innocent_var': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
|
||||
b"'my_innocent_var': '$$_insights_redacted_based_on_masking_rules_$$'" in output
|
||||
)
|
||||
assert b"'__should_be_ignored':" not in output
|
||||
|
||||
@@ -160,12 +160,12 @@ def test_code_variables_context_override(tmpdir):
|
||||
dedent(
|
||||
"""
|
||||
import os
|
||||
import posthog
|
||||
from posthog import Posthog
|
||||
import hanzo_insights
|
||||
from hanzo_insights import Insights
|
||||
|
||||
posthog_client = Posthog(
|
||||
insights_client = Insights(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
host='https://eu.i.insights.hanzo.ai',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=False,
|
||||
@@ -178,10 +178,10 @@ def test_code_variables_context_override(tmpdir):
|
||||
|
||||
1/0
|
||||
|
||||
with posthog.new_context(client=posthog_client):
|
||||
posthog.set_capture_exception_code_variables_context(True)
|
||||
posthog.set_code_variables_mask_patterns_context([r"(?i).*bank.*"])
|
||||
posthog.set_code_variables_ignore_patterns_context([])
|
||||
with hanzo_insights.new_context(client=insights_client):
|
||||
hanzo_insights.set_capture_exception_code_variables_context(True)
|
||||
hanzo_insights.set_code_variables_mask_patterns_context([r"(?i).*bank.*"])
|
||||
hanzo_insights.set_code_variables_ignore_patterns_context([])
|
||||
|
||||
process_data()
|
||||
"""
|
||||
@@ -195,7 +195,7 @@ def test_code_variables_context_override(tmpdir):
|
||||
|
||||
assert b"ZeroDivisionError" in output
|
||||
assert b"code_variables" in output
|
||||
assert b"'bank': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
|
||||
assert b"'bank': '$$_insights_redacted_based_on_masking_rules_$$'" in output
|
||||
assert b"'__dunder_var': 'should_be_visible'" in output
|
||||
|
||||
|
||||
@@ -205,11 +205,11 @@ def test_code_variables_size_limiter(tmpdir):
|
||||
dedent(
|
||||
"""
|
||||
import os
|
||||
from posthog import Posthog
|
||||
from hanzo_insights import Insights
|
||||
|
||||
posthog = Posthog(
|
||||
client = Insights(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
host='https://eu.i.insights.hanzo.ai',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=True,
|
||||
@@ -299,11 +299,11 @@ def test_code_variables_disabled_capture(tmpdir):
|
||||
dedent(
|
||||
"""
|
||||
import os
|
||||
from posthog import Posthog
|
||||
from hanzo_insights import Insights
|
||||
|
||||
posthog = Posthog(
|
||||
client = Insights(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
host='https://eu.i.insights.hanzo.ai',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=False,
|
||||
@@ -340,12 +340,12 @@ def test_code_variables_enabled_then_disabled_in_context(tmpdir):
|
||||
dedent(
|
||||
"""
|
||||
import os
|
||||
import posthog
|
||||
from posthog import Posthog
|
||||
import hanzo_insights
|
||||
from hanzo_insights import Insights
|
||||
|
||||
posthog_client = Posthog(
|
||||
insights_client = Insights(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
host='https://eu.i.insights.hanzo.ai',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=True,
|
||||
@@ -358,8 +358,8 @@ def test_code_variables_enabled_then_disabled_in_context(tmpdir):
|
||||
|
||||
1/0
|
||||
|
||||
with posthog.new_context(client=posthog_client):
|
||||
posthog.set_capture_exception_code_variables_context(False)
|
||||
with hanzo_insights.new_context(client=insights_client):
|
||||
hanzo_insights.set_capture_exception_code_variables_context(False)
|
||||
|
||||
process_data()
|
||||
"""
|
||||
@@ -388,15 +388,15 @@ def test_code_variables_repr_fallback(tmpdir):
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from fractions import Fraction
|
||||
from posthog import Posthog
|
||||
from hanzo_insights import Insights
|
||||
|
||||
class CustomReprClass:
|
||||
def __repr__(self):
|
||||
return '<CustomReprClass: custom representation>'
|
||||
|
||||
posthog = Posthog(
|
||||
client = Insights(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
host='https://eu.i.insights.hanzo.ai',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=True,
|
||||
@@ -458,11 +458,11 @@ def test_code_variables_too_long_string_value_replaced(tmpdir):
|
||||
dedent(
|
||||
"""
|
||||
import os
|
||||
from posthog import Posthog
|
||||
from hanzo_insights import Insights
|
||||
|
||||
posthog = Posthog(
|
||||
client = Insights(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
host='https://eu.i.insights.hanzo.ai',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=True,
|
||||
@@ -491,9 +491,9 @@ def test_code_variables_too_long_string_value_replaced(tmpdir):
|
||||
|
||||
assert "'short_value': 'I am short'" in output
|
||||
|
||||
assert "$$_posthog_value_too_long_$$" in output
|
||||
assert "$$_insights_value_too_long_$$" in output
|
||||
|
||||
assert "'long_blob': '$$_posthog_value_too_long_$$'" in output
|
||||
assert "'long_blob': '$$_insights_value_too_long_$$'" in output
|
||||
|
||||
|
||||
def test_code_variables_too_long_string_in_nested_dict(tmpdir):
|
||||
@@ -502,11 +502,11 @@ def test_code_variables_too_long_string_in_nested_dict(tmpdir):
|
||||
dedent(
|
||||
"""
|
||||
import os
|
||||
from posthog import Posthog
|
||||
from hanzo_insights import Insights
|
||||
|
||||
posthog = Posthog(
|
||||
client = Insights(
|
||||
'phc_x',
|
||||
host='https://eu.i.posthog.com',
|
||||
host='https://eu.i.insights.hanzo.ai',
|
||||
debug=True,
|
||||
enable_exception_autocapture=True,
|
||||
capture_exception_code_variables=True,
|
||||
@@ -541,13 +541,13 @@ def test_code_variables_too_long_string_in_nested_dict(tmpdir):
|
||||
assert "short_val" in output
|
||||
assert "ok" in output
|
||||
|
||||
assert "$$_posthog_value_too_long_$$" in output
|
||||
assert "$$_insights_value_too_long_$$" in output
|
||||
assert "y" * 1000 not in output
|
||||
assert "z" * 1000 not in output
|
||||
|
||||
|
||||
def test_mask_sensitive_data_too_long_dict_key():
|
||||
from posthog.exception_utils import (
|
||||
from hanzo_insights.exception_utils import (
|
||||
CODE_VARIABLES_TOO_LONG_VALUE,
|
||||
_compile_patterns,
|
||||
_mask_sensitive_data,
|
||||
@@ -567,11 +567,11 @@ def test_mask_sensitive_data_too_long_dict_key():
|
||||
assert result["short"] == "visible"
|
||||
# This then gets shortened by the JSON truncation at 1024 chars anyways so no worries
|
||||
assert result["k" * 20000] == CODE_VARIABLES_TOO_LONG_VALUE
|
||||
assert result["password"] == "$$_posthog_redacted_based_on_masking_rules_$$"
|
||||
assert result["password"] == "$$_insights_redacted_based_on_masking_rules_$$"
|
||||
|
||||
|
||||
def test_mask_sensitive_data_circular_ref():
|
||||
from posthog.exception_utils import _compile_patterns, _mask_sensitive_data
|
||||
from hanzo_insights.exception_utils import _compile_patterns, _mask_sensitive_data
|
||||
|
||||
compiled_mask = _compile_patterns([r"(?i)password"])
|
||||
|
||||
@@ -593,7 +593,7 @@ def test_mask_sensitive_data_circular_ref():
|
||||
|
||||
|
||||
def test_compile_patterns_fast_path_and_regex_fallback():
|
||||
from posthog.exception_utils import _compile_patterns, _pattern_matches
|
||||
from hanzo_insights.exception_utils import _compile_patterns, _pattern_matches
|
||||
|
||||
# Simple case-insensitive patterns should become substrings
|
||||
simple_only = _compile_patterns([r"(?i)password", r"(?i)token", r"(?i)jwt"])
|
||||
@@ -642,7 +642,7 @@ def test_compile_patterns_fast_path_and_regex_fallback():
|
||||
|
||||
|
||||
def test_mask_sensitive_data_large_dict_replaced():
|
||||
from posthog.exception_utils import (
|
||||
from hanzo_insights.exception_utils import (
|
||||
CODE_VARIABLES_TOO_LONG_VALUE,
|
||||
_compile_patterns,
|
||||
_mask_sensitive_data,
|
||||
@@ -658,7 +658,7 @@ def test_mask_sensitive_data_large_dict_replaced():
|
||||
|
||||
|
||||
def test_mask_sensitive_data_large_list_replaced():
|
||||
from posthog.exception_utils import (
|
||||
from hanzo_insights.exception_utils import (
|
||||
CODE_VARIABLES_TOO_LONG_VALUE,
|
||||
_compile_patterns,
|
||||
_mask_sensitive_data,
|
||||
@@ -674,7 +674,7 @@ def test_mask_sensitive_data_large_list_replaced():
|
||||
|
||||
|
||||
def test_mask_sensitive_data_large_tuple_replaced():
|
||||
from posthog.exception_utils import (
|
||||
from hanzo_insights.exception_utils import (
|
||||
CODE_VARIABLES_TOO_LONG_VALUE,
|
||||
_compile_patterns,
|
||||
_mask_sensitive_data,
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from posthog.types import FeatureFlag, FlagMetadata, FlagReason, LegacyFlagMetadata
|
||||
from hanzo_insights.types import FeatureFlag, FlagMetadata, FlagReason, LegacyFlagMetadata
|
||||
|
||||
|
||||
class TestFeatureFlag(unittest.TestCase):
|
||||
+27
-27
@@ -2,9 +2,9 @@ import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from posthog.client import Client
|
||||
from posthog.test.test_utils import FAKE_TEST_API_KEY
|
||||
from posthog.types import (
|
||||
from hanzo_insights.client import Client
|
||||
from hanzo_insights.test.test_utils import FAKE_TEST_API_KEY
|
||||
from hanzo_insights.types import (
|
||||
FeatureFlag,
|
||||
FeatureFlagError,
|
||||
FeatureFlagResult,
|
||||
@@ -328,7 +328,7 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_boolean_decide(self, patch_capture, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
@@ -375,7 +375,7 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
captured_properties = patch_capture.call_args[1]["properties"]
|
||||
self.assertNotIn("$feature_flag_error", captured_properties)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_variant_decide(self, patch_capture, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
@@ -421,7 +421,7 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
captured_properties = patch_capture.call_args[1]["properties"]
|
||||
self.assertNotIn("$feature_flag_error", captured_properties)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_unknown_flag(self, patch_capture, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
@@ -461,7 +461,7 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_with_errors_while_computing_flags(
|
||||
self, patch_capture, patch_flags
|
||||
@@ -507,7 +507,7 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_flag_not_in_response(
|
||||
self, patch_capture, patch_flags
|
||||
@@ -549,7 +549,7 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_errors_computing_and_flag_missing(
|
||||
self, patch_capture, patch_flags
|
||||
@@ -585,7 +585,7 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_unknown_error(self, patch_capture, patch_flags):
|
||||
"""Test that unexpected exceptions are captured as unknown_error."""
|
||||
@@ -608,11 +608,11 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_timeout_error(self, patch_capture, patch_flags):
|
||||
"""Test that timeout errors are captured specifically."""
|
||||
from posthog.request import RequestsTimeout
|
||||
from hanzo_insights.request import RequestsTimeout
|
||||
|
||||
patch_flags.side_effect = RequestsTimeout("Request timed out")
|
||||
|
||||
@@ -633,11 +633,11 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_connection_error(self, patch_capture, patch_flags):
|
||||
"""Test that connection errors are captured specifically."""
|
||||
from posthog.request import RequestsConnectionError
|
||||
from hanzo_insights.request import RequestsConnectionError
|
||||
|
||||
patch_flags.side_effect = RequestsConnectionError("Connection refused")
|
||||
|
||||
@@ -658,11 +658,11 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_api_error(self, patch_capture, patch_flags):
|
||||
"""Test that API errors include the status code."""
|
||||
from posthog.request import APIError
|
||||
from hanzo_insights.request import APIError
|
||||
|
||||
patch_flags.side_effect = APIError(500, "Internal server error")
|
||||
|
||||
@@ -683,11 +683,11 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_get_feature_flag_result_quota_limited(self, patch_capture, patch_flags):
|
||||
"""Test that quota limit errors are captured specifically."""
|
||||
from posthog.request import QuotaLimitError
|
||||
from hanzo_insights.request import QuotaLimitError
|
||||
|
||||
patch_flags.side_effect = QuotaLimitError(429, "Rate limit exceeded")
|
||||
|
||||
@@ -712,7 +712,7 @@ class TestGetFeatureFlagResult(unittest.TestCase):
|
||||
class TestFeatureFlagErrorWithStaleCacheFallback(unittest.TestCase):
|
||||
"""Tests for stale cache fallback behavior when flag evaluation fails.
|
||||
|
||||
When the PostHog API is unavailable (timeout, connection error, etc.), the SDK
|
||||
When the Insights API is unavailable (timeout, connection error, etc.), the SDK
|
||||
falls back to stale cached flag values if available. These tests verify that:
|
||||
1. The stale cached value is returned when an error occurs
|
||||
2. The $feature_flag_error property is still set (for debugging)
|
||||
@@ -741,11 +741,11 @@ class TestFeatureFlagErrorWithStaleCacheFallback(unittest.TestCase):
|
||||
flag_definition_version=self.client.flag_definition_version,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_timeout_error_returns_stale_cached_value(self, patch_capture, patch_flags):
|
||||
"""Test that timeout errors return stale cached value when available."""
|
||||
from posthog.request import RequestsTimeout
|
||||
from hanzo_insights.request import RequestsTimeout
|
||||
|
||||
# Pre-populate cache with a flag result
|
||||
cached_result = FeatureFlagResult.from_value_and_payload(
|
||||
@@ -779,13 +779,13 @@ class TestFeatureFlagErrorWithStaleCacheFallback(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_connection_error_returns_stale_cached_value(
|
||||
self, patch_capture, patch_flags
|
||||
):
|
||||
"""Test that connection errors return stale cached value when available."""
|
||||
from posthog.request import RequestsConnectionError
|
||||
from hanzo_insights.request import RequestsConnectionError
|
||||
|
||||
# Pre-populate cache with a boolean flag result
|
||||
cached_result = FeatureFlagResult.from_value_and_payload("my-flag", True, None)
|
||||
@@ -816,11 +816,11 @@ class TestFeatureFlagErrorWithStaleCacheFallback(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_api_error_returns_stale_cached_value(self, patch_capture, patch_flags):
|
||||
"""Test that API errors return stale cached value when available."""
|
||||
from posthog.request import APIError
|
||||
from hanzo_insights.request import APIError
|
||||
|
||||
# Pre-populate cache
|
||||
cached_result = FeatureFlagResult.from_value_and_payload(
|
||||
@@ -852,11 +852,11 @@ class TestFeatureFlagErrorWithStaleCacheFallback(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch.object(Client, "capture")
|
||||
def test_error_without_cache_returns_none(self, patch_capture, patch_flags):
|
||||
"""Test that errors return None when no stale cache is available."""
|
||||
from posthog.request import RequestsTimeout
|
||||
from hanzo_insights.request import RequestsTimeout
|
||||
|
||||
# Do NOT populate cache - no fallback available
|
||||
|
||||
@@ -5,14 +5,14 @@ import mock
|
||||
from dateutil import parser, tz
|
||||
from freezegun import freeze_time
|
||||
|
||||
from posthog.client import Client
|
||||
from posthog.feature_flags import (
|
||||
from hanzo_insights.client import Client
|
||||
from hanzo_insights.feature_flags import (
|
||||
InconclusiveMatchError,
|
||||
match_property,
|
||||
relative_date_parse_for_feature_flag_matching,
|
||||
)
|
||||
from posthog.request import APIError, GetResponse
|
||||
from posthog.test.test_utils import FAKE_TEST_API_KEY
|
||||
from hanzo_insights.request import APIError, GetResponse
|
||||
from hanzo_insights.test.test_utils import FAKE_TEST_API_KEY
|
||||
|
||||
|
||||
class TestLocalEvaluation(unittest.TestCase):
|
||||
@@ -35,7 +35,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.failed = False
|
||||
self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_flag_person_properties(self, patch_get):
|
||||
self.client.feature_flags = [
|
||||
{
|
||||
@@ -137,8 +137,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_flag_group_properties(self, patch_get, patch_flags):
|
||||
self.client.feature_flags = [
|
||||
{
|
||||
@@ -254,8 +254,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
group_properties={},
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_flag_with_complex_definition(self, patch_get, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {"complex-flag": "decide-fallback-value"}
|
||||
@@ -384,8 +384,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_flags_fallback_to_flags(self, patch_get, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {"beta-feature": "alakazam", "beta-feature2": "alakazam2"}
|
||||
@@ -450,8 +450,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(feature_flag_match, "alakazam2")
|
||||
self.assertEqual(patch_flags.call_count, 2)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_flags_dont_fallback_to_flags_when_only_local_evaluation_is_true(
|
||||
self, patch_get, patch_flags
|
||||
):
|
||||
@@ -534,8 +534,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_flag_never_returns_undefined_during_regular_evaluation(
|
||||
self, patch_get, patch_flags
|
||||
):
|
||||
@@ -569,8 +569,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertFalse(client.feature_enabled("beta-feature2", "some-distinct-id"))
|
||||
self.assertEqual(patch_flags.call_count, 2)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_flag_return_none_when_decide_errors_out(
|
||||
self, patch_get, patch_flags
|
||||
):
|
||||
@@ -585,7 +585,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertIsNone(client.feature_enabled("beta-feature2", "some-distinct-id"))
|
||||
self.assertEqual(patch_flags.call_count, 2)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_experience_continuity_flag_not_evaluated_locally(self, patch_flags):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {"beta-feature": "decide-fallback-value"}
|
||||
@@ -617,7 +617,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 1)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_with_fallback(self, patch_flags, patch_capture):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {
|
||||
@@ -685,7 +685,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_and_payloads_with_fallback(self, patch_flags, patch_capture):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"},
|
||||
@@ -758,7 +758,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_with_fallback_empty_local_flags(
|
||||
self, patch_flags, patch_capture
|
||||
):
|
||||
@@ -776,7 +776,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_and_payloads_with_fallback_empty_local_flags(
|
||||
self, patch_flags, patch_capture
|
||||
):
|
||||
@@ -795,7 +795,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_with_no_fallback(self, patch_flags, patch_capture):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"}
|
||||
@@ -841,7 +841,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_and_payloads_with_no_fallback(
|
||||
self, patch_flags, patch_capture
|
||||
):
|
||||
@@ -894,7 +894,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_with_fallback_but_only_local_evaluation_set(
|
||||
self, patch_flags, patch_capture
|
||||
):
|
||||
@@ -956,7 +956,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_and_payloads_with_fallback_but_only_local_evaluation_set(
|
||||
self, patch_flags, patch_capture
|
||||
):
|
||||
@@ -1033,7 +1033,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_compute_inactive_flags_locally(self, patch_flags, patch_capture):
|
||||
client = self.client
|
||||
client.feature_flags = [
|
||||
@@ -1115,8 +1115,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_flags_local_evaluation_None_values(self, patch_get, patch_flags):
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
client.feature_flags = [
|
||||
@@ -1190,8 +1190,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
self.assertEqual(feature_flag_match, True)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_flags_local_evaluation_for_cohorts(self, patch_get, patch_flags):
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
client.feature_flags = [
|
||||
@@ -1276,8 +1276,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
self.assertEqual(patch_get.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_flags_local_evaluation_for_negated_cohorts(
|
||||
self, patch_get, patch_flags
|
||||
):
|
||||
@@ -1376,9 +1376,9 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
self.assertEqual(patch_get.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.feature_flags.log")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.feature_flags.log")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_flags_with_flag_dependencies(
|
||||
self, patch_get, patch_flags, mock_log
|
||||
):
|
||||
@@ -1439,8 +1439,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 2) # Called twice now
|
||||
self.assertEqual(patch_get.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_flag_dependencies_simple_chain(self, patch_get, patch_flags):
|
||||
"""Test basic flag dependency: flag-b depends on flag-a"""
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
@@ -1506,8 +1506,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(result, False)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_flag_dependencies_circular_dependency(self, patch_get, patch_flags):
|
||||
"""Test circular dependency handling: flag-a depends on flag-b, flag-b depends on flag-a"""
|
||||
# Mock remote flags call to return empty for these flags (fallback returns None)
|
||||
@@ -1568,8 +1568,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
result_b = client.get_feature_flag("flag-b", "test-user")
|
||||
self.assertIsNone(result_b)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_flag_dependencies_missing_flag(self, patch_get, patch_flags):
|
||||
"""Test handling of missing flag dependency"""
|
||||
# Mock remote flags call to return empty for this flag (fallback returns None)
|
||||
@@ -1605,8 +1605,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
result = client.get_feature_flag("flag-a", "test-user")
|
||||
self.assertIsNone(result)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_flag_dependencies_complex_chain(self, patch_get, patch_flags):
|
||||
"""Test complex dependency chain: flag-d -> flag-c -> [flag-a, flag-b]"""
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
@@ -1701,8 +1701,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
result = client.get_feature_flag("flag-d", "test-user")
|
||||
self.assertEqual(result, False)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_flag_dependencies_mixed_conditions(self, patch_get, patch_flags):
|
||||
"""Test flag dependency mixed with other property conditions"""
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
@@ -1776,8 +1776,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(result, False)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_flag_dependencies_malformed_chain(self, patch_get, patch_flags):
|
||||
"""Test handling of malformed dependency chains"""
|
||||
# Mock remote flags call to return empty for this flag (fallback returns None)
|
||||
@@ -1829,7 +1829,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
def test_flag_dependencies_without_context_raises_inconclusive(self):
|
||||
"""Test that missing flags_by_key raises InconclusiveMatchError"""
|
||||
from posthog.feature_flags import (
|
||||
from hanzo_insights.feature_flags import (
|
||||
evaluate_flag_dependency,
|
||||
InconclusiveMatchError,
|
||||
)
|
||||
@@ -1856,8 +1856,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertIn("Cannot evaluate flag dependency", str(cm.exception))
|
||||
self.assertIn("some-flag", str(cm.exception))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_multi_level_multivariate_dependency_chain(self, patch_get, patch_flags):
|
||||
"""Test multi-level multivariate dependency chain: dependent-flag -> intermediate-flag -> leaf-flag"""
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
@@ -2093,7 +2093,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
def test_matches_dependency_value(self):
|
||||
"""Test the matches_dependency_value function logic"""
|
||||
from posthog.feature_flags import matches_dependency_value
|
||||
from hanzo_insights.feature_flags import matches_dependency_value
|
||||
|
||||
# String variant matches string exactly (case-sensitive)
|
||||
self.assertTrue(matches_dependency_value("control", "control"))
|
||||
@@ -2121,8 +2121,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertFalse(matches_dependency_value(123, "control"))
|
||||
self.assertFalse(matches_dependency_value("control", True))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_production_style_multivariate_dependency_chain(
|
||||
self, patch_get, patch_flags
|
||||
):
|
||||
@@ -2366,8 +2366,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(unknown_intermediate_result, False) # Dependency not satisfied
|
||||
self.assertEqual(unknown_root_result, False) # Chain broken
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_load_feature_flags(self, patch_get, patch_poll):
|
||||
patch_get.return_value = GetResponse(
|
||||
data={
|
||||
@@ -2403,8 +2403,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# Verify ETag is stored
|
||||
self.assertEqual(client._flags_etag, '"abc123"')
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_load_feature_flags_sends_etag_on_subsequent_requests(
|
||||
self, patch_get, patch_poll
|
||||
):
|
||||
@@ -2431,8 +2431,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
second_call_kwargs = patch_get.call_args_list[1][1]
|
||||
self.assertEqual(second_call_kwargs.get("etag"), '"initial-etag"')
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_load_feature_flags_304_not_modified(self, patch_get, patch_poll):
|
||||
"""Test that 304 Not Modified responses skip flag processing"""
|
||||
# First response with flags
|
||||
@@ -2468,8 +2468,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
|
||||
self.assertEqual(client.group_type_mapping, {"0": "company"})
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_load_feature_flags_etag_updated_on_new_response(
|
||||
self, patch_get, patch_poll
|
||||
):
|
||||
@@ -2501,8 +2501,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(client._flags_etag, '"etag-v2"')
|
||||
self.assertEqual(client.feature_flags[0]["key"], "flag-v2")
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_load_feature_flags_clears_etag_when_server_stops_sending(
|
||||
self, patch_get, patch_poll
|
||||
):
|
||||
@@ -2534,23 +2534,23 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertIsNone(client._flags_etag)
|
||||
self.assertEqual(client.feature_flags[0]["key"], "flag-v2")
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_load_feature_flags_wrong_key(self, patch_get, _patch_poll):
|
||||
patch_get.side_effect = APIError(401, "Unauthorized")
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
|
||||
with self.assertLogs("posthog", level="ERROR") as logs:
|
||||
with self.assertLogs("hanzo_insights", level="ERROR") as logs:
|
||||
client.load_feature_flags()
|
||||
self.assertEqual(
|
||||
logs.output[0],
|
||||
"ERROR:posthog:[FEATURE FLAGS] Error loading feature flags: To use feature flags, please set a valid personal_api_key. More information: https://posthog.com/docs/api/overview",
|
||||
"ERROR:hanzo_insights:[FEATURE FLAGS] Error loading feature flags: To use feature flags, please set a valid personal_api_key. More information: https://insights.hanzo.ai/docs/api/overview",
|
||||
)
|
||||
client.debug = True
|
||||
self.assertRaises(APIError, client.load_feature_flags)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_enabled_simple(self, patch_get, patch_flags):
|
||||
client = Client(FAKE_TEST_API_KEY)
|
||||
client.feature_flags = [
|
||||
@@ -2573,8 +2573,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_enabled_simple_is_false(self, patch_get, patch_flags):
|
||||
client = Client(FAKE_TEST_API_KEY)
|
||||
client.feature_flags = [
|
||||
@@ -2597,8 +2597,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertFalse(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_enabled_simple_is_true_when_rollout_is_undefined(
|
||||
self, patch_get, patch_flags
|
||||
):
|
||||
@@ -2623,7 +2623,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_enabled_simple_with_project_api_key(self, patch_get):
|
||||
client = Client(project_api_key=FAKE_TEST_API_KEY, on_error=self.set_fail)
|
||||
client.feature_flags = [
|
||||
@@ -2645,7 +2645,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
]
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_feature_enabled_request_multi_variate(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
@@ -2670,7 +2670,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_feature_enabled_simple_without_rollout_percentage(self, patch_get):
|
||||
client = Client(FAKE_TEST_API_KEY)
|
||||
client.feature_flags = [
|
||||
@@ -2690,7 +2690,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
]
|
||||
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_feature_flag(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
@@ -2723,8 +2723,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_feature_enabled_doesnt_exist(self, patch_flags, patch_poll):
|
||||
client = Client(FAKE_TEST_API_KEY)
|
||||
client.feature_flags = []
|
||||
@@ -2735,8 +2735,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
patch_flags.side_effect = APIError(401, "decide error")
|
||||
self.assertIsNone(client.feature_enabled("doesnt-exist", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_personal_api_key_doesnt_exist(self, patch_flags, patch_poll):
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
client.feature_flags = []
|
||||
@@ -2745,8 +2745,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
self.assertTrue(client.feature_enabled("feature-flag", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.Poller")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_load_feature_flags_error(self, patch_get, patch_poll):
|
||||
def raise_effect():
|
||||
raise Exception("http exception")
|
||||
@@ -2757,7 +2757,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_feature_flag_with_variant_overrides(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
@@ -2820,7 +2820,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_flag_with_clashing_variant_overrides(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
@@ -2901,7 +2901,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_flag_with_invalid_variant_overrides(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
@@ -2964,7 +2964,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_conditions_evaluated_in_order(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"order-test": "server-variant"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
@@ -3022,7 +3022,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# server not called because this can be evaluated locally
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_boolean_feature_flag_payloads_local(self, patch_flags):
|
||||
basic_flag = {
|
||||
"id": 1,
|
||||
@@ -3067,7 +3067,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_boolean_feature_flag_payload_decide(self, patch_flags, patch_capture):
|
||||
patch_flags.return_value = {
|
||||
"featureFlags": {"person-flag": True},
|
||||
@@ -3094,7 +3094,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_capture.call_count, 1)
|
||||
patch_capture.reset_mock()
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_multivariate_feature_flag_payloads(self, patch_flags):
|
||||
multivariate_flag = {
|
||||
"id": 1,
|
||||
@@ -3175,8 +3175,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_fallback_to_api_when_flag_has_static_cohort_in_multi_condition(
|
||||
self, patch_get, patch_flags
|
||||
):
|
||||
@@ -3244,7 +3244,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# Verify API was called (fallback occurred)
|
||||
self.assertEqual(patch_flags.call_count, 1)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_device_id_bucketing_uses_device_id_for_hash(self, patch_flags):
|
||||
"""
|
||||
When a flag has bucketing_identifier: "device_id", the device_id should be
|
||||
@@ -3292,7 +3292,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
match_feature_flag_properties should preserve backward compatibility when
|
||||
bucketing_value is omitted, while warning about deprecation.
|
||||
"""
|
||||
from posthog.feature_flags import match_feature_flag_properties
|
||||
from hanzo_insights.feature_flags import match_feature_flag_properties
|
||||
|
||||
flag = {
|
||||
"id": 1,
|
||||
@@ -3321,7 +3321,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
self.assertTrue(result)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_device_id_bucketing_same_device_different_users_same_result(
|
||||
self, patch_flags
|
||||
):
|
||||
@@ -3365,7 +3365,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_device_id_bucketing_fallback_when_device_id_missing(self, patch_flags):
|
||||
"""
|
||||
When a flag requires device_id for bucketing but none is provided,
|
||||
@@ -3398,7 +3398,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# API should have been called
|
||||
self.assertEqual(patch_flags.call_count, 1)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_device_id_bucketing_returns_none_when_only_evaluate_locally_and_no_device_id(
|
||||
self, patch_flags
|
||||
):
|
||||
@@ -3434,7 +3434,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
# API should NOT have been called
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_default_bucketing_identifier_uses_distinct_id(self, patch_flags):
|
||||
"""
|
||||
When bucketing_identifier is not set or is 'distinct_id', should use
|
||||
@@ -3467,7 +3467,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(result1, result2)
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_device_id_bucketing_with_multivariate_flag(self, patch_flags):
|
||||
"""
|
||||
Multivariate flag variant selection should use device_id when
|
||||
@@ -3512,13 +3512,13 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_device_id_bucketing_from_context(self, patch_flags):
|
||||
"""
|
||||
When device_id is not passed as a parameter but is set in the context,
|
||||
it should be resolved from context.
|
||||
"""
|
||||
from posthog.contexts import new_context, set_context_device_id
|
||||
from hanzo_insights.contexts import new_context, set_context_device_id
|
||||
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
|
||||
@@ -3548,7 +3548,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_group_flags_ignore_bucketing_identifier(self, patch_flags):
|
||||
"""
|
||||
Group flags should continue to use the group identifier for hashing,
|
||||
@@ -3586,7 +3586,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_group_flag_dependency_receives_device_id(self, patch_flags):
|
||||
"""
|
||||
Group flag dependency evaluation should receive device_id so dependent
|
||||
@@ -3645,7 +3645,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_group_flag_dependency_ignores_device_id_bucketing_identifier(
|
||||
self, patch_flags
|
||||
):
|
||||
@@ -3706,7 +3706,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_with_device_id_bucketing(self, patch_flags):
|
||||
"""
|
||||
get_all_flags_and_payloads should properly handle flags with device_id bucketing.
|
||||
@@ -3740,7 +3740,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(result["device-flag"], True)
|
||||
self.assertEqual(patch_flags.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_fallback_when_device_id_missing_for_some_flags(
|
||||
self, patch_flags
|
||||
):
|
||||
@@ -4198,6 +4198,207 @@ class TestMatchProperties(unittest.TestCase):
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
self.assertFalse(match_property(property_k, {"key": "random"}))
|
||||
|
||||
def test_match_properties_semver_eq(self):
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_eq")
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.3"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.4"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.2"}))
|
||||
self.assertFalse(match_property(prop, {"version": "2.0.0"}))
|
||||
|
||||
# Pre-release suffix is stripped for comparison
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.3-alpha.1"}))
|
||||
|
||||
# Partial versions default missing parts to 0
|
||||
prop_partial = self.property(key="version", value="1.2", operator="semver_eq")
|
||||
self.assertTrue(match_property(prop_partial, {"version": "1.2.0"}))
|
||||
self.assertFalse(match_property(prop_partial, {"version": "1.2.1"}))
|
||||
|
||||
def test_match_properties_semver_neq(self):
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_neq")
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.3"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.4"}))
|
||||
self.assertTrue(match_property(prop, {"version": "2.0.0"}))
|
||||
|
||||
def test_match_properties_semver_gt(self):
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_gt")
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.4"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.3.0"}))
|
||||
self.assertTrue(match_property(prop, {"version": "2.0.0"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.3"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.2"}))
|
||||
self.assertFalse(match_property(prop, {"version": "0.9.0"}))
|
||||
|
||||
def test_match_properties_semver_gte(self):
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_gte")
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.3"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.4"}))
|
||||
self.assertTrue(match_property(prop, {"version": "2.0.0"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.2"}))
|
||||
self.assertFalse(match_property(prop, {"version": "0.9.0"}))
|
||||
|
||||
def test_match_properties_semver_lt(self):
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_lt")
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.2"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.1.0"}))
|
||||
self.assertTrue(match_property(prop, {"version": "0.9.0"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.3"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.4"}))
|
||||
self.assertFalse(match_property(prop, {"version": "2.0.0"}))
|
||||
|
||||
def test_match_properties_semver_lte(self):
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_lte")
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.3"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.2"}))
|
||||
self.assertTrue(match_property(prop, {"version": "0.9.0"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.4"}))
|
||||
self.assertFalse(match_property(prop, {"version": "2.0.0"}))
|
||||
|
||||
def test_match_properties_semver_tilde(self):
|
||||
# ~1.2.3 means >=1.2.3 <1.3.0
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_tilde")
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.3"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.5"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.99"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.3.0"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.2"}))
|
||||
self.assertFalse(match_property(prop, {"version": "2.0.0"}))
|
||||
|
||||
def test_match_properties_semver_caret(self):
|
||||
# ^1.2.3 means >=1.2.3 <2.0.0
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_caret")
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.3"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.9.0"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.99.99"}))
|
||||
self.assertFalse(match_property(prop, {"version": "2.0.0"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.2"}))
|
||||
self.assertFalse(match_property(prop, {"version": "0.9.0"}))
|
||||
|
||||
# ^0.2.3 means >=0.2.3 <0.3.0 (leftmost non-zero is minor)
|
||||
prop_zero_major = self.property(
|
||||
key="version", value="0.2.3", operator="semver_caret"
|
||||
)
|
||||
self.assertTrue(match_property(prop_zero_major, {"version": "0.2.3"}))
|
||||
self.assertTrue(match_property(prop_zero_major, {"version": "0.2.9"}))
|
||||
self.assertFalse(match_property(prop_zero_major, {"version": "0.3.0"}))
|
||||
self.assertFalse(match_property(prop_zero_major, {"version": "1.0.0"}))
|
||||
|
||||
# ^0.0.3 means >=0.0.3 <0.0.4 (leftmost non-zero is patch)
|
||||
prop_zero_minor = self.property(
|
||||
key="version", value="0.0.3", operator="semver_caret"
|
||||
)
|
||||
self.assertTrue(match_property(prop_zero_minor, {"version": "0.0.3"}))
|
||||
self.assertFalse(match_property(prop_zero_minor, {"version": "0.0.4"}))
|
||||
self.assertFalse(match_property(prop_zero_minor, {"version": "0.1.0"}))
|
||||
|
||||
def test_match_properties_semver_wildcard(self):
|
||||
# 1.2.* means >=1.2.0 <1.3.0
|
||||
prop = self.property(key="version", value="1.2.*", operator="semver_wildcard")
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.0"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.5"}))
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.99"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.3.0"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.1.9"}))
|
||||
self.assertFalse(match_property(prop, {"version": "2.0.0"}))
|
||||
|
||||
# 1.* means >=1.0.0 <2.0.0
|
||||
prop_major = self.property(
|
||||
key="version", value="1.*", operator="semver_wildcard"
|
||||
)
|
||||
self.assertTrue(match_property(prop_major, {"version": "1.0.0"}))
|
||||
self.assertTrue(match_property(prop_major, {"version": "1.99.99"}))
|
||||
self.assertFalse(match_property(prop_major, {"version": "2.0.0"}))
|
||||
self.assertFalse(match_property(prop_major, {"version": "0.9.0"}))
|
||||
|
||||
def test_match_properties_semver_with_prerelease(self):
|
||||
# Pre-release suffixes are stripped before comparison
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_gt")
|
||||
self.assertTrue(match_property(prop, {"version": "1.3.0-beta.1"}))
|
||||
self.assertFalse(match_property(prop, {"version": "1.2.2-rc.1"}))
|
||||
|
||||
# Flag value can also have pre-release suffix
|
||||
prop_pre = self.property(
|
||||
key="version", value="1.2.3-alpha", operator="semver_gte"
|
||||
)
|
||||
self.assertTrue(match_property(prop_pre, {"version": "1.2.3"}))
|
||||
self.assertTrue(match_property(prop_pre, {"version": "2.0.0"}))
|
||||
self.assertFalse(match_property(prop_pre, {"version": "1.2.2"}))
|
||||
|
||||
def test_match_properties_semver_edge_cases(self):
|
||||
"""Test semver parsing handles v-prefix, whitespace, leading zeros, and other common formats."""
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_eq")
|
||||
|
||||
# v-prefix: "v1.2.3" -> extracts "1.2.3"
|
||||
self.assertTrue(match_property(prop, {"version": "v1.2.3"}))
|
||||
|
||||
# Leading space: " 1.2.3" -> extracts "1.2.3"
|
||||
self.assertTrue(match_property(prop, {"version": " 1.2.3"}))
|
||||
|
||||
# Trailing space: "1.2.3 " -> extracts "1.2.3"
|
||||
self.assertTrue(match_property(prop, {"version": "1.2.3 "}))
|
||||
|
||||
# Leading zeros: "01.02.03" -> int("01")=1, int("02")=2, int("03")=3
|
||||
self.assertTrue(match_property(prop, {"version": "01.02.03"}))
|
||||
|
||||
# Flag value with v-prefix
|
||||
prop_v = self.property(key="version", value="v1.2.3", operator="semver_eq")
|
||||
self.assertTrue(match_property(prop_v, {"version": "1.2.3"}))
|
||||
|
||||
# 0.0.0 minimal version
|
||||
prop_min = self.property(key="version", value="0.0.0", operator="semver_eq")
|
||||
self.assertTrue(match_property(prop_min, {"version": "0.0.0"}))
|
||||
|
||||
prop_gt_min = self.property(key="version", value="0.0.0", operator="semver_gt")
|
||||
self.assertTrue(match_property(prop_gt_min, {"version": "0.0.1"}))
|
||||
self.assertFalse(match_property(prop_gt_min, {"version": "0.0.0"}))
|
||||
|
||||
# 4-part version: regex extracts "1.2.3.4" -> takes first 3 parts
|
||||
prop_four = self.property(key="version", value="1.2.3", operator="semver_eq")
|
||||
self.assertTrue(match_property(prop_four, {"version": "1.2.3.4"}))
|
||||
|
||||
# Truly invalid values raise InconclusiveMatchError
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(prop, {"version": "abc"})
|
||||
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(prop, {"version": ""})
|
||||
|
||||
# Leading dot: ".1.2.3" -> invalid, empty first component
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(prop, {"version": ".1.2.3"})
|
||||
|
||||
# Caret with v-prefix in flag value
|
||||
prop_caret_v = self.property(
|
||||
key="version", value="v1.2.3", operator="semver_caret"
|
||||
)
|
||||
self.assertTrue(match_property(prop_caret_v, {"version": "1.5.0"}))
|
||||
self.assertFalse(match_property(prop_caret_v, {"version": "2.0.0"}))
|
||||
|
||||
# Wildcard with v-prefix in property value
|
||||
prop_wild = self.property(
|
||||
key="version", value="1.2.*", operator="semver_wildcard"
|
||||
)
|
||||
self.assertTrue(match_property(prop_wild, {"version": "v1.2.5"}))
|
||||
self.assertFalse(match_property(prop_wild, {"version": "v1.3.0"}))
|
||||
|
||||
def test_match_properties_semver_invalid_values(self):
|
||||
prop = self.property(key="version", value="1.2.3", operator="semver_eq")
|
||||
|
||||
# Invalid person property value
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(prop, {"version": "not-a-version"})
|
||||
|
||||
# Missing key
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(prop, {"other_key": "1.2.3"})
|
||||
|
||||
# None override value returns False (handled before semver logic)
|
||||
self.assertFalse(match_property(prop, {"version": None}))
|
||||
|
||||
# Invalid flag value
|
||||
prop_bad = self.property(key="version", value="not-valid", operator="semver_gt")
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(prop_bad, {"version": "1.2.3"})
|
||||
|
||||
def test_unknown_operator(self):
|
||||
property_a = self.property(key="key", value="2022-05-01", operator="is_unknown")
|
||||
with self.assertRaises(InconclusiveMatchError) as exception_context:
|
||||
@@ -4409,7 +4610,7 @@ class TestRelativeDateParsing(unittest.TestCase):
|
||||
|
||||
class TestCaptureCalls(unittest.TestCase):
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_is_called(self, patch_flags, patch_capture):
|
||||
patch_flags.return_value = {"featureFlags": {"decide-flag": "decide-value"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
@@ -4524,7 +4725,7 @@ class TestCaptureCalls(unittest.TestCase):
|
||||
)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_is_called_with_flag_details(self, patch_flags, patch_capture):
|
||||
patch_flags.return_value = {
|
||||
"flags": {
|
||||
@@ -4583,7 +4784,7 @@ class TestCaptureCalls(unittest.TestCase):
|
||||
)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_is_called_with_flag_details_and_payload(
|
||||
self, patch_flags, patch_capture
|
||||
):
|
||||
@@ -4636,7 +4837,7 @@ class TestCaptureCalls(unittest.TestCase):
|
||||
disable_geoip=None,
|
||||
)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_is_called_but_does_not_add_all_flags(self, patch_flags):
|
||||
patch_flags.return_value = {"featureFlags": {"decide-flag": "decide-value"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
@@ -4688,7 +4889,7 @@ class TestCaptureCalls(unittest.TestCase):
|
||||
assert "$active_feature_flags" not in msg["properties"]
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_feature_flag_payload_does_not_send_feature_flag_called_events(
|
||||
self, patch_flags, patch_capture
|
||||
):
|
||||
@@ -4727,7 +4928,7 @@ class TestCaptureCalls(unittest.TestCase):
|
||||
self.assertIsNotNone(payload)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_fallback_to_api_in_get_feature_flag_payload_when_flag_has_static_cohort(
|
||||
self, patch_flags
|
||||
):
|
||||
@@ -4782,7 +4983,7 @@ class TestCaptureCalls(unittest.TestCase):
|
||||
self.assertEqual(patch_flags.call_count, 1)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_disable_geoip_get_flag_capture_call(self, patch_flags, patch_capture):
|
||||
patch_flags.return_value = {"featureFlags": {"decide-flag": "decide-value"}}
|
||||
client = Client(
|
||||
@@ -4826,7 +5027,7 @@ class TestCaptureCalls(unittest.TestCase):
|
||||
)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_capture_multiple_users_doesnt_out_of_memory(
|
||||
self, patch_flags, patch_capture
|
||||
):
|
||||
@@ -4896,7 +5097,7 @@ class TestConsistency(unittest.TestCase):
|
||||
self.failed = False
|
||||
self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_simple_flag_consistency(self, patch_get):
|
||||
self.client.feature_flags = [
|
||||
{
|
||||
@@ -5923,7 +6124,7 @@ class TestConsistency(unittest.TestCase):
|
||||
else:
|
||||
self.assertFalse(feature_flag_match)
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_multivariate_flag_consistency(self, patch_get):
|
||||
self.client.feature_flags = [
|
||||
{
|
||||
@@ -6980,7 +7181,7 @@ class TestConsistency(unittest.TestCase):
|
||||
else:
|
||||
self.assertFalse(feature_flag_match)
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_feature_flag_case_sensitive(self, mock_decide):
|
||||
mock_decide.return_value = {
|
||||
"featureFlags": {}
|
||||
@@ -7005,7 +7206,7 @@ class TestConsistency(unittest.TestCase):
|
||||
self.assertFalse(client.feature_enabled("beta-feature", "user1"))
|
||||
self.assertFalse(client.feature_enabled("BETA-FEATURE", "user1"))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_feature_flag_payload_case_sensitive(self, mock_decide):
|
||||
mock_decide.return_value = {
|
||||
"featureFlags": {"Beta-Feature": True},
|
||||
@@ -7036,7 +7237,7 @@ class TestConsistency(unittest.TestCase):
|
||||
self.assertIsNone(client.get_feature_flag_payload("beta-feature", "user1"))
|
||||
self.assertIsNone(client.get_feature_flag_payload("BETA-FEATURE", "user1"))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_feature_flag_case_sensitive_consistency(self, mock_decide):
|
||||
mock_decide.return_value = {
|
||||
"featureFlags": {"Beta-Feature": True},
|
||||
@@ -7072,7 +7273,7 @@ class TestConsistency(unittest.TestCase):
|
||||
for case in test_cases:
|
||||
self.assertFalse(client.feature_enabled(case, "user1"))
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_with_flag_keys_to_evaluate(self, mock_flags):
|
||||
"""Test that get_all_flags with flag_keys_to_evaluate only evaluates specified flags"""
|
||||
mock_flags.return_value = {
|
||||
@@ -7104,7 +7305,7 @@ class TestConsistency(unittest.TestCase):
|
||||
# Check the result
|
||||
self.assertEqual(result, {"flag1": "value1", "flag2": True})
|
||||
|
||||
@mock.patch("posthog.client.flags")
|
||||
@mock.patch("hanzo_insights.client.flags")
|
||||
def test_get_all_flags_and_payloads_with_flag_keys_to_evaluate(self, mock_flags):
|
||||
"""Test that get_all_flags_and_payloads with flag_keys_to_evaluate only evaluates specified flags"""
|
||||
mock_flags.return_value = {
|
||||
+28
-28
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Tests for FlagDefinitionCacheProvider functionality.
|
||||
|
||||
These tests follow the patterns from the TypeScript implementation in posthog-js/packages/node.
|
||||
These tests follow the patterns from the TypeScript implementation in insights-js/packages/node.
|
||||
"""
|
||||
|
||||
import threading
|
||||
@@ -9,13 +9,13 @@ import unittest
|
||||
from typing import Optional
|
||||
from unittest import mock
|
||||
|
||||
from posthog.client import Client
|
||||
from posthog.flag_definition_cache import (
|
||||
from hanzo_insights.client import Client
|
||||
from hanzo_insights.flag_definition_cache import (
|
||||
FlagDefinitionCacheData,
|
||||
FlagDefinitionCacheProvider,
|
||||
)
|
||||
from posthog.request import GetResponse
|
||||
from posthog.test.test_utils import FAKE_TEST_API_KEY
|
||||
from hanzo_insights.request import GetResponse
|
||||
from hanzo_insights.test.test_utils import FAKE_TEST_API_KEY
|
||||
|
||||
|
||||
class MockCacheProvider:
|
||||
@@ -63,8 +63,8 @@ class TestFlagDefinitionCacheProvider(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Prevent real HTTP requests
|
||||
cls.client_post_patcher = mock.patch("posthog.client.batch_post")
|
||||
cls.consumer_post_patcher = mock.patch("posthog.consumer.batch_post")
|
||||
cls.client_post_patcher = mock.patch("hanzo_insights.client.batch_post")
|
||||
cls.consumer_post_patcher = mock.patch("hanzo_insights.consumer.batch_post")
|
||||
cls.client_post_patcher.start()
|
||||
cls.consumer_post_patcher.start()
|
||||
|
||||
@@ -102,7 +102,7 @@ class TestFlagDefinitionCacheProvider(unittest.TestCase):
|
||||
class TestCacheInitialization(TestFlagDefinitionCacheProvider):
|
||||
"""Tests for cache initialization behavior."""
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_uses_cached_data_when_should_fetch_returns_false(self, mock_get):
|
||||
"""When should_fetch returns False and cache has data, use cached data."""
|
||||
self.cache_provider.should_fetch_return_value = False
|
||||
@@ -124,7 +124,7 @@ class TestCacheInitialization(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_fetches_from_api_when_should_fetch_returns_true(self, mock_get):
|
||||
"""When should_fetch returns True, fetch from API."""
|
||||
self.cache_provider.should_fetch_return_value = True
|
||||
@@ -148,7 +148,7 @@ class TestCacheInitialization(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_emergency_fallback_when_cache_empty_and_no_flags(self, mock_get):
|
||||
"""When should_fetch=False but cache is empty and no flags loaded, fetch anyway."""
|
||||
self.cache_provider.should_fetch_return_value = False
|
||||
@@ -169,7 +169,7 @@ class TestCacheInitialization(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_preserves_existing_flags_when_cache_returns_none(self, mock_get):
|
||||
"""When cache returns None but client has flags, preserve existing flags."""
|
||||
self.cache_provider.should_fetch_return_value = False
|
||||
@@ -197,7 +197,7 @@ class TestCacheInitialization(TestFlagDefinitionCacheProvider):
|
||||
class TestFetchCoordination(TestFlagDefinitionCacheProvider):
|
||||
"""Tests for fetch coordination between workers."""
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_calls_should_fetch_before_each_poll(self, mock_get):
|
||||
"""should_fetch_flag_definitions is called before each poll cycle."""
|
||||
self.cache_provider.should_fetch_return_value = True
|
||||
@@ -218,7 +218,7 @@ class TestFetchCoordination(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_does_not_call_on_received_when_fetch_skipped(self, mock_get):
|
||||
"""on_flag_definitions_received is NOT called when fetch is skipped."""
|
||||
self.cache_provider.should_fetch_return_value = False
|
||||
@@ -232,7 +232,7 @@ class TestFetchCoordination(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_stores_data_in_cache_after_api_fetch(self, mock_get):
|
||||
"""on_flag_definitions_received receives the fetched data."""
|
||||
self.cache_provider.should_fetch_return_value = True
|
||||
@@ -251,7 +251,7 @@ class TestFetchCoordination(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_304_not_modified_does_not_update_cache(self, mock_get):
|
||||
"""When API returns 304 Not Modified, cache should not be updated."""
|
||||
self.cache_provider.should_fetch_return_value = True
|
||||
@@ -293,7 +293,7 @@ class TestFetchCoordination(TestFlagDefinitionCacheProvider):
|
||||
class TestErrorHandling(TestFlagDefinitionCacheProvider):
|
||||
"""Tests for error handling in cache provider operations."""
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_should_fetch_error_defaults_to_fetching(self, mock_get):
|
||||
"""When should_fetch throws an error, default to fetching from API."""
|
||||
self.cache_provider.should_fetch_error = Exception("Lock acquisition failed")
|
||||
@@ -313,7 +313,7 @@ class TestErrorHandling(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_get_error_falls_back_to_api_fetch(self, mock_get):
|
||||
"""When get_flag_definitions throws an error, fetch from API."""
|
||||
self.cache_provider.should_fetch_return_value = False
|
||||
@@ -331,7 +331,7 @@ class TestErrorHandling(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_on_received_error_keeps_flags_in_memory(self, mock_get):
|
||||
"""When on_flag_definitions_received throws, flags are still in memory."""
|
||||
self.cache_provider.should_fetch_return_value = True
|
||||
@@ -350,7 +350,7 @@ class TestErrorHandling(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_shutdown_error_is_logged_but_continues(self, mock_get):
|
||||
"""When shutdown throws an error, it's logged but shutdown continues."""
|
||||
self.cache_provider.shutdown_error = Exception("Lock release failed")
|
||||
@@ -372,7 +372,7 @@ class TestErrorHandling(TestFlagDefinitionCacheProvider):
|
||||
class TestShutdownLifecycle(TestFlagDefinitionCacheProvider):
|
||||
"""Tests for shutdown lifecycle."""
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_shutdown_calls_cache_provider_shutdown(self, mock_get):
|
||||
"""Client shutdown calls cache provider shutdown."""
|
||||
mock_get.return_value = GetResponse(
|
||||
@@ -387,7 +387,7 @@ class TestShutdownLifecycle(TestFlagDefinitionCacheProvider):
|
||||
|
||||
self.assertEqual(self.cache_provider.shutdown_call_count, 1)
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_shutdown_called_even_without_fetching(self, mock_get):
|
||||
"""Shutdown is called even when cache was used instead of fetching."""
|
||||
self.cache_provider.should_fetch_return_value = False
|
||||
@@ -400,7 +400,7 @@ class TestShutdownLifecycle(TestFlagDefinitionCacheProvider):
|
||||
# Shutdown should still be called
|
||||
self.assertEqual(self.cache_provider.shutdown_call_count, 1)
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_multiple_join_calls_only_shutdown_once(self, mock_get):
|
||||
"""Calling join() multiple times should only call cache provider shutdown once."""
|
||||
mock_get.return_value = GetResponse(
|
||||
@@ -423,7 +423,7 @@ class TestShutdownLifecycle(TestFlagDefinitionCacheProvider):
|
||||
class TestBackwardCompatibility(TestFlagDefinitionCacheProvider):
|
||||
"""Tests for backward compatibility without cache provider."""
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_works_without_cache_provider(self, mock_get):
|
||||
"""Client works normally without a cache provider configured."""
|
||||
mock_get.return_value = GetResponse(
|
||||
@@ -451,7 +451,7 @@ class TestBackwardCompatibility(TestFlagDefinitionCacheProvider):
|
||||
class TestDataIntegrity(TestFlagDefinitionCacheProvider):
|
||||
"""Tests for data integrity between cache and client state."""
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_cached_flags_available_for_evaluation(self, mock_get):
|
||||
"""Flags loaded from cache are available for local evaluation."""
|
||||
self.cache_provider.should_fetch_return_value = False
|
||||
@@ -483,7 +483,7 @@ class TestDataIntegrity(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_group_type_mapping_loaded_from_cache(self, mock_get):
|
||||
"""Group type mapping is correctly loaded from cache."""
|
||||
self.cache_provider.should_fetch_return_value = False
|
||||
@@ -497,7 +497,7 @@ class TestDataIntegrity(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_cohorts_loaded_from_cache(self, mock_get):
|
||||
"""Cohorts are correctly loaded from cache."""
|
||||
self.cache_provider.should_fetch_return_value = False
|
||||
@@ -510,7 +510,7 @@ class TestDataIntegrity(TestFlagDefinitionCacheProvider):
|
||||
|
||||
client.join()
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_cache_updated_when_api_returns_new_data(self, mock_get):
|
||||
"""State transition: cache has old data -> API returns new -> cache updated."""
|
||||
# Start with old cached data
|
||||
@@ -556,7 +556,7 @@ class TestDataIntegrity(TestFlagDefinitionCacheProvider):
|
||||
class TestConcurrency(TestFlagDefinitionCacheProvider):
|
||||
"""Tests for thread safety and concurrent access."""
|
||||
|
||||
@mock.patch("posthog.client.get")
|
||||
@mock.patch("hanzo_insights.client.get")
|
||||
def test_concurrent_load_feature_flags_is_thread_safe(self, mock_get):
|
||||
"""Multiple threads calling _load_feature_flags should not cause errors."""
|
||||
mock_get.return_value = GetResponse(
|
||||
@@ -1,10 +1,10 @@
|
||||
import unittest
|
||||
|
||||
from posthog import Posthog
|
||||
from hanzo_insights import Insights
|
||||
|
||||
|
||||
class TestModule(unittest.TestCase):
|
||||
posthog = None
|
||||
client = None
|
||||
|
||||
def _assert_enqueue_result(self, result):
|
||||
self.assertEqual(type(result[0]), str)
|
||||
@@ -14,19 +14,19 @@ class TestModule(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.failed = False
|
||||
self.posthog = Posthog(
|
||||
self.client = Insights(
|
||||
"testsecret", host="http://localhost:8000", on_error=self.failed
|
||||
)
|
||||
|
||||
def test_track(self):
|
||||
res = self.posthog.capture("python module event", distinct_id="distinct_id")
|
||||
res = self.client.capture("python module event", distinct_id="distinct_id")
|
||||
self._assert_enqueue_result(res)
|
||||
self.posthog.flush()
|
||||
self.client.flush()
|
||||
|
||||
def test_alias(self):
|
||||
res = self.posthog.alias("previousId", "distinct_id")
|
||||
res = self.client.alias("previousId", "distinct_id")
|
||||
self._assert_enqueue_result(res)
|
||||
self.posthog.flush()
|
||||
self.client.flush()
|
||||
|
||||
def test_flush(self):
|
||||
self.posthog.flush()
|
||||
self.client.flush()
|
||||
@@ -6,8 +6,8 @@ import mock
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
import posthog.request as request_module
|
||||
from posthog.request import (
|
||||
import hanzo_insights.request as request_module
|
||||
from hanzo_insights.request import (
|
||||
APIError,
|
||||
DatetimeSerializer,
|
||||
GetResponse,
|
||||
@@ -23,7 +23,7 @@ from posthog.request import (
|
||||
get,
|
||||
set_socket_options,
|
||||
)
|
||||
from posthog.test.test_utils import TEST_API_KEY
|
||||
from hanzo_insights.test.test_utils import TEST_API_KEY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -128,7 +128,7 @@ class TestRequests(unittest.TestCase):
|
||||
}
|
||||
).encode("utf-8")
|
||||
|
||||
with mock.patch("posthog.request._session.post", return_value=mock_response):
|
||||
with mock.patch("hanzo_insights.request._session.post", return_value=mock_response):
|
||||
with self.assertRaises(QuotaLimitError) as cm:
|
||||
decide("fake_key", "fake_host")
|
||||
|
||||
@@ -146,7 +146,7 @@ class TestRequests(unittest.TestCase):
|
||||
}
|
||||
).encode("utf-8")
|
||||
|
||||
with mock.patch("posthog.request._session.post", return_value=mock_response):
|
||||
with mock.patch("hanzo_insights.request._session.post", return_value=mock_response):
|
||||
response = decide("fake_key", "fake_host")
|
||||
self.assertEqual(response["featureFlags"], {"flag1": True})
|
||||
|
||||
@@ -154,7 +154,7 @@ class TestRequests(unittest.TestCase):
|
||||
class TestGet(unittest.TestCase):
|
||||
"""Unit tests for the get() function HTTP-level behavior."""
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_returns_data_and_etag(self, mock_get):
|
||||
"""Test that get() returns GetResponse with data and etag from headers."""
|
||||
mock_response = requests.Response()
|
||||
@@ -172,7 +172,7 @@ class TestGet(unittest.TestCase):
|
||||
self.assertEqual(response.etag, '"abc123"')
|
||||
self.assertFalse(response.not_modified)
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_sends_if_none_match_header_when_etag_provided(self, mock_get):
|
||||
"""Test that If-None-Match header is sent when etag parameter is provided."""
|
||||
mock_response = requests.Response()
|
||||
@@ -186,7 +186,7 @@ class TestGet(unittest.TestCase):
|
||||
call_kwargs = mock_get.call_args[1]
|
||||
self.assertEqual(call_kwargs["headers"]["If-None-Match"], '"previous-etag"')
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_does_not_send_if_none_match_when_no_etag(self, mock_get):
|
||||
"""Test that If-None-Match header is not sent when no etag provided."""
|
||||
mock_response = requests.Response()
|
||||
@@ -199,7 +199,7 @@ class TestGet(unittest.TestCase):
|
||||
call_kwargs = mock_get.call_args[1]
|
||||
self.assertNotIn("If-None-Match", call_kwargs["headers"])
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_handles_304_not_modified(self, mock_get):
|
||||
"""Test that 304 Not Modified response returns not_modified=True with no data."""
|
||||
mock_response = requests.Response()
|
||||
@@ -216,7 +216,7 @@ class TestGet(unittest.TestCase):
|
||||
self.assertEqual(response.etag, '"unchanged-etag"')
|
||||
self.assertTrue(response.not_modified)
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_304_without_etag_header_uses_request_etag(self, mock_get):
|
||||
"""Test that 304 response without ETag header falls back to request etag."""
|
||||
mock_response = requests.Response()
|
||||
@@ -231,7 +231,7 @@ class TestGet(unittest.TestCase):
|
||||
self.assertTrue(response.not_modified)
|
||||
self.assertEqual(response.etag, '"original-etag"')
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_200_without_etag_header(self, mock_get):
|
||||
"""Test that 200 response without ETag header returns None for etag."""
|
||||
mock_response = requests.Response()
|
||||
@@ -246,7 +246,7 @@ class TestGet(unittest.TestCase):
|
||||
self.assertIsNone(response.etag)
|
||||
self.assertEqual(response.data, {"flags": []})
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_error_response_raises_api_error(self, mock_get):
|
||||
"""Test that error responses raise APIError."""
|
||||
mock_response = requests.Response()
|
||||
@@ -260,7 +260,7 @@ class TestGet(unittest.TestCase):
|
||||
self.assertEqual(ctx.exception.status, 401)
|
||||
self.assertEqual(ctx.exception.message, "Unauthorized")
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_sends_authorization_header(self, mock_get):
|
||||
"""Test that Authorization header is sent with Bearer token."""
|
||||
mock_response = requests.Response()
|
||||
@@ -273,7 +273,7 @@ class TestGet(unittest.TestCase):
|
||||
call_kwargs = mock_get.call_args[1]
|
||||
self.assertEqual(call_kwargs["headers"]["Authorization"], "Bearer my-api-key")
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_sends_user_agent_header(self, mock_get):
|
||||
"""Test that User-Agent header is sent."""
|
||||
mock_response = requests.Response()
|
||||
@@ -286,10 +286,10 @@ class TestGet(unittest.TestCase):
|
||||
call_kwargs = mock_get.call_args[1]
|
||||
self.assertIn("User-Agent", call_kwargs["headers"])
|
||||
self.assertTrue(
|
||||
call_kwargs["headers"]["User-Agent"].startswith("posthog-python/")
|
||||
call_kwargs["headers"]["User-Agent"].startswith("hanzo-insights-python/")
|
||||
)
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_passes_timeout(self, mock_get):
|
||||
"""Test that timeout parameter is passed to the request."""
|
||||
mock_response = requests.Response()
|
||||
@@ -302,7 +302,7 @@ class TestGet(unittest.TestCase):
|
||||
call_kwargs = mock_get.call_args[1]
|
||||
self.assertEqual(call_kwargs["timeout"], 30)
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_constructs_full_url(self, mock_get):
|
||||
"""Test that host and url are combined correctly."""
|
||||
mock_response = requests.Response()
|
||||
@@ -315,7 +315,7 @@ class TestGet(unittest.TestCase):
|
||||
call_args = mock_get.call_args[0]
|
||||
self.assertEqual(call_args[0], "https://example.com/api/flags")
|
||||
|
||||
@mock.patch("posthog.request._session.get")
|
||||
@mock.patch("hanzo_insights.request._session.get")
|
||||
def test_get_removes_trailing_slash_from_host(self, mock_get):
|
||||
"""Test that trailing slash is removed from host."""
|
||||
mock_response = requests.Response()
|
||||
@@ -339,13 +339,13 @@ class TestGet(unittest.TestCase):
|
||||
("https://us.posthog.com.rg.proxy.com", "https://us.posthog.com.rg.proxy.com"),
|
||||
("app.posthog.com", "app.posthog.com"),
|
||||
("eu.posthog.com", "eu.posthog.com"),
|
||||
("https://app.posthog.com", "https://us.i.posthog.com"),
|
||||
("https://eu.posthog.com", "https://eu.i.posthog.com"),
|
||||
("https://us.posthog.com", "https://us.i.posthog.com"),
|
||||
("https://app.posthog.com/", "https://us.i.posthog.com"),
|
||||
("https://eu.posthog.com/", "https://eu.i.posthog.com"),
|
||||
("https://us.posthog.com/", "https://us.i.posthog.com"),
|
||||
(None, "https://us.i.posthog.com"),
|
||||
("https://app.posthog.com", "https://us.i.insights.hanzo.ai"),
|
||||
("https://eu.posthog.com", "https://eu.i.insights.hanzo.ai"),
|
||||
("https://us.posthog.com", "https://us.i.insights.hanzo.ai"),
|
||||
("https://app.posthog.com/", "https://us.i.insights.hanzo.ai"),
|
||||
("https://eu.posthog.com/", "https://eu.i.insights.hanzo.ai"),
|
||||
("https://us.posthog.com/", "https://us.i.insights.hanzo.ai"),
|
||||
(None, "https://us.i.insights.hanzo.ai"),
|
||||
],
|
||||
)
|
||||
def test_routing_to_custom_host(host, expected):
|
||||
@@ -355,7 +355,7 @@ def test_routing_to_custom_host(host, expected):
|
||||
def test_enable_keep_alive_sets_socket_options():
|
||||
try:
|
||||
enable_keep_alive()
|
||||
from posthog.request import _session
|
||||
from hanzo_insights.request import _session
|
||||
|
||||
adapter = _session.get_adapter("https://example.com")
|
||||
assert adapter.socket_options == KEEP_ALIVE_SOCKET_OPTIONS
|
||||
@@ -367,7 +367,7 @@ def test_set_socket_options_clears_with_none():
|
||||
try:
|
||||
enable_keep_alive()
|
||||
set_socket_options(None)
|
||||
from posthog.request import _session
|
||||
from hanzo_insights.request import _session
|
||||
|
||||
adapter = _session.get_adapter("https://example.com")
|
||||
assert adapter.socket_options is None
|
||||
@@ -401,17 +401,17 @@ class TestFlagsSession(unittest.TestCase):
|
||||
|
||||
def test_retry_status_forcelist_excludes_rate_limits(self):
|
||||
"""Verify 429 (rate limit) is NOT retried - need to wait, not hammer."""
|
||||
from posthog.request import RETRY_STATUS_FORCELIST
|
||||
from hanzo_insights.request import RETRY_STATUS_FORCELIST
|
||||
|
||||
self.assertNotIn(429, RETRY_STATUS_FORCELIST)
|
||||
|
||||
def test_retry_status_forcelist_excludes_quota_errors(self):
|
||||
"""Verify 402 (payment required/quota) is NOT retried - won't resolve."""
|
||||
from posthog.request import RETRY_STATUS_FORCELIST
|
||||
from hanzo_insights.request import RETRY_STATUS_FORCELIST
|
||||
|
||||
self.assertNotIn(402, RETRY_STATUS_FORCELIST)
|
||||
|
||||
@mock.patch("posthog.request._get_flags_session")
|
||||
@mock.patch("hanzo_insights.request._get_flags_session")
|
||||
def test_flags_uses_flags_session(self, mock_get_flags_session):
|
||||
"""flags() uses the dedicated flags session, not the general session."""
|
||||
mock_response = requests.Response()
|
||||
@@ -434,7 +434,7 @@ class TestFlagsSession(unittest.TestCase):
|
||||
mock_get_flags_session.assert_called_once()
|
||||
mock_session.post.assert_called_once()
|
||||
|
||||
@mock.patch("posthog.request._get_flags_session")
|
||||
@mock.patch("hanzo_insights.request._get_flags_session")
|
||||
def test_flags_no_retry_on_quota_limit(self, mock_get_flags_session):
|
||||
"""flags() raises QuotaLimitError without retrying (at application level)."""
|
||||
mock_response = requests.Response()
|
||||
@@ -470,7 +470,7 @@ class TestFlagsSessionNetworkRetries(unittest.TestCase):
|
||||
retries on network-level failures (DNS failures, connection refused,
|
||||
connection reset, etc.) up to 2 times each.
|
||||
"""
|
||||
from posthog.request import _build_flags_session
|
||||
from hanzo_insights.request import _build_flags_session
|
||||
|
||||
session = _build_flags_session()
|
||||
|
||||
@@ -491,7 +491,7 @@ class TestFlagsSessionNetworkRetries(unittest.TestCase):
|
||||
This tests the status_forcelist configuration which specifies
|
||||
which HTTP status codes should trigger a retry.
|
||||
"""
|
||||
from posthog.request import _build_flags_session, RETRY_STATUS_FORCELIST
|
||||
from hanzo_insights.request import _build_flags_session, RETRY_STATUS_FORCELIST
|
||||
|
||||
session = _build_flags_session()
|
||||
adapter = session.get_adapter("https://test.posthog.com")
|
||||
@@ -518,7 +518,7 @@ class TestFlagsSessionNetworkRetries(unittest.TestCase):
|
||||
"""
|
||||
Verify that retries use exponential backoff to avoid thundering herd.
|
||||
"""
|
||||
from posthog.request import _build_flags_session
|
||||
from hanzo_insights.request import _build_flags_session
|
||||
|
||||
session = _build_flags_session()
|
||||
adapter = session.get_adapter("https://test.posthog.com")
|
||||
@@ -545,7 +545,7 @@ class TestFlagsSessionRetryIntegration(unittest.TestCase):
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
from urllib3.util.retry import Retry
|
||||
from posthog.request import HTTPAdapterWithSocketOptions, RETRY_STATUS_FORCELIST
|
||||
from hanzo_insights.request import HTTPAdapterWithSocketOptions, RETRY_STATUS_FORCELIST
|
||||
|
||||
request_count = 0
|
||||
|
||||
@@ -631,7 +631,7 @@ class TestFlagsSessionRetryIntegration(unittest.TestCase):
|
||||
import socket
|
||||
import time
|
||||
from urllib3.util.retry import Retry
|
||||
from posthog.request import HTTPAdapterWithSocketOptions, RETRY_STATUS_FORCELIST
|
||||
from hanzo_insights.request import HTTPAdapterWithSocketOptions, RETRY_STATUS_FORCELIST
|
||||
|
||||
# Get an available port by binding then closing a socket
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
+1
-1
@@ -2,7 +2,7 @@ import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from posthog import utils
|
||||
from hanzo_insights import utils
|
||||
|
||||
|
||||
class TestSizeLimitedDict(unittest.TestCase):
|
||||
@@ -2,7 +2,7 @@ import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from posthog.types import (
|
||||
from hanzo_insights.types import (
|
||||
FeatureFlag,
|
||||
FlagMetadata,
|
||||
FlagReason,
|
||||
@@ -13,8 +13,8 @@ from parameterized import parameterized
|
||||
from pydantic import BaseModel
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
|
||||
from posthog import utils
|
||||
from posthog.types import FeatureFlagResult
|
||||
from hanzo_insights import utils
|
||||
from hanzo_insights.types import FeatureFlagResult
|
||||
|
||||
TEST_API_KEY = "kOOlRy2QlMY9jHZQv0bKz0FZyazBUoY8Arj0lFVNjs4"
|
||||
FAKE_TEST_API_KEY = "random_key"
|
||||
@@ -96,8 +96,8 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
("http://posthog.io/", "http://posthog.io"),
|
||||
("http://posthog.io", "http://posthog.io"),
|
||||
("http://hanzo_insights.io/", "http://hanzo_insights.io"),
|
||||
("http://hanzo_insights.io", "http://hanzo_insights.io"),
|
||||
("https://example.com/path/", "https://example.com/path"),
|
||||
("https://example.com/path", "https://example.com/path"),
|
||||
]
|
||||
@@ -16,7 +16,7 @@ import distro # For Linux OS detection
|
||||
import six
|
||||
from dateutil.tz import tzlocal, tzutc
|
||||
|
||||
log = logging.getLogger("posthog")
|
||||
log = logging.getLogger("hanzo_insights")
|
||||
|
||||
|
||||
def is_naive(dt):
|
||||
@@ -277,7 +277,7 @@ class FlagCache:
|
||||
|
||||
class RedisFlagCache:
|
||||
def __init__(
|
||||
self, redis_client, default_ttl=300, stale_ttl=3600, key_prefix="posthog:flags:"
|
||||
self, redis_client, default_ttl=300, stale_ttl=3600, key_prefix="insights:flags:"
|
||||
):
|
||||
self.redis = redis_client
|
||||
self.default_ttl = default_ttl
|
||||
@@ -0,0 +1 @@
|
||||
VERSION = "7.9.7"
|
||||
@@ -0,0 +1,3 @@
|
||||
# Convenience re-export so `from insights import Insights` works.
|
||||
from hanzo_insights import * # noqa: F401, F403
|
||||
from hanzo_insights import Insights, Client # noqa: F401
|
||||
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
Test that verifies exception capture functionality.
|
||||
|
||||
These tests verify that exceptions are actually captured to PostHog, not just that
|
||||
These tests verify that exceptions are actually captured to Insights, not just that
|
||||
500 responses are returned.
|
||||
|
||||
Without process_exception(), view exceptions are NOT captured to PostHog (v6.7.11 and earlier).
|
||||
Without process_exception(), view exceptions are NOT captured to Insights (v6.7.11 and earlier).
|
||||
With process_exception(), Django calls this method to capture exceptions before
|
||||
converting them to 500 responses.
|
||||
"""
|
||||
@@ -30,7 +30,7 @@ def asgi_app():
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_exception_is_captured(asgi_app):
|
||||
"""
|
||||
Test that async view exceptions are captured to PostHog.
|
||||
Test that async view exceptions are captured to Insights.
|
||||
|
||||
The middleware's process_exception() method ensures exceptions are captured.
|
||||
Without it (v6.7.11 and earlier), exceptions are NOT captured even though 500 is returned.
|
||||
@@ -50,8 +50,8 @@ async def test_async_exception_is_captured(asgi_app):
|
||||
}
|
||||
)
|
||||
|
||||
# Patch at the posthog module level where middleware imports from
|
||||
with patch("posthog.capture_exception", side_effect=mock_capture):
|
||||
# Patch at the hanzo_insights module level where middleware imports from
|
||||
with patch("hanzo_insights.capture_exception", side_effect=mock_capture):
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=asgi_app), base_url="http://testserver"
|
||||
) as ac:
|
||||
@@ -60,8 +60,8 @@ async def test_async_exception_is_captured(asgi_app):
|
||||
# Django returns 500
|
||||
assert response.status_code == 500
|
||||
|
||||
# CRITICAL: Verify PostHog captured the exception
|
||||
assert len(captured) > 0, "Exception was NOT captured to PostHog!"
|
||||
# CRITICAL: Verify Insights captured the exception
|
||||
assert len(captured) > 0, "Exception was NOT captured to Insights!"
|
||||
|
||||
# Verify it's the right exception
|
||||
exception_data = captured[0]
|
||||
@@ -72,7 +72,7 @@ async def test_async_exception_is_captured(asgi_app):
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_exception_is_captured(asgi_app):
|
||||
"""
|
||||
Test that sync view exceptions are captured to PostHog.
|
||||
Test that sync view exceptions are captured to Insights.
|
||||
|
||||
The middleware's process_exception() method ensures exceptions are captured.
|
||||
Without it (v6.7.11 and earlier), exceptions are NOT captured even though 500 is returned.
|
||||
@@ -92,8 +92,8 @@ async def test_sync_exception_is_captured(asgi_app):
|
||||
}
|
||||
)
|
||||
|
||||
# Patch at the posthog module level where middleware imports from
|
||||
with patch("posthog.capture_exception", side_effect=mock_capture):
|
||||
# Patch at the hanzo_insights module level where middleware imports from
|
||||
with patch("hanzo_insights.capture_exception", side_effect=mock_capture):
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=asgi_app), base_url="http://testserver"
|
||||
) as ac:
|
||||
@@ -102,8 +102,8 @@ async def test_sync_exception_is_captured(asgi_app):
|
||||
# Django returns 500
|
||||
assert response.status_code == 500
|
||||
|
||||
# CRITICAL: Verify PostHog captured the exception
|
||||
assert len(captured) > 0, "Exception was NOT captured to PostHog!"
|
||||
# CRITICAL: Verify Insights captured the exception
|
||||
assert len(captured) > 0, "Exception was NOT captured to Insights!"
|
||||
|
||||
# Verify it's the right exception
|
||||
exception_data = captured[0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Tests for PostHog Django middleware in async context.
|
||||
Tests for Insights Django middleware in async context.
|
||||
|
||||
These tests verify that the middleware correctly handles:
|
||||
1. Async user access (request.auser() in Django 5)
|
||||
@@ -103,7 +103,7 @@ async def test_async_authenticated_user_access(asgi_app):
|
||||
|
||||
# Make request with session cookie - this should trigger the bug in v6.7.11
|
||||
# Disable exception capture to see the SynchronousOnlyOperation clearly
|
||||
with override_settings(POSTHOG_MW_CAPTURE_EXCEPTIONS=False):
|
||||
with override_settings(INSIGHTS_MW_CAPTURE_EXCEPTIONS=False):
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=asgi_app),
|
||||
base_url="http://testserver",
|
||||
@@ -139,10 +139,10 @@ async def test_async_exception_capture(asgi_app):
|
||||
"""
|
||||
Test that middleware handles exceptions from async views.
|
||||
|
||||
The middleware's process_exception() method captures view exceptions to PostHog
|
||||
The middleware's process_exception() method captures view exceptions to Insights
|
||||
before Django converts them to 500 responses. This test verifies the exception
|
||||
causes a 500 response. See test_exception_capture.py for tests that verify
|
||||
actual exception capture to PostHog.
|
||||
actual exception capture to Insights.
|
||||
"""
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=asgi_app), base_url="http://testserver"
|
||||
@@ -158,7 +158,7 @@ async def test_sync_exception_capture(asgi_app):
|
||||
"""
|
||||
Test that middleware handles exceptions from sync views.
|
||||
|
||||
The middleware's process_exception() method captures view exceptions to PostHog.
|
||||
The middleware's process_exception() method captures view exceptions to Insights.
|
||||
This test verifies the exception causes a 500 response.
|
||||
"""
|
||||
async with AsyncClient(
|
||||
|
||||
@@ -47,7 +47,7 @@ MIDDLEWARE = [
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"posthog.integrations.django.PosthogContextMiddleware", # Test PostHog middleware
|
||||
"hanzo_insights.integrations.django.InsightsContextMiddleware", # Test Insights middleware
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "testdjango.urls"
|
||||
@@ -123,7 +123,7 @@ STATIC_URL = "static/"
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
|
||||
# PostHog settings for testing
|
||||
POSTHOG_API_KEY = "test-key"
|
||||
# Insights settings for testing
|
||||
INSIGHTS_API_KEY = "test-key"
|
||||
POSTHOG_HOST = "https://app.posthog.com"
|
||||
POSTHOG_MW_CAPTURE_EXCEPTIONS = True
|
||||
INSIGHTS_MW_CAPTURE_EXCEPTIONS = True
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Test views for validating PostHog middleware with Django 5 ASGI.
|
||||
Test views for validating Insights middleware with Django 5 ASGI.
|
||||
"""
|
||||
|
||||
from django.http import JsonResponse
|
||||
|
||||
@@ -17,10 +17,10 @@ ignore_missing_imports = True
|
||||
[mypy-sentry_sdk.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-posthog.test.*]
|
||||
[mypy-hanzo_insights.test.*]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-posthog.*.test.*]
|
||||
[mypy-hanzo_insights.*.test.*]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-openai.*]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from posthog.ai.prompts import Prompts
|
||||
|
||||
__all__ = ["Prompts"]
|
||||
@@ -1 +0,0 @@
|
||||
VERSION = "7.9.4"
|
||||
+21
-19
@@ -3,11 +3,11 @@ requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "posthog"
|
||||
version = "7.9.4"
|
||||
description = "Integrate PostHog into any python application."
|
||||
authors = [{ name = "PostHog", email = "hey@posthog.com" }]
|
||||
maintainers = [{ name = "PostHog", email = "hey@posthog.com" }]
|
||||
name = "hanzo-insights"
|
||||
version = "7.9.7"
|
||||
description = "Integrate Hanzo Insights into any python application."
|
||||
authors = [{ name = "Hanzo AI", email = "hey@hanzo.ai" }]
|
||||
maintainers = [{ name = "Hanzo AI", email = "hey@hanzo.ai" }]
|
||||
license = { text = "MIT" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -24,6 +24,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.14",
|
||||
]
|
||||
dependencies = [
|
||||
# All runtime deps are pure-Python — fully compatible with free-threaded 3.13t.
|
||||
"requests>=2.7,<3.0",
|
||||
"six>=1.5",
|
||||
"python-dateutil>=2.2",
|
||||
@@ -33,8 +34,8 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/posthog/posthog-python"
|
||||
Repository = "https://github.com/posthog/posthog-python"
|
||||
Homepage = "https://github.com/hanzoai/insights"
|
||||
Repository = "https://github.com/hanzoai/posthog-python"
|
||||
|
||||
[project.optional-dependencies]
|
||||
langchain = ["langchain>=0.2.0"]
|
||||
@@ -80,21 +81,22 @@ test = [
|
||||
|
||||
[tool.setuptools]
|
||||
packages = [
|
||||
"posthog",
|
||||
"posthog.ai",
|
||||
"posthog.ai.langchain",
|
||||
"posthog.ai.openai",
|
||||
"posthog.ai.openai_agents",
|
||||
"posthog.ai.anthropic",
|
||||
"posthog.ai.gemini",
|
||||
"posthog.test",
|
||||
"posthog.test.ai",
|
||||
"posthog.test.ai.openai_agents",
|
||||
"posthog.integrations",
|
||||
"hanzo_insights",
|
||||
"hanzo_insights.ai",
|
||||
"hanzo_insights.ai.langchain",
|
||||
"hanzo_insights.ai.openai",
|
||||
"hanzo_insights.ai.openai_agents",
|
||||
"hanzo_insights.ai.anthropic",
|
||||
"hanzo_insights.ai.gemini",
|
||||
"hanzo_insights.test",
|
||||
"hanzo_insights.test.ai",
|
||||
"hanzo_insights.test.ai.openai_agents",
|
||||
"hanzo_insights.integrations",
|
||||
"insights",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
testpaths = ["posthog/test"]
|
||||
testpaths = ["hanzo_insights/test"]
|
||||
norecursedirs = ["integration_tests"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
PostHog Python SDK Test Adapter
|
||||
Insights Python SDK Test Adapter
|
||||
|
||||
This adapter implements the SDK Test Adapter Interface defined in the PostHog Capture API Contract.
|
||||
It wraps the posthog-python SDK and exposes a REST API for the test harness to exercise.
|
||||
This adapter implements the SDK Test Adapter Interface defined in the Capture API Contract.
|
||||
It wraps the insights-python SDK and exposes a REST API for the test harness to exercise.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -13,9 +13,9 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
from flask import Flask, jsonify, request
|
||||
|
||||
from posthog import Client
|
||||
from posthog.request import batch_post as original_batch_post
|
||||
from posthog.version import VERSION
|
||||
from hanzo_insights import Client
|
||||
from hanzo_insights.request import batch_post as original_batch_post
|
||||
from hanzo_insights.version import VERSION
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
@@ -178,14 +178,14 @@ def patched_batch_post(
|
||||
|
||||
|
||||
# Monkey-patch the batch_post function
|
||||
import posthog.request # noqa: E402
|
||||
import hanzo_insights.request # noqa: E402
|
||||
|
||||
posthog.request.batch_post = patched_batch_post
|
||||
hanzo_insights.request.batch_post = patched_batch_post
|
||||
|
||||
# Also patch in consumer module
|
||||
import posthog.consumer # noqa: E402
|
||||
import hanzo_insights.consumer # noqa: E402
|
||||
|
||||
posthog.consumer.batch_post = patched_batch_post
|
||||
hanzo_insights.consumer.batch_post = patched_batch_post
|
||||
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
@@ -193,7 +193,7 @@ def health():
|
||||
"""Health check endpoint"""
|
||||
return jsonify(
|
||||
{
|
||||
"sdk_name": "posthog-python",
|
||||
"sdk_name": "insights-python",
|
||||
"sdk_version": VERSION,
|
||||
"adapter_version": "1.0.0",
|
||||
}
|
||||
|
||||
@@ -6,30 +6,26 @@ try:
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
# Don't import analytics-python module here, since deps may not be installed
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "posthog"))
|
||||
# Don't import module here, since deps may not be installed
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "hanzo_insights"))
|
||||
from version import VERSION # noqa: E402
|
||||
|
||||
long_description = """
|
||||
PostHog is developer-friendly, self-hosted product analytics.
|
||||
posthog-python is the python package.
|
||||
Hanzo Insights is developer-friendly, self-hosted product analytics.
|
||||
hanzo_insights is the python package.
|
||||
|
||||
This package requires Python 3.10 or higher.
|
||||
"""
|
||||
|
||||
# Minimal setup.py for backward compatibility
|
||||
# Most configuration is now in pyproject.toml
|
||||
setup(
|
||||
name="posthog",
|
||||
name="hanzo_insights",
|
||||
version=VERSION,
|
||||
# Basic fields for backward compatibility
|
||||
url="https://github.com/posthog/posthog-python",
|
||||
author="Posthog",
|
||||
author_email="hey@posthog.com",
|
||||
maintainer="PostHog",
|
||||
maintainer_email="hey@posthog.com",
|
||||
url="https://github.com/hanzoai/insights",
|
||||
author="Hanzo AI",
|
||||
author_email="hey@hanzo.ai",
|
||||
maintainer="Hanzo AI",
|
||||
maintainer_email="hey@hanzo.ai",
|
||||
license="MIT License",
|
||||
description="Integrate PostHog into any python application.",
|
||||
description="Integrate Hanzo Insights into any python application.",
|
||||
long_description=long_description,
|
||||
# This will fallback to pyproject.toml for detailed configuration
|
||||
)
|
||||
|
||||
+6
-6
@@ -25,14 +25,14 @@ with open("pyproject.toml", "rb") as f:
|
||||
config["project"]["name"] = "posthoganalytics"
|
||||
config["project"]["readme"] = "README_ANALYTICS.md"
|
||||
|
||||
# Rename packages from posthog.* to posthoganalytics.*
|
||||
# Rename packages from hanzo_insights.* to posthoganalytics.*
|
||||
if "packages" in config["tool"]["setuptools"]:
|
||||
new_packages = []
|
||||
for package in config["tool"]["setuptools"]["packages"]:
|
||||
if package == "posthog":
|
||||
if package == "hanzo_insights":
|
||||
new_packages.append("posthoganalytics")
|
||||
elif package.startswith("posthog."):
|
||||
new_packages.append(package.replace("posthog.", "posthoganalytics.", 1))
|
||||
elif package.startswith("hanzo_insights."):
|
||||
new_packages.append(package.replace("hanzo_insights.", "posthoganalytics.", 1))
|
||||
else:
|
||||
new_packages.append(package)
|
||||
config["tool"]["setuptools"]["packages"] = new_packages
|
||||
@@ -56,9 +56,9 @@ setup(
|
||||
# Basic fields for backward compatibility
|
||||
url="https://github.com/posthog/posthog-python",
|
||||
author="Posthog",
|
||||
author_email="hey@posthog.com",
|
||||
author_email="hey@hanzo.ai",
|
||||
maintainer="PostHog",
|
||||
maintainer_email="hey@posthog.com",
|
||||
maintainer_email="hey@hanzo.ai",
|
||||
license="MIT License",
|
||||
description="Integrate PostHog into any python application.",
|
||||
long_description=long_description,
|
||||
|
||||
@@ -901,6 +901,110 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hanzo-insights"
|
||||
version = "7.9.7"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "backoff" },
|
||||
{ name = "distro" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "requests" },
|
||||
{ name = "six" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "django-stubs" },
|
||||
{ name = "lxml" },
|
||||
{ name = "mypy" },
|
||||
{ name = "mypy-baseline" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "ruff" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "tomli" },
|
||||
{ name = "tomli-w" },
|
||||
{ name = "twine" },
|
||||
{ name = "types-mock" },
|
||||
{ name = "types-python-dateutil" },
|
||||
{ name = "types-requests" },
|
||||
{ name = "types-setuptools" },
|
||||
{ name = "types-six" },
|
||||
{ name = "wheel" },
|
||||
]
|
||||
langchain = [
|
||||
{ name = "langchain" },
|
||||
]
|
||||
test = [
|
||||
{ name = "anthropic" },
|
||||
{ name = "coverage" },
|
||||
{ name = "django" },
|
||||
{ name = "freezegun" },
|
||||
{ name = "google-genai" },
|
||||
{ name = "langchain-anthropic" },
|
||||
{ name = "langchain-community" },
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langchain-openai" },
|
||||
{ name = "langgraph" },
|
||||
{ name = "mock" },
|
||||
{ name = "openai" },
|
||||
{ name = "parameterized" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-timeout" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anthropic", marker = "extra == 'test'", specifier = ">=0.72" },
|
||||
{ name = "backoff", specifier = ">=1.10.0" },
|
||||
{ name = "coverage", marker = "extra == 'test'" },
|
||||
{ name = "distro", specifier = ">=1.5.0" },
|
||||
{ name = "django", marker = "extra == 'test'" },
|
||||
{ name = "django-stubs", marker = "extra == 'dev'" },
|
||||
{ name = "freezegun", marker = "extra == 'test'", specifier = "==1.5.1" },
|
||||
{ name = "google-genai", marker = "extra == 'test'" },
|
||||
{ name = "langchain", marker = "extra == 'langchain'", specifier = ">=0.2.0" },
|
||||
{ name = "langchain-anthropic", marker = "extra == 'test'", specifier = ">=1.0" },
|
||||
{ name = "langchain-community", marker = "extra == 'test'", specifier = ">=0.4" },
|
||||
{ name = "langchain-core", marker = "extra == 'test'", specifier = ">=1.0" },
|
||||
{ name = "langchain-openai", marker = "extra == 'test'", specifier = ">=1.0" },
|
||||
{ name = "langgraph", marker = "extra == 'test'", specifier = ">=1.0" },
|
||||
{ name = "lxml", marker = "extra == 'dev'" },
|
||||
{ name = "mock", marker = "extra == 'test'", specifier = ">=2.0.0" },
|
||||
{ name = "mypy", marker = "extra == 'dev'" },
|
||||
{ name = "mypy-baseline", marker = "extra == 'dev'" },
|
||||
{ name = "openai", marker = "extra == 'test'", specifier = ">=2.0" },
|
||||
{ name = "packaging", marker = "extra == 'dev'" },
|
||||
{ name = "parameterized", marker = "extra == 'test'", specifier = ">=0.8.1" },
|
||||
{ name = "pre-commit", marker = "extra == 'dev'" },
|
||||
{ name = "pydantic", marker = "extra == 'dev'" },
|
||||
{ name = "pydantic", marker = "extra == 'test'" },
|
||||
{ name = "pytest", marker = "extra == 'test'" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'test'" },
|
||||
{ name = "pytest-timeout", marker = "extra == 'test'" },
|
||||
{ name = "python-dateutil", specifier = ">=2.2" },
|
||||
{ name = "requests", specifier = ">=2.7,<3.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'" },
|
||||
{ name = "setuptools", marker = "extra == 'dev'" },
|
||||
{ name = "six", specifier = ">=1.5" },
|
||||
{ name = "tomli", marker = "extra == 'dev'" },
|
||||
{ name = "tomli-w", marker = "extra == 'dev'" },
|
||||
{ name = "twine", marker = "extra == 'dev'" },
|
||||
{ name = "types-mock", marker = "extra == 'dev'" },
|
||||
{ name = "types-python-dateutil", marker = "extra == 'dev'" },
|
||||
{ name = "types-requests", marker = "extra == 'dev'" },
|
||||
{ name = "types-setuptools", marker = "extra == 'dev'" },
|
||||
{ name = "types-six", marker = "extra == 'dev'" },
|
||||
{ name = "typing-extensions", specifier = ">=4.2.0" },
|
||||
{ name = "wheel", marker = "extra == 'dev'" },
|
||||
]
|
||||
provides-extras = ["langchain", "dev", "test"]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
@@ -2127,110 +2231,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "posthog"
|
||||
version = "7.9.4"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "backoff" },
|
||||
{ name = "distro" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "requests" },
|
||||
{ name = "six" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "django-stubs" },
|
||||
{ name = "lxml" },
|
||||
{ name = "mypy" },
|
||||
{ name = "mypy-baseline" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "ruff" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "tomli" },
|
||||
{ name = "tomli-w" },
|
||||
{ name = "twine" },
|
||||
{ name = "types-mock" },
|
||||
{ name = "types-python-dateutil" },
|
||||
{ name = "types-requests" },
|
||||
{ name = "types-setuptools" },
|
||||
{ name = "types-six" },
|
||||
{ name = "wheel" },
|
||||
]
|
||||
langchain = [
|
||||
{ name = "langchain" },
|
||||
]
|
||||
test = [
|
||||
{ name = "anthropic" },
|
||||
{ name = "coverage" },
|
||||
{ name = "django" },
|
||||
{ name = "freezegun" },
|
||||
{ name = "google-genai" },
|
||||
{ name = "langchain-anthropic" },
|
||||
{ name = "langchain-community" },
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langchain-openai" },
|
||||
{ name = "langgraph" },
|
||||
{ name = "mock" },
|
||||
{ name = "openai" },
|
||||
{ name = "parameterized" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-timeout" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anthropic", marker = "extra == 'test'", specifier = ">=0.72" },
|
||||
{ name = "backoff", specifier = ">=1.10.0" },
|
||||
{ name = "coverage", marker = "extra == 'test'" },
|
||||
{ name = "distro", specifier = ">=1.5.0" },
|
||||
{ name = "django", marker = "extra == 'test'" },
|
||||
{ name = "django-stubs", marker = "extra == 'dev'" },
|
||||
{ name = "freezegun", marker = "extra == 'test'", specifier = "==1.5.1" },
|
||||
{ name = "google-genai", marker = "extra == 'test'" },
|
||||
{ name = "langchain", marker = "extra == 'langchain'", specifier = ">=0.2.0" },
|
||||
{ name = "langchain-anthropic", marker = "extra == 'test'", specifier = ">=1.0" },
|
||||
{ name = "langchain-community", marker = "extra == 'test'", specifier = ">=0.4" },
|
||||
{ name = "langchain-core", marker = "extra == 'test'", specifier = ">=1.0" },
|
||||
{ name = "langchain-openai", marker = "extra == 'test'", specifier = ">=1.0" },
|
||||
{ name = "langgraph", marker = "extra == 'test'", specifier = ">=1.0" },
|
||||
{ name = "lxml", marker = "extra == 'dev'" },
|
||||
{ name = "mock", marker = "extra == 'test'", specifier = ">=2.0.0" },
|
||||
{ name = "mypy", marker = "extra == 'dev'" },
|
||||
{ name = "mypy-baseline", marker = "extra == 'dev'" },
|
||||
{ name = "openai", marker = "extra == 'test'", specifier = ">=2.0" },
|
||||
{ name = "packaging", marker = "extra == 'dev'" },
|
||||
{ name = "parameterized", marker = "extra == 'test'", specifier = ">=0.8.1" },
|
||||
{ name = "pre-commit", marker = "extra == 'dev'" },
|
||||
{ name = "pydantic", marker = "extra == 'dev'" },
|
||||
{ name = "pydantic", marker = "extra == 'test'" },
|
||||
{ name = "pytest", marker = "extra == 'test'" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'test'" },
|
||||
{ name = "pytest-timeout", marker = "extra == 'test'" },
|
||||
{ name = "python-dateutil", specifier = ">=2.2" },
|
||||
{ name = "requests", specifier = ">=2.7,<3.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'" },
|
||||
{ name = "setuptools", marker = "extra == 'dev'" },
|
||||
{ name = "six", specifier = ">=1.5" },
|
||||
{ name = "tomli", marker = "extra == 'dev'" },
|
||||
{ name = "tomli-w", marker = "extra == 'dev'" },
|
||||
{ name = "twine", marker = "extra == 'dev'" },
|
||||
{ name = "types-mock", marker = "extra == 'dev'" },
|
||||
{ name = "types-python-dateutil", marker = "extra == 'dev'" },
|
||||
{ name = "types-requests", marker = "extra == 'dev'" },
|
||||
{ name = "types-setuptools", marker = "extra == 'dev'" },
|
||||
{ name = "types-six", marker = "extra == 'dev'" },
|
||||
{ name = "typing-extensions", specifier = ">=4.2.0" },
|
||||
{ name = "wheel", marker = "extra == 'dev'" },
|
||||
]
|
||||
provides-extras = ["langchain", "dev", "test"]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.4.0"
|
||||
|
||||
Reference in New Issue
Block a user