Skip to content

Commit a2bbf02

Browse files
committed
init
1 parent 7a6e185 commit a2bbf02

File tree

6 files changed

+282
-31
lines changed

6 files changed

+282
-31
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.24.2 – 2025-04-15
2+
3+
1. Roll out new flags endpoint to 10% of /decide traffic
4+
15
## 3.24.1 – 2025-04-11
26

37
1. Add `log_captured_exceptions` option to proxy setup
@@ -13,7 +17,7 @@
1317
## 3.22.0 – 2025-03-26
1418

1519
1. Add more information to `$feature_flag_called` events.
16-
2. Support for the `/decide?v=3` endpoint which contains more information about feature flags.
20+
2. Support for the `/decide?v=4` endpoint which contains more information about feature flags.
1721

1822
## 3.21.0 – 2025-03-17
1923

posthog/client.py

Lines changed: 193 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,22 @@
1111

1212
import distro # For Linux OS detection
1313
from dateutil.tz import tzutc
14+
import hashlib
1415
from six import string_types
1516

1617
from posthog.consumer import Consumer
1718
from posthog.exception_capture import ExceptionCapture
1819
from posthog.exception_utils import exc_info_from_error, exceptions_from_error_tuple, handle_in_app
1920
from posthog.feature_flags import InconclusiveMatchError, match_feature_flag_properties
2021
from posthog.poller import Poller
21-
from posthog.request import DEFAULT_HOST, APIError, batch_post, decide, determine_server_host, get, remote_config
22+
from posthog.request import DEFAULT_HOST, APIError, batch_post, decide, determine_server_host, get, flags, remote_config
2223
from posthog.types import (
23-
DecideResponse,
24+
FlagsResponse,
2425
FeatureFlag,
2526
FlagMetadata,
2627
FlagsAndPayloads,
2728
FlagValue,
28-
normalize_decide_response,
29+
normalize_flags_response,
2930
to_flags_and_payloads,
3031
to_payloads,
3132
to_values,
@@ -42,6 +43,75 @@
4243
ID_TYPES = (numbers.Number, string_types, UUID)
4344
MAX_DICT_SIZE = 50_000
4445

46+
# TODO: Get rid of these when you're done rolling out `/flags` to all customers
47+
ROLLOUT_PERCENTAGE = 0.1
48+
INCLUDED_HASHES = set({'c4c6803067869081a8c4686780f32de979ade862c6af9ff9ebe5b7161e18362f'}) # this is PostHog's API key
49+
# Explicitly excluding all the API tokens associated with the top 10 customers; we'll get to them soon, but don't want to rollout to them just yet
50+
EXCLUDED_HASHES = set({
51+
'5fbb169efa185c2a78d43574b01b56c66d7bb594b310f72702e1f167e4e283a9',
52+
'374be8e6556709787d472e276ebe3c46c0ab4b868ec99f4c96168a44df8307df',
53+
'6c8a2d5e9dbd4c71854aebca3026fe50045b05e19a16780dccea5439625ee1b4',
54+
'c4c6803067869081a8c4686780f32de979ade862c6af9ff9ebe5b7161e18362f',
55+
'0f1fa079412bb39b5fce8d96af3539925ede61cbc561ffcd38e27c8e8ae64edb',
56+
'e3bdce3350e62638ffbf79872c2fd69ef6cbbd35712d9faf735f874cf77ccbfc',
57+
'f96fe01cdf22f1ec75bc7c897e9605e6431fb5d8f6a8bb9d0e8fce2b0a1384a6',
58+
'6859b51ac773ea98e146bae47e98759f97ec64c253b9c0524ab56793cc5b6c75',
59+
'06b28c04e490ce1c9c017396b8b8e16fce1176a8b5de131a99d9af4df1d0fbc9',
60+
'd9c0afa45a34c9f3c1e615bfa77394b79ad7b434ea46856e3503445d5974d640',
61+
'320eb50509e2c58a50d80fac848ee0b86290c848a173a0402abdbb760b794595',
62+
'7380abb65605420dd6e61534c8eecaa6f14d25a6f90ec2edba811f7383123ded',
63+
'3182881fa027d1c8e4eea108df66dcb0387e375d1e4b551c3a3579fdb1e696d1',
64+
'd685aeb7d02ec757c4cbe591050a168d34be2f5305d9071d9695ed773057ef16',
65+
'875ab92bec4da51cf229145565364e98347fafaa2316a4a8e20f5d852bc95aed',
66+
'4a0d726e4b56d6f6d0407faf5396847146084bbabd042ca0dedba2873d8f9236',
67+
'a9dc6415c1ccd1874ed1cd303e3d5bf92ddb17ac2af968abed14a51dfb0c53be',
68+
'5f10a055c9e379869a159306b1d7242fec25584ce895f677f82a13133741c7f1',
69+
'e3e7608bbda7c15bf82fd7e2945ca74052f8b99e2090962318b6ef983c0ddb16',
70+
'7f0cbd50e11b475f6c2ed50e620c473e4bfc8df1f4c5174b49ecee1fcec6853e',
71+
'03004fb2209e6e4186c4364c71e5abc9cf272caf83cf58fb538c42684fd42fb0',
72+
'8721e8bf608c5eb4d74eeaf26fe588b4e5414742e0494ca7e67a89e1a297332b',
73+
'ac0d5c7daee8d2f89d5b3861fba0b9a0e560b0eb6944e974f37cdc52274f2d1f',
74+
'6581d65cf0c4c536122beb5d581ba2b128ed44b7528c07d4ec7837ea33d0cffe',
75+
'd0c2d4e122ecd4520af7bac133b09fde357622f20aa5a0f7a9328d25c9e9f28e',
76+
'd09de64bec03c750493b0771c9f2731204bc9a5f0479628848803e2ccded9aca',
77+
'a9f483f0cdc028a5e05d03d7ab683738f09a940c0173d9e6b004fbe85738a1f5',
78+
'2ffb5817a9fc465b9bb37b9112393cc1a274185f7f18618192421b7511b98830',
79+
'a6785a722fdb0f975a1a30302f8312709ae069358c901c609f4898a9ae14bdf2',
80+
'3d9ba35cab44358cf47c867f48c95f75b9ad54ca5407ed19576da55a085d3a8f',
81+
'ff59d2907ecb66f4d4a1705435460124a390d8cf7762dc7860d4b4171f832976',
82+
'aac9e8036d3e0efed49cd5fbea19ea8354c4e1dfc95a1585300c5178189e5bac',
83+
'1e7fa74813f733e35ea820f8272c6562b4b0c70429f1b549605cc9e8016f632e',
84+
'2cb74b224cb20b8e5a5a52f3fe5ca62672e5c77ce7f30223698bb4d4abff2293',
85+
'17a90589bfe29f40f826e2df4753c0bce17a05f4c04b9a0924304e7418aba9e8',
86+
'0925e4c5bc65ced02c65aa3afba5eaa98aed288d193f719a8fbaebafdeafc1ce',
87+
'a0308973730b505f1d6af7cd2f39c69bd86ea2a35b9d27118910e1c58d9a6a1a',
88+
'c780092461636d6d62179723f03cbfe4a7b5808a6b46de749d8b32c3384f1e74',
89+
'65d6083548c27387f9381ff2aa37581a41ba1d5e6162afdc18cf8130be528052',
90+
'e2241631d1211e15688735ec6d9f56b4839e65d2095f278630c884bd49f00be8',
91+
'f2d9e1c10371912c32e9eba18f348782345ff70d383ae8b38bc9e6b12c7841e7',
92+
'57411b20e1c406ac4339718287b3eaa83635291fb593c9a4068dd08ec1d03692',
93+
'06e91ecd6b2a9a02234951ab3a5a95aeb84ef34499a5001629aaa13d907ba1dc',
94+
'4d2f47e99000f6820307e525fcf972421335a86f39b6ada1c93d67410520af49',
95+
'538d3b1415c3feccbe68d59b5ad9ed35aa418fc64658ff603855494abf75f647',
96+
'68b11387ac9f805bdbea486b9d3e0724856180646f2b12617a81174d5c27833c',
97+
'a74797287c3d29f92fc729c2a8b3f17638cb273388e12cb8ffd972bcfbcdfdb8',
98+
'b53d2b6551ebd8d68321dbd2727a299b1d23ff15853be02fffb0c54f1f0e1349',
99+
'abad9dc57c9cb9a244b89b11f0a9123baf924a6908443dd8527cf6b411bbb33a',
100+
'd17b55c7d72052d76d76a039e1ceb613d443401d30eae91ac903a07d5ee0d2d2',
101+
'274a08018c6e4609dedc37e31aea589c527cd7b93242d305591c3f5313408ee8',
102+
'75ed9cca6d877ea218647d6021b89c5959156eed2ce4ccad29d4e497d9cd0119',
103+
'4862317bab4b4efc876a810b92a6841bcf6ba69ac7aa7ff792358862528e7fa8',
104+
'f0498fff4318e52729573a8bf451d7b978c5242af51ec8b1699798090bc00d32',
105+
'a6a3435402f66a94eefd07b16297f6b4a61e26992e8ed7742de2e49d7ea71104',
106+
'72d8ede07d3ef0fd8eb0cd7261d29f4f33b3554e06a726db151138a25a01b539',
107+
'937c4aae120326c861eb3ec23371e029d3cea21f5849e4d52d75e47e06473e5c',
108+
'e0138f35502faac574232bbbaab7ad769e2dcd449b596e32454368cb3cc035f9',
109+
'084e32dc89830d7bb120492ed55cc543de0405c7ae3d0c16c8f64ab07c44506d',
110+
'd59f0ce1670146019b2c77b56ff8faca6346adfcc93443712a613a89298e3fb9',
111+
'b99bd54a29c2e9adc17527f9df539415a1c0a83293f72e3e0c8744c5677ea1a1',
112+
'c252a61d3c19f58062ca9fe2b13dfe378bc11380705cec703d9d8d0a0e167995'
113+
})
114+
45115

46116
def get_os_info():
47117
"""
@@ -96,6 +166,37 @@ def system_context() -> dict[str, Any]:
96166
"$os_version": os_version,
97167
}
98168

169+
def is_token_in_rollout(token: str, percentage: float = 0, included_hashes: set[str] = None, excluded_hashes: set[str] = None) -> bool:
170+
"""
171+
Determines if a token should be included in a rollout based on:
172+
1. If its hash matches any included_hashes provided
173+
2. If its hash falls within the percentage rollout
174+
175+
Args:
176+
token: String to hash (usually API key)
177+
percentage: Float between 0 and 1 representing rollout percentage
178+
included_hashes: Optional set of specific SHA1 hashes to match against
179+
excluded_hashes: Optional set of specific SHA1 hashes to exclude from rollout
180+
Returns:
181+
bool: True if token should be included in rollout
182+
"""
183+
# First generate SHA1 hash of token
184+
token_hash = hashlib.sha1(token.encode('utf-8')).hexdigest()
185+
186+
# Check if hash matches any included hashes
187+
if included_hashes and token_hash in included_hashes:
188+
return True
189+
190+
# Check if hash matches any excluded hashes
191+
if excluded_hashes and token_hash in excluded_hashes:
192+
return False
193+
194+
# Convert first 8 chars of hash to int and divide by max value to get number between 0-1
195+
hash_int = int(token_hash[:8], 16)
196+
hash_float = hash_int / 0xffffffff
197+
198+
return hash_float < percentage
199+
99200

100201
class Client(object):
101202
"""Create a new PostHog client."""
@@ -263,7 +364,7 @@ def get_feature_variants(
263364
"""
264365
Get feature flag variants for a distinct_id by calling decide.
265366
"""
266-
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties, disable_geoip)
367+
resp_data = self.get_flags_decision(distinct_id, groups, person_properties, group_properties, disable_geoip)
267368
return to_values(resp_data) or {}
268369

269370
def get_feature_payloads(
@@ -272,7 +373,7 @@ def get_feature_payloads(
272373
"""
273374
Get feature flag payloads for a distinct_id by calling decide.
274375
"""
275-
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties, disable_geoip)
376+
resp_data = self.get_flags_decision(distinct_id, groups, person_properties, group_properties, disable_geoip)
276377
return to_payloads(resp_data) or {}
277378

278379
def get_feature_flags_and_payloads(
@@ -281,12 +382,15 @@ def get_feature_flags_and_payloads(
281382
"""
282383
Get feature flags and payloads for a distinct_id by calling decide.
283384
"""
284-
resp = self.get_decide(distinct_id, groups, person_properties, group_properties, disable_geoip)
385+
resp = self.get_flags_decision(distinct_id, groups, person_properties, group_properties, disable_geoip)
285386
return to_flags_and_payloads(resp)
286387

287-
def get_decide(
388+
def get_flags_decision(
288389
self, distinct_id, groups=None, person_properties=None, group_properties=None, disable_geoip=None
289-
) -> DecideResponse:
390+
) -> FlagsResponse:
391+
"""
392+
Get feature flags decision, using either flags() or decide() API based on rollout.
393+
"""
290394
require("distinct_id", distinct_id, ID_TYPES)
291395

292396
if disable_geoip is None:
@@ -304,9 +408,85 @@ def get_decide(
304408
"group_properties": group_properties,
305409
"disable_geoip": disable_geoip,
306410
}
307-
resp_data = decide(self.api_key, self.host, timeout=self.feature_flags_request_timeout_seconds, **request_data)
308411

309-
return normalize_decide_response(resp_data)
412+
# Check if this API key should use flags() instead of decide()
413+
# You can adjust these values as needed for the rollout
414+
ROLLOUT_PERCENTAGE = 0.1
415+
INCLUDED_HASHES = set({'c4c6803067869081a8c4686780f32de979ade862c6af9ff9ebe5b7161e18362f'}) # this is PostHog's API key
416+
# Explicitly excluding all the API tokens associated with the top 10 customers; we'll get to them soon, but don't want to rollout to them just yet
417+
EXCLUDED_HASHES = set({
418+
'5fbb169efa185c2a78d43574b01b56c66d7bb594b310f72702e1f167e4e283a9',
419+
'374be8e6556709787d472e276ebe3c46c0ab4b868ec99f4c96168a44df8307df',
420+
'6c8a2d5e9dbd4c71854aebca3026fe50045b05e19a16780dccea5439625ee1b4',
421+
'c4c6803067869081a8c4686780f32de979ade862c6af9ff9ebe5b7161e18362f',
422+
'0f1fa079412bb39b5fce8d96af3539925ede61cbc561ffcd38e27c8e8ae64edb',
423+
'e3bdce3350e62638ffbf79872c2fd69ef6cbbd35712d9faf735f874cf77ccbfc',
424+
'f96fe01cdf22f1ec75bc7c897e9605e6431fb5d8f6a8bb9d0e8fce2b0a1384a6',
425+
'6859b51ac773ea98e146bae47e98759f97ec64c253b9c0524ab56793cc5b6c75',
426+
'06b28c04e490ce1c9c017396b8b8e16fce1176a8b5de131a99d9af4df1d0fbc9',
427+
'd9c0afa45a34c9f3c1e615bfa77394b79ad7b434ea46856e3503445d5974d640',
428+
'320eb50509e2c58a50d80fac848ee0b86290c848a173a0402abdbb760b794595',
429+
'7380abb65605420dd6e61534c8eecaa6f14d25a6f90ec2edba811f7383123ded',
430+
'3182881fa027d1c8e4eea108df66dcb0387e375d1e4b551c3a3579fdb1e696d1',
431+
'd685aeb7d02ec757c4cbe591050a168d34be2f5305d9071d9695ed773057ef16',
432+
'875ab92bec4da51cf229145565364e98347fafaa2316a4a8e20f5d852bc95aed',
433+
'4a0d726e4b56d6f6d0407faf5396847146084bbabd042ca0dedba2873d8f9236',
434+
'a9dc6415c1ccd1874ed1cd303e3d5bf92ddb17ac2af968abed14a51dfb0c53be',
435+
'5f10a055c9e379869a159306b1d7242fec25584ce895f677f82a13133741c7f1',
436+
'e3e7608bbda7c15bf82fd7e2945ca74052f8b99e2090962318b6ef983c0ddb16',
437+
'7f0cbd50e11b475f6c2ed50e620c473e4bfc8df1f4c5174b49ecee1fcec6853e',
438+
'03004fb2209e6e4186c4364c71e5abc9cf272caf83cf58fb538c42684fd42fb0',
439+
'8721e8bf608c5eb4d74eeaf26fe588b4e5414742e0494ca7e67a89e1a297332b',
440+
'ac0d5c7daee8d2f89d5b3861fba0b9a0e560b0eb6944e974f37cdc52274f2d1f',
441+
'6581d65cf0c4c536122beb5d581ba2b128ed44b7528c07d4ec7837ea33d0cffe',
442+
'd0c2d4e122ecd4520af7bac133b09fde357622f20aa5a0f7a9328d25c9e9f28e',
443+
'd09de64bec03c750493b0771c9f2731204bc9a5f0479628848803e2ccded9aca',
444+
'a9f483f0cdc028a5e05d03d7ab683738f09a940c0173d9e6b004fbe85738a1f5',
445+
'2ffb5817a9fc465b9bb37b9112393cc1a274185f7f18618192421b7511b98830',
446+
'a6785a722fdb0f975a1a30302f8312709ae069358c901c609f4898a9ae14bdf2',
447+
'3d9ba35cab44358cf47c867f48c95f75b9ad54ca5407ed19576da55a085d3a8f',
448+
'ff59d2907ecb66f4d4a1705435460124a390d8cf7762dc7860d4b4171f832976',
449+
'aac9e8036d3e0efed49cd5fbea19ea8354c4e1dfc95a1585300c5178189e5bac',
450+
'1e7fa74813f733e35ea820f8272c6562b4b0c70429f1b549605cc9e8016f632e',
451+
'2cb74b224cb20b8e5a5a52f3fe5ca62672e5c77ce7f30223698bb4d4abff2293',
452+
'17a90589bfe29f40f826e2df4753c0bce17a05f4c04b9a0924304e7418aba9e8',
453+
'0925e4c5bc65ced02c65aa3afba5eaa98aed288d193f719a8fbaebafdeafc1ce',
454+
'a0308973730b505f1d6af7cd2f39c69bd86ea2a35b9d27118910e1c58d9a6a1a',
455+
'c780092461636d6d62179723f03cbfe4a7b5808a6b46de749d8b32c3384f1e74',
456+
'65d6083548c27387f9381ff2aa37581a41ba1d5e6162afdc18cf8130be528052',
457+
'e2241631d1211e15688735ec6d9f56b4839e65d2095f278630c884bd49f00be8',
458+
'f2d9e1c10371912c32e9eba18f348782345ff70d383ae8b38bc9e6b12c7841e7',
459+
'57411b20e1c406ac4339718287b3eaa83635291fb593c9a4068dd08ec1d03692',
460+
'06e91ecd6b2a9a02234951ab3a5a95aeb84ef34499a5001629aaa13d907ba1dc',
461+
'4d2f47e99000f6820307e525fcf972421335a86f39b6ada1c93d67410520af49',
462+
'538d3b1415c3feccbe68d59b5ad9ed35aa418fc64658ff603855494abf75f647',
463+
'68b11387ac9f805bdbea486b9d3e0724856180646f2b12617a81174d5c27833c',
464+
'a74797287c3d29f92fc729c2a8b3f17638cb273388e12cb8ffd972bcfbcdfdb8',
465+
'b53d2b6551ebd8d68321dbd2727a299b1d23ff15853be02fffb0c54f1f0e1349',
466+
'abad9dc57c9cb9a244b89b11f0a9123baf924a6908443dd8527cf6b411bbb33a',
467+
'd17b55c7d72052d76d76a039e1ceb613d443401d30eae91ac903a07d5ee0d2d2',
468+
'274a08018c6e4609dedc37e31aea589c527cd7b93242d305591c3f5313408ee8',
469+
'75ed9cca6d877ea218647d6021b89c5959156eed2ce4ccad29d4e497d9cd0119',
470+
'4862317bab4b4efc876a810b92a6841bcf6ba69ac7aa7ff792358862528e7fa8',
471+
'f0498fff4318e52729573a8bf451d7b978c5242af51ec8b1699798090bc00d32',
472+
'a6a3435402f66a94eefd07b16297f6b4a61e26992e8ed7742de2e49d7ea71104',
473+
'72d8ede07d3ef0fd8eb0cd7261d29f4f33b3554e06a726db151138a25a01b539',
474+
'937c4aae120326c861eb3ec23371e029d3cea21f5849e4d52d75e47e06473e5c',
475+
'e0138f35502faac574232bbbaab7ad769e2dcd449b596e32454368cb3cc035f9',
476+
'084e32dc89830d7bb120492ed55cc543de0405c7ae3d0c16c8f64ab07c44506d',
477+
'd59f0ce1670146019b2c77b56ff8faca6346adfcc93443712a613a89298e3fb9',
478+
'b99bd54a29c2e9adc17527f9df539415a1c0a83293f72e3e0c8744c5677ea1a1',
479+
'c252a61d3c19f58062ca9fe2b13dfe378bc11380705cec703d9d8d0a0e167995'
480+
})
481+
482+
use_flags = is_token_in_rollout(self.api_key, ROLLOUT_PERCENTAGE, included_hashes=INCLUDED_HASHES, excluded_hashes=EXCLUDED_HASHES)
483+
484+
if use_flags:
485+
resp_data = flags(self.api_key, self.host, timeout=self.feature_flags_request_timeout_seconds, **request_data)
486+
else:
487+
resp_data = decide(self.api_key, self.host, timeout=self.feature_flags_request_timeout_seconds, **request_data)
488+
489+
return normalize_flags_response(resp_data)
310490

311491
def capture(
312492
self,
@@ -970,7 +1150,7 @@ def _get_feature_flag_details_from_decide(
9701150
"""
9711151
Calls /decide and returns the flag details and request id
9721152
"""
973-
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties, disable_geoip)
1153+
resp_data = self.get_flags_decision(distinct_id, groups, person_properties, group_properties, disable_geoip)
9741154
request_id = resp_data.get("requestId")
9751155
flags = resp_data.get("flags")
9761156
flag_details = flags.get(key) if flags else None
@@ -1101,7 +1281,7 @@ def get_all_flags_and_payloads(
11011281

11021282
if fallback_to_decide and not only_evaluate_locally:
11031283
try:
1104-
decide_response = self.get_decide(
1284+
decide_response = self.get_flags_decision(
11051285
distinct_id,
11061286
groups=groups,
11071287
person_properties=person_properties,
@@ -1181,4 +1361,4 @@ def stringify_id(val):
11811361
return None
11821362
if isinstance(val, string_types):
11831363
return val
1184-
return str(val)
1364+
return str(val)

posthog/request.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def post(
5252
url = remove_trailing_slash(host or DEFAULT_HOST) + path
5353
body["api_key"] = api_key
5454
data = json.dumps(body, cls=DatetimeSerializer)
55-
log.debug("making request: %s", data)
55+
log.debug("making request: %s to url: %s", data, url)
5656
headers = {"Content-Type": "application/json", "User-Agent": USER_AGENT}
5757
if gzip:
5858
headers["Content-Encoding"] = "gzip"
@@ -105,6 +105,11 @@ def decide(api_key: str, host: Optional[str] = None, gzip: bool = False, timeout
105105
res = post(api_key, host, "/decide/?v=4", gzip, timeout, **kwargs)
106106
return _process_response(res, success_message="Feature flags decided successfully")
107107

108+
def flags(api_key: str, host: Optional[str] = None, gzip: bool = False, timeout: int = 15, **kwargs) -> Any:
109+
"""Post the `kwargs to the flags API endpoint"""
110+
res = post(api_key, host, "/flags/?v=2", gzip, timeout, **kwargs)
111+
return _process_response(res, success_message="Feature flags decided successfully")
112+
108113

109114
def remote_config(personal_api_key: str, host: Optional[str] = None, key: str = "", timeout: int = 15) -> Any:
110115
"""Get remote config flag value from remote_config API endpoint"""

0 commit comments

Comments
 (0)