From 05998d0f6aa42839218631167bf42ef762b0d56d Mon Sep 17 00:00:00 2001 From: Anders <754494+andehen@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:59:47 +0100 Subject: [PATCH 1/7] feat(flags): read the /flags?v=4 response (#1031) (cherry picked from commit e094dc59522b8d7d7b68a74cac4259a61daef60f) --- .sampo/changesets/flags-v4-response.md | 5 + posthog/_async_request.py | 2 +- posthog/async_client.py | 36 +- posthog/client.py | 58 +- posthog/feature_flag_evaluations.py | 72 +- posthog/request.py | 2 +- .../feature_flag_rules_v2/SOURCE.json | 26 + .../legacy_projection.json | 661 ++++ .../feature_flag_rules_v2/readers.json | 1078 ++++++ .../feature_flag_rules_v2/responses.json | 1151 ++++++ .../v2_value_evaluation.json | 3080 +++++++++++++++++ .../test/fixtures/released_flags_7_82_1.json | 2258 ++++++++++++ posthog/test/released_flags_scenarios.py | 319 ++ posthog/test/snapshots/flags_request.json | 2 +- posthog/test/test_async_request.py | 2 +- posthog/test/test_feature_flag.py | 2 +- posthog/test/test_flags_released_behaviour.py | 75 + posthog/test/test_flags_v4_response.py | 1186 +++++++ posthog/types.py | 336 +- references/public_api_snapshot.txt | 6 +- sdk_compliance_adapter/adapter.py | 2 + sdk_compliance_adapter/test_adapter.py | 1 + 22 files changed, 10270 insertions(+), 90 deletions(-) create mode 100644 .sampo/changesets/flags-v4-response.md create mode 100644 posthog/test/fixtures/feature_flag_rules_v2/SOURCE.json create mode 100644 posthog/test/fixtures/feature_flag_rules_v2/legacy_projection.json create mode 100644 posthog/test/fixtures/feature_flag_rules_v2/readers.json create mode 100644 posthog/test/fixtures/feature_flag_rules_v2/responses.json create mode 100644 posthog/test/fixtures/feature_flag_rules_v2/v2_value_evaluation.json create mode 100644 posthog/test/fixtures/released_flags_7_82_1.json create mode 100644 posthog/test/released_flags_scenarios.py create mode 100644 posthog/test/test_flags_released_behaviour.py create mode 100644 posthog/test/test_flags_v4_response.py diff --git a/.sampo/changesets/flags-v4-response.md b/.sampo/changesets/flags-v4-response.md new file mode 100644 index 000000000..829f5b02f --- /dev/null +++ b/.sampo/changesets/flags-v4-response.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: minor +--- + +Request `/flags?v=4` and read each flag's typed value, reason and metadata. They stay internal for now: this release adds no public methods, fields or types for them, and `repr`, equality and `dataclasses.asdict` of a flag show only its existing fields. Servers that still send the older response keep working, and a flag record with a field of the wrong JSON type fails only that flag. For valid flag records, existing getters, callbacks and `$feature_flag_called` properties return the same values as before: a number or object value reads as `True`, with the value as its payload. Two things can differ: the human-readable reason text of a config version 2 flag is the one the server sends at v4, and a record with a known field of the wrong JSON type (for example a string `reason`) now reads as a failed flag instead of being read anyway or failing the whole response. The SDK logs once per client when a server sends a response without typed values. `FlagMetadata.id` and `FlagMetadata.version` are `None` when a record has none, instead of `0`, so their type is now `Optional[int]`. diff --git a/posthog/_async_request.py b/posthog/_async_request.py index c45b9093d..2cb72b1c6 100644 --- a/posthog/_async_request.py +++ b/posthog/_async_request.py @@ -97,7 +97,7 @@ async def async_flags( _serialize_flags_body, api_key, request_data ) response = await http_client.post( - "/flags/?v=2", content=data, headers=headers, timeout=timeout + "/flags/?v=4", content=data, headers=headers, timeout=timeout ) return await asyncio.to_thread(_process_flags_response, response) except httpx_module.TransportError: diff --git a/posthog/async_client.py b/posthog/async_client.py index 2f9576b5b..d3fb2a86f 100644 --- a/posthog/async_client.py +++ b/posthog/async_client.py @@ -51,8 +51,6 @@ MAX_DICT_SIZE as _MAX_DICT_SIZE, _MINIMAL_FLAG_CALLED_EVENT_PROPERTIES, Client as _SyncClient, - _metadata_has_experiment, - _parse_flag_payload, _add_context_session_id, _context_tag_defaults, _explicit_distinct_id, @@ -94,11 +92,17 @@ from .feature_flag_evaluations import ( FeatureFlagEvaluations, _EvaluatedFlagRecord, + _RemoteFlagRecord, _FeatureFlagEvaluationsHost, ) from .release_id import _resolve_release_id from .request import QuotaLimitError, determine_server_host, normalize_host -from .types import FlagMetadata, FlagValue, normalize_flags_response +from .types import ( + FlagValue, + _has_untyped_flag_records, + _UNTYPED_FLAGS_RESPONSE_MESSAGE, + normalize_flags_response, +) from .utils import ( SizeLimitedDict, _is_personal_api_key, @@ -289,6 +293,8 @@ def __init__( self.log.setLevel(logging.DEBUG) else: self.log.setLevel(logging.WARNING) + # Whether this client has logged that /flags sent records without typed values. + self._logged_untyped_flags_response = False if not self.api_key: self.log.error( @@ -1107,6 +1113,11 @@ async def _get_flags_decision( client=self._get_http_client(), **request_data, ) + if not self._logged_untyped_flags_response and _has_untyped_flag_records( + response + ): + self._logged_untyped_flags_response = True + self.log.info(_UNTYPED_FLAGS_RESPONSE_MESSAGE) return normalize_flags_response(response) async def evaluate_flags( @@ -1184,24 +1195,7 @@ async def evaluate_flags( for key, detail in response.get("flags", {}).items(): if requested_keys is not None and key not in requested_keys: continue - metadata = detail.metadata - records[key] = _EvaluatedFlagRecord( - key=key, - enabled=detail.enabled, - variant=detail.variant, - payload=_parse_flag_payload(metadata.payload), - id=metadata.id if isinstance(metadata, FlagMetadata) else None, - version=( - metadata.version if isinstance(metadata, FlagMetadata) else None - ), - reason=( - detail.reason.description - if detail.reason and detail.reason.description - else None - ), - locally_evaluated=False, - has_experiment=_metadata_has_experiment(metadata), - ) + records[key] = _RemoteFlagRecord(detail) except QuotaLimitError: self.log.warning( "[FEATURE FLAGS] Feature flag evaluation was quota limited" diff --git a/posthog/client.py b/posthog/client.py index 1d0620523..7033e6bfd 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -94,7 +94,9 @@ ) from posthog.feature_flag_evaluations import ( FeatureFlagEvaluations, + _LocalFlagRecord, _EvaluatedFlagRecord, + _RemoteFlagRecord, _FeatureFlagEvaluationsHost, ) from posthog.feature_flags import ( @@ -135,6 +137,8 @@ FlagValue, SendFeatureFlagsOptions, UnresolvedFlagReason, + _has_untyped_flag_records, + _UNTYPED_FLAGS_RESPONSE_MESSAGE, normalize_flags_response, to_flags_and_payloads, to_payloads, @@ -1122,6 +1126,8 @@ def __init__( # the /flags v2 response and the local-evaluation payload. False until the # server reports it, so full events are the fail-safe. self._minimal_flag_called_events: bool = False + # Whether this client has logged that /flags sent records without typed values. + self._logged_untyped_flags_response = False self.capture_trace_context = capture_trace_context self.capture_exception_code_variables = capture_exception_code_variables @@ -1837,6 +1843,11 @@ def _get_flags_decision( **request_data, ) + if not self._logged_untyped_flags_response and _has_untyped_flag_records( + resp_data + ): + self._logged_untyped_flags_response = True + self.log.info(_UNTYPED_FLAGS_RESPONSE_MESSAGE) response = normalize_flags_response(resp_data) # Server-controlled gate for minimal $feature_flag_called events. Only the # v2 response shape carries it; absent (legacy shape, older server, team @@ -4376,9 +4387,23 @@ def _get_feature_flag_result( flag_details, override_match_value ) + if ( + flag_details is not None + and flag_details._malformed_field is not None + ): + # The record could not be read, so it reads as disabled. It must + # not replace the last good cached value, which it falls back to + # like a failed request, and it reports what a failed request + # reports: an unknown error and no request details. + feature_flag_error = FeatureFlagError.UNKNOWN_ERROR + request_id = None + evaluated_at = None + flag_result = ( + self._get_stale_flag_fallback(distinct_id, key) or flag_result + ) # The request-start generation is an invalidation boundary, not # a claim about the server's definitions. Refresh rejects late writes. - if self.flag_cache and flag_result: + elif self.flag_cache and flag_result: self.flag_cache._set_cached_remote_flag( distinct_id, key, flag_result, local_definition_version ) @@ -5189,7 +5214,7 @@ def evaluate_flags( for key, value in local_flags.items(): flag_def = feature_flags_by_key.get(key) or {} - records[key] = _EvaluatedFlagRecord( + records[key] = _LocalFlagRecord( key=key, enabled=value is not False, variant=value if isinstance(value, str) else None, @@ -5232,34 +5257,7 @@ def evaluate_flags( continue if key in locally_evaluated_keys: continue - payload = _parse_flag_payload( - detail.metadata.payload - if isinstance(detail.metadata, FlagMetadata) - else getattr(detail.metadata, "payload", None) - ) - records[key] = _EvaluatedFlagRecord( - key=key, - enabled=detail.enabled, - variant=detail.variant, - payload=payload, - id=( - detail.metadata.id - if isinstance(detail.metadata, FlagMetadata) - else None - ), - version=( - detail.metadata.version - if isinstance(detail.metadata, FlagMetadata) - else None - ), - reason=( - detail.reason.description - if detail.reason and detail.reason.description - else None - ), - locally_evaluated=False, - has_experiment=_metadata_has_experiment(detail.metadata), - ) + records[key] = _RemoteFlagRecord(detail) except QuotaLimitError as e: self.log.warning(f"[FEATURE FLAGS] Quota limit exceeded: {e}") quota_limited = True diff --git a/posthog/feature_flag_evaluations.py b/posthog/feature_flag_evaluations.py index ef4859b46..28e5bf31e 100644 --- a/posthog/feature_flag_evaluations.py +++ b/posthog/feature_flag_evaluations.py @@ -9,12 +9,14 @@ from types import MappingProxyType from typing import Any, Callable, Dict, List, Mapping, Optional, Set, Union -from posthog.types import FlagValue, UnresolvedFlagReason +from .types import FeatureFlag as _FeatureFlag +from .types import FlagMetadata as _FlagMetadata +from .types import FlagValue, UnresolvedFlagReason, _parse_flag_payload @dataclass(frozen=True) -class _EvaluatedFlagRecord: - """Internal per-flag record stored by a FeatureFlagEvaluations instance.""" +class _LocalFlagRecord: + """Internal per-flag record of a locally evaluated flag.""" key: str enabled: bool @@ -29,6 +31,70 @@ class _EvaluatedFlagRecord: has_experiment: Optional[bool] = None +_UNDECODED: Any = object() + + +class _RemoteFlagRecord: + """Internal per-flag record of a remotely evaluated flag. + + The parsed ``/flags`` record is the only copy of the flag: the record fields are + read from it, and the payload is decoded on its first read and then kept. + """ + + __slots__ = ("details", "_payload") + + locally_evaluated = False + + def __init__(self, details: _FeatureFlag) -> None: + # The parsed /flags record, with its typed value, reason and metadata. + self.details = details + self._payload: Any = _UNDECODED + + @property + def key(self) -> str: + return self.details.key + + @property + def enabled(self) -> bool: + return self.details.enabled + + @property + def variant(self) -> Optional[str]: + return self.details.variant + + @property + def payload(self) -> Optional[Any]: + if self._payload is _UNDECODED: + self._payload = _parse_flag_payload( + getattr(self.details.metadata, "payload", None) + ) + return self._payload + + @property + def id(self) -> Optional[int]: + metadata = self.details.metadata + return metadata.id if isinstance(metadata, _FlagMetadata) else None + + @property + def version(self) -> Optional[int]: + metadata = self.details.metadata + return metadata.version if isinstance(metadata, _FlagMetadata) else None + + @property + def reason(self) -> Optional[str]: + reason = self.details.reason + return reason.description if reason and reason.description else None + + @property + def has_experiment(self) -> Optional[bool]: + metadata = self.details.metadata + return metadata.has_experiment if isinstance(metadata, _FlagMetadata) else None + + +# The per-flag record stored by a FeatureFlagEvaluations instance. +_EvaluatedFlagRecord = Union[_LocalFlagRecord, _RemoteFlagRecord] + + @dataclass class _FeatureFlagEvaluationsHost: """Callbacks the evaluations object uses to talk back to the client. diff --git a/posthog/request.py b/posthog/request.py index 9f5ff3f33..e8dbd4725 100644 --- a/posthog/request.py +++ b/posthog/request.py @@ -339,7 +339,7 @@ def flags( res = post( api_key, host, - "/flags/?v=2", + "/flags/?v=4", timeout=timeout, session=_get_flags_session(), **kwargs, diff --git a/posthog/test/fixtures/feature_flag_rules_v2/SOURCE.json b/posthog/test/fixtures/feature_flag_rules_v2/SOURCE.json new file mode 100644 index 000000000..df7f1c7e0 --- /dev/null +++ b/posthog/test/fixtures/feature_flag_rules_v2/SOURCE.json @@ -0,0 +1,26 @@ +{ + "description": "Copied unchanged from the released feature flag rules v2 contract in PostHog/posthog-sdk-test-harness. Do not edit these files; copy a newer release instead.", + "repository": "https://github.com/PostHog/posthog-sdk-test-harness", + "release": "1.22.0", + "revision": "17d861f8e230fe943934d70e3eebf5cff6bf3aa4", + "contract_directory": "contracts/feature_flag_rules_v2", + "contract_version": "7.0.0", + "files": { + "readers.json": { + "source_path": "fixtures/wire/readers.json", + "sha256": "fd8edd89f827b732c4659ce7246eab534b0106b96a465a4a2d98cfa0a17d7827" + }, + "responses.json": { + "source_path": "fixtures/wire/responses.json", + "sha256": "39aa62e340d66903e2fe86da226e622979c7dbc2efbf2b32afd040248a707c61" + }, + "legacy_projection.json": { + "source_path": "corpus/legacy_projection.json", + "sha256": "536c8386eefa75721d113bd8718ddeb7fb1aad3b4f2cce1fbcf516a346a16e26" + }, + "v2_value_evaluation.json": { + "source_path": "corpus/v2_value_evaluation.json", + "sha256": "aaddd05e47fc5a4ae07ea42ada5a659798f4b5fc8d0c0827f6a68e4daf68c128" + } + } +} diff --git a/posthog/test/fixtures/feature_flag_rules_v2/legacy_projection.json b/posthog/test/fixtures/feature_flag_rules_v2/legacy_projection.json new file mode 100644 index 000000000..3a144281c --- /dev/null +++ b/posthog/test/fixtures/feature_flag_rules_v2/legacy_projection.json @@ -0,0 +1,661 @@ +{ + "corpus_version": "1.1.0", + "config_version": 1, + "description": "Projection matrix from a version 1 evaluation outcome to each response protocol version. Response protocol versions are independent of the config version, which is 1 for every case.", + "response_protocols": { + "flags_v2": { + "endpoint": "/flags", + "selected_by": "v=2 or higher, or /decide with v=4 or higher", + "shape": "flags map of evaluation details with reason and metadata; errorsWhileComputingFlags at the top level" + }, + "flags_v1": { + "endpoint": "/flags", + "selected_by": "v=1 or absent, or /decide with v=3", + "shape": "featureFlags map of every evaluated flag plus featureFlagPayloads for flags with a payload; errorsWhileComputingFlags at the top level" + }, + "decide_v2": { + "endpoint": "/decide", + "selected_by": "v=2", + "shape": "featureFlags map of enabled flags only" + }, + "decide_v1": { + "endpoint": "/decide", + "selected_by": "v=1 or absent", + "shape": "featureFlags list of enabled flag keys" + } + }, + "asserted_fields": "flags_v2 records key, enabled, variant, failed when true, reason.code, reason.condition_index, metadata.id, metadata.version, and metadata.payload. Live responses carry additional request-level and metadata fields that this corpus does not pin.", + "cases": [ + { + "id": "projection.boolean_true", + "config_version": 1, + "outcome": { + "key": "beta-search", + "flag_id": 101, + "flag_version": 3, + "enabled": true, + "variant": null, + "payload": null, + "failed": false, + "omitted": false, + "reason": { + "code": "condition_match", + "condition_index": 0 + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": { + "beta-search": { + "key": "beta-search", + "enabled": true, + "variant": null, + "reason": { + "code": "condition_match", + "condition_index": 0 + }, + "metadata": { + "id": 101, + "version": 3, + "payload": null + } + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": { + "beta-search": true + }, + "featureFlagPayloads": {} + }, + "decide_v2": { + "response_version": 2, + "featureFlags": { + "beta-search": true + } + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [ + "beta-search" + ] + } + } + }, + { + "id": "projection.boolean_true_with_payload", + "config_version": 1, + "outcome": { + "key": "beta-search", + "flag_id": 101, + "flag_version": 3, + "enabled": true, + "variant": null, + "payload": "{\"cta\":\"Try beta search\"}", + "failed": false, + "omitted": false, + "reason": { + "code": "condition_match", + "condition_index": 0 + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": { + "beta-search": { + "key": "beta-search", + "enabled": true, + "variant": null, + "reason": { + "code": "condition_match", + "condition_index": 0 + }, + "metadata": { + "id": 101, + "version": 3, + "payload": "{\"cta\":\"Try beta search\"}" + } + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": { + "beta-search": true + }, + "featureFlagPayloads": { + "beta-search": "{\"cta\":\"Try beta search\"}" + } + }, + "decide_v2": { + "response_version": 2, + "featureFlags": { + "beta-search": true + } + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [ + "beta-search" + ] + } + }, + "notes": "The payload is the stored JSON string; no response version parses it." + }, + { + "id": "projection.boolean_false_targeting_miss", + "config_version": 1, + "outcome": { + "key": "beta-search", + "flag_id": 101, + "flag_version": 3, + "enabled": false, + "variant": null, + "payload": null, + "failed": false, + "omitted": false, + "reason": { + "code": "no_condition_match", + "condition_index": 0 + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": { + "beta-search": { + "key": "beta-search", + "enabled": false, + "variant": null, + "reason": { + "code": "no_condition_match", + "condition_index": 0 + }, + "metadata": { + "id": 101, + "version": 3, + "payload": null + } + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": { + "beta-search": false + }, + "featureFlagPayloads": {} + }, + "decide_v2": { + "response_version": 2, + "featureFlags": {} + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [] + } + }, + "notes": "Every evaluated flag appears in the flags_v1 map, including false results. decide_v2 and decide_v1 carry enabled flags only." + }, + { + "id": "projection.boolean_false_out_of_rollout", + "config_version": 1, + "outcome": { + "key": "beta-search-rollout", + "flag_id": 102, + "flag_version": 1, + "enabled": false, + "variant": null, + "payload": null, + "failed": false, + "omitted": false, + "reason": { + "code": "out_of_rollout_bound", + "condition_index": 0 + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": { + "beta-search-rollout": { + "key": "beta-search-rollout", + "enabled": false, + "variant": null, + "reason": { + "code": "out_of_rollout_bound", + "condition_index": 0 + }, + "metadata": { + "id": 102, + "version": 1, + "payload": null + } + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": { + "beta-search-rollout": false + }, + "featureFlagPayloads": {} + }, + "decide_v2": { + "response_version": 2, + "featureFlags": {} + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [] + } + } + }, + { + "id": "projection.variant", + "config_version": 1, + "outcome": { + "key": "checkout-layout", + "flag_id": 106, + "flag_version": 5, + "enabled": true, + "variant": "control", + "payload": null, + "failed": false, + "omitted": false, + "reason": { + "code": "condition_match", + "condition_index": 1 + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": { + "checkout-layout": { + "key": "checkout-layout", + "enabled": true, + "variant": "control", + "reason": { + "code": "condition_match", + "condition_index": 1 + }, + "metadata": { + "id": 106, + "version": 5, + "payload": null + } + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": { + "checkout-layout": "control" + }, + "featureFlagPayloads": {} + }, + "decide_v2": { + "response_version": 2, + "featureFlags": { + "checkout-layout": "control" + } + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [ + "checkout-layout" + ] + } + }, + "notes": "A variant projects as its string in every map and as membership in the list." + }, + { + "id": "projection.variant_with_payload", + "config_version": 1, + "outcome": { + "key": "checkout-layout", + "flag_id": 106, + "flag_version": 5, + "enabled": true, + "variant": "compact", + "payload": "{\"density\":\"compact\"}", + "failed": false, + "omitted": false, + "reason": { + "code": "condition_match", + "condition_index": 1 + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": { + "checkout-layout": { + "key": "checkout-layout", + "enabled": true, + "variant": "compact", + "reason": { + "code": "condition_match", + "condition_index": 1 + }, + "metadata": { + "id": 106, + "version": 5, + "payload": "{\"density\":\"compact\"}" + } + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": { + "checkout-layout": "compact" + }, + "featureFlagPayloads": { + "checkout-layout": "{\"density\":\"compact\"}" + } + }, + "decide_v2": { + "response_version": 2, + "featureFlags": { + "checkout-layout": "compact" + } + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [ + "checkout-layout" + ] + } + } + }, + { + "id": "projection.holdout_variant", + "config_version": 1, + "outcome": { + "key": "checkout-experiment", + "flag_id": 113, + "flag_version": 4, + "enabled": true, + "variant": "holdout-7", + "payload": null, + "failed": false, + "omitted": false, + "reason": { + "code": "holdout_condition_value", + "condition_index": null + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": { + "checkout-experiment": { + "key": "checkout-experiment", + "enabled": true, + "variant": "holdout-7", + "reason": { + "code": "holdout_condition_value", + "condition_index": null + }, + "metadata": { + "id": 113, + "version": 4, + "payload": null + } + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": { + "checkout-experiment": "holdout-7" + }, + "featureFlagPayloads": {} + }, + "decide_v2": { + "response_version": 2, + "featureFlags": { + "checkout-experiment": "holdout-7" + } + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [ + "checkout-experiment" + ] + } + }, + "notes": "The synthesized holdout variant projects like any other string value." + }, + { + "id": "projection.no_group_type", + "config_version": 1, + "outcome": { + "key": "org-dashboard", + "flag_id": 109, + "flag_version": 2, + "enabled": false, + "variant": null, + "payload": null, + "failed": false, + "omitted": false, + "reason": { + "code": "no_group_type", + "condition_index": 0 + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": { + "org-dashboard": { + "key": "org-dashboard", + "enabled": false, + "variant": null, + "reason": { + "code": "no_group_type", + "condition_index": 0 + }, + "metadata": { + "id": 109, + "version": 2, + "payload": null + } + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": { + "org-dashboard": false + }, + "featureFlagPayloads": {} + }, + "decide_v2": { + "response_version": 2, + "featureFlags": {} + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [] + } + } + }, + { + "id": "projection.missing_dependency", + "config_version": 1, + "outcome": { + "key": "beta-search-suggestions-orphan", + "flag_id": 112, + "flag_version": 1, + "enabled": false, + "variant": null, + "payload": null, + "failed": false, + "omitted": false, + "reason": { + "code": "missing_dependency", + "condition_index": null + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": { + "beta-search-suggestions-orphan": { + "key": "beta-search-suggestions-orphan", + "enabled": false, + "variant": null, + "reason": { + "code": "missing_dependency", + "condition_index": null + }, + "metadata": { + "id": 112, + "version": 1, + "payload": null + } + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": { + "beta-search-suggestions-orphan": false + }, + "featureFlagPayloads": {} + }, + "decide_v2": { + "response_version": 2, + "featureFlags": {} + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [] + } + }, + "notes": "A missing dependency is a closed false result, not an evaluation failure, so errorsWhileComputingFlags stays false." + }, + { + "id": "projection.failed_evaluation", + "config_version": 1, + "outcome": { + "key": "beta-search", + "flag_id": 101, + "flag_version": 3, + "enabled": false, + "variant": null, + "payload": null, + "failed": true, + "omitted": false, + "reason": { + "code": "timeout", + "condition_index": null + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": true, + "flags": { + "beta-search": { + "key": "beta-search", + "enabled": false, + "variant": null, + "reason": { + "code": "timeout", + "condition_index": null + }, + "metadata": { + "id": 101, + "version": 3, + "payload": null + }, + "failed": true + } + } + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": true, + "featureFlags": { + "beta-search": false + }, + "featureFlagPayloads": {} + }, + "decide_v2": { + "response_version": 2, + "featureFlags": {} + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [] + } + }, + "notes": "A failed evaluation projects as false in the flags_v1 map with errorsWhileComputingFlags true, is omitted from decide_v2 and decide_v1, and carries failed true in flags_v2. The reason code is representative." + }, + { + "id": "projection.inactive_omitted", + "config_version": 1, + "outcome": { + "key": "retired-banner", + "flag_id": 114, + "flag_version": 9, + "enabled": false, + "variant": null, + "payload": null, + "failed": false, + "omitted": true, + "reason": { + "code": "no_condition_match", + "condition_index": null + } + }, + "projections": { + "flags_v2": { + "response_version": 2, + "errorsWhileComputingFlags": false, + "flags": {} + }, + "flags_v1": { + "response_version": 1, + "errorsWhileComputingFlags": false, + "featureFlags": {}, + "featureFlagPayloads": {} + }, + "decide_v2": { + "response_version": 2, + "featureFlags": {} + }, + "decide_v1": { + "response_version": 1, + "featureFlags": [] + } + }, + "notes": "Inactive, archived, and deleted flags are omitted from every response version." + } + ] +} diff --git a/posthog/test/fixtures/feature_flag_rules_v2/readers.json b/posthog/test/fixtures/feature_flag_rules_v2/readers.json new file mode 100644 index 000000000..7ed8c252b --- /dev/null +++ b/posthog/test/fixtures/feature_flag_rules_v2/readers.json @@ -0,0 +1,1078 @@ +{ + "schema": "schemas/flags_response_v4.schema.json", + "templates": { + "targeting_match_targeted_release": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "targeting_match", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null, + "rule_type": "targeted_release", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "targeting_match_percentage_rollout": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "targeting_match", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null, + "rule_type": "percentage_rollout", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "experiment_split": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "experiment_split", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001", + "variant_key": "arm_a" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "rollout_miss_percentage_rollout": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "rollout_miss", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null, + "rule_type": "percentage_rollout", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "rollout_miss_experiment": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "rollout_miss", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "experiment_paused": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "experiment_paused", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "holdout": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "holdout", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "no_rule_match": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "no_rule_match", + "condition_index": null + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "missing_group_key": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": null, + "reason": { + "code": "missing_group_key", + "condition_index": null + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "dependency_error": { + "errorsWhileComputingFlags": true, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": null, + "reason": { + "code": "dependency_error", + "condition_index": null + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null + }, + "failed": true + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "error": { + "errorsWhileComputingFlags": true, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": null, + "reason": { + "code": "error", + "condition_index": null + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null + }, + "failed": true + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "v1": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": "compact", + "reason": { + "code": "condition_match", + "condition_index": 0 + }, + "metadata": { + "id": 202, + "version": 9, + "config_version": 1, + "has_experiment": false, + "variant_key": "compact", + "payload": "{\"columns\":2}" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "omitted": { + "errorsWhileComputingFlags": false, + "flags": {}, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "old_server": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "enabled": true, + "variant": "compact", + "reason": { + "code": "condition_match", + "condition_index": 0 + }, + "metadata": { + "id": 202, + "version": 9, + "config_version": 2, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001", + "variant_key": "arm_a" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + } + }, + "cases": [ + { + "id": "readers.complete_split", + "template": "experiment_split", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": true, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.partial_split_missing_config_version", + "template": "experiment_split", + "remove": [ + "/flags/garden-layout/metadata/config_version" + ], + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": false, + "config_version": null, + "experiment_exposure": false, + "error_code": null + }, + "producer_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout/metadata", + "message_contains": "config_version" + }, + "producer_expected": "invalid" + }, + { + "id": "readers.partial_split_missing_rule_type", + "template": "experiment_split", + "remove": [ + "/flags/garden-layout/metadata/rule_type" + ], + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout/metadata", + "message_contains": "rule_type" + }, + "producer_expected": "invalid" + }, + { + "id": "readers.partial_split_missing_rule_id", + "template": "experiment_split", + "remove": [ + "/flags/garden-layout/metadata/rule_id" + ], + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout/metadata", + "message_contains": "rule_id" + }, + "producer_expected": "invalid" + }, + { + "id": "readers.partial_split_missing_variant_key", + "template": "experiment_split", + "remove": [ + "/flags/garden-layout/metadata/variant_key" + ], + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout/metadata", + "message_contains": "variant_key" + }, + "producer_expected": "invalid" + }, + { + "id": "readers.split_wrong_type_config_version", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/config_version": "2" + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": true, + "config_version": null, + "experiment_exposure": false, + "error_code": "PARSE_ERROR" + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "enum", + "instance_path": "/flags/garden-layout/metadata/config_version" + } + }, + { + "id": "readers.split_wrong_type_rule_type", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/rule_type": 2 + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": true, + "config_version": null, + "experiment_exposure": false, + "error_code": "PARSE_ERROR" + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "enum", + "instance_path": "/flags/garden-layout/metadata/rule_type" + } + }, + { + "id": "readers.split_wrong_type_rule_id", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/rule_id": 3 + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": true, + "config_version": null, + "experiment_exposure": false, + "error_code": "PARSE_ERROR" + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/garden-layout/metadata/rule_id" + } + }, + { + "id": "readers.split_wrong_type_experiment_id", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/experiment_id": "301" + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": true, + "config_version": null, + "experiment_exposure": false, + "error_code": "PARSE_ERROR" + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/garden-layout/metadata/experiment_id" + } + }, + { + "id": "readers.split_extra_experiment_id", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/experiment_id": 301 + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": false, + "config_version": 2, + "experiment_exposure": true, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "readers.split_wrong_type_variant_key", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/variant_key": true + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": true, + "config_version": null, + "experiment_exposure": false, + "error_code": "PARSE_ERROR" + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/garden-layout/metadata/variant_key" + } + }, + { + "id": "readers.wrong_rule_type_literal", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/rule_type": "percentage_rollout" + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "const", + "instance_path": "/flags/garden-layout/metadata/rule_type" + } + }, + { + "id": "readers.unknown_fields", + "template": "experiment_split", + "set": { + "/future_envelope": { + "nested": true + }, + "/flags/garden-layout/future_record": [], + "/flags/garden-layout/reason/future_reason": 3, + "/flags/garden-layout/metadata/future_metadata": "ignored" + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": true, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "additionalProperties", + "instance_path": "/flags/garden-layout" + } + }, + { + "id": "readers.old_server_variant", + "template": "old_server", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": "compact", + "config_version": 1, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout", + "message_contains": "value" + } + }, + { + "id": "readers.old_server_boolean", + "template": "old_server", + "remove": [ + "/flags/garden-layout/variant" + ], + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": true, + "config_version": 1, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout", + "message_contains": "value" + } + }, + { + "id": "readers.old_server_null_variant", + "template": "old_server", + "set": { + "/flags/garden-layout/variant": null + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": true, + "config_version": 1, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout", + "message_contains": "value" + } + }, + { + "id": "readers.old_server_false", + "template": "old_server", + "set": { + "/flags/garden-layout/enabled": false + }, + "remove": [ + "/flags/garden-layout/variant" + ], + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 1, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout", + "message_contains": "value" + } + }, + { + "id": "readers.v4_value_takes_precedence", + "template": "experiment_split", + "set": { + "/flags/garden-layout/enabled": true, + "/flags/garden-layout/variant": "wrong-channel" + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": true, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "additionalProperties", + "instance_path": "/flags/garden-layout" + } + }, + { + "id": "readers.missing_flag", + "template": "omitted", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": null, + "experiment_exposure": false, + "error_code": "FLAG_NOT_FOUND" + }, + "producer_expected": "valid" + }, + { + "id": "readers.targeting_match_targeted_release", + "template": "targeting_match_targeted_release", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.targeting_match_percentage_rollout", + "template": "targeting_match_percentage_rollout", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.rollout_miss_percentage_rollout", + "template": "rollout_miss_percentage_rollout", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.rollout_miss_experiment", + "template": "rollout_miss_experiment", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.experiment_paused", + "template": "experiment_paused", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.holdout", + "template": "holdout", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.holdout_variant_key", + "template": "holdout", + "set": { + "/flags/garden-layout/metadata/variant_key": "arm_a" + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "presence", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "readers.no_rule_match", + "template": "no_rule_match", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.missing_group_key", + "template": "missing_group_key", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": "INVALID_CONTEXT" + }, + "producer_expected": "valid" + }, + { + "id": "readers.dependency_error", + "template": "dependency_error", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": "GENERAL" + }, + "producer_expected": "valid" + }, + { + "id": "readers.error", + "template": "error", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": false, + "error_code": "GENERAL" + }, + "producer_expected": "valid" + }, + { + "id": "readers.error_with_value", + "template": "error", + "set": { + "/flags/garden-layout/value": false + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": true, + "config_version": 2, + "experiment_exposure": false, + "error_code": "GENERAL" + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/garden-layout/value" + } + }, + { + "id": "readers.malformed_sibling_isolation", + "template": "experiment_split", + "set": { + "/flags/sibling": { + "key": "sibling", + "value": [], + "reason": { + "code": "no_rule_match", + "condition_index": null + }, + "metadata": { + "id": 203, + "version": 1, + "config_version": 2, + "has_experiment": false + } + } + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": false, + "config_version": 2, + "experiment_exposure": true, + "error_code": null + }, + "sibling_expectations": { + "sibling": { + "error_code": "PARSE_ERROR", + "experiment_exposure": false + } + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/sibling/value" + } + }, + { + "id": "readers.partial_split_string_value", + "template": "experiment_split", + "set": { + "/flags/garden-layout/value": "usable-copy" + }, + "remove": [ + "/flags/garden-layout/metadata/variant_key" + ], + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": "usable-copy", + "config_version": 2, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "invalid", + "producer_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout/metadata", + "message_contains": "variant_key" + } + }, + { + "id": "readers.v1_at_v4", + "template": "v1", + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": false, + "value": "compact", + "config_version": 1, + "experiment_exposure": false, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.v1_failed", + "template": "v1", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/flags/garden-layout/reason": { + "code": "missing_dependency", + "condition_index": null + }, + "/errorsWhileComputingFlags": true + }, + "remove": [ + "/flags/garden-layout/metadata/variant_key" + ], + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": true, + "config_version": 1, + "experiment_exposure": false, + "error_code": "GENERAL" + }, + "producer_expected": "valid" + }, + { + "id": "readers.integral_json_number_context", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/config_version": 2.0, + "/flags/garden-layout/metadata/id": 201.0 + }, + "expected": "reader", + "reader": { + "requested_key": "garden-layout", + "caller_default": true, + "value": false, + "config_version": 2, + "experiment_exposure": true, + "error_code": null + }, + "producer_expected": "valid" + }, + { + "id": "readers.flag_key_seed", + "template": "omitted", + "set": { + "/flags/seed": { + "key": "seed", + "value": false, + "reason": { + "code": "experiment_split", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001", + "variant_key": "arm_a" + } + } + }, + "expected": "reader", + "reader": { + "requested_key": "seed", + "caller_default": true, + "value": false, + "config_version": 2, + "experiment_exposure": true, + "error_code": null + }, + "producer_expected": "valid" + } + ] +} diff --git a/posthog/test/fixtures/feature_flag_rules_v2/responses.json b/posthog/test/fixtures/feature_flag_rules_v2/responses.json new file mode 100644 index 000000000..1d1c60969 --- /dev/null +++ b/posthog/test/fixtures/feature_flag_rules_v2/responses.json @@ -0,0 +1,1151 @@ +{ + "schema": "schemas/flags_response_v4.schema.json", + "templates": { + "targeting_match_targeted_release": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "targeting_match", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null, + "rule_type": "targeted_release", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "targeting_match_percentage_rollout": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "targeting_match", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null, + "rule_type": "percentage_rollout", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "experiment_split": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "experiment_split", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001", + "variant_key": "arm_a" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "rollout_miss_percentage_rollout": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "rollout_miss", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null, + "rule_type": "percentage_rollout", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "rollout_miss_experiment": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "rollout_miss", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "experiment_paused": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "experiment_paused", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "holdout": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "holdout", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "no_rule_match": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": false, + "reason": { + "code": "no_rule_match", + "condition_index": null + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "missing_group_key": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": null, + "reason": { + "code": "missing_group_key", + "condition_index": null + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "dependency_error": { + "errorsWhileComputingFlags": true, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": null, + "reason": { + "code": "dependency_error", + "condition_index": null + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null + }, + "failed": true + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "error": { + "errorsWhileComputingFlags": true, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": null, + "reason": { + "code": "error", + "condition_index": null + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": false, + "payload": null + }, + "failed": true + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "v1": { + "errorsWhileComputingFlags": false, + "flags": { + "garden-layout": { + "key": "garden-layout", + "value": "compact", + "reason": { + "code": "condition_match", + "condition_index": 0 + }, + "metadata": { + "id": 202, + "version": 9, + "config_version": 1, + "has_experiment": false, + "variant_key": "compact", + "payload": "{\"columns\":2}" + } + } + }, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + }, + "omitted": { + "errorsWhileComputingFlags": false, + "flags": {}, + "requestId": "20000000-0000-4000-8000-000000000002", + "evaluatedAt": 1800000000000 + } + }, + "cases": [ + { + "id": "responses.targeting_match_targeted_release", + "template": "targeting_match_targeted_release", + "expected": "valid" + }, + { + "id": "responses.missing_id", + "template": "targeting_match_targeted_release", + "remove": [ + "/flags/garden-layout/metadata/id" + ], + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout/metadata", + "message_contains": "id" + } + }, + { + "id": "responses.missing_version", + "template": "targeting_match_targeted_release", + "remove": [ + "/flags/garden-layout/metadata/version" + ], + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout/metadata", + "message_contains": "version" + } + }, + { + "id": "responses.missing_config_version", + "template": "targeting_match_targeted_release", + "remove": [ + "/flags/garden-layout/metadata/config_version" + ], + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout/metadata", + "message_contains": "config_version" + } + }, + { + "id": "responses.targeting_match_targeted_release_forbidden_failed", + "template": "targeting_match_targeted_release", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/errorsWhileComputingFlags": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.targeting_match_targeted_release_flag_with_experiment", + "template": "targeting_match_targeted_release", + "set": { + "/flags/garden-layout/metadata/has_experiment": true + }, + "expected": "valid" + }, + { + "id": "responses.targeting_match_percentage_rollout", + "template": "targeting_match_percentage_rollout", + "expected": "valid" + }, + { + "id": "responses.targeting_match_percentage_rollout_forbidden_failed", + "template": "targeting_match_percentage_rollout", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/errorsWhileComputingFlags": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.experiment_split", + "template": "experiment_split", + "expected": "valid" + }, + { + "id": "responses.experiment_split_forbidden_failed", + "template": "experiment_split", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/errorsWhileComputingFlags": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.experiment_split_flag_without_experiment", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/has_experiment": false + }, + "expected": "valid" + }, + { + "id": "responses.rollout_miss_percentage_rollout", + "template": "rollout_miss_percentage_rollout", + "expected": "valid" + }, + { + "id": "responses.rollout_miss_percentage_rollout_forbidden_failed", + "template": "rollout_miss_percentage_rollout", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/errorsWhileComputingFlags": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.rollout_miss_experiment", + "template": "rollout_miss_experiment", + "expected": "valid" + }, + { + "id": "responses.rollout_miss_experiment_forbidden_failed", + "template": "rollout_miss_experiment", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/errorsWhileComputingFlags": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.experiment_paused", + "template": "experiment_paused", + "expected": "valid" + }, + { + "id": "responses.experiment_paused_forbidden_failed", + "template": "experiment_paused", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/errorsWhileComputingFlags": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.holdout", + "template": "holdout", + "expected": "valid" + }, + { + "id": "responses.holdout_missing_rule_id", + "template": "holdout", + "remove": [ + "/flags/garden-layout/metadata/rule_id" + ], + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "required", + "instance_path": "/flags/garden-layout/metadata", + "message_contains": "rule_id" + } + }, + { + "id": "responses.holdout_forbidden_failed", + "template": "holdout", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/errorsWhileComputingFlags": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.no_rule_match", + "template": "no_rule_match", + "expected": "valid" + }, + { + "id": "responses.no_rule_match_forbidden_failed", + "template": "no_rule_match", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/errorsWhileComputingFlags": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "not", + "instance_path": "/flags/garden-layout" + } + }, + { + "id": "responses.missing_group_key", + "template": "missing_group_key", + "expected": "valid" + }, + { + "id": "responses.missing_group_key_forbidden_failed", + "template": "missing_group_key", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/errorsWhileComputingFlags": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "not", + "instance_path": "/flags/garden-layout" + } + }, + { + "id": "responses.dependency_error", + "template": "dependency_error", + "expected": "valid" + }, + { + "id": "responses.dependency_error_missing_failed", + "template": "dependency_error", + "remove": [ + "/flags/garden-layout/failed" + ], + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout", + "message_contains": "failed" + } + }, + { + "id": "responses.dependency_error_nonnull_value", + "template": "dependency_error", + "set": { + "/flags/garden-layout/value": false + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/garden-layout/value" + } + }, + { + "id": "responses.error", + "template": "error", + "expected": "valid" + }, + { + "id": "responses.error_missing_failed", + "template": "error", + "remove": [ + "/flags/garden-layout/failed" + ], + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "required", + "instance_path": "/flags/garden-layout", + "message_contains": "failed" + } + }, + { + "id": "responses.error_nonnull_value", + "template": "error", + "set": { + "/flags/garden-layout/value": false + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/garden-layout/value" + } + }, + { + "id": "responses.v1", + "template": "v1", + "expected": "valid" + }, + { + "id": "responses.omitted", + "template": "omitted", + "expected": "valid" + }, + { + "id": "responses.split_wrong_type_config_version", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/config_version": "2" + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "enum", + "instance_path": "/flags/garden-layout/metadata/config_version" + } + }, + { + "id": "responses.split_wrong_type_rule_type", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/rule_type": 2 + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "enum", + "instance_path": "/flags/garden-layout/metadata/rule_type" + } + }, + { + "id": "responses.split_wrong_type_rule_id", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/rule_id": 3 + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/garden-layout/metadata/rule_id" + } + }, + { + "id": "responses.split_wrong_type_variant_key", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/variant_key": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/garden-layout/metadata/variant_key" + } + }, + { + "id": "responses.forbidden_legacy_enabled", + "template": "experiment_split", + "set": { + "/flags/garden-layout/enabled": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "additionalProperties", + "instance_path": "/flags/garden-layout", + "message_contains": "enabled" + } + }, + { + "id": "responses.forbidden_legacy_variant", + "template": "experiment_split", + "set": { + "/flags/garden-layout/variant": "arm_a" + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "additionalProperties", + "instance_path": "/flags/garden-layout", + "message_contains": "variant" + } + }, + { + "id": "responses.split_arm_b_equal_value", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/variant_key": "arm_b" + }, + "expected": "valid" + }, + { + "id": "responses.split_string_value", + "template": "experiment_split", + "set": { + "/flags/garden-layout/value": "same-copy" + }, + "expected": "valid" + }, + { + "id": "responses.null_default", + "template": "no_rule_match", + "set": { + "/flags/garden-layout/value": null + }, + "expected": "valid" + }, + { + "id": "responses.request_failure_omission", + "template": "omitted", + "set": { + "/errorsWhileComputingFlags": true + }, + "expected": "valid" + }, + { + "id": "responses.missing_group_nonnull_value", + "template": "missing_group_key", + "set": { + "/flags/garden-layout/value": false + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "type", + "instance_path": "/flags/garden-layout/value" + } + }, + { + "id": "responses.v1_failed", + "template": "v1", + "set": { + "/flags/garden-layout/failed": true, + "/flags/garden-layout/value": null, + "/flags/garden-layout/reason": { + "code": "missing_dependency", + "condition_index": null + }, + "/errorsWhileComputingFlags": true + }, + "remove": [ + "/flags/garden-layout/metadata/variant_key" + ], + "expected": "valid" + }, + { + "id": "responses.v1_quick_win", + "template": "v1", + "set": { + "/flags/garden-layout/metadata/experiment_id": 301, + "/flags/garden-layout/metadata/forced_variant": true, + "/flags/garden-layout/metadata/has_experiment": true + }, + "expected": "valid" + }, + { + "id": "responses.v2_forced_variant", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/forced_variant": true + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.key_mismatch", + "template": "v1", + "set": { + "/flags/garden-layout/key": "different-key" + }, + "expected": "invalid", + "expected_failure": { + "layer": "semantic", + "keyword": "map_key", + "instance_path": "/flags/garden-layout/key" + } + }, + { + "id": "responses.failure_envelope", + "template": "error", + "set": { + "/errorsWhileComputingFlags": false + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "const", + "instance_path": "/errorsWhileComputingFlags" + } + }, + { + "id": "responses.local_only_reason", + "template": "no_rule_match", + "set": { + "/flags/garden-layout/reason/code": "flag_disabled" + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "enum", + "instance_path": "/flags/garden-layout/reason/code" + } + }, + { + "id": "responses.nested_seed", + "template": "no_rule_match", + "set": { + "/flags/garden-layout/conditions": [ + { + "nested": [ + { + "seed": "invented-assignment" + } + ] + } + ] + }, + "expected": "invalid", + "expected_failure": { + "layer": "seed", + "keyword": "assignment_seed", + "instance_path": "/flags/garden-layout/conditions/0/nested/0/seed" + } + }, + { + "id": "responses.passthrough_seed", + "template": "omitted", + "set": { + "/future": { + "items": [ + { + "holdout_seed": "invented-holdout" + } + ] + } + }, + "expected": "invalid", + "expected_failure": { + "layer": "seed", + "keyword": "assignment_seed", + "instance_path": "/future/items/0/holdout_seed" + } + }, + { + "id": "responses.reserved_number_wire_value", + "template": "no_rule_match", + "set": { + "/flags/garden-layout/value": 7 + }, + "expected": "valid" + }, + { + "id": "responses.reserved_object_wire_value", + "template": "no_rule_match", + "set": { + "/flags/garden-layout/value": { + "layout": "compact" + } + }, + "expected": "valid" + }, + { + "id": "responses.targeting_match_targeted_release_wrong_rule_type", + "template": "targeting_match_targeted_release", + "set": { + "/flags/garden-layout/metadata/rule_type": "experiment" + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "const", + "instance_path": "/flags/garden-layout/metadata/rule_type" + } + }, + { + "id": "responses.experiment_split_wrong_rule_type", + "template": "experiment_split", + "set": { + "/flags/garden-layout/metadata/rule_type": "percentage_rollout" + }, + "expected": "invalid", + "expected_failure": { + "layer": "schema", + "keyword": "const", + "instance_path": "/flags/garden-layout/metadata/rule_type" + } + }, + { + "id": "responses.experiment_paused_wrong_rule_type", + "template": "experiment_paused", + "set": { + "/flags/garden-layout/metadata/rule_type": "percentage_rollout" + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "const", + "instance_path": "/flags/garden-layout/metadata/rule_type" + } + }, + { + "id": "responses.holdout_wrong_rule_type", + "template": "holdout", + "set": { + "/flags/garden-layout/metadata/rule_type": "percentage_rollout" + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "const", + "instance_path": "/flags/garden-layout/metadata/rule_type" + } + }, + { + "id": "responses.targeting_match_targeted_release_null_value", + "template": "targeting_match_targeted_release", + "set": { + "/flags/garden-layout/value": null + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "type", + "instance_path": "/flags/garden-layout/value" + } + }, + { + "id": "responses.targeting_match_percentage_rollout_null_value", + "template": "targeting_match_percentage_rollout", + "set": { + "/flags/garden-layout/value": null + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "type", + "instance_path": "/flags/garden-layout/value" + } + }, + { + "id": "responses.experiment_split_null_value", + "template": "experiment_split", + "set": { + "/flags/garden-layout/value": null + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "type", + "instance_path": "/flags/garden-layout/value" + } + }, + { + "id": "responses.empty_v2_string", + "template": "no_rule_match", + "set": { + "/flags/garden-layout/value": "" + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "minLength", + "instance_path": "/flags/garden-layout/value" + } + }, + { + "id": "responses.v1_experiment_without_linkage", + "template": "v1", + "set": { + "/flags/garden-layout/metadata/experiment_id": 301 + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "const", + "instance_path": "/flags/garden-layout/metadata/has_experiment" + } + }, + { + "id": "responses.v1_ambiguous_experiment", + "template": "v1", + "set": { + "/flags/garden-layout/metadata/has_experiment": true + }, + "expected": "valid" + }, + { + "id": "responses.flag_key_seed", + "template": "omitted", + "set": { + "/flags/seed": { + "key": "seed", + "value": false, + "reason": { + "code": "experiment_split", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001", + "variant_key": "arm_a" + } + } + }, + "expected": "valid" + }, + { + "id": "responses.seed_named_flag_nested_seed", + "template": "omitted", + "set": { + "/flags/seed": { + "key": "seed", + "value": false, + "reason": { + "code": "experiment_split", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001", + "variant_key": "arm_a" + }, + "conditions": [ + { + "nested": [ + { + "seed": "invented-assignment" + } + ] + } + ] + } + }, + "expected": "invalid", + "expected_failure": { + "layer": "seed", + "keyword": "assignment_seed", + "instance_path": "/flags/seed/conditions/0/nested/0/seed" + } + }, + { + "id": "responses.flag_key_assignment_seed", + "template": "omitted", + "set": { + "/flags/assignment_seed": { + "key": "assignment_seed", + "value": false, + "reason": { + "code": "experiment_split", + "condition_index": 0 + }, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": true, + "payload": null, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001", + "variant_key": "arm_a" + } + } + }, + "expected": "valid" + }, + { + "id": "responses.v1_forbidden_rule_id", + "template": "v1", + "set": { + "/flags/garden-layout/metadata/rule_id": "10000000-0000-4000-8000-000000000001" + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.v1_forbidden_rule_type", + "template": "v1", + "set": { + "/flags/garden-layout/metadata/rule_type": "targeted_release" + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "not", + "instance_path": "/flags/garden-layout/metadata" + } + }, + { + "id": "responses.v1_object_payload", + "template": "v1", + "set": { + "/flags/garden-layout/metadata/payload": { + "columns": 2 + } + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "type", + "instance_path": "/flags/garden-layout/metadata/payload" + } + }, + { + "id": "responses.v1_null_value_not_failed", + "template": "v1", + "set": { + "/flags/garden-layout/value": null + }, + "expected": "invalid", + "expected_failure": { + "layer": "presence", + "keyword": "type", + "instance_path": "/flags/garden-layout/value" + } + } + ] +} diff --git a/posthog/test/fixtures/feature_flag_rules_v2/v2_value_evaluation.json b/posthog/test/fixtures/feature_flag_rules_v2/v2_value_evaluation.json new file mode 100644 index 000000000..c48e40782 --- /dev/null +++ b/posthog/test/fixtures/feature_flag_rules_v2/v2_value_evaluation.json @@ -0,0 +1,3080 @@ +{ + "corpus_version": "1.0.0", + "config_version": 2, + "description": "Person string, number and object ordered evaluation with the legacy rendering of each result. Expected terminal fields and legacy cells are exhaustive. Parser rows run at the parser boundary.", + "cases": [ + { + "id": "v2_value.string.target", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "compact" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": "compact", + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": "compact", + "value": "compact", + "payload": null + } + }, + { + "id": "v2_value.string.rollout_hit", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": "compact", + "rollout_percentage": 100, + "on_rollout_miss": "continue", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": "compact", + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": "compact", + "value": "compact", + "payload": null + } + }, + { + "id": "v2_value.string.miss_return_default", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": "compact", + "rollout_percentage": 0, + "on_rollout_miss": "return_default", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "wide" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": "standard", + "reason": "rollout_miss", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": "standard", + "value": "standard", + "payload": null + } + }, + { + "id": "v2_value.string.miss_return_default_null", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": "compact", + "rollout_percentage": 0, + "on_rollout_miss": "return_default", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "wide" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": null, + "reason": "rollout_miss", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "index": 0 + } + }, + "legacy": { + "enabled": false, + "variant": null, + "value": false, + "payload": null + } + }, + { + "id": "v2_value.string.miss_continue", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": "compact", + "rollout_percentage": 0, + "on_rollout_miss": "continue", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "wide" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": "wide", + "reason": "targeting_match", + "rule": { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "index": 1 + } + }, + "legacy": { + "enabled": true, + "variant": "wide", + "value": "wide", + "payload": null + } + }, + { + "id": "v2_value.string.no_match_default", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "free" + ], + "negation": false + } + ] + }, + "value": "compact" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": "standard", + "reason": "no_rule_match" + }, + "legacy": { + "enabled": true, + "variant": "standard", + "value": "standard", + "payload": null + } + }, + { + "id": "v2_value.string.no_match_null", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "free" + ], + "negation": false + } + ] + }, + "value": "compact" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": null, + "reason": "no_rule_match" + }, + "legacy": { + "enabled": false, + "variant": null, + "value": false, + "payload": null + } + }, + { + "id": "v2_value.string.order_first_match", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "pro" + ], + "negation": false + } + ] + }, + "value": "compact" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "wide" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": "compact", + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": "compact", + "value": "compact", + "payload": null + } + }, + { + "id": "v2_value.string.order_targeting_miss", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "free" + ], + "negation": false + } + ] + }, + "value": "compact" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "pro" + ], + "negation": false + } + ] + }, + "value": "wide" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": "wide", + "reason": "targeting_match", + "rule": { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "index": 1 + } + }, + "legacy": { + "enabled": true, + "variant": "wide", + "value": "wide", + "payload": null + } + }, + { + "id": "v2_value.number.target", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": 1.25 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": 1.25, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": 1.25 + } + }, + { + "id": "v2_value.number.rollout_hit", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": 1.25, + "rollout_percentage": 100, + "on_rollout_miss": "continue", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": 1.25, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": 1.25 + } + }, + { + "id": "v2_value.number.miss_return_default", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": 1.25, + "rollout_percentage": 0, + "on_rollout_miss": "return_default", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": -40 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": 0, + "reason": "rollout_miss", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": 0 + } + }, + { + "id": "v2_value.number.miss_return_default_null", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": 1.25, + "rollout_percentage": 0, + "on_rollout_miss": "return_default", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": -40 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": null, + "reason": "rollout_miss", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "index": 0 + } + }, + "legacy": { + "enabled": false, + "variant": null, + "value": false, + "payload": null + } + }, + { + "id": "v2_value.number.miss_continue", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": 1.25, + "rollout_percentage": 0, + "on_rollout_miss": "continue", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": -40 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": -40, + "reason": "targeting_match", + "rule": { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "index": 1 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": -40 + } + }, + { + "id": "v2_value.number.no_match_default", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "free" + ], + "negation": false + } + ] + }, + "value": 1.25 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": 0, + "reason": "no_rule_match" + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": 0 + } + }, + { + "id": "v2_value.number.no_match_null", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "free" + ], + "negation": false + } + ] + }, + "value": 1.25 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": null, + "reason": "no_rule_match" + }, + "legacy": { + "enabled": false, + "variant": null, + "value": false, + "payload": null + } + }, + { + "id": "v2_value.number.order_first_match", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "pro" + ], + "negation": false + } + ] + }, + "value": 1.25 + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": -40 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": 1.25, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": 1.25 + } + }, + { + "id": "v2_value.number.order_targeting_miss", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "free" + ], + "negation": false + } + ] + }, + "value": 1.25 + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "pro" + ], + "negation": false + } + ] + }, + "value": -40 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": -40, + "reason": "targeting_match", + "rule": { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "index": 1 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": -40 + } + }, + { + "id": "v2_value.object.target", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": { + "layout": "standard" + }, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + }, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + } + } + }, + { + "id": "v2_value.object.rollout_hit", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": { + "layout": "standard" + }, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + }, + "rollout_percentage": 100, + "on_rollout_miss": "continue", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + }, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + } + } + }, + { + "id": "v2_value.object.miss_return_default", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": { + "layout": "standard" + }, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + }, + "rollout_percentage": 0, + "on_rollout_miss": "return_default", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "layout": "wide" + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": { + "layout": "standard" + }, + "reason": "rollout_miss", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": { + "layout": "standard" + } + } + }, + { + "id": "v2_value.object.miss_return_default_null", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + }, + "rollout_percentage": 0, + "on_rollout_miss": "return_default", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "layout": "wide" + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": null, + "reason": "rollout_miss", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "index": 0 + } + }, + "legacy": { + "enabled": false, + "variant": null, + "value": false, + "payload": null + } + }, + { + "id": "v2_value.object.miss_continue", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": { + "layout": "standard" + }, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "percentage_rollout", + "targeting": { + "properties": [] + }, + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + }, + "rollout_percentage": 0, + "on_rollout_miss": "continue", + "assignment_algorithm": "sha1_60_v1", + "seed": "example-allocation", + "assign_by": "person" + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "layout": "wide" + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": { + "layout": "wide" + }, + "reason": "targeting_match", + "rule": { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "index": 1 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": { + "layout": "wide" + } + } + }, + { + "id": "v2_value.object.no_match_default", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": { + "layout": "standard" + }, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "free" + ], + "negation": false + } + ] + }, + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": { + "layout": "standard" + }, + "reason": "no_rule_match" + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": { + "layout": "standard" + } + } + }, + { + "id": "v2_value.object.no_match_null", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "free" + ], + "negation": false + } + ] + }, + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": null, + "reason": "no_rule_match" + }, + "legacy": { + "enabled": false, + "variant": null, + "value": false, + "payload": null + } + }, + { + "id": "v2_value.object.order_first_match", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": { + "layout": "standard" + }, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "pro" + ], + "negation": false + } + ] + }, + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + } + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "layout": "wide" + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + }, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + } + } + }, + { + "id": "v2_value.object.order_targeting_miss", + "family": "ordering", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": { + "layout": "standard" + }, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "free" + ], + "negation": false + } + ] + }, + "value": { + "layout": "compact", + "options": [ + 1, + true, + null + ] + } + }, + { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "targeting": { + "properties": [ + { + "key": "plan", + "type": "person", + "operator": "exact", + "value": [ + "pro" + ], + "negation": false + } + ] + }, + "value": { + "layout": "wide" + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": { + "layout": "wide" + }, + "reason": "targeting_match", + "rule": { + "id": "22222222-2222-4222-8222-222222222222", + "rule_type": "targeted_release", + "index": 1 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": { + "layout": "wide" + } + } + }, + { + "id": "v2_value.string.unicode", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "Grüße 👋" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": "Grüße 👋", + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": "Grüße 👋", + "value": "Grüße 👋", + "payload": null + } + }, + { + "id": "v2_value.string.looks_boolean", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "false" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": "false", + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": "false", + "value": "false", + "payload": null + } + }, + { + "id": "v2_value.number.zero", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": 0 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": 0, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": 0 + } + }, + { + "id": "v2_value.number.negative_decimal", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": -2.5 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": -2.5, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": -2.5 + } + }, + { + "id": "v2_value.number.short_decimal", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": 0.1 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": 0.1, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": 0.1 + } + }, + { + "id": "v2_value.number.exponent", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": 1.5e-07 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": 1.5e-07, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": 1.5e-07 + } + }, + { + "id": "v2_value.number.max_safe_integer", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": 9007199254740991 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": 9007199254740991, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": 9007199254740991 + } + }, + { + "id": "v2_value.number.min_safe_integer", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": -9007199254740991 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": -9007199254740991, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": -9007199254740991 + } + }, + { + "id": "v2_value.object.empty", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": {} + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": {}, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": {} + } + }, + { + "id": "v2_value.object.nested", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "theme": { + "colors": [ + "#000000", + "#ffffff" + ], + "radius": 4.5 + }, + "flags": [ + true, + false, + null + ], + "label": "Ünïcode" + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": { + "theme": { + "colors": [ + "#000000", + "#ffffff" + ], + "radius": 4.5 + }, + "flags": [ + true, + false, + null + ], + "label": "Ünïcode" + }, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": { + "theme": { + "colors": [ + "#000000", + "#ffffff" + ], + "radius": 4.5 + }, + "flags": [ + true, + false, + null + ], + "label": "Ünïcode" + } + } + }, + { + "id": "v2_value.object.max_depth", + "family": "values", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "leaf": true + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "success", + "value": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "leaf": true + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "reason": "targeting_match", + "rule": { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "index": 0 + } + }, + "legacy": { + "enabled": true, + "variant": null, + "value": true, + "payload": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "leaf": true + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + { + "id": "v2_value.parser.string_empty_value", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.string_empty_default", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "compact" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.string_value_mismatch", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "string", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": true + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.number_value_mismatch", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": "1.25" + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.number_default_mismatch", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": true, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": 1.25 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.number_above_safe_integer", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": 9007199254740992.0 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.number_below_safe_integer", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "number", + "default_value": 0, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": -9007199254740992.0 + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.object_array_value", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": [ + { + "layout": "compact" + } + ] + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.object_default_mismatch", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": "standard", + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "layout": "compact" + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.object_too_deep", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "child": { + "leaf": true + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + }, + { + "id": "v2_value.parser.object_nested_number_above_safe_integer", + "family": "parser", + "flag": { + "key": "example-display-key", + "name": "Example display name", + "active": true, + "deleted": false, + "requested": true + }, + "config": { + "version": 2, + "return_type": "object", + "default_value": null, + "rules": [ + { + "id": "11111111-1111-4111-8111-111111111111", + "rule_type": "targeted_release", + "targeting": { + "properties": [] + }, + "value": { + "limit": 9007199254740992.0 + } + } + ] + }, + "context": { + "identifier": "person-example", + "properties": { + "plan": "pro" + }, + "properties_complete": true, + "timezone": "UTC", + "explicit_exact_matching": true, + "now": "2024-03-31T12:00:00Z" + }, + "expected": { + "status": "parse_error", + "kind": "malformed" + } + } + ] +} diff --git a/posthog/test/fixtures/released_flags_7_82_1.json b/posthog/test/fixtures/released_flags_7_82_1.json new file mode 100644 index 000000000..627f5336e --- /dev/null +++ b/posthog/test/fixtures/released_flags_7_82_1.json @@ -0,0 +1,2258 @@ +{ + "description": "Results of the scenarios in posthog/test/released_flags_scenarios.py, recorded with posthog 7.82.1 from the v2 response shape. See that module to regenerate.", + "results": { + "cache histories": { + "good, disabled, timeout": { + "events": [ + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "blue", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": false, + "$feature_flag": "cached", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": false, + "$feature_flag": "cached", + "$feature_flag_error": "timeout", + "$feature_flag_response": false, + "locally_evaluated": false + } + ] + ], + "results": [ + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + false, + null, + null, + "Matched condition set 1" + ], + [ + false, + null, + null, + "Matched condition set 1" + ] + ] + }, + "good, each request failure": { + "events": [ + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "blue", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_error": "timeout", + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_response": "blue", + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_error": "connection_error", + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_response": "blue", + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_error": "api_error_500", + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_response": "blue", + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_error": "quota_limited", + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_response": "blue", + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_error": "unknown_error", + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_response": "blue", + "locally_evaluated": false + } + ] + ], + "results": [ + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ] + ] + }, + "good, missing with errors, timeout": { + "events": [ + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "blue", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": null, + "$feature_flag": "cached", + "$feature_flag_error": "errors_while_computing_flags,flag_missing", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_error": "timeout", + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_response": "blue", + "locally_evaluated": false + } + ] + ], + "results": [ + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + null, + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ] + ] + }, + "good, other variant, timeout": { + "events": [ + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "blue", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "green", + "$feature_flag": "cached", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "green", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "green", + "$feature_flag": "cached", + "$feature_flag_error": "timeout", + "$feature_flag_response": "green", + "locally_evaluated": false + } + ] + ], + "results": [ + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + true, + "green", + null, + "Matched condition set 1" + ], + [ + true, + "green", + null, + "Matched condition set 1" + ] + ] + }, + "good, server failed, timeout": { + "events": [ + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "blue", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": false, + "$feature_flag": "cached", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": false, + "$feature_flag": "cached", + "$feature_flag_error": "timeout", + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_response": false, + "locally_evaluated": false + } + ] + ], + "results": [ + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + false, + null, + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + false, + null, + { + "theme": "dark" + }, + "Matched condition set 1" + ] + ] + }, + "good, unreadable record, timeout": { + "events": [ + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "blue", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_error": "unknown_error", + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_response": "blue", + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/cached": "blue", + "$feature_flag": "cached", + "$feature_flag_error": "timeout", + "$feature_flag_payload": { + "theme": "dark" + }, + "$feature_flag_response": "blue", + "locally_evaluated": false + } + ] + ], + "results": [ + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ], + [ + true, + "blue", + { + "theme": "dark" + }, + "Matched condition set 1" + ] + ] + } + }, + "getters": { + "events": [ + [ + "$feature_flag_called", + { + "$feature/bool-on": true, + "$feature_flag": "bool-on", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/bool-on": true, + "$feature_flag": "bool-on", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/bool-on": true, + "$feature_flag": "bool-on", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/bool-off": false, + "$feature_flag": "bool-off", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/bool-off": false, + "$feature_flag": "bool-off", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/bool-off": false, + "$feature_flag": "bool-off", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": "control", + "$feature_flag": "v1-variant", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": true, + "$feature_flag_id": 701, + "$feature_flag_payload": { + "columns": 2 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "control", + "$feature_flag_version": 4, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": "control", + "$feature_flag": "v1-variant", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": true, + "$feature_flag_id": 701, + "$feature_flag_payload": { + "columns": 2 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "control", + "$feature_flag_version": 4, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": "control", + "$feature_flag": "v1-variant", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": true, + "$feature_flag_id": 701, + "$feature_flag_payload": { + "columns": 2 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "control", + "$feature_flag_version": 4, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-bool-payload": true, + "$feature_flag": "v1-bool-payload", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "cta": "Try it", + "n": 1 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-bool-payload": true, + "$feature_flag": "v1-bool-payload", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "cta": "Try it", + "n": 1 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-bool-payload": true, + "$feature_flag": "v1-bool-payload", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "cta": "Try it", + "n": 1 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-string-payload": true, + "$feature_flag": "v1-string-payload", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": "plain", + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-string-payload": true, + "$feature_flag": "v1-string-payload", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": "plain", + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-string-payload": true, + "$feature_flag": "v1-string-payload", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": "plain", + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-string": "compact", + "$feature_flag": "v2-string", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "compact", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-string": "compact", + "$feature_flag": "v2-string", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "compact", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-string": "compact", + "$feature_flag": "v2-string", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "compact", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-null": false, + "$feature_flag": "v2-null", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-null": false, + "$feature_flag": "v2-null", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-null": false, + "$feature_flag": "v2-null", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-number": true, + "$feature_flag": "v2-number", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": 12.5, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-number": true, + "$feature_flag": "v2-number", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": 12.5, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-number": true, + "$feature_flag": "v2-number", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": 12.5, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-object": true, + "$feature_flag": "v2-object", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "layout": "compact", + "n": [ + 1, + true + ] + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-object": true, + "$feature_flag": "v2-object", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "layout": "compact", + "n": [ + 1, + true + ] + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-object": true, + "$feature_flag": "v2-object", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "layout": "compact", + "n": [ + 1, + true + ] + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/sparse": "compact", + "$feature_flag": "sparse", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "compact", + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/sparse": "compact", + "$feature_flag": "sparse", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "compact", + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/sparse": "compact", + "$feature_flag": "sparse", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "compact", + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/server-failed": false, + "$feature_flag": "server-failed", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/server-failed": false, + "$feature_flag": "server-failed", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/server-failed": false, + "$feature_flag": "server-failed", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "flag_missing", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "flag_missing", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "flag_missing", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/bool-on": true, + "$feature_flag": "bool-on", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/bool-off": false, + "$feature_flag": "bool-off", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": "control", + "$feature_flag": "v1-variant", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": true, + "$feature_flag_id": 701, + "$feature_flag_payload": { + "columns": 2 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "control", + "$feature_flag_version": 4, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-bool-payload": true, + "$feature_flag": "v1-bool-payload", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "cta": "Try it", + "n": 1 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-string-payload": true, + "$feature_flag": "v1-string-payload", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": "plain", + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-string": "compact", + "$feature_flag": "v2-string", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "compact", + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-null": false, + "$feature_flag": "v2-null", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-number": true, + "$feature_flag": "v2-number", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": 12.5, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v2-object": true, + "$feature_flag": "v2-object", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_payload": { + "layout": "compact", + "n": [ + 1, + true + ] + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": true, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/sparse": "compact", + "$feature_flag": "sparse", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "compact", + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/server-failed": false, + "$feature_flag": "server-failed", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": false, + "$feature_flag_id": 700, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "$feature_flag_version": 3, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "flag_missing", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "snapshot-event", + { + "$active_feature_flags": [ + "bool-on", + "sparse", + "v1-bool-payload", + "v1-string-payload", + "v1-variant", + "v2-number", + "v2-object", + "v2-string" + ], + "$feature/bool-off": false, + "$feature/bool-on": true, + "$feature/server-failed": false, + "$feature/sparse": "compact", + "$feature/v1-bool-payload": true, + "$feature/v1-string-payload": true, + "$feature/v1-variant": "control", + "$feature/v2-null": false, + "$feature/v2-number": true, + "$feature/v2-object": true, + "$feature/v2-string": "compact" + } + ], + [ + "fetched-event", + { + "$active_feature_flags": [ + "bool-on", + "v1-variant", + "v1-bool-payload", + "v1-string-payload", + "v2-string", + "v2-number", + "v2-object", + "sparse" + ], + "$feature/bool-off": false, + "$feature/bool-on": true, + "$feature/server-failed": false, + "$feature/sparse": "compact", + "$feature/v1-bool-payload": true, + "$feature/v1-string-payload": true, + "$feature/v1-variant": "control", + "$feature/v2-null": false, + "$feature/v2-number": true, + "$feature/v2-object": true, + "$feature/v2-string": "compact" + } + ] + ], + "feature_enabled:bool-off": false, + "feature_enabled:bool-on": true, + "feature_enabled:missing": null, + "feature_enabled:server-failed": false, + "feature_enabled:sparse": true, + "feature_enabled:v1-bool-payload": true, + "feature_enabled:v1-string-payload": true, + "feature_enabled:v1-variant": true, + "feature_enabled:v2-null": false, + "feature_enabled:v2-number": true, + "feature_enabled:v2-object": true, + "feature_enabled:v2-string": true, + "get_all_flags": { + "bool-off": false, + "bool-on": true, + "server-failed": false, + "sparse": "compact", + "v1-bool-payload": true, + "v1-string-payload": true, + "v1-variant": "control", + "v2-null": false, + "v2-number": true, + "v2-object": true, + "v2-string": "compact" + }, + "get_all_flags_and_payloads": { + "featureFlagPayloads": { + "v1-bool-payload": "{\"cta\": \"Try it\", \"n\": 1}", + "v1-string-payload": "\"plain\"", + "v1-variant": "{\"columns\": 2}", + "v2-number": "12.5", + "v2-object": "{\"layout\":\"compact\",\"n\":[1,true]}" + }, + "featureFlags": { + "bool-off": false, + "bool-on": true, + "server-failed": false, + "sparse": "compact", + "v1-bool-payload": true, + "v1-string-payload": true, + "v1-variant": "control", + "v2-null": false, + "v2-number": true, + "v2-object": true, + "v2-string": "compact" + } + }, + "get_feature_flag:bool-off": false, + "get_feature_flag:bool-on": true, + "get_feature_flag:missing": null, + "get_feature_flag:server-failed": false, + "get_feature_flag:sparse": "compact", + "get_feature_flag:v1-bool-payload": true, + "get_feature_flag:v1-string-payload": true, + "get_feature_flag:v1-variant": "control", + "get_feature_flag:v2-null": false, + "get_feature_flag:v2-number": true, + "get_feature_flag:v2-object": true, + "get_feature_flag:v2-string": "compact", + "get_feature_flag_payload:bool-off": null, + "get_feature_flag_payload:bool-on": null, + "get_feature_flag_payload:missing": null, + "get_feature_flag_payload:server-failed": null, + "get_feature_flag_payload:sparse": null, + "get_feature_flag_payload:v1-bool-payload": { + "cta": "Try it", + "n": 1 + }, + "get_feature_flag_payload:v1-string-payload": "plain", + "get_feature_flag_payload:v1-variant": { + "columns": 2 + }, + "get_feature_flag_payload:v2-null": null, + "get_feature_flag_payload:v2-number": 12.5, + "get_feature_flag_payload:v2-object": { + "layout": "compact", + "n": [ + 1, + true + ] + }, + "get_feature_flag_payload:v2-string": null, + "get_feature_flag_result:bool-off": [ + false, + null, + null, + "" + ], + "get_feature_flag_result:bool-on": [ + true, + null, + null, + "Matched condition set 1" + ], + "get_feature_flag_result:missing": null, + "get_feature_flag_result:server-failed": [ + false, + null, + null, + "Matched condition set 1" + ], + "get_feature_flag_result:sparse": [ + true, + "compact", + null, + null + ], + "get_feature_flag_result:v1-bool-payload": [ + true, + null, + { + "cta": "Try it", + "n": 1 + }, + "Matched condition set 1" + ], + "get_feature_flag_result:v1-string-payload": [ + true, + null, + "plain", + "Matched condition set 1" + ], + "get_feature_flag_result:v1-variant": [ + true, + "control", + { + "columns": 2 + }, + "Matched condition set 1" + ], + "get_feature_flag_result:v2-null": [ + false, + null, + null, + "Matched condition set 1" + ], + "get_feature_flag_result:v2-number": [ + true, + null, + 12.5, + "Matched condition set 1" + ], + "get_feature_flag_result:v2-object": [ + true, + null, + { + "layout": "compact", + "n": [ + 1, + true + ] + }, + "Matched condition set 1" + ], + "get_feature_flag_result:v2-string": [ + true, + "compact", + null, + "Matched condition set 1" + ], + "get_feature_flags_and_payloads": { + "featureFlagPayloads": { + "v1-bool-payload": "{\"cta\": \"Try it\", \"n\": 1}", + "v1-string-payload": "\"plain\"", + "v1-variant": "{\"columns\": 2}", + "v2-number": "12.5", + "v2-object": "{\"layout\":\"compact\",\"n\":[1,true]}" + }, + "featureFlags": { + "bool-off": false, + "bool-on": true, + "server-failed": false, + "sparse": "compact", + "v1-bool-payload": true, + "v1-string-payload": true, + "v1-variant": "control", + "v2-null": false, + "v2-number": true, + "v2-object": true, + "v2-string": "compact" + } + }, + "get_feature_payloads": { + "v1-bool-payload": "{\"cta\": \"Try it\", \"n\": 1}", + "v1-string-payload": "\"plain\"", + "v1-variant": "{\"columns\": 2}", + "v2-number": "12.5", + "v2-object": "{\"layout\":\"compact\",\"n\":[1,true]}" + }, + "get_feature_variants": { + "bool-off": false, + "bool-on": true, + "server-failed": false, + "sparse": "compact", + "v1-bool-payload": true, + "v1-string-payload": true, + "v1-variant": "control", + "v2-null": false, + "v2-number": true, + "v2-object": true, + "v2-string": "compact" + }, + "get_flags_decision": { + "errorsWhileComputingFlags": false, + "evaluatedAt": 1800000000000, + "flags": { + "bool-off": { + "__class__": "FeatureFlag", + "enabled": false, + "key": "bool-off", + "metadata": { + "__class__": "FlagMetadata", + "description": null, + "has_experiment": false, + "id": 700, + "payload": null, + "version": 3 + }, + "reason": { + "__class__": "FlagReason", + "code": "no_condition_match", + "condition_index": null, + "description": "" + }, + "variant": null + }, + "bool-on": { + "__class__": "FeatureFlag", + "enabled": true, + "key": "bool-on", + "metadata": { + "__class__": "FlagMetadata", + "description": null, + "has_experiment": false, + "id": 700, + "payload": null, + "version": 3 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": null + }, + "server-failed": { + "__class__": "FeatureFlag", + "enabled": false, + "key": "server-failed", + "metadata": { + "__class__": "FlagMetadata", + "description": null, + "has_experiment": false, + "id": 700, + "payload": null, + "version": 3 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": null + }, + "sparse": { + "__class__": "FeatureFlag", + "enabled": true, + "key": "sparse", + "metadata": { + "__class__": "FlagMetadata", + "description": "", + "has_experiment": null, + "id": 0, + "payload": null, + "version": 0 + }, + "reason": null, + "variant": "compact" + }, + "v1-bool-payload": { + "__class__": "FeatureFlag", + "enabled": true, + "key": "v1-bool-payload", + "metadata": { + "__class__": "FlagMetadata", + "description": null, + "has_experiment": false, + "id": 700, + "payload": "{\"cta\": \"Try it\", \"n\": 1}", + "version": 3 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": null + }, + "v1-string-payload": { + "__class__": "FeatureFlag", + "enabled": true, + "key": "v1-string-payload", + "metadata": { + "__class__": "FlagMetadata", + "description": null, + "has_experiment": false, + "id": 700, + "payload": "\"plain\"", + "version": 3 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": null + }, + "v1-variant": { + "__class__": "FeatureFlag", + "enabled": true, + "key": "v1-variant", + "metadata": { + "__class__": "FlagMetadata", + "description": "A variant flag", + "has_experiment": true, + "id": 701, + "payload": "{\"columns\": 2}", + "version": 4 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": "control" + }, + "v2-null": { + "__class__": "FeatureFlag", + "enabled": false, + "key": "v2-null", + "metadata": { + "__class__": "FlagMetadata", + "description": null, + "has_experiment": false, + "id": 700, + "payload": null, + "version": 3 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": null + }, + "v2-number": { + "__class__": "FeatureFlag", + "enabled": true, + "key": "v2-number", + "metadata": { + "__class__": "FlagMetadata", + "description": null, + "has_experiment": false, + "id": 700, + "payload": "12.5", + "version": 3 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": null + }, + "v2-object": { + "__class__": "FeatureFlag", + "enabled": true, + "key": "v2-object", + "metadata": { + "__class__": "FlagMetadata", + "description": null, + "has_experiment": false, + "id": 700, + "payload": "{\"layout\":\"compact\",\"n\":[1,true]}", + "version": 3 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": null + }, + "v2-string": { + "__class__": "FeatureFlag", + "enabled": true, + "key": "v2-string", + "metadata": { + "__class__": "FlagMetadata", + "description": null, + "has_experiment": false, + "id": 700, + "payload": null, + "version": 3 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": "compact" + } + }, + "requestId": "60000000-0000-4000-8000-000000000000" + }, + "snapshot.get_flag:bool-off": false, + "snapshot.get_flag:bool-on": true, + "snapshot.get_flag:missing": null, + "snapshot.get_flag:server-failed": false, + "snapshot.get_flag:sparse": "compact", + "snapshot.get_flag:v1-bool-payload": true, + "snapshot.get_flag:v1-string-payload": true, + "snapshot.get_flag:v1-variant": "control", + "snapshot.get_flag:v2-null": false, + "snapshot.get_flag:v2-number": true, + "snapshot.get_flag:v2-object": true, + "snapshot.get_flag:v2-string": "compact", + "snapshot.get_flag_payload:bool-off": null, + "snapshot.get_flag_payload:bool-on": null, + "snapshot.get_flag_payload:missing": null, + "snapshot.get_flag_payload:server-failed": null, + "snapshot.get_flag_payload:sparse": null, + "snapshot.get_flag_payload:v1-bool-payload": { + "cta": "Try it", + "n": 1 + }, + "snapshot.get_flag_payload:v1-string-payload": "plain", + "snapshot.get_flag_payload:v1-variant": { + "columns": 2 + }, + "snapshot.get_flag_payload:v2-null": null, + "snapshot.get_flag_payload:v2-number": 12.5, + "snapshot.get_flag_payload:v2-object": { + "layout": "compact", + "n": [ + 1, + true + ] + }, + "snapshot.get_flag_payload:v2-string": null, + "snapshot.is_enabled:bool-off": false, + "snapshot.is_enabled:bool-on": true, + "snapshot.is_enabled:missing": false, + "snapshot.is_enabled:server-failed": false, + "snapshot.is_enabled:sparse": true, + "snapshot.is_enabled:v1-bool-payload": true, + "snapshot.is_enabled:v1-string-payload": true, + "snapshot.is_enabled:v1-variant": true, + "snapshot.is_enabled:v2-null": false, + "snapshot.is_enabled:v2-number": true, + "snapshot.is_enabled:v2-object": true, + "snapshot.is_enabled:v2-string": true, + "snapshot.keys": [ + "bool-off", + "bool-on", + "server-failed", + "sparse", + "v1-bool-payload", + "v1-string-payload", + "v1-variant", + "v2-null", + "v2-number", + "v2-object", + "v2-string" + ] + }, + "getters with an unreadable record": { + "events": [ + [ + "$feature_flag_called", + { + "$feature/v1-variant": null, + "$feature_flag": "v1-variant", + "$feature_flag_error": "unknown_error", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": null, + "$feature_flag": "v1-variant", + "$feature_flag_error": "unknown_error", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": null, + "$feature_flag": "v1-variant", + "$feature_flag_error": "unknown_error", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/unreadable": null, + "$feature_flag": "unreadable", + "$feature_flag_error": "unknown_error", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/unreadable": null, + "$feature_flag": "unreadable", + "$feature_flag_error": "unknown_error", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/unreadable": null, + "$feature_flag": "unreadable", + "$feature_flag_error": "unknown_error", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "unknown_error", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "unknown_error", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "unknown_error", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": null, + "$feature_flag": "v1-variant", + "$feature_flag_error": "flag_missing", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/unreadable": null, + "$feature_flag": "unreadable", + "$feature_flag_error": "flag_missing", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "flag_missing", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "snapshot-event", + {} + ], + [ + "fetched-event", + {} + ] + ], + "feature_enabled:missing": null, + "feature_enabled:unreadable": null, + "feature_enabled:v1-variant": null, + "get_all_flags": {}, + "get_all_flags_and_payloads": { + "featureFlagPayloads": {}, + "featureFlags": {} + }, + "get_feature_flag:missing": null, + "get_feature_flag:unreadable": null, + "get_feature_flag:v1-variant": null, + "get_feature_flag_payload:missing": null, + "get_feature_flag_payload:unreadable": null, + "get_feature_flag_payload:v1-variant": null, + "get_feature_flag_result:missing": null, + "get_feature_flag_result:unreadable": null, + "get_feature_flag_result:v1-variant": null, + "get_feature_flags_and_payloads": { + "featureFlagPayloads": {}, + "featureFlags": {} + }, + "get_feature_payloads": {}, + "get_feature_variants": {}, + "get_flags_decision": { + "flags": {}, + "requestId": null + }, + "snapshot.get_flag:missing": null, + "snapshot.get_flag:unreadable": null, + "snapshot.get_flag:v1-variant": null, + "snapshot.get_flag_payload:missing": null, + "snapshot.get_flag_payload:unreadable": null, + "snapshot.get_flag_payload:v1-variant": null, + "snapshot.is_enabled:missing": false, + "snapshot.is_enabled:unreadable": false, + "snapshot.is_enabled:v1-variant": false, + "snapshot.keys": [] + }, + "getters with the unreadable record read as disabled": { + "events": [ + [ + "$feature_flag_called", + { + "$feature/v1-variant": "control", + "$feature_flag": "v1-variant", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": true, + "$feature_flag_id": 701, + "$feature_flag_payload": { + "columns": 2 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "control", + "$feature_flag_version": 4, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": "control", + "$feature_flag": "v1-variant", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": true, + "$feature_flag_id": 701, + "$feature_flag_payload": { + "columns": 2 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "control", + "$feature_flag_version": 4, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": "control", + "$feature_flag": "v1-variant", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": true, + "$feature_flag_id": 701, + "$feature_flag_payload": { + "columns": 2 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "control", + "$feature_flag_version": 4, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/unreadable": false, + "$feature_flag": "unreadable", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/unreadable": false, + "$feature_flag": "unreadable", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/unreadable": false, + "$feature_flag": "unreadable", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "flag_missing", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "flag_missing", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "flag_missing", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/v1-variant": "control", + "$feature_flag": "v1-variant", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_has_experiment": true, + "$feature_flag_id": 701, + "$feature_flag_payload": { + "columns": 2 + }, + "$feature_flag_reason": "Matched condition set 1", + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": "control", + "$feature_flag_version": 4, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/unreadable": false, + "$feature_flag": "unreadable", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": false, + "locally_evaluated": false + } + ], + [ + "$feature_flag_called", + { + "$feature/missing": null, + "$feature_flag": "missing", + "$feature_flag_error": "flag_missing", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_request_id": "60000000-0000-4000-8000-000000000000", + "$feature_flag_response": null, + "locally_evaluated": false + } + ], + [ + "snapshot-event", + { + "$active_feature_flags": [ + "v1-variant" + ], + "$feature/unreadable": false, + "$feature/v1-variant": "control" + } + ], + [ + "fetched-event", + { + "$active_feature_flags": [ + "v1-variant" + ], + "$feature/unreadable": false, + "$feature/v1-variant": "control" + } + ] + ], + "feature_enabled:missing": null, + "feature_enabled:unreadable": false, + "feature_enabled:v1-variant": true, + "get_all_flags": { + "unreadable": false, + "v1-variant": "control" + }, + "get_all_flags_and_payloads": { + "featureFlagPayloads": { + "v1-variant": "{\"columns\": 2}" + }, + "featureFlags": { + "unreadable": false, + "v1-variant": "control" + } + }, + "get_feature_flag:missing": null, + "get_feature_flag:unreadable": false, + "get_feature_flag:v1-variant": "control", + "get_feature_flag_payload:missing": null, + "get_feature_flag_payload:unreadable": null, + "get_feature_flag_payload:v1-variant": { + "columns": 2 + }, + "get_feature_flag_result:missing": null, + "get_feature_flag_result:unreadable": [ + false, + null, + null, + null + ], + "get_feature_flag_result:v1-variant": [ + true, + "control", + { + "columns": 2 + }, + "Matched condition set 1" + ], + "get_feature_flags_and_payloads": { + "featureFlagPayloads": { + "v1-variant": "{\"columns\": 2}" + }, + "featureFlags": { + "unreadable": false, + "v1-variant": "control" + } + }, + "get_feature_payloads": { + "v1-variant": "{\"columns\": 2}" + }, + "get_feature_variants": { + "unreadable": false, + "v1-variant": "control" + }, + "get_flags_decision": { + "errorsWhileComputingFlags": false, + "evaluatedAt": 1800000000000, + "flags": { + "unreadable": { + "__class__": "FeatureFlag", + "enabled": false, + "key": "unreadable", + "metadata": { + "__class__": "LegacyFlagMetadata", + "payload": null + }, + "reason": null, + "variant": null + }, + "v1-variant": { + "__class__": "FeatureFlag", + "enabled": true, + "key": "v1-variant", + "metadata": { + "__class__": "FlagMetadata", + "description": "A variant flag", + "has_experiment": true, + "id": 701, + "payload": "{\"columns\": 2}", + "version": 4 + }, + "reason": { + "__class__": "FlagReason", + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1" + }, + "variant": "control" + } + }, + "requestId": "60000000-0000-4000-8000-000000000000" + }, + "snapshot.get_flag:missing": null, + "snapshot.get_flag:unreadable": false, + "snapshot.get_flag:v1-variant": "control", + "snapshot.get_flag_payload:missing": null, + "snapshot.get_flag_payload:unreadable": null, + "snapshot.get_flag_payload:v1-variant": { + "columns": 2 + }, + "snapshot.is_enabled:missing": false, + "snapshot.is_enabled:unreadable": false, + "snapshot.is_enabled:v1-variant": true, + "snapshot.keys": [ + "unreadable", + "v1-variant" + ] + } + } +} diff --git a/posthog/test/released_flags_scenarios.py b/posthog/test/released_flags_scenarios.py new file mode 100644 index 000000000..b3cacdac1 --- /dev/null +++ b/posthog/test/released_flags_scenarios.py @@ -0,0 +1,319 @@ +"""Flag scenarios whose results are pinned to a released SDK. + +``fixtures/released_flags_7_82_1.json`` holds the results of these scenarios as +posthog 7.82.1 produced them from the ``v2`` response shape, the shape that release +requests. ``test_flags_released_behaviour.py`` runs the same scenarios on this SDK +from the ``v2`` and the ``v4`` shape and compares them with that file. + +This module uses only the public client API, so it also runs on 7.82.1. To +regenerate the file, install ``posthog==7.82.1`` in a separate environment, load this +module by its path from a directory outside the checkout, so that the installed +package is imported, and write ``{"description": DESCRIPTION, "results": run_all("v2")}`` +to the file as JSON with sorted keys and an indent of 2. +""" + +import copy +import dataclasses +import json +import warnings +from typing import Any, Callable, Dict, List, Optional +from unittest import mock + +from requests.exceptions import ConnectionError as RequestsConnectionError +from requests.exceptions import Timeout as RequestsTimeout + +from posthog import Client +from posthog.request import APIError, QuotaLimitError + +DESCRIPTION = ( + "Results of the scenarios in posthog/test/released_flags_scenarios.py, recorded" + " with posthog 7.82.1 from the v2 response shape. See that module to regenerate." +) +API_KEY = "phc_released_flags_scenarios" +DISTINCT_ID = "user-1" +REQUEST_ID = "60000000-0000-4000-8000-000000000000" +EVALUATED_AT = 1800000000000 +MATCHED = { + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1", +} + + +def _flag(key: str, value: Any, **fields: Any) -> Dict[str, Any]: + """One flag outcome, rendered into a response by ``build_response``.""" + flag: Dict[str, Any] = { + "key": key, + "value": value, + "config_version": 1, + "payload": None, + "metadata": {"id": 700, "version": 3, "description": None}, + "has_experiment": False, + "reason": MATCHED, + "failed": False, + "set": {}, + } + flag.update(fields) + return flag + + +# Flags read by the getter scenario. A config version 2 flag carries a typed value; +# the v2 response renders it the way the server does for older SDKs. +GETTER_FLAGS = [ + _flag("bool-on", True), + _flag("bool-off", False, reason={"code": "no_condition_match"}), + _flag( + "v1-variant", + "control", + payload='{"columns": 2}', + has_experiment=True, + metadata={"id": 701, "version": 4, "description": "A variant flag"}, + ), + _flag("v1-bool-payload", True, payload='{"cta": "Try it", "n": 1}'), + _flag("v1-string-payload", True, payload='"plain"'), + _flag("v2-string", "compact", config_version=2), + _flag("v2-null", None, config_version=2), + _flag("v2-number", 12.5, config_version=2), + _flag("v2-object", {"layout": "compact", "n": [1, True]}, config_version=2), + _flag("sparse", "compact", metadata={}, has_experiment=None, reason=None), + _flag("server-failed", "compact", failed=True), +] + + +def build_response( + shape: str, flags: List[Dict[str, Any]], **top_level: Any +) -> Dict[str, Any]: + """The ``/flags`` response of the given shape (``"v2"`` or ``"v4"``) for flags.""" + records: Dict[str, Any] = {} + for flag in flags: + value = None if flag["failed"] else flag["value"] + metadata = None if flag["metadata"] is None else dict(flag["metadata"]) + if metadata is not None and flag["has_experiment"] is not None: + metadata["has_experiment"] = flag["has_experiment"] + record: Dict[str, Any] = {"key": flag["key"]} + if shape == "v2": + typed = flag["config_version"] != 1 and not isinstance( + value, (bool, str, type(None)) + ) + record["enabled"] = value is not None and value is not False + record["variant"] = value if isinstance(value, str) else None + if metadata is not None: + metadata["payload"] = ( + json.dumps(value, separators=(",", ":")) + if typed + else flag["payload"] + ) + else: + record["value"] = value + if metadata is not None: + metadata["config_version"] = flag["config_version"] + if flag["config_version"] == 1: + metadata["payload"] = flag["payload"] + if isinstance(value, str): + metadata["variant_key"] = value + if flag["failed"]: + record["failed"] = True + if flag["reason"] is not None: + record["reason"] = flag["reason"] + if metadata is not None: + record["metadata"] = metadata + record.update(copy.deepcopy(flag["set"])) + records[flag["key"]] = record + response: Dict[str, Any] = { + "flags": records, + "errorsWhileComputingFlags": False, + "requestId": REQUEST_ID, + "evaluatedAt": EVALUATED_AT, + } + response.update(top_level) + return response + + +def plain(value: Any) -> Any: + """A JSON-compatible copy of a result, with each dataclass's public fields.""" + if dataclasses.is_dataclass(value) and not isinstance(value, type): + fields = { + field.name: plain(getattr(value, field.name)) + for field in dataclasses.fields(value) + } + return {"__class__": type(value).__name__, **fields} + if isinstance(value, dict): + return {str(key): plain(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [plain(item) for item in value] + return value + + +def _flag_event_properties(message: Dict[str, Any]) -> Dict[str, Any]: + return { + key: item + for key, item in message["properties"].items() + if key.startswith("$feature") + or key in ("$active_feature_flags", "locally_evaluated") + } + + +def _with_client( + responses: List[Any], body: Callable[[Client, Dict[str, Any]], None], **options: Any +) -> Dict[str, Any]: + """Run ``body`` on a client whose ``/flags`` requests return ``responses`` in turn.""" + client = Client(API_KEY, send=False, **options) + outputs: Dict[str, Any] = {} + events: List[Any] = [] + pending = list(responses) + + def flags(*args: Any, **kwargs: Any) -> Any: + response = pending.pop(0) if len(pending) > 1 else pending[0] + if isinstance(response, Exception): + raise response + return copy.deepcopy(response) + + def enqueue(message: Dict[str, Any], *args: Any, **kwargs: Any) -> Optional[str]: + events.append([message["event"], _flag_event_properties(message)]) + return None + + try: + with ( + mock.patch("posthog.client.flags", side_effect=flags), + mock.patch.object(client, "_enqueue", side_effect=enqueue), + warnings.catch_warnings(), + ): + warnings.simplefilter("ignore") + body(client, outputs) + finally: + client.shutdown() + outputs["events"] = events + return plain(outputs) + + +def _read_every_getter( + client: Client, outputs: Dict[str, Any], keys: List[str] +) -> None: + outputs["get_flags_decision"] = client.get_flags_decision(DISTINCT_ID) + outputs["get_feature_variants"] = client.get_feature_variants(DISTINCT_ID) + outputs["get_feature_payloads"] = client.get_feature_payloads(DISTINCT_ID) + outputs["get_feature_flags_and_payloads"] = client.get_feature_flags_and_payloads( + DISTINCT_ID + ) + outputs["get_all_flags"] = client.get_all_flags(DISTINCT_ID) + outputs["get_all_flags_and_payloads"] = client.get_all_flags_and_payloads( + DISTINCT_ID + ) + for key in keys + ["missing"]: + result = client.get_feature_flag_result(key, DISTINCT_ID) + outputs[f"get_feature_flag_result:{key}"] = ( + None + if result is None + else [result.enabled, result.variant, result.payload, result.reason] + ) + outputs[f"get_feature_flag:{key}"] = client.get_feature_flag(key, "user-2") + outputs[f"feature_enabled:{key}"] = client.feature_enabled(key, "user-3") + outputs[f"get_feature_flag_payload:{key}"] = client.get_feature_flag_payload( + key, DISTINCT_ID + ) + snapshot = client.evaluate_flags("snapshot-user") + outputs["snapshot.keys"] = sorted(snapshot.keys) + for key in keys + ["missing"]: + outputs[f"snapshot.is_enabled:{key}"] = snapshot.is_enabled(key) + outputs[f"snapshot.get_flag:{key}"] = snapshot.get_flag(key) + outputs[f"snapshot.get_flag_payload:{key}"] = snapshot.get_flag_payload(key) + client.capture("snapshot-event", distinct_id="snapshot-user", flags=snapshot) + client.capture("fetched-event", distinct_id="capture-user", send_feature_flags=True) + + +def getters(shape: str, flags: List[Dict[str, Any]]) -> Dict[str, Any]: + """Every legacy getter, snapshot read and flag event for one response.""" + keys = [flag["key"] for flag in flags] + return _with_client( + [build_response(shape, flags)], + lambda client, outputs: _read_every_getter(client, outputs, keys), + ) + + +def _good(shape: str, **fields: Any) -> Dict[str, Any]: + return build_response( + shape, [_flag("cached", "blue", payload='{"theme": "dark"}', **fields)] + ) + + +# Single-flag histories with the memory fallback cache: the responses, in order, to +# the same flag read for the same user. +def cache_histories(shape: str) -> Dict[str, List[Any]]: + return { + "good, unreadable record, timeout": [ + _good(shape), + _good(shape, set={"reason": "oops"}), + RequestsTimeout("timed out"), + ], + "good, each request failure": [ + _good(shape), + RequestsTimeout("timed out"), + RequestsConnectionError("refused"), + APIError(500, "server error"), + QuotaLimitError(429, "quota limited"), + ValueError("unexpected"), + ], + "good, disabled, timeout": [ + _good(shape), + build_response(shape, [_flag("cached", False)]), + RequestsTimeout("timed out"), + ], + "good, server failed, timeout": [ + _good(shape), + _good(shape, failed=True), + RequestsTimeout("timed out"), + ], + "good, missing with errors, timeout": [ + _good(shape), + build_response(shape, [], errorsWhileComputingFlags=True), + RequestsTimeout("timed out"), + ], + "good, other variant, timeout": [ + _good(shape), + build_response(shape, [_flag("cached", "green")]), + RequestsTimeout("timed out"), + ], + } + + +def cache_history(responses: List[Any]) -> Dict[str, Any]: + def body(client: Client, outputs: Dict[str, Any]) -> None: + results = [] + for _ in responses: + # Forget sent events so that every read sends one, with its error. + client.distinct_ids_feature_flags_reported.clear() + result = client.get_feature_flag_result("cached", DISTINCT_ID) + results.append( + None + if result is None + else [result.enabled, result.variant, result.payload, result.reason] + ) + outputs["results"] = results + + return _with_client( + responses + [RequestsTimeout("unused")], + body, + flag_fallback_cache_url="memory://local/?ttl=300&size=100", + ) + + +UNREADABLE = _flag("unreadable", "compact", set={"reason": "oops"}) +# What a record that cannot be read reads as: disabled, without reason or metadata. +UNREADABLE_AS_DISABLED = _flag("unreadable", False, metadata=None, reason=None) + + +def run_all(shape: str) -> Dict[str, Any]: + """Every scenario's results for the given response shape.""" + return { + "getters": getters(shape, GETTER_FLAGS), + "getters with an unreadable record": getters( + shape, [GETTER_FLAGS[2], UNREADABLE] + ), + "getters with the unreadable record read as disabled": getters( + shape, [GETTER_FLAGS[2], UNREADABLE_AS_DISABLED] + ), + "cache histories": { + name: cache_history(responses) + for name, responses in cache_histories(shape).items() + }, + } diff --git a/posthog/test/snapshots/flags_request.json b/posthog/test/snapshots/flags_request.json index 50a018401..5e08287bd 100644 --- a/posthog/test/snapshots/flags_request.json +++ b/posthog/test/snapshots/flags_request.json @@ -28,5 +28,5 @@ "User-Agent": "posthog-python/" }, "timeout": 3, - "url": "https://example.posthog.test/flags/?v=2" + "url": "https://example.posthog.test/flags/?v=4" } diff --git a/posthog/test/test_async_request.py b/posthog/test/test_async_request.py index e82cd4d41..876c5ca06 100644 --- a/posthog/test/test_async_request.py +++ b/posthog/test/test_async_request.py @@ -99,7 +99,7 @@ async def test_async_flags_sends_v2_request_payload(): assert result == {"flags": {}} _, args, kwargs = client.calls[0] - assert args == ("/flags/?v=2",) + assert args == ("/flags/?v=4",) payload = json.loads(kwargs["content"]) assert payload["token"] == "project-key" assert "api_key" not in payload diff --git a/posthog/test/test_feature_flag.py b/posthog/test/test_feature_flag.py index 9faa56f1c..147a8186f 100644 --- a/posthog/test/test_feature_flag.py +++ b/posthog/test/test_feature_flag.py @@ -127,7 +127,7 @@ def test_flag_metadata_from_json(self): metadata = FlagMetadata.from_json(resp) self.assertEqual(metadata.id, 123) self.assertIsNone(metadata.payload) - self.assertEqual(metadata.version, 0) # default value + self.assertIsNone(metadata.version) # absent, not invented self.assertEqual(metadata.description, "") # default value # Test with None diff --git a/posthog/test/test_flags_released_behaviour.py b/posthog/test/test_flags_released_behaviour.py new file mode 100644 index 000000000..7ad8022f8 --- /dev/null +++ b/posthog/test/test_flags_released_behaviour.py @@ -0,0 +1,75 @@ +"""Legacy flag results pinned to the results posthog 7.82.1 recorded. + +``fixtures/released_flags_7_82_1.json`` was recorded with posthog 7.82.1 from the +``v2`` response shape; ``released_flags_scenarios.py`` describes the scenarios and how +to regenerate the file. Each scenario runs here from the ``v2`` shape and from the +``v4`` shape of the same flags, so a change shared by both readers is caught too. +Where the expected result differs from 7.82.1, the test says which decision makes +it differ. +""" + +import copy +import json +from pathlib import Path +from typing import Any, Dict + +import pytest + +from posthog.test import released_flags_scenarios as scenarios + +RELEASED = json.loads( + (Path(__file__).with_name("fixtures") / "released_flags_7_82_1.json").read_text( + encoding="utf-8" + ) +)["results"] +SHAPES = ["v2", "v4"] + + +@pytest.fixture(scope="module", params=SHAPES) +def results(request: Any) -> Dict[str, Any]: + return scenarios.run_all(request.param) + + +def test_every_legacy_getter_and_event_matches_7_82_1(results): + expected = copy.deepcopy(RELEASED["getters"]) + # A record without a flag ID or version reads them as absent (None), where 7.82.1 + # read 0. Events already left out both, so only the parsed record differs. + sparse = expected["get_flags_decision"]["flags"]["sparse"]["metadata"] + assert (sparse["id"], sparse["version"]) == (0, 0) + sparse["id"] = sparse["version"] = None + + assert results["getters"] == expected + + +def test_an_unreadable_record_fails_only_itself(results): + # 7.82.1 failed the whole response when one record had a field of the wrong + # type. That record now fails alone and reads as a disabled flag without reason + # or metadata, and its siblings read as before. + assert RELEASED["getters with an unreadable record"]["get_all_flags"] == {} + + as_disabled = RELEASED["getters with the unreadable record read as disabled"] + assert results["getters with the unreadable record read as disabled"] == as_disabled + + # A single-flag read of the unreadable record still reports what 7.82.1 + # reported when the whole response failed: an unknown_error and no request + # details. Only its value differs, disabled instead of None. + expected = copy.deepcopy(as_disabled) + released_events = RELEASED["getters with an unreadable record"]["events"] + for event, released in zip(expected["events"], released_events): + if ( + released[1].get("$feature_flag") == "unreadable" + and released[1].get("$feature_flag_error") == "unknown_error" + ): + event[1].pop("$feature_flag_request_id") + event[1].pop("$feature_flag_evaluated_at") + event[1]["$feature_flag_error"] = "unknown_error" + assert results["getters with an unreadable record"] == expected + + +@pytest.mark.parametrize("name", sorted(RELEASED["cache histories"])) +def test_fallback_cache_histories_match_7_82_1(results, name): + # An unreadable record now fails only itself, but a single-flag read of it + # still reports what 7.82.1 reported when the whole response failed: an + # unknown_error, no request details, and the cached value. + expected = RELEASED["cache histories"][name] + assert results["cache histories"][name] == expected diff --git a/posthog/test/test_flags_v4_response.py b/posthog/test/test_flags_v4_response.py new file mode 100644 index 000000000..662011dce --- /dev/null +++ b/posthog/test/test_flags_v4_response.py @@ -0,0 +1,1186 @@ +"""Reading the ``/flags?v=4`` response, checked against the released wire contract. + +The files in ``fixtures/feature_flag_rules_v2`` are copied unchanged from a released +``PostHog/posthog-sdk-test-harness`` contract. ``SOURCE.json`` records the release and +the digest of each file. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +from dataclasses import asdict +from pathlib import Path +from typing import Any, Dict, List, Optional +from unittest import mock + +import pytest + +from posthog import AsyncPosthog +from posthog.client import Client +from posthog.feature_flag_evaluations import FeatureFlagEvaluations +from posthog.request import RequestsTimeout +from posthog.test.test_utils import FAKE_TEST_API_KEY +from posthog.types import ( + FeatureFlag, + FeatureFlagResult, + FlagMetadata, + FlagReason, + normalize_flags_response, + to_flags_and_payloads, +) + +FIXTURES = Path(__file__).with_name("fixtures") / "feature_flag_rules_v2" +DISTINCT_ID = "user-1" + + +def _load(name: str) -> Any: + return json.loads((FIXTURES / name).read_text(encoding="utf-8")) + + +READERS = _load("readers.json") +RESPONSES = _load("responses.json") +LEGACY_PROJECTION = _load("legacy_projection.json") +VALUE_CORPUS = _load("v2_value_evaluation.json") + + +def _pointer(pointer: str) -> List[str]: + return [ + token.replace("~1", "/").replace("~0", "~") for token in pointer.split("/")[1:] + ] + + +def _build(fixture: Dict[str, Any], case: Dict[str, Any]) -> Dict[str, Any]: + """Apply a fixture case: copy its template, remove members, then set members.""" + document = copy.deepcopy(fixture["templates"][case["template"]]) + for pointer in case.get("remove", []): + *parents, last = _pointer(pointer) + target = document + for token in parents: + target = target[token] + del target[last] + for pointer, value in case.get("set", {}).items(): + *parents, last = _pointer(pointer) + target = document + for token in parents: + target = target[token] + target[last] = copy.deepcopy(value) + return document + + +@pytest.fixture +def client(): + client = Client(FAKE_TEST_API_KEY, send=False) + yield client + client.shutdown() + + +def _evaluate(client: Client, response: Dict[str, Any]) -> FeatureFlagEvaluations: + with mock.patch("posthog.client.flags", return_value=copy.deepcopy(response)): + return client.evaluate_flags(DISTINCT_ID) + + +def test_vendored_fixtures_match_the_recorded_release_digests(): + source = _load("SOURCE.json") + assert source["release"] == "1.22.0" + assert source["contract_version"] == "7.0.0" + for name, entry in source["files"].items(): + digest = hashlib.sha256((FIXTURES / name).read_bytes()).hexdigest() + assert digest == entry["sha256"], name + + +# The /flags?v=4 metadata fields that stay private until the typed accessors ship. +_PRIVATE_METADATA_FIELDS = { + "config_version", + "rule_type", + "rule_id", + "variant_key", + "forced_variant", +} + +VALID_RESPONSES = [case for case in RESPONSES["cases"] if case["expected"] == "valid"] + + +@pytest.mark.parametrize("case", VALID_RESPONSES, ids=lambda case: case["id"]) +def test_valid_producer_responses_are_retained_as_sent(case): + response = _build(RESPONSES, case) + records = copy.deepcopy(response["flags"]) + flags = normalize_flags_response(response)["flags"] + + assert set(flags) == set(records) + for key, record in records.items(): + flag = flags[key] + assert flag._value == record["value"] + assert flag._failed is record.get("failed", False) + assert flag.reason is not None + assert flag.reason.code == record["reason"]["code"] + assert flag.reason.condition_index == record["reason"]["condition_index"] + assert isinstance(flag.metadata, FlagMetadata) + for field, value in record["metadata"].items(): + # Only config version 1 records carry an experiment ID; it is not kept. + if field not in ("payload", "experiment_id"): + if field in _PRIVATE_METADATA_FIELDS: + field = f"_{field}" + assert getattr(flag.metadata, field) == value, field + + +def _typed_response(values: Dict[str, Any]) -> Dict[str, Any]: + flags = {} + for index, (key, value) in enumerate(values.items()): + flags[key] = { + "key": key, + "value": value, + "reason": { + "code": "targeting_match", + "condition_index": 0, + "description": "Matched rule 1", + }, + "metadata": { + "id": 300 + index, + "version": 2, + "config_version": 2, + "description": None, + "payload": None, + "has_experiment": False, + "rule_type": "targeted_release", + "rule_id": f"30000000-0000-4000-8000-00000000000{index}", + }, + } + return { + "flags": flags, + "errorsWhileComputingFlags": False, + "requestId": "30000000-0000-4000-8000-000000000000", + "evaluatedAt": 1800000000000, + } + + +def _old_server_response(**records: Dict[str, Any]) -> Dict[str, Any]: + return { + "flags": { + key: {"key": key, "metadata": {"id": 1, "version": 1}, **record} + for key, record in records.items() + }, + "errorsWhileComputingFlags": False, + } + + +@pytest.mark.parametrize("shape", ["v4", "older"]) +def test_one_log_per_client_for_records_without_typed_values(shape, caplog): + if shape == "v4": + response = _typed_response({"string-flag": "compact"}) + else: + response = _old_server_response( + **{"string-flag": {"enabled": True, "variant": "compact"}} + ) + client = Client(FAKE_TEST_API_KEY, send=False) + other_client = Client(FAKE_TEST_API_KEY, send=False) + try: + with caplog.at_level("INFO", logger="posthog"): + for _ in range(2): + _evaluate(client, response) + _evaluate(other_client, response) + finally: + client.shutdown() + other_client.shutdown() + + logged = [r for r in caplog.records if "no typed flag values" in r.getMessage()] + assert len(logged) == (0 if shape == "v4" else 2) + + +async def test_the_async_client_logs_records_without_typed_values_once(caplog): + response = _old_server_response(**{"flag": {"enabled": True, "variant": None}}) + with mock.patch( + "posthog.async_client._async_flags", + new=mock.AsyncMock(side_effect=lambda *a, **k: copy.deepcopy(response)), + ): + client = AsyncPosthog("project-key", send=False) + with caplog.at_level("INFO", logger="posthog"): + await client.evaluate_flags(DISTINCT_ID) + snapshot = await client.evaluate_flags(DISTINCT_ID) + await client.shutdown() + + assert snapshot.get_flag("flag") is True + logged = [r for r in caplog.records if "no typed flag values" in r.getMessage()] + assert len(logged) == 1 + + +def test_a_failed_record_never_renders_its_value(client): + response = _build(RESPONSES, {"template": "error"}) + response["flags"]["garden-layout"]["value"] = "compact" + snapshot = _evaluate(client, response) + + assert snapshot.get_flag("garden-layout") is False + assert snapshot.is_enabled("garden-layout", True) is False + + +@pytest.mark.parametrize( + "make_record", + [ + lambda **overrides: _record(**{"metadata.config_version": 1, **overrides}), + lambda **overrides: _older_record(**overrides), + ], + ids=["v4", "older"], +) +def test_an_unreadable_record_keeps_the_last_good_cached_value(make_record): + responses: List[Any] = [ + {"flags": {"garden-layout": make_record()}, "requestId": "r1"}, + {"flags": {"garden-layout": make_record(reason="oops")}, "requestId": "r2"}, + RequestsTimeout("timed out"), + ] + + def flags(*args: Any, **kwargs: Any) -> Dict[str, Any]: + response = responses.pop(0) + if isinstance(response, Exception): + raise response + return response + + client = Client( + FAKE_TEST_API_KEY, + send=False, + flag_fallback_cache_url="memory://local/?ttl=300&size=100", + ) + try: + with mock.patch("posthog.client.flags", side_effect=flags): + values = [ + client.get_feature_flag( + "garden-layout", DISTINCT_ID, send_feature_flag_events=False + ) + for _ in range(3) + ] + finally: + client.shutdown() + + # The unreadable record falls back to the cached value instead of replacing it, + # so the timeout after it still has the last good value. + assert values == ["compact", "compact", "compact"] + + +def test_an_unreadable_record_without_a_cached_value_reads_as_disabled(client): + response = {"flags": {"garden-layout": _older_record(reason="oops")}} + with mock.patch("posthog.client.flags", return_value=response): + assert ( + client.get_feature_flag( + "garden-layout", DISTINCT_ID, send_feature_flag_events=False + ) + is False + ) + + +def _record(**overrides: Any) -> Dict[str, Any]: + record: Dict[str, Any] = { + "key": "garden-layout", + "value": "compact", + "reason": {"code": "targeting_match", "condition_index": 0}, + "metadata": { + "id": 201, + "version": 7, + "config_version": 2, + "has_experiment": False, + "payload": None, + "rule_type": "targeted_release", + "rule_id": "10000000-0000-4000-8000-000000000001", + }, + } + for path, value in overrides.items(): + *parents, last = path.split(".") + target = record + for token in parents: + target = target[token] + target[last] = value + return record + + +@pytest.mark.parametrize( + "path, value", + [ + ("value", []), + ("value", float("nan")), + ("failed", "true"), + ("reason", "targeting_match"), + ("reason.code", 3), + ("reason.condition_index", "0"), + ("reason.condition_index", 0.5), + ("reason.description", ["Matched"]), + ("metadata", []), + ("metadata.id", "201"), + ("metadata.version", True), + ("metadata.config_version", "2"), + ("metadata.description", 1), + ("metadata.has_experiment", "false"), + ("metadata.rule_type", 2), + ("metadata.rule_id", 3), + ("metadata.experiment_id", "301"), + ("metadata.variant_key", True), + ("metadata.forced_variant", "yes"), + ], +) +def test_a_known_field_with_the_wrong_type_fails_only_that_record(path, value): + response = { + "flags": { + "garden-layout": _record(**{path: value}), + "sibling": _record(**{"key": "sibling"}), + } + } + + flags = normalize_flags_response(response)["flags"] + + malformed = flags["garden-layout"] + assert malformed._failed is True + assert malformed._value is None + assert (malformed.enabled, malformed.variant, malformed.get_value()) == ( + False, + None, + False, + ) + # The record keeps that it could not be read, apart from a server failure. + assert malformed._malformed_field == path + sibling = flags["sibling"] + assert sibling._failed is False + assert sibling._value == "compact" + assert sibling._malformed_field is None + + +def test_a_record_that_is_not_an_object_fails_only_that_record(): + response = {"flags": {"broken": ["compact"], "sibling": _record(key="sibling")}} + + flags = normalize_flags_response(response)["flags"] + + assert flags["broken"]._failed is True + assert flags["broken"].get_value() is False + assert flags["sibling"]._value == "compact" + + +def test_unknown_fields_and_null_optional_fields_are_ignored(): + record = _record(**{"future_field": {"nested": True}}) + record["reason"]["future_reason"] = 1 + record["metadata"].update(future_metadata=[1], variant_key=None) + record["conditions"] = "not read" + + flag = normalize_flags_response({"flags": {"garden-layout": record}})["flags"][ + "garden-layout" + ] + + assert flag._failed is False + assert flag._value == "compact" + assert isinstance(flag.metadata, FlagMetadata) + assert flag.metadata._variant_key is None + + +@pytest.mark.parametrize( + "field, value", + [ + ("experiment_id", 301), + ("holdout_id", 401), + ("holdout_id", "401"), + ], +) +def test_experiment_and_holdout_ids_in_the_metadata_are_ignored(field, value): + # Only config version 1 records carry an experiment ID, and no record carries a + # holdout ID: experiments link to rules on the server, and holdouts are + # rule-local. Neither is kept. An experiment ID of the wrong type is a parse + # error, like any known field; a holdout ID is an unknown key. + record = _record(**{f"metadata.{field}": value}) + + flag = normalize_flags_response({"flags": {"garden-layout": record}})["flags"][ + "garden-layout" + ] + + assert flag._failed is False + assert flag._value == "compact" + assert isinstance(flag.metadata, FlagMetadata) + assert not hasattr(flag.metadata, field) + + +def test_a_record_without_value_is_read_as_an_older_server_record(): + # An older server sends `enabled` and `variant`. Any rule context it carries + # is discarded, and the config version reads as 1. + record = { + "key": "garden-layout", + "enabled": True, + "variant": "compact", + "reason": {"code": "condition_match", "condition_index": 0}, + "metadata": { + "id": 202, + "version": 9, + "payload": None, + "config_version": 2, + "rule_type": "experiment", + "rule_id": "10000000-0000-4000-8000-000000000001", + "variant_key": "compact", + }, + } + + flag = normalize_flags_response({"flags": {"garden-layout": record}})["flags"][ + "garden-layout" + ] + + assert flag._value == "compact" + assert isinstance(flag.metadata, FlagMetadata) + assert flag.metadata._config_version == 1 + assert flag.metadata._rule_type is None + assert flag.metadata._rule_id is None + assert flag.metadata._variant_key is None + + +def test_a_failed_older_server_record_is_kept_as_failed(): + record = { + "key": "garden-layout", + "enabled": False, + "variant": None, + "failed": True, + "reason": { + "code": "timeout", + "condition_index": None, + "description": "Timed out", + }, + "metadata": {"id": 202, "version": 9, "payload": None}, + } + + flag = normalize_flags_response({"flags": {"garden-layout": record}})["flags"][ + "garden-layout" + ] + + assert flag._failed is True + assert flag._value is False + assert flag.reason is not None and flag.reason.description == "Timed out" + + +def _older_record(**overrides: Any) -> Dict[str, Any]: + record: Dict[str, Any] = { + "key": "garden-layout", + "enabled": True, + "variant": "compact", + "failed": False, + "reason": { + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1", + }, + "metadata": { + "id": 202, + "version": 9, + "description": "Garden layout", + "payload": None, + "has_experiment": False, + }, + } + for path, value in overrides.items(): + *parents, last = path.split(".") + target = record + for token in parents: + target = target[token] + target[last] = value + return record + + +def test_one_unreadable_older_server_record_keeps_its_siblings(): + response = { + "flags": { + "bad": { + "enabled": True, + "variant": None, + "reason": "oops", + "metadata": {"id": 1, "version": 1, "payload": None}, + }, + "good": { + "enabled": True, + "variant": None, + "reason": None, + "metadata": {"id": 2, "version": 1, "payload": None}, + }, + }, + "requestId": "x", + } + + flags = normalize_flags_response(response)["flags"] + + assert flags["bad"]._failed is True + assert flags["bad"]._malformed_field == "reason" + assert flags["good"]._failed is False + assert flags["good"].get_value() is True + + +def test_a_value_too_deeply_nested_to_encode_fails_only_that_record(): + deep: Dict[str, Any] = {} + inner = deep + for _ in range(200_000): + inner["a"] = {} + inner = inner["a"] + response = { + "flags": { + "deep": _record(key="deep", value=deep), + "sibling": _record(key="sibling"), + } + } + + flags = normalize_flags_response(response)["flags"] + + assert flags["deep"]._failed is True + assert flags["deep"]._malformed_field == "value" + assert flags["sibling"].get_value() == "compact" + + +@pytest.mark.parametrize( + "path, value", + [ + ("key", 123), + ("enabled", "true"), + ("variant", 3), + ("failed", "yes"), + ("reason", "condition_match"), + ("reason", ""), + ("reason.code", 3), + ("reason.condition_index", "0"), + ("reason.condition_index", 0.5), + ("reason.description", ["Matched"]), + ("metadata", []), + ("metadata.id", "202"), + ("metadata.version", True), + ("metadata.description", 1), + ("metadata.has_experiment", "false"), + ], +) +def test_a_known_field_with_the_wrong_type_fails_only_that_older_server_record( + path, value +): + response = { + "flags": { + "garden-layout": _older_record(**{path: value}), + "sibling": _older_record(key="sibling"), + } + } + + flags = normalize_flags_response(response)["flags"] + + malformed = flags["garden-layout"] + assert malformed.key == "garden-layout" + assert malformed._failed is True + assert malformed._malformed_field == path + assert malformed._value is None + assert (malformed.enabled, malformed.variant, malformed.get_value()) == ( + False, + None, + False, + ) + sibling = flags["sibling"] + assert ( + sibling + == normalize_flags_response( + {"flags": {"sibling": _older_record(key="sibling")}} + )["flags"]["sibling"] + ) + assert (sibling._failed, sibling._value, sibling._malformed_field) == ( + False, + "compact", + None, + ) + + +def test_a_valid_older_server_record_renders_as_before(): + flag = normalize_flags_response({"flags": {"garden-layout": _older_record()}})[ + "flags" + ]["garden-layout"] + + assert flag == FeatureFlag( + key="garden-layout", + enabled=True, + variant="compact", + reason=FlagReason( + code="condition_match", + condition_index=0, + description="Matched condition set 1", + ), + metadata=FlagMetadata( + id=202, + payload=None, + version=9, + description="Garden layout", + has_experiment=False, + ), + ) + + +def test_the_private_v4_details_stay_out_of_the_public_dataclass_fields(): + # The typed value and rule metadata are private until the typed accessors ship: + # repr, equality, hashing and asdict() of a parsed flag see only public fields. + record = _record(value={"beds": 4}, **{"metadata.rule_id": "rule-1"}) + flag = normalize_flags_response({"flags": {"garden-layout": record}})["flags"][ + "garden-layout" + ] + assert flag._value == {"beds": 4} + assert flag.metadata._rule_id == "rule-1" + + public = FeatureFlag( + key=flag.key, + enabled=flag.enabled, + variant=flag.variant, + reason=flag.reason, + metadata=FlagMetadata( + id=flag.metadata.id, + payload=flag.metadata.payload, + version=flag.metadata.version, + description=flag.metadata.description, + has_experiment=flag.metadata.has_experiment, + ), + ) + assert flag == public + assert hash(flag) == hash(public) + assert repr(flag) == repr(public) + assert asdict(flag) == asdict(public) + fields = [*asdict(flag), *asdict(flag)["metadata"]] + assert not [name for name in fields if name.startswith("_")] + + +@pytest.mark.parametrize("make_record", [_record, _older_record]) +def test_a_record_key_of_the_wrong_type_fails_only_that_record(make_record): + flags = normalize_flags_response( + { + "flags": { + "garden-layout": make_record(key=123), + "sibling": make_record(key="sibling"), + } + } + )["flags"] + + assert flags["garden-layout"].key == "garden-layout" + assert flags["garden-layout"]._failed is True + assert flags["garden-layout"]._malformed_field == "key" + assert flags["sibling"]._failed is False + + +@pytest.mark.parametrize("make_record", [_record, _older_record]) +@pytest.mark.parametrize("key", ["other-flag", None]) +def test_a_record_is_read_under_its_map_key(make_record, key): + # Reader rules fail a record only for a field of the wrong JSON type. A string + # key that differs from the map key is a producer error, so the map key wins. + record = make_record(key=key) + + flag = normalize_flags_response({"flags": {"garden-layout": record}})["flags"][ + "garden-layout" + ] + + assert flag.key == "garden-layout" + assert flag._failed is False + assert flag._value == "compact" + + +def test_a_server_failed_record_is_not_marked_unreadable(): + flags = normalize_flags_response( + { + "flags": { + "v4": _build(RESPONSES, {"template": "error"})["flags"][ + "garden-layout" + ], + "older": _older_record(failed=True, enabled=False, variant=None), + } + } + )["flags"] + + for flag in flags.values(): + assert flag._failed is True + assert flag._malformed_field is None + + +@pytest.mark.parametrize("make_record", [_record, _older_record]) +def test_absent_flag_id_and_version_stay_absent(make_record, client): + record = make_record() + record["key"] = "garden-layout" + del record["metadata"]["id"] + del record["metadata"]["version"] + response = { + "flags": {"garden-layout": record}, + "errorsWhileComputingFlags": False, + "requestId": "60000000-0000-4000-8000-000000000000", + } + + flag = normalize_flags_response(copy.deepcopy(response))["flags"]["garden-layout"] + assert isinstance(flag.metadata, FlagMetadata) + assert (flag.metadata.id, flag.metadata.version) == (None, None) + + with ( + mock.patch("posthog.client.flags", return_value=copy.deepcopy(response)), + mock.patch.object(client, "capture") as capture, + ): + client.get_feature_flag_result("garden-layout", DISTINCT_ID) + properties = capture.call_args.kwargs["properties"] + assert "$feature_flag_id" not in properties + assert "$feature_flag_version" not in properties + + # A new client, so the event is not deduplicated against the one above. + snapshot_client = Client(FAKE_TEST_API_KEY, send=False) + try: + with mock.patch.object(snapshot_client, "capture") as capture: + _evaluate(snapshot_client, response).get_flag("garden-layout") + finally: + snapshot_client.shutdown() + properties = capture.call_args.kwargs["properties"] + assert "$feature_flag_id" not in properties + assert "$feature_flag_version" not in properties + + +@pytest.mark.parametrize( + "value, encoded", [(25, "25"), ({"columns": 2}, '{"columns":2}')] +) +def test_a_number_or_object_value_is_its_own_payload(value, encoded, client): + # A config version 2 number or object value is its payload; a payload the + # record also sends is ignored. + response = _typed_response({"upload-limit": value}) + response["flags"]["upload-limit"]["metadata"]["payload"] = "999" + + flag = normalize_flags_response(copy.deepcopy(response))["flags"]["upload-limit"] + assert flag.metadata.payload == encoded + + snapshot = _evaluate(client, response) + assert snapshot.get_flag_payload("upload-limit") == value + + +def test_a_config_version_1_payload_is_kept(): + response = _typed_response({"bool-flag": True, "string-flag": "compact"}) + for record in response["flags"].values(): + record["metadata"].update(config_version=1, payload='{"columns": 2}') + + flags = normalize_flags_response(response)["flags"] + + assert flags["bool-flag"].metadata.payload == '{"columns": 2}' + assert flags["string-flag"].metadata.payload == '{"columns": 2}' + + +def test_feature_flag_value_defaults_to_the_legacy_value(): + assert FeatureFlag.from_value_and_payload("a", "control", None)._value == "control" + assert FeatureFlag.from_value_and_payload("a", True, None)._value is True + assert FeatureFlag.from_value_and_payload("a", False, None)._value is False + + +class _CustomFlag(FeatureFlag): + pass + + +class _CustomMetadata(FlagMetadata): + pass + + +class _CustomReason(FlagReason): + pass + + +@pytest.mark.parametrize( + "record", + [ + _record(), + _older_record(), + {"key": "garden-layout", "enabled": True}, + _record(**{"metadata.id": "201"}), + _older_record(reason="oops"), + ], + ids=["v4", "older", "minimal-older", "unreadable-v4", "unreadable-older"], +) +def test_from_json_builds_the_subclass_it_is_called_on(record): + flag = _CustomFlag.from_json(record) + + assert type(flag) is _CustomFlag + assert flag.key == "garden-layout" + + +def test_metadata_and_reason_from_json_build_the_subclass_they_are_called_on(): + assert type(_CustomMetadata.from_json({"id": 1, "version": 2})) is _CustomMetadata + assert type(_CustomReason.from_json({"code": "condition_match"})) is _CustomReason + + +# Legacy accessor parity: a v4 record and the equivalent older-server record must give +# identical legacy results. + + +def _legacy_maps(response: Dict[str, Any]) -> Dict[str, Any]: + maps = to_flags_and_payloads(normalize_flags_response(copy.deepcopy(response))) + payloads = maps["featureFlagPayloads"] or {} + return { + "featureFlags": maps["featureFlags"], + "featureFlagPayloads": { + key: json.loads(value) for key, value in payloads.items() + }, + } + + +def _v4_record_for_v1_outcome(outcome: Dict[str, Any]) -> Dict[str, Any]: + """The v4 record of a v1 flag: ``value`` is the variant, or ``enabled`` without one.""" + if outcome["failed"]: + value = None + elif outcome["variant"] is not None: + value = outcome["variant"] + else: + value = outcome["enabled"] + metadata: Dict[str, Any] = { + "id": outcome["flag_id"], + "version": outcome["flag_version"], + "config_version": 1, + "payload": outcome["payload"], + "has_experiment": False, + } + if outcome["variant"] is not None: + metadata["variant_key"] = outcome["variant"] + record = { + "key": outcome["key"], + "value": value, + "reason": outcome["reason"], + "metadata": metadata, + } + if outcome["failed"]: + record["failed"] = True + return record + + +@pytest.mark.parametrize( + "case", LEGACY_PROJECTION["cases"], ids=lambda case: case["id"] +) +def test_v1_flags_render_the_same_legacy_values_from_both_shapes(case): + v2_response = case["projections"]["flags_v2"] + v1_maps = case["projections"]["flags_v1"] + outcome = case["outcome"] + v4_flags = ( + {} + if outcome["omitted"] + else {outcome["key"]: _v4_record_for_v1_outcome(outcome)} + ) + v4_response = { + "errorsWhileComputingFlags": v2_response["errorsWhileComputingFlags"], + "flags": v4_flags, + } + expected = { + "featureFlags": v1_maps["featureFlags"], + "featureFlagPayloads": { + key: json.loads(value) + for key, value in v1_maps["featureFlagPayloads"].items() + }, + } + + assert _legacy_maps(v2_response) == expected + assert _legacy_maps(v4_response) == expected + + +VALUE_SUCCESS_CASES = [ + case for case in VALUE_CORPUS["cases"] if case["expected"]["status"] == "success" +] + + +def _value_case_records(case: Dict[str, Any]) -> tuple[Dict[str, Any], Dict[str, Any]]: + expected, legacy = case["expected"], case["legacy"] + rule = expected.get("rule") + reason = { + "code": expected["reason"], + "condition_index": rule["index"] if rule else None, + "description": "Evaluated", + } + metadata = {"id": 401, "version": 3, "description": None, "has_experiment": False} + v4_metadata = {**metadata, "config_version": 2, "payload": None} + if rule: + v4_metadata.update(rule_type=rule["rule_type"], rule_id=rule["id"]) + payload = legacy["payload"] + v2_record = { + "key": "typed-flag", + "enabled": legacy["enabled"], + "variant": legacy["variant"], + "reason": reason, + "metadata": { + **metadata, + "payload": None if payload is None else json.dumps(payload), + }, + } + v4_record = { + "key": "typed-flag", + "value": expected["value"], + "reason": reason, + "metadata": v4_metadata, + } + return v2_record, v4_record + + +@pytest.mark.parametrize("case", VALUE_SUCCESS_CASES, ids=lambda case: case["id"]) +def test_typed_values_render_the_corpus_legacy_cells_from_both_shapes(case): + v2_record, v4_record = _value_case_records(case) + legacy = case["legacy"] + expected = { + "featureFlags": {"typed-flag": legacy["value"]}, + "featureFlagPayloads": {} + if legacy["payload"] is None + else {"typed-flag": legacy["payload"]}, + } + + for record in (v2_record, v4_record): + maps = _legacy_maps({"flags": {"typed-flag": record}}) + assert maps == expected + assert type(maps["featureFlags"]["typed-flag"]) is type(legacy["value"]) + flag = normalize_flags_response( + {"flags": {"typed-flag": copy.deepcopy(record)}} + )["flags"]["typed-flag"] + assert (flag.enabled, flag.variant) == (legacy["enabled"], legacy["variant"]) + result = FeatureFlagResult.from_flag_details(flag) + assert result is not None + assert result.get_value() == legacy["value"] + assert result.payload == legacy["payload"] + + +def test_a_snapshot_reads_its_flags_from_the_parsed_records(client): + response = _typed_response({"object-flag": {"layout": "compact"}, "off": False}) + response["flags"]["off"]["metadata"]["payload"] = "not json" + snapshot = _evaluate(client, response) + + record = snapshot._flags["object-flag"] + assert (record.key, record.enabled, record.variant) == ("object-flag", True, None) + assert (record.id, record.version, record.reason) == (300, 2, "Matched rule 1") + assert record.has_experiment is False + assert record.locally_evaluated is False + # The payload is decoded once and the same object is returned on every read. + payload = snapshot.get_flag_payload("object-flag") + assert payload == {"layout": "compact"} + assert snapshot.get_flag_payload("object-flag") is payload + assert snapshot.get_flag_payload("off") is None + + +def test_number_and_object_payloads_are_compact_json_in_wire_order(): + response = _typed_response( + {"number": 12.5, "object": {"b": "ü", "a": [1, True, None]}} + ) + + payloads = to_flags_and_payloads(normalize_flags_response(response))[ + "featureFlagPayloads" + ] + + assert payloads == {"number": "12.5", "object": '{"b":"ü","a":[1,true,null]}'} + + +# Every legacy getter and event, end to end: a v2-shaped and a v4-shaped response for +# the same flags give identical results. + +PARITY_FLAGS = [ + # key, v2 record fields, v4 value + ("bool-on", {"enabled": True, "variant": None}, True), + ("bool-off", {"enabled": False, "variant": None}, False), + ("v1-variant", {"enabled": True, "variant": "control"}, "control"), + ("v2-string", {"enabled": True, "variant": "compact"}, "compact"), + ("v2-null", {"enabled": False, "variant": None}, None), + ("v2-number", {"enabled": True, "variant": None}, 12.5), + ("v2-object", {"enabled": True, "variant": None}, {"layout": "compact"}), +] +PAYLOADS = {"v1-variant": '{"columns": 2}', "v2-number": "12.5"} +PAYLOADS["v2-object"] = '{"layout":"compact"}' + + +def _parity_response(shape: str) -> Dict[str, Any]: + flags = {} + for index, (key, fields, value) in enumerate(PARITY_FLAGS): + metadata: Dict[str, Any] = { + "id": 500 + index, + "version": 4, + "description": None, + "has_experiment": key == "v1-variant", + } + reason = { + "code": "condition_match", + "condition_index": 0, + "description": "Matched condition set 1", + } + if shape == "v2": + payload = PAYLOADS.get(key) + flags[key] = { + "key": key, + **fields, + "reason": reason, + "metadata": {**metadata, "payload": payload}, + } + else: + config_version = 1 if key.startswith(("bool", "v1")) else 2 + payload = PAYLOADS.get(key) if config_version == 1 else None + flags[key] = { + "key": key, + "value": value, + "reason": reason, + "metadata": { + **metadata, + "config_version": config_version, + "payload": payload, + }, + } + return { + "flags": flags, + "errorsWhileComputingFlags": False, + "requestId": "50000000-0000-4000-8000-000000000000", + "evaluatedAt": 1800000000000, + } + + +def _flag_event_properties(message: Dict[str, Any]) -> Dict[str, Any]: + return { + key: value + for key, value in message["properties"].items() + if key.startswith("$feature") + or key in ("$active_feature_flags", "locally_evaluated") + } + + +def _legacy_outputs(shape: str) -> Dict[str, Any]: + client = Client(FAKE_TEST_API_KEY, send=False) + outputs: Dict[str, Any] = {} + events: List[Any] = [] + keys = [key for key, _, _ in PARITY_FLAGS] + + def response(*args: Any, **kwargs: Any) -> Dict[str, Any]: + return _parity_response(shape) + + def enqueue(message: Dict[str, Any], *args: Any, **kwargs: Any) -> Optional[str]: + events.append((message["event"], _flag_event_properties(message))) + return None + + try: + with ( + mock.patch("posthog.client.flags", side_effect=response), + mock.patch.object(client, "_enqueue", side_effect=enqueue), + pytest.warns(DeprecationWarning), + ): + outputs["get_feature_variants"] = client.get_feature_variants(DISTINCT_ID) + outputs["get_feature_payloads"] = client.get_feature_payloads(DISTINCT_ID) + outputs["get_feature_flags_and_payloads"] = ( + client.get_feature_flags_and_payloads(DISTINCT_ID) + ) + outputs["get_all_flags"] = client.get_all_flags(DISTINCT_ID) + outputs["get_all_flags_and_payloads"] = client.get_all_flags_and_payloads( + DISTINCT_ID + ) + for key in keys + ["missing"]: + result = client.get_feature_flag_result(key, DISTINCT_ID) + outputs[f"get_feature_flag_result:{key}"] = ( + None + if result is None + else (result.enabled, result.variant, result.payload, result.reason) + ) + outputs[f"get_feature_flag:{key}"] = client.get_feature_flag( + key, "other-user" + ) + outputs[f"feature_enabled:{key}"] = client.feature_enabled( + key, "third-user" + ) + outputs[f"get_feature_flag_payload:{key}"] = ( + client.get_feature_flag_payload(key, DISTINCT_ID) + ) + snapshot = client.evaluate_flags("snapshot-user") + for key in keys + ["missing"]: + outputs[f"snapshot.is_enabled:{key}"] = snapshot.is_enabled(key) + outputs[f"snapshot.get_flag:{key}"] = snapshot.get_flag(key) + outputs[f"snapshot.get_flag_payload:{key}"] = snapshot.get_flag_payload( + key + ) + client.capture( + "snapshot-event", distinct_id="snapshot-user", flags=snapshot + ) + client.capture( + "fetched-event", distinct_id="capture-user", send_feature_flags=True + ) + finally: + client.shutdown() + outputs["events"] = events + return outputs + + +def test_every_legacy_getter_and_event_is_identical_for_both_shapes(): + v2_outputs = _legacy_outputs("v2") + v4_outputs = _legacy_outputs("v4") + + assert v4_outputs == v2_outputs + # Spot-check the legacy rendering itself. + assert v4_outputs["get_all_flags"] == { + "bool-on": True, + "bool-off": False, + "v1-variant": "control", + "v2-string": "compact", + "v2-null": False, + "v2-number": True, + "v2-object": True, + } + assert v4_outputs["get_feature_flag_payload:v2-object"] == {"layout": "compact"} + assert v4_outputs["snapshot.get_flag_payload:v2-number"] == 12.5 + + +def test_feature_flag_called_properties_are_unchanged_for_a_v1_flag(client): + response = _build(READERS, {"template": "v1"}) + with ( + mock.patch("posthog.client.flags", return_value=response), + mock.patch.object(client, "capture") as capture, + ): + result = client.get_feature_flag_result("garden-layout", DISTINCT_ID) + + assert result is not None + assert (result.enabled, result.variant, result.payload) == ( + True, + "compact", + {"columns": 2}, + ) + capture.assert_called_once_with( + "$feature_flag_called", + distinct_id=DISTINCT_ID, + properties={ + "$feature_flag": "garden-layout", + "$feature_flag_response": "compact", + "locally_evaluated": False, + "$feature/garden-layout": "compact", + "$feature_flag_payload": {"columns": 2}, + "$feature_flag_request_id": "20000000-0000-4000-8000-000000000002", + "$feature_flag_evaluated_at": 1800000000000, + "$feature_flag_version": 9, + "$feature_flag_id": 202, + "$feature_flag_has_experiment": False, + }, + groups={}, + disable_geoip=None, + ) + + +async def test_async_client_keeps_healthy_flags_beside_unreadable_and_sparse_ones(): + sparse = _record(key="sparse") + del sparse["metadata"]["id"], sparse["metadata"]["version"] + response = { + "flags": { + "bad-older": _older_record(key="bad-older", reason="oops"), + "bad-v4": _record(key="bad-v4", **{"metadata.id": "202"}), + "sparse": sparse, + "healthy": _older_record(key="healthy"), + }, + "requestId": "x", + } + with mock.patch( + "posthog.async_client._async_flags", new=mock.AsyncMock(return_value=response) + ): + client = AsyncPosthog("project-key", send=False) + with mock.patch.object(client, "capture") as capture: + snapshot = await client.evaluate_flags(DISTINCT_ID) + assert snapshot.get_flag("bad-older") is False + assert snapshot.get_flag("bad-v4") is False + assert snapshot.get_flag("sparse") == "compact" + assert snapshot.get_flag("healthy") == "compact" + await client.shutdown() + + sparse_event = next( + call.kwargs["properties"] + for call in capture.call_args_list + if call.kwargs["properties"]["$feature_flag"] == "sparse" + ) + assert "$feature_flag_id" not in sparse_event + assert "$feature_flag_version" not in sparse_event + + +async def test_async_client_reads_the_v4_response(): + with mock.patch( + "posthog.async_client._async_flags", + new=mock.AsyncMock( + return_value=_typed_response( + {"float-flag": 12.5, "object-flag": {"layout": "compact"}} + ) + ), + ): + client = AsyncPosthog("project-key", send=False) + snapshot = await client.evaluate_flags(DISTINCT_ID) + await client.shutdown() + + assert snapshot.get_flag("object-flag") is True + assert snapshot.get_flag_payload("object-flag") == {"layout": "compact"} + assert snapshot.get_flag("float-flag") is True + assert snapshot.get_flag_payload("float-flag") == 12.5 diff --git a/posthog/types.py b/posthog/types.py index 994b0786c..85938a939 100644 --- a/posthog/types.py +++ b/posthog/types.py @@ -1,8 +1,21 @@ import json import logging +import math from dataclasses import dataclass from enum import Enum -from typing import Any, Callable, List, Optional, TypedDict, Union, cast +from typing import ( + Any, + Callable, + ClassVar, + Dict, + List, + Optional, + Type, + TypedDict, + TypeVar, + Union, + cast, +) FlagValue = Union[bool, str] @@ -162,40 +175,75 @@ class FlagMetadata: """Feature flag metadata returned by the feature flag API. Attributes: - id: Numeric feature flag ID. - payload: Payload configured for the matched flag value, if any. - version: Feature flag version. + id: Numeric feature flag ID, or ``None`` when the record has none. + payload: Payload configured for the matched flag value, if any. For a + number or object value it is the value encoded as JSON, the way + servers before ``/flags?v=4`` sent it. + version: Feature flag version, or ``None`` when the record has none. description: Feature flag description. has_experiment: Whether the flag has a linked experiment. ``None`` when the server does not report the field (older deployments). """ - id: int + id: Optional[int] payload: Optional[str] - version: int + version: Optional[int] description: str has_experiment: Optional[bool] = None + # Work in progress: the ``/flags?v=4`` metadata is private until the typed flag + # value accessors ship. It is kept in instance attributes, not dataclass fields, + # so ``repr``, equality, hashing and ``dataclasses.asdict`` see only the public + # fields; these class attributes are the defaults. ``_config_version`` is the + # configuration format of the flag (``1`` for a record without a typed + # ``value``); the rule fields name the rule that produced the result, and the + # variant fields the assigned variant. + _config_version: ClassVar[Optional[int]] = None + _rule_type: ClassVar[Optional[str]] = None + _rule_id: ClassVar[Optional[str]] = None + _variant_key: ClassVar[Optional[str]] = None + _forced_variant: ClassVar[Optional[bool]] = None @classmethod def from_json(cls, resp: Any) -> Union["FlagMetadata", LegacyFlagMetadata]: + """Parse the metadata of a record without a typed ``value``. + + Such a record comes from a server before ``/flags?v=4``, so only the + fields that response carries are read, and ``config_version`` is ``1``. + """ if not resp: return LegacyFlagMetadata(payload=None) raw_has_experiment = resp.get("has_experiment") - return cls( - id=resp.get("id", 0), + metadata = cls( + id=resp.get("id"), payload=resp.get("payload"), - version=resp.get("version", 0), + version=resp.get("version"), description=resp.get("description", ""), has_experiment=raw_has_experiment if isinstance(raw_has_experiment, bool) else None, ) + return _set_private_fields(metadata, _config_version=1) + + +_T = TypeVar("_T") + + +def _set_private_fields(obj: _T, **fields: Any) -> _T: + """Set the private, work-in-progress attributes of a frozen dataclass.""" + for name, value in fields.items(): + # A value that is already the default is not stored on the instance. + if getattr(obj, name) is not value: + object.__setattr__(obj, name, value) + return obj @dataclass(frozen=True) class FeatureFlag: """Detailed feature flag evaluation returned by the flags API. + ``enabled``, ``variant`` and ``metadata.payload`` are the value rendered the way + responses before ``/flags?v=4`` carried it, which the legacy getters return. + Attributes: key: Feature flag key. enabled: Whether the flag is enabled for the evaluated user or group. @@ -209,29 +257,38 @@ class FeatureFlag: variant: Optional[str] reason: Optional[FlagReason] metadata: Union[FlagMetadata, LegacyFlagMetadata] + # Work in progress: private until the typed flag value accessors ship. Like the + # private ``FlagMetadata`` attributes, these are instance attributes, not + # dataclass fields. ``_value`` is the typed flag value: a boolean, string, + # number, object, or ``None`` when the flag has no value; it defaults to + # ``variant``, or ``enabled`` when there is no variant. ``_failed`` says whether + # the flag failed to evaluate, or its record could not be read. + _value: ClassVar[Any] = None + _failed: ClassVar[bool] = False + # The wire path of the field that made the record unreadable, such as + # ``"metadata.id"``. ``None`` unless the record could not be read; a record the + # server marks failed has ``_failed`` set and this ``None``. + _malformed_field: ClassVar[Optional[str]] = None + + def __post_init__(self) -> None: + object.__setattr__( + self, + "_value", + self.variant if self.variant is not None else self.enabled, + ) def get_value(self) -> FlagValue: return self.variant or self.enabled @classmethod def from_json(cls, resp: Any) -> "FeatureFlag": - reason = None - if resp.get("reason"): - reason = FlagReason.from_json(resp.get("reason")) - - metadata = None - if resp.get("metadata"): - metadata = FlagMetadata.from_json(resp.get("metadata")) - else: - metadata = LegacyFlagMetadata(payload=None) + """Parse one flag record of a ``/flags`` response. - return cls( - key=resp.get("key"), - enabled=resp.get("enabled"), - variant=resp.get("variant"), - reason=reason, - metadata=metadata, - ) + A record with a ``value`` member is a ``/flags?v=4`` record. A record + without one comes from an older server: its value is ``variant``, or + ``enabled`` when there is no variant, and its config version is ``1``. + """ + return _parse_record(cls, resp.get("key"), resp) @classmethod def from_value_and_payload( @@ -249,6 +306,230 @@ def from_value_and_payload( ) +_F = TypeVar("_F", bound=FeatureFlag) + + +class _MalformedRecord(Exception): + """A known field of a flag record has the wrong JSON type.""" + + +def _read_int(value: Any, field: str) -> Optional[int]: + # JSON has one number type, so an integral number such as 2.0 is an integer. + if value is None: + return None + if isinstance(value, int) and not isinstance(value, bool): + return value + if isinstance(value, float) and math.isfinite(value) and value.is_integer(): + return int(value) + raise _MalformedRecord(field) + + +def _read_typed(value: Any, field: str, expected: type) -> Any: + if value is None or isinstance(value, expected): + return value + raise _MalformedRecord(field) + + +def _read_value(value: Any) -> Any: + if value is None or isinstance(value, (bool, str, dict)): + return value + if isinstance(value, int) or (isinstance(value, float) and math.isfinite(value)): + return value + raise _MalformedRecord("value") + + +def _legacy_rendering(value: Any) -> tuple[bool, Optional[str], Optional[str]]: + """Render a typed value as ``(enabled, variant, encoded payload)``. + + A string is the variant, ``None`` is disabled, and a number or object is enabled + with the value as a JSON-encoded payload, as servers before ``/flags?v=4`` sent it. + """ + if value is None or isinstance(value, bool): + return bool(value), None, None + if isinstance(value, str): + return True, value, None + return True, None, json.dumps(value, separators=(",", ":"), ensure_ascii=False) + + +def _parse_typed_record(cls: Type[_F], key: str, resp: Any) -> _F: + """Parse a ``/flags?v=4`` record, which carries a typed ``value``. + + Unknown fields are ignored. A known field with the wrong JSON type fails only + this record, which then reads as disabled. An absent or null field reads as absent, + except ``value``, where null means the flag has no value. + """ + try: + if not isinstance(resp, dict): + raise _MalformedRecord("record") + _read_record_key(resp) + value = _read_value(resp.get("value")) + failed = _read_typed(resp.get("failed"), "failed", bool) is True + + reason = None + raw_reason = _read_typed(resp.get("reason"), "reason", dict) + if raw_reason is not None: + reason = FlagReason( + code=_read_typed(raw_reason.get("code"), "reason.code", str) or "", + condition_index=_read_int( + raw_reason.get("condition_index"), "reason.condition_index" + ), + description=_read_typed( + raw_reason.get("description"), "reason.description", str + ) + or "", + ) + + if failed: + # A failed record has no value, whatever it carries. + value = None + enabled, variant, encoded_payload = _legacy_rendering(value) + + metadata: Union[FlagMetadata, LegacyFlagMetadata] + raw_metadata = _read_typed(resp.get("metadata"), "metadata", dict) + if raw_metadata is None: + metadata = LegacyFlagMetadata(payload=encoded_payload) + else: + metadata = FlagMetadata( + id=_read_int(raw_metadata.get("id"), "metadata.id"), + # A number or object value is its own payload; a payload the record + # also carries is ignored. + payload=encoded_payload + if encoded_payload is not None + else raw_metadata.get("payload"), + version=_read_int(raw_metadata.get("version"), "metadata.version"), + description=_read_typed( + raw_metadata.get("description", ""), "metadata.description", str + ), + has_experiment=_read_typed( + raw_metadata.get("has_experiment"), "metadata.has_experiment", bool + ), + ) + _set_private_fields( + metadata, + _config_version=_read_int( + raw_metadata.get("config_version"), "metadata.config_version" + ), + _rule_type=_read_typed( + raw_metadata.get("rule_type"), "metadata.rule_type", str + ), + _rule_id=_read_typed( + raw_metadata.get("rule_id"), "metadata.rule_id", str + ), + _variant_key=_read_typed( + raw_metadata.get("variant_key"), "metadata.variant_key", str + ), + _forced_variant=_read_typed( + raw_metadata.get("forced_variant"), "metadata.forced_variant", bool + ), + ) + # Only config version 1 records carry an experiment ID. It is checked, + # like every known field, but not kept. + _read_int(raw_metadata.get("experiment_id"), "metadata.experiment_id") + except _MalformedRecord as error: + return _malformed_record(cls, key, str(error)) + except RecursionError: + # A value nested too deeply to encode as a payload fails only this record. + return _malformed_record(cls, key, "value") + + flag = cls( + key=key, + enabled=enabled, + variant=variant, + reason=reason, + metadata=metadata, + ) + return _set_private_fields(flag, _value=value, _failed=failed) + + +def _parse_older_record(cls: Type[_F], key: str, resp: Dict[str, Any]) -> _F: + """Parse a record without a typed ``value``, from a server before ``/flags?v=4``. + + Its value is ``variant``, or ``enabled`` when there is no variant, and its config + version is ``1``. As for a ``/flags?v=4`` record, a known field with the wrong + JSON type fails only this record. + """ + try: + _read_record_key(resp) + _read_typed(resp.get("enabled"), "enabled", bool) + _read_typed(resp.get("variant"), "variant", str) + _read_typed(resp.get("failed"), "failed", bool) + raw_reason = _read_typed(resp.get("reason"), "reason", dict) + if raw_reason: + _read_typed(raw_reason.get("code"), "reason.code", str) + _read_int(raw_reason.get("condition_index"), "reason.condition_index") + _read_typed(raw_reason.get("description"), "reason.description", str) + raw_metadata = _read_typed(resp.get("metadata"), "metadata", dict) + if raw_metadata: + _read_int(raw_metadata.get("id"), "metadata.id") + _read_int(raw_metadata.get("version"), "metadata.version") + _read_typed(raw_metadata.get("description"), "metadata.description", str) + _read_typed( + raw_metadata.get("has_experiment"), "metadata.has_experiment", bool + ) + except _MalformedRecord as error: + return _malformed_record(cls, key, str(error)) + + flag = cls( + key=key, + # An older server always sends ``enabled``; a record without it keeps + # ``None``, as before. + enabled=cast(bool, resp.get("enabled")), + variant=resp.get("variant"), + reason=FlagReason.from_json(raw_reason), + metadata=FlagMetadata.from_json(raw_metadata), + ) + return _set_private_fields(flag, _failed=resp.get("failed") is True) + + +def _read_record_key(resp: Dict[str, Any]) -> None: + # The flag key is the record's map key. A ``key`` member of the wrong type fails + # the record, like any known field; a string that differs is ignored. + _read_typed(resp.get("key"), "key", str) + + +def _malformed_record(cls: Type[_F], key: str, malformed_field: str) -> _F: + """A record with a known field of the wrong JSON type: failed, and disabled.""" + flag = cls( + key=key, + enabled=False, + variant=None, + reason=None, + metadata=LegacyFlagMetadata(payload=None), + ) + return _set_private_fields( + flag, _value=None, _failed=True, _malformed_field=malformed_field + ) + + +def _parse_record(cls: Type[_F], key: str, resp: Any) -> _F: + """Parse one record of a ``/flags`` response, stored under ``key``.""" + if isinstance(resp, dict) and "value" not in resp: + return _parse_older_record(cls, key, resp) + return _parse_typed_record(cls, key, resp) + + +def _has_untyped_flag_records(resp: Any) -> bool: + """Whether a raw ``/flags`` response has a record without a typed ``value``. + + Such a record comes from a server that does not send ``/flags?v=4`` records. + """ + if not isinstance(resp, dict): + return False + flags = resp.get("flags") + if isinstance(flags, dict): + return any( + isinstance(record, dict) and "value" not in record + for record in flags.values() + ) + return bool(resp.get("featureFlags")) + + +_UNTYPED_FLAGS_RESPONSE_MESSAGE = ( + "[FEATURE FLAGS] The /flags response has no typed flag values, so the server" + " may not support /flags?v=4. Every flag is read as config version 1." +) + + class FlagsResponse(TypedDict, total=False): """Normalized response from the PostHog feature flags API.""" @@ -366,7 +647,7 @@ def normalize_flags_response(resp: Any) -> FlagsResponse: Normalize the response from the flags API endpoint into a FlagsResponse. Args: - resp: A v1 or v2 response from the flags API endpoint. + resp: A v1, v2 or v4 response from the flags API endpoint. Returns: A FlagsResponse containing feature flags and their details. @@ -379,8 +660,7 @@ def normalize_flags_response(resp: Any) -> FlagsResponse: for key, value in flags.items(): if isinstance(value, FeatureFlag): continue - value["key"] = key - flags[key] = FeatureFlag.from_json(value) + flags[key] = _parse_record(FeatureFlag, key, value) else: # Handle legacy format featureFlags = resp.get("featureFlags", {}) diff --git a/references/public_api_snapshot.txt b/references/public_api_snapshot.txt index 2fc71d7c9..c6d71aef6 100644 --- a/references/public_api_snapshot.txt +++ b/references/public_api_snapshot.txt @@ -1174,9 +1174,9 @@ attribute posthog.types.FeatureFlagResult.reason: Optional[str] attribute posthog.types.FeatureFlagResult.variant: Optional[str] attribute posthog.types.FlagMetadata.description: str attribute posthog.types.FlagMetadata.has_experiment: Optional[bool] = None -attribute posthog.types.FlagMetadata.id: int +attribute posthog.types.FlagMetadata.id: Optional[int] attribute posthog.types.FlagMetadata.payload: Optional[str] -attribute posthog.types.FlagMetadata.version: int +attribute posthog.types.FlagMetadata.version: Optional[int] attribute posthog.types.FlagReason.code: str attribute posthog.types.FlagReason.condition_index: Optional[int] attribute posthog.types.FlagReason.description: str @@ -1370,7 +1370,7 @@ class posthog.types.FeatureFlag(key: str, enabled: bool, variant: Optional[str], class posthog.types.FeatureFlagError class posthog.types.FeatureFlagEvaluationRuntime class posthog.types.FeatureFlagResult(key: str, enabled: bool, variant: Optional[str], payload: Optional[Any], reason: Optional[str]) -class posthog.types.FlagMetadata(id: int, payload: Optional[str], version: int, description: str, has_experiment: Optional[bool] = None) +class posthog.types.FlagMetadata(id: Optional[int], payload: Optional[str], version: Optional[int], description: str, has_experiment: Optional[bool] = None) class posthog.types.FlagReason(code: str, condition_index: Optional[int], description: str) class posthog.types.FlagsAndPayloads class posthog.types.FlagsResponse diff --git a/sdk_compliance_adapter/adapter.py b/sdk_compliance_adapter/adapter.py index 31f5a6a37..84e6df4cb 100644 --- a/sdk_compliance_adapter/adapter.py +++ b/sdk_compliance_adapter/adapter.py @@ -318,6 +318,8 @@ def health(): "fail_open", "encoding_" + compression_name(), "feature_flags_local_evaluation_v1", + # Remote /flags requests send ?v=4 and read the v4 flag record. + "flags_response_v4", ] return jsonify( { diff --git a/sdk_compliance_adapter/test_adapter.py b/sdk_compliance_adapter/test_adapter.py index ad2e550bc..df649f509 100644 --- a/sdk_compliance_adapter/test_adapter.py +++ b/sdk_compliance_adapter/test_adapter.py @@ -97,6 +97,7 @@ def test_health_and_init_use_one_codec(adapter, monkeypatch, env_value, codec): "disable_geoip", "historical_migration", } <= set(capabilities) + assert "flags_response_v4" in capabilities assert "capture_v0" not in capabilities assert "capture_ai_v0" not in capabilities assert [c for c in capabilities if c.startswith("encoding_")] == [ From 190e1bd95e8342447b47cd9f0576cb60b39c60e7 Mon Sep 17 00:00:00 2001 From: "releaser-posthog-python[bot]" <262414804+releaser-posthog-python[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:02:17 +0000 Subject: [PATCH 2/7] chore: Release v7.83.0 [skip ci] (cherry picked from commit 43c7545717f0941c700f13679e365433ff4d5995) --- .sampo/changesets/flags-v4-response.md | 5 ----- openfeature-provider/CHANGELOG.md | 6 ++++++ openfeature-provider/pyproject.toml | 2 +- posthog/CHANGELOG.md | 6 ++++++ posthog/version.py | 2 +- pyproject.toml | 2 +- uv.lock | 4 ++-- 7 files changed, 17 insertions(+), 10 deletions(-) delete mode 100644 .sampo/changesets/flags-v4-response.md diff --git a/.sampo/changesets/flags-v4-response.md b/.sampo/changesets/flags-v4-response.md deleted file mode 100644 index 829f5b02f..000000000 --- a/.sampo/changesets/flags-v4-response.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -pypi/posthog: minor ---- - -Request `/flags?v=4` and read each flag's typed value, reason and metadata. They stay internal for now: this release adds no public methods, fields or types for them, and `repr`, equality and `dataclasses.asdict` of a flag show only its existing fields. Servers that still send the older response keep working, and a flag record with a field of the wrong JSON type fails only that flag. For valid flag records, existing getters, callbacks and `$feature_flag_called` properties return the same values as before: a number or object value reads as `True`, with the value as its payload. Two things can differ: the human-readable reason text of a config version 2 flag is the one the server sends at v4, and a record with a known field of the wrong JSON type (for example a string `reason`) now reads as a failed flag instead of being read anyway or failing the whole response. The SDK logs once per client when a server sends a response without typed values. `FlagMetadata.id` and `FlagMetadata.version` are `None` when a record has none, instead of `0`, so their type is now `Optional[int]`. diff --git a/openfeature-provider/CHANGELOG.md b/openfeature-provider/CHANGELOG.md index 855f7952f..1c1793814 100644 --- a/openfeature-provider/CHANGELOG.md +++ b/openfeature-provider/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to `openfeature-provider-posthog` are documented here. This file is maintained by [Sampo](https://github.com/bruits/sampo) from changesets in `.sampo/changesets/` that target `pypi/openfeature-provider-posthog`. +## 0.1.141 — 2026-11-03 + +### Patch changes + +- Updated dependencies: posthog@7.83.0 + ## 0.1.140 — 2026-11-02 ### Patch changes diff --git a/openfeature-provider/pyproject.toml b/openfeature-provider/pyproject.toml index 017d0d00e..37c1ada54 100644 --- a/openfeature-provider/pyproject.toml +++ b/openfeature-provider/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openfeature-provider-posthog" -version = "0.1.140" +version = "0.1.141" description = "Official PostHog provider for the OpenFeature Python SDK." readme = "README.md" authors = [{ name = "PostHog", email = "engineering@posthog.com" }] diff --git a/posthog/CHANGELOG.md b/posthog/CHANGELOG.md index 333c2e0d7..631a7caac 100644 --- a/posthog/CHANGELOG.md +++ b/posthog/CHANGELOG.md @@ -1,5 +1,11 @@ # posthog +## 7.83.0 — 2026-11-03 + +### Minor changes + +- [e094dc5](https://github.com/posthog/posthog-python/commit/e094dc59522b8d7d7b68a74cac4259a61daef60f) Request `/flags?v=4` and read each flag's typed value, reason and metadata. They stay internal for now: this release adds no public methods, fields or types for them, and `repr`, equality and `dataclasses.asdict` of a flag show only its existing fields. Servers that still send the older response keep working, and a flag record with a field of the wrong JSON type fails only that flag. For valid flag records, existing getters, callbacks and `$feature_flag_called` properties return the same values as before: a number or object value reads as `True`, with the value as its payload. Two things can differ: the human-readable reason text of a config version 2 flag is the one the server sends at v4, and a record with a known field of the wrong JSON type (for example a string `reason`) now reads as a failed flag instead of being read anyway or failing the whole response. The SDK logs once per client when a server sends a response without typed values. `FlagMetadata.id` and `FlagMetadata.version` are `None` when a record has none, instead of `0`, so their type is now `Optional[int]`. — Thanks @andehen! + ## 7.82.2 — 2026-11-02 ### Patch changes diff --git a/posthog/version.py b/posthog/version.py index 798aad33f..36d7c0455 100644 --- a/posthog/version.py +++ b/posthog/version.py @@ -1 +1 @@ -VERSION = "7.82.2" +VERSION = "7.83.0" diff --git a/pyproject.toml b/pyproject.toml index 01ada4ea8..f647d6d68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "posthog" -version = "7.82.2" +version = "7.83.0" description = "Integrate PostHog into any python application." authors = [{ name = "PostHog", email = "engineering@posthog.com" }] maintainers = [{ name = "PostHog", email = "engineering@posthog.com" }] diff --git a/uv.lock b/uv.lock index b4ff2dd88..8655cb212 100644 --- a/uv.lock +++ b/uv.lock @@ -2660,7 +2660,7 @@ wheels = [ [[package]] name = "openfeature-provider-posthog" -version = "0.1.140" +version = "0.1.141" source = { editable = "openfeature-provider" } dependencies = [ { name = "openfeature-sdk" }, @@ -3197,7 +3197,7 @@ wheels = [ [[package]] name = "posthog" -version = "7.82.2" +version = "7.83.0" source = { editable = "." } dependencies = [ { name = "distro" }, From 44a12d639b59605af05be89b84fb5594f735dd86 Mon Sep 17 00:00:00 2001 From: "releaser-posthog-python[bot]" <262414804+releaser-posthog-python[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:02:21 +0000 Subject: [PATCH 3/7] Update generated references (cherry picked from commit 314fad070507fce6a95ebb58d8a550f18a1a34ac) --- .../posthog-python-references-7.83.0.json | 3566 +++++++++++++++++ .../posthog-python-references-latest.json | 6 +- 2 files changed, 3569 insertions(+), 3 deletions(-) create mode 100644 references/posthog-python-references-7.83.0.json diff --git a/references/posthog-python-references-7.83.0.json b/references/posthog-python-references-7.83.0.json new file mode 100644 index 000000000..ea89d2637 --- /dev/null +++ b/references/posthog-python-references-7.83.0.json @@ -0,0 +1,3566 @@ +{ + "id": "posthog-python", + "hogRef": "0.3", + "info": { + "version": "7.83.0", + "id": "posthog-python", + "title": "PostHog Python SDK", + "description": "Integrate PostHog into any python application.", + "slugPrefix": "posthog-python", + "specUrl": "https://github.com/PostHog/posthog-python" + }, + "types": [ + { + "id": "FeatureFlag", + "name": "FeatureFlag", + "path": "posthog.types.FeatureFlag", + "properties": [ + { + "name": "key", + "type": "str", + "description": "Field: key" + }, + { + "name": "enabled", + "type": "bool", + "description": "Field: enabled" + }, + { + "name": "variant", + "type": "Optional[str]", + "description": "Field: variant" + }, + { + "name": "reason", + "type": "Optional[FlagReason]", + "description": "Field: reason" + }, + { + "name": "metadata", + "type": "Union[FlagMetadata, LegacyFlagMetadata]", + "description": "Field: metadata" + } + ], + "example": "" + }, + { + "id": "FeatureFlagResult", + "name": "FeatureFlagResult", + "path": "posthog.types.FeatureFlagResult", + "properties": [ + { + "name": "key", + "type": "str", + "description": "Field: key" + }, + { + "name": "enabled", + "type": "bool", + "description": "Field: enabled" + }, + { + "name": "variant", + "type": "Optional[str]", + "description": "Field: variant" + }, + { + "name": "payload", + "type": "Optional[Any]", + "description": "Field: payload" + }, + { + "name": "reason", + "type": "Optional[str]", + "description": "Field: reason" + } + ], + "example": "" + }, + { + "id": "FlagMetadata", + "name": "FlagMetadata", + "path": "posthog.types.FlagMetadata", + "properties": [ + { + "name": "id", + "type": "Optional[int]", + "description": "Field: id" + }, + { + "name": "payload", + "type": "Optional[str]", + "description": "Field: payload" + }, + { + "name": "version", + "type": "Optional[int]", + "description": "Field: version" + }, + { + "name": "description", + "type": "str", + "description": "Field: description" + }, + { + "name": "has_experiment", + "type": "Optional[bool]", + "description": "Field: has_experiment" + } + ], + "example": "" + }, + { + "id": "FlagReason", + "name": "FlagReason", + "path": "posthog.types.FlagReason", + "properties": [ + { + "name": "code", + "type": "str", + "description": "Field: code" + }, + { + "name": "condition_index", + "type": "Optional[int]", + "description": "Field: condition_index" + }, + { + "name": "description", + "type": "str", + "description": "Field: description" + } + ], + "example": "" + }, + { + "id": "FlagsAndPayloads", + "name": "FlagsAndPayloads", + "path": "posthog.types.FlagsAndPayloads", + "properties": [ + { + "name": "featureFlags", + "type": "Optional[dict[str, Union[bool, str]]]", + "description": "Field: featureFlags" + }, + { + "name": "featureFlagPayloads", + "type": "Optional[dict[str, Any]]", + "description": "Field: featureFlagPayloads" + } + ], + "example": "" + }, + { + "id": "FlagsResponse", + "name": "FlagsResponse", + "path": "posthog.types.FlagsResponse", + "properties": [ + { + "name": "flags", + "type": "dict[str, FeatureFlag]", + "description": "Field: flags" + }, + { + "name": "errorsWhileComputingFlags", + "type": "bool", + "description": "Field: errorsWhileComputingFlags" + }, + { + "name": "requestId", + "type": "str", + "description": "Field: requestId" + }, + { + "name": "quotaLimit", + "type": "Optional[list[str]]", + "description": "Field: quotaLimit" + }, + { + "name": "evaluatedAt", + "type": "Optional[int]", + "description": "Field: evaluatedAt" + }, + { + "name": "minimalFlagCalledEvents", + "type": "bool", + "description": "Field: minimalFlagCalledEvents" + } + ], + "example": "" + }, + { + "id": "LegacyFlagMetadata", + "name": "LegacyFlagMetadata", + "path": "posthog.types.LegacyFlagMetadata", + "properties": [ + { + "name": "payload", + "type": "Any", + "description": "Field: payload" + } + ], + "example": "" + }, + { + "id": "SendFeatureFlagsOptions", + "name": "SendFeatureFlagsOptions", + "path": "posthog.types.SendFeatureFlagsOptions", + "properties": [ + { + "name": "should_send", + "type": "bool", + "description": "Field: should_send" + }, + { + "name": "only_evaluate_locally", + "type": "Optional[bool]", + "description": "Field: only_evaluate_locally" + }, + { + "name": "person_properties", + "type": "Optional[dict[str, Any]]", + "description": "Field: person_properties" + }, + { + "name": "group_properties", + "type": "Optional[dict[str, dict[str, Any]]]", + "description": "Field: group_properties" + }, + { + "name": "flag_keys_filter", + "type": "Optional[list[str]]", + "description": "Field: flag_keys_filter" + } + ], + "example": "" + }, + { + "id": "OptionalCaptureArgs", + "name": "OptionalCaptureArgs", + "path": "posthog.args.OptionalCaptureArgs", + "properties": [ + { + "name": "distinct_id", + "type": "NotRequired[Union[Number, str, UUID, int, any]]", + "description": "Field: distinct_id" + }, + { + "name": "properties", + "type": "NotRequired[Optional[dict[str, Any]]]", + "description": "Field: properties" + }, + { + "name": "timestamp", + "type": "NotRequired[Union[datetime, str, any]]", + "description": "Field: timestamp" + }, + { + "name": "uuid", + "type": "NotRequired[Union[str, UUID, any]]", + "description": "Field: uuid" + }, + { + "name": "groups", + "type": "NotRequired[Optional[dict[str, str]]]", + "description": "Field: groups" + }, + { + "name": "flags", + "type": "NotRequired[Optional[ForwardRef('FeatureFlagEvaluations')]]", + "description": "Field: flags" + }, + { + "name": "send_feature_flags", + "type": "NotRequired[Union[bool, SendFeatureFlagsOptions, any]]", + "description": "Field: send_feature_flags" + }, + { + "name": "disable_geoip", + "type": "NotRequired[Optional[bool]]", + "description": "Field: disable_geoip" + }, + { + "name": "_property_allowlist", + "type": "NotRequired[Optional[frozenset[str]]]", + "description": "Field: _property_allowlist" + } + ], + "example": "" + }, + { + "id": "OptionalSetArgs", + "name": "OptionalSetArgs", + "path": "posthog.args.OptionalSetArgs", + "properties": [ + { + "name": "distinct_id", + "type": "NotRequired[Union[Number, str, UUID, int, any]]", + "description": "Field: distinct_id" + }, + { + "name": "properties", + "type": "NotRequired[Optional[dict[str, Any]]]", + "description": "Field: properties" + }, + { + "name": "timestamp", + "type": "NotRequired[Union[datetime, str, any]]", + "description": "Field: timestamp" + }, + { + "name": "uuid", + "type": "NotRequired[Union[str, UUID, any]]", + "description": "Field: uuid" + }, + { + "name": "disable_geoip", + "type": "NotRequired[Optional[bool]]", + "description": "Field: disable_geoip" + } + ], + "example": "" + }, + { + "id": "SendFeatureFlagsOptions", + "name": "SendFeatureFlagsOptions", + "path": "posthog.types.SendFeatureFlagsOptions", + "properties": [ + { + "name": "should_send", + "type": "bool", + "description": "Field: should_send" + }, + { + "name": "only_evaluate_locally", + "type": "Optional[bool]", + "description": "Field: only_evaluate_locally" + }, + { + "name": "person_properties", + "type": "Optional[dict[str, Any]]", + "description": "Field: person_properties" + }, + { + "name": "group_properties", + "type": "Optional[dict[str, dict[str, Any]]]", + "description": "Field: group_properties" + }, + { + "name": "flag_keys_filter", + "type": "Optional[list[str]]", + "description": "Field: flag_keys_filter" + } + ], + "example": "" + } + ], + "classes": [ + { + "id": "PostHog", + "title": "PostHog", + "description": "This is the SDK reference for the PostHog 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. For long-running applications, create one client during application startup and reuse it for the lifetime of the process. This keeps background queues predictable and makes shutdown flushing straightforward. Multiple clients are still supported for intentional multi-project or multi-host setups.", + "functions": [ + { + "id": "__init__", + "title": "Client", + "description": "Initialize a new PostHog client instance.", + "details": "", + "category": "Initialization", + "params": [ + { + "name": "project_api_key", + "description": "PostHog project API key/token.", + "isOptional": true, + "type": "str" + }, + { + "name": "host", + "description": "PostHog host. Defaults to the US ingestion endpoint when not set. App hosts such as ``https://us.posthog.com`` are mapped to the corresponding ingestion host.", + "isOptional": false, + "type": "any" + }, + { + "name": "debug", + "description": "Enable verbose SDK logging and re-raise errors from public API methods.", + "isOptional": false, + "type": "bool" + }, + { + "name": "max_queue_size", + "description": "Maximum number of events buffered before upload.", + "isOptional": false, + "type": "int" + }, + { + "name": "send", + "description": "If False, queueing succeeds but events are not sent.", + "isOptional": false, + "type": "bool" + }, + { + "name": "on_error", + "description": "Optional callback invoked by background consumers when an upload fails. Keep it short and non-blocking. Calling lifecycle methods directly is safe and deferred, but do not start another thread or task that calls ``flush()``, ``join()``, or ``shutdown()`` and then wait for it from the callback.", + "isOptional": false, + "type": "any" + }, + { + "name": "flush_at", + "description": "Number of queued events that triggers a batch upload.", + "isOptional": false, + "type": "int" + }, + { + "name": "flush_interval", + "description": "Maximum seconds a background consumer waits before flushing a partial batch.", + "isOptional": false, + "type": "float" + }, + { + "name": "gzip", + "description": "Whether to gzip event upload payloads.", + "isOptional": false, + "type": "bool" + }, + { + "name": "max_retries", + "description": "Number of upload retries. Values below 0 are treated as 0.", + "isOptional": false, + "type": "int" + }, + { + "name": "sync_mode", + "description": "If True, send each event synchronously instead of using background worker threads. This blocks the calling thread; in asyncio applications such as FastAPI, use ``AsyncPosthog`` instead.", + "isOptional": false, + "type": "bool" + }, + { + "name": "timeout", + "description": "HTTP request timeout in seconds for event uploads.", + "isOptional": false, + "type": "float" + }, + { + "name": "thread", + "description": "Number of background consumer threads.", + "isOptional": false, + "type": "int" + }, + { + "name": "poll_interval", + "description": "Seconds between local feature flag definition refreshes.", + "isOptional": false, + "type": "int" + }, + { + "name": "personal_api_key", + "description": "Deprecated alias for ``secret_key``. Still honored for backwards compatibility; prefer ``secret_key``, which also accepts a Project Secret API Key.", + "isOptional": false, + "type": "any" + }, + { + "name": "disabled", + "description": "If True, disable captures and API requests. Useful in tests.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable server-side GeoIP enrichment. Defaults to True.", + "isOptional": false, + "type": "bool" + }, + { + "name": "is_server", + "description": "Whether events are emitted from a server-side runtime. Defaults to True; set to False when using the SDK as a client/CLI so the device OS is attributed to the person normally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "historical_migration", + "description": "Mark events as historical migration imports.", + "isOptional": false, + "type": "bool" + }, + { + "name": "feature_flags_request_timeout_seconds", + "description": "Timeout in seconds for feature flag and remote config requests.", + "isOptional": false, + "type": "float" + }, + { + "name": "feature_flags_request_max_retries", + "description": "Number of retries for feature flag requests after network, transport, or timeout failures. Defaults to 1. Set to 0 to disable retries.", + "isOptional": false, + "type": "int" + }, + { + "name": "super_properties", + "description": "Properties merged into every captured event.", + "isOptional": false, + "type": "any" + }, + { + "name": "enable_exception_autocapture", + "description": "Automatically capture uncaught exceptions.", + "isOptional": false, + "type": "bool" + }, + { + "name": "log_captured_exceptions", + "description": "Also log exceptions captured by error tracking.", + "isOptional": false, + "type": "bool" + }, + { + "name": "project_root", + "description": "Root path used to determine in-app stack frames for captured exceptions. Defaults to the current working directory.", + "isOptional": false, + "type": "any" + }, + { + "name": "privacy_mode", + "description": "For AI observability, capture usage metadata without prompt inputs or outputs.", + "isOptional": false, + "type": "bool" + }, + { + "name": "before_send", + "description": "Optional callback that can modify or drop events before upload. Return ``None`` to drop an event.", + "isOptional": false, + "type": "any" + }, + { + "name": "flag_fallback_cache_url", + "description": "Optional feature flag fallback cache URL, such as ``memory://local/?ttl=300&size=10000`` or a Redis URL.", + "isOptional": false, + "type": "any" + }, + { + "name": "enable_local_evaluation", + "description": "Whether to poll feature flag definitions for local evaluation when a personal API key is configured.", + "isOptional": false, + "type": "bool" + }, + { + "name": "flag_definition_cache_provider", + "description": "Optional external cache provider for sharing feature flag definitions across workers.", + "isOptional": true, + "type": "FlagDefinitionCacheProvider" + }, + { + "name": "capture_exception_code_variables", + "description": "Capture local variable values on exception stack frames.", + "isOptional": false, + "type": "bool" + }, + { + "name": "code_variables_mask_patterns", + "description": "Variable-name patterns to mask when capturing code variables.", + "isOptional": false, + "type": "any" + }, + { + "name": "code_variables_ignore_patterns", + "description": "Variable-name patterns to omit when capturing code variables.", + "isOptional": false, + "type": "any" + }, + { + "name": "code_variables_mask_url_credentials", + "description": "Scrub credentials embedded in URLs/DSNs (e.g. ``user:pass@host``) from captured code variables, regardless of the surrounding variable name. Defaults to True.", + "isOptional": false, + "type": "any" + }, + { + "name": "code_variables_detect_secrets", + "description": "Last-resort entropy-based detection that redacts high-entropy secret-looking values (API keys, tokens, strong passwords) sitting in innocuously-named variables, after the name and URL checks. Skips structured ids (UUIDs, ObjectIds, hashes). Defaults to True.", + "isOptional": false, + "type": "any" + }, + { + "name": "in_app_modules", + "description": "Module/package prefixes treated as in-app frames in captured exceptions.", + "isOptional": false, + "type": "UnionType[list[str], any]" + }, + { + "name": "enable_exception_autocapture_rate_limiting", + "description": "Rate limit autocaptured exceptions client-side with a token bucket per exception type. Disabled by default.", + "isOptional": false, + "type": "bool" + }, + { + "name": "exception_autocapture_bucket_size", + "description": "Maximum burst of autocaptured exceptions allowed per exception type (token bucket size, clamped to 0-100).", + "isOptional": false, + "type": "int" + }, + { + "name": "exception_autocapture_refill_rate", + "description": "Tokens restored per refill interval for each exception type's bucket.", + "isOptional": false, + "type": "int" + }, + { + "name": "exception_autocapture_refill_interval_seconds", + "description": "Seconds between token refills for autocaptured exception rate limiting.", + "isOptional": false, + "type": "int" + }, + { + "name": "capture_mode", + "description": "Capture wire protocol to use. Defaults to ``CaptureMode.V0`` (legacy ``/batch/``). Set ``CaptureMode.V1`` (or pass the string ``\"v1\"``) to opt into ``/i/v1/analytics/events``. When omitted, the ``POSTHOG_CAPTURE_MODE`` env var is consulted, then ``V0``.", + "isOptional": false, + "type": "CaptureMode" + }, + { + "name": "capture_compression", + "description": "Request-body compression for capture-v1 uploads (ignored in V0, which uses ``gzip``). ``CaptureCompression.GZIP`` or ``DEFLATE`` (or the strings ``\"gzip\"``/``\"deflate\"``). When omitted, the ``POSTHOG_CAPTURE_COMPRESSION`` env var is consulted, then the legacy ``gzip`` flag, then no compression.", + "isOptional": false, + "type": "CaptureCompression" + }, + { + "name": "secret_key", + "description": "A Personal API Key or Project Secret API Key, used to authenticate local feature flag evaluation, remote config payloads, and decrypted flag payloads. Example:: posthog.Client(project_api_key, secret_key=\"phx_...\")", + "isOptional": false, + "type": "any" + }, + { + "name": "metrics", + "description": "", + "isOptional": true, + "type": "dict" + }, + { + "name": "enable_full_ai_capture", + "description": "Route PostHog AI wrapper events through the dedicated AI capture endpoint and capture full AI content: skips string truncation and passes media (base64/data URIs) through unredacted. ``privacy_mode`` always wins. Defaults to False.", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_trace_context", + "description": "When OpenTelemetry is installed and a valid span is active at capture time, add its trace and span IDs as ``$trace_id`` and ``$span_id`` properties to events captured with ``capture()`` and ``capture_ai()``, so they can be correlated with backend traces. Explicit ``$trace_id``/``$span_id`` values passed in ``properties`` win. Exception events (``capture_exception``) always attach these IDs regardless of this setting. Defaults to False.", + "isOptional": false, + "type": "bool" + }, + { + "name": "_use_ai_lane", + "description": "", + "isOptional": false, + "type": "bool" + }, + { + "name": "_enable_multimodal_capture", + "description": "", + "isOptional": false, + "type": "bool" + }, + { + "name": "traces", + "description": "Config dict for distributed tracing: ``service_name``, ``service_version``, ``environment``, ``resource_attributes``, ``flush_interval`` (5 s), ``max_queue_size`` (2048), ``max_export_batch_size`` (512), ``max_live_spans`` (10000), ``max_span_age`` (3600 s), ``max_attributes_per_span`` (128), ``max_events_per_span`` (128), ``max_attribute_value_length`` (8192). ``before_span_send`` is a callable, or a list run in order, that receives each finished span as a dict (``trace_id``, ``span_id`` and ``parent_span_id`` are read-only) and returns it, edited, or ``None`` to drop it; a hook that raises drops the span. Tracing is off until this is provided. Spans export on a background timer even with ``sync_mode``; serverless handlers should call ``flush()`` before returning. Defaults to None.", + "isOptional": true, + "type": "dict" + }, + { + "name": "app_host", + "description": "Your PostHog app host, such as ``https://posthog.example.com``. ``emails`` sends there. Needed only when ``host`` is not PostHog Cloud, for example a reverse proxy, which must not receive the secret key.", + "isOptional": true, + "type": "str" + }, + { + "name": "email_request_timeout_seconds", + "description": "Timeout in seconds for each email send request. Defaults to 15.", + "isOptional": false, + "type": "float" + }, + { + "name": "email_request_max_retries", + "description": "Number of retries for an email send after a network error, a rate limit, or an unavailable service. Defaults to 2. Set to 0 when a job runner already retries the send.", + "isOptional": false, + "type": "int" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import Posthog\n\nposthog = Posthog('', host='')" + } + ] + }, + { + "id": "alias", + "title": "alias", + "description": "Create an alias between two distinct IDs.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "previous_id", + "description": "The previous distinct ID. Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "Number" + }, + { + "name": "distinct_id", + "description": "The new distinct ID to alias to. Falls back to the context distinct ID; the call is dropped with a warning if neither is available.", + "isOptional": true, + "type": "str" + }, + { + "name": "timestamp", + "description": "The timestamp of the event. UTC is preferred; non-UTC datetimes and parseable ISO timestamp strings are converted to UTC.", + "isOptional": false, + "type": "datetime" + }, + { + "name": "uuid", + "description": "A unique identifier for the event. If provided, it must be a valid UUID string or uuid.UUID instance; invalid values are ignored and replaced with a newly generated UUID.", + "isOptional": true, + "type": "str" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this event.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.alias(previous_id='distinct_id', distinct_id='alias_id')" + } + ] + }, + { + "id": "capture", + "title": "capture", + "description": "Captures an event manually. [Learn about capture best practices](https://posthog.com/docs/product-analytics/capture-events)", + "details": "", + "category": "Capture", + "params": [ + { + "name": "event", + "description": "The event name to capture.", + "isOptional": true, + "type": "str" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Anonymous event", + "code": "# Anonymous event\nposthog.capture('some-anon-event')" + }, + { + "id": "example_2", + "name": "Context usage", + "code": "# Context usage\nfrom posthog import identify_context, new_context\nwith new_context():\n identify_context('distinct_id_of_the_user')\n posthog.capture('user_signed_up')\n posthog.capture('user_logged_in')\n posthog.capture('some-custom-action', distinct_id='distinct_id_of_the_user')" + }, + { + "id": "example_3", + "name": "Set event properties", + "code": "# Set event properties\nposthog.capture(\n \"user_signed_up\",\n distinct_id=\"distinct_id_of_the_user\",\n properties={\n \"login_type\": \"email\",\n \"is_free_trial\": \"true\"\n }\n)" + }, + { + "id": "example_4", + "name": "Page view event", + "code": "# Page view event\nposthog.capture('$pageview', distinct_id=\"distinct_id_of_the_user\", properties={'$current_url': 'https://example.com'})" + } + ] + }, + { + "id": "capture_ai", + "title": "capture_ai", + "description": "Capture an AI event on the dedicated AI capture endpoint. Beta: the signature is stable; operational limits (per-event size cap, batching, endpoint) may change without notice. Takes the same arguments and returns the same value as `capture()`: the event UUID, or None when the event was not admitted (disabled client, or dropped by `before_send`). The event is queued on an isolated AI lane with its own consumer pool and a higher per-event size cap, posting to the dedicated AI ingestion endpoint. The payload is sent as given \u2014 no redaction or truncation is applied here.", + "details": "", + "category": "Capture", + "params": [ + { + "name": "event", + "description": "", + "isOptional": true, + "type": "str" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + } + }, + { + "id": "capture_exception", + "title": "capture_exception", + "description": "Capture an exception for error tracking. When OpenTelemetry is installed and a valid span is active, its trace and span IDs are added as ``$trace_id`` and ``$span_id`` event properties.", + "details": "", + "category": "Error Tracking", + "params": [ + { + "name": "exception", + "description": "The exception to capture.", + "isOptional": true, + "type": "BaseException" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "try:\n # Some code that might fail\n pass\nexcept Exception as e:\n posthog.capture_exception(e, 'user_distinct_id', properties=additional_properties)" + } + ] + }, + { + "id": "evaluate_flags", + "title": "evaluate_flags", + "description": "Evaluate all feature flags for a user in a single call and return a :class:`FeatureFlagEvaluations` snapshot. Branch on ``.is_enabled()`` / ``.get_flag()`` and pass the same snapshot to :meth:`capture` via the ``flags`` option so events carry the exact flag values the code branched on. Prefer this over repeated ``get_feature_flag()`` calls and over ``capture(send_feature_flags=True)`` \u2014 it consolidates flag evaluation into a single ``/flags`` request per incoming request. Local evaluation is transparent: when the poller resolves a flag, the snapshot's ``$feature_flag_called`` events are tagged ``locally_evaluated=True`` and reason ``\"Evaluated locally\"``.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The user's distinct ID. If ``None``, falls back to the context distinct_id. If still unresolvable, returns an empty snapshot.", + "isOptional": false, + "type": "Number" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "If True, never fall back to remote evaluation \u2014 flags that can't be evaluated locally are omitted from the snapshot and listed with a reason in its ``unresolved_flags``.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys", + "description": "Optional list that scopes local evaluation, the underlying ``/flags`` request, and the returned snapshot. When omitted or ``None``, all flags are evaluated. An empty list returns an empty snapshot without evaluating flags. A requested key absent from loaded local definitions is included in one remote fallback per ``evaluate_flags`` call unless ``only_evaluate_locally`` is True. If the server also does not know the key, it is omitted from the snapshot.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "Optional device ID override. If not provided, falls back to the context device_id (which may be set via tracing headers). Used by experience-continuity flags to match users across distinct_id changes.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FeatureFlagEvaluations" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "flags = posthog.evaluate_flags(\n \"user_123\",\n person_properties={\"plan\": \"enterprise\"},\n)\nif flags.is_enabled(\"new-dashboard\"):\n render_new_dashboard()\nposthog.capture(\"page_viewed\", distinct_id=\"user_123\", flags=flags)" + } + ] + }, + { + "id": "feature_enabled", + "title": "feature_enabled", + "description": "Check if a feature flag is enabled for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[bool]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user')\nif is_my_flag_enabled:\n # Do something differently for this user\n # Optional: fetch the payload\n matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "feature_flag_definitions", + "title": "feature_flag_definitions", + "description": "Return feature flag definitions loaded for local evaluation. Returns: The currently loaded feature flag definitions, or ``None`` before local evaluation has loaded definitions.", + "details": "", + "category": "Feature flags", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "flush", + "title": "flush", + "description": "Attempt immediate delivery of queued events, ended spans and autocaptured metrics, keeping the client reusable. For a client shared across requests or serverless invocations (including the module-level client), call ``flush()`` before a short-lived invocation returns. Use ``shutdown()`` instead only for final process cleanup or when disposing of a client that will not be reused. Flush does not stop the background workers or feature flag poller, and does not end open spans. Returning does not guarantee server receipt: pending work can remain after a timeout or span export failure, and failed events may be dropped by the consumers. Calls made directly from SDK callbacks such as ``on_error`` are deferred rather than blocking the worker that invoked the callback.", + "details": "", + "category": null, + "params": [ + { + "name": "timeout_seconds", + "description": "Seconds to wait for event queues to drain, shared across event lanes. Defaults to 10 seconds. Pass ``None`` to wait without a time limit; this does not guarantee successful delivery. Queued spans flush concurrently. The span exporter waits up to this many seconds for an in-flight flush, then uses a fresh drain budget once it acquires the lock. If the lock wait times out, the in-flight flush is left to finish. At least one span request is attempted after acquiring the lock, even with no budget left; retriable failures are retried after backoff while budget remains, with one last attempt at the deadline. Each span request is bounded by the client's ``timeout`` in seconds, and the last request is not cut short. This is therefore not a strict wall-clock limit for the whole call. With ``None``, a retriable span failure is left for a later flush rather than retried within this call.", + "isOptional": true, + "type": "float" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.capture('event_name', distinct_id='user_id')\n# Before a serverless invocation returns; keep the shared client open.\nposthog.flush(timeout_seconds=10)" + } + ] + }, + { + "id": "get_active_span", + "title": "get_active_span", + "description": "The span that is active in the current context, or ``None``. Alpha. Only entering a span (``with posthog.start_span(...) as span:``) makes it active; a span started manually is not. Use it to propagate the trace to the next service: ``span.traceparent()`` is the header value.", + "details": "", + "category": "Tracing", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[Span]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "span = posthog.get_active_span()\nif span is not None:\n headers[\"traceparent\"] = span.traceparent()" + } + ] + }, + { + "id": "get_all_flags", + "title": "get_all_flags", + "description": "Get all feature flags for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[dict[str, Union[bool, str]]]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.get_all_flags('distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_all_flags_and_payloads", + "title": "get_all_flags_and_payloads", + "description": "Get all feature flags and their payloads for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FlagsAndPayloads" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.get_all_flags_and_payloads('distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_feature_flag", + "title": "get_feature_flag", + "description": "Get multivariate feature flag value for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Union[bool, str, any]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user')\nif enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant\n # Do something differently for this user\n # Optional: fetch the payload\n matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_feature_flag_evaluation_runtime", + "title": "get_feature_flag_evaluation_runtime", + "description": "Return where a locally loaded feature flag is meant to be evaluated.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[FeatureFlagEvaluationRuntime]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import FeatureFlagEvaluationRuntime\n\nruntime = posthog.get_feature_flag_evaluation_runtime(\"my-flag\")\nif runtime is FeatureFlagEvaluationRuntime.SERVER:\n ..." + } + ] + }, + { + "id": "get_feature_flag_keys_by_evaluation_runtime", + "title": "get_feature_flag_keys_by_evaluation_runtime", + "description": "Return the keys of locally loaded flags that a runtime can evaluate. A flag set to ``FeatureFlagEvaluationRuntime.ALL`` suits either runtime, so it is returned for ``CLIENT`` and for ``SERVER``, and asking for ``ALL`` returns every loaded flag. Use this to decide which flags to hand to a browser when a backend serves flags to its own frontend.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "evaluation_runtime", + "description": "The runtime to match, as a ``FeatureFlagEvaluationRuntime`` or its string value.", + "isOptional": true, + "type": "FeatureFlagEvaluationRuntime" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "list[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import FeatureFlagEvaluationRuntime\n\nclient_keys = posthog.get_feature_flag_keys_by_evaluation_runtime(\n FeatureFlagEvaluationRuntime.CLIENT\n)" + } + ] + }, + { + "id": "get_feature_flag_payload", + "title": "get_feature_flag_payload", + "description": "Get the payload for a feature flag.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "match_value", + "description": "The specific flag value to get payload for.", + "isOptional": false, + "type": "bool" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Deprecated. Use get_feature_flag() instead if you need events.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[object]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user')\n\nif is_my_flag_enabled:\n # Do something differently for this user\n # Optional: fetch the payload\n matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_feature_flag_result", + "title": "get_feature_flag_result", + "description": "Get a FeatureFlagResult object which contains the flag result and payload for a key by evaluating locally or remotely depending on whether local evaluation is enabled and the flag can be locally evaluated. This also captures the `$feature_flag_called` event unless `send_feature_flag_events` is `False`.", + "details": "", + "category": null, + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[FeatureFlagResult]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "flag_result = posthog.get_feature_flag_result('flag-key', 'distinct_id_of_your_user')\nif flag_result and flag_result.get_value() == 'variant-key':\n # Do something differently for this user\n # Optional: fetch the payload\n matched_flag_payload = flag_result.payload" + } + ] + }, + { + "id": "get_feature_flags_and_payloads", + "title": "get_feature_flags_and_payloads", + "description": "Get feature flags and payloads for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FlagsAndPayloads" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "result = posthog.get_feature_flags_and_payloads('')" + } + ] + }, + { + "id": "get_feature_payloads", + "title": "get_feature_payloads", + "description": "Get feature flag payloads for a user, preserving valid serialized JSON.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "dict[str, Optional[str]]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "payloads = posthog.get_feature_payloads('')" + } + ] + }, + { + "id": "get_feature_variants", + "title": "get_feature_variants", + "description": "Get feature flag variants for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "dict[str, Union[bool, str]]" + } + }, + { + "id": "get_flags_decision", + "title": "get_flags_decision", + "description": "Get feature flags decision.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": false, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FlagsResponse" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "decision = posthog.get_flags_decision('user123')" + } + ] + }, + { + "id": "get_remote_config_payload", + "title": "get_remote_config_payload", + "description": "Get the payload for a remote config feature flag.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The remote config feature flag key.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "get_tags", + "title": "get_tags", + "description": "Get all tags from the current context. Returns: Dict of all tags in the current context.", + "details": "", + "category": "Contexts", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "dict[str, Any]" + } + }, + { + "id": "group_identify", + "title": "group_identify", + "description": "Identify a group and set its properties.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "group_type", + "description": "The type of group (e.g., 'company', 'team'). Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "str" + }, + { + "name": "group_key", + "description": "The unique identifier for the group. Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "str" + }, + { + "name": "properties", + "description": "A dictionary of properties to set on the group.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "timestamp", + "description": "The timestamp of the event. UTC is preferred; non-UTC datetimes and parseable ISO timestamp strings are converted to UTC.", + "isOptional": false, + "type": "datetime" + }, + { + "name": "uuid", + "description": "A unique identifier for the event. If provided, it must be a valid UUID string or uuid.UUID instance; invalid values are ignored and replaced with a newly generated UUID.", + "isOptional": false, + "type": "str" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this event.", + "isOptional": true, + "type": "bool" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user performing the action.", + "isOptional": false, + "type": "Number" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.group_identify('company', 'company_id_in_your_db', {\n 'name': 'Awesome Inc.',\n 'employees': 11\n})" + } + ] + }, + { + "id": "identify_context", + "title": "identify_context", + "description": "Identify the current context with a distinct ID.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID to associate with the current context and its children.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + } + }, + { + "id": "join", + "title": "join", + "description": "Attempt to process queued events and end the consumer threads. Do not use directly, call `shutdown()` instead. Failed or undrainable events may be dropped and reported through logging or ``on_error``; returning does not guarantee server receipt. Lifecycle cleanup is attempted once, and cleanup failures are logged without retry.", + "details": "", + "category": null, + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.join()" + } + ] + }, + { + "id": "load_feature_flags", + "title": "load_feature_flags", + "description": "Load feature flags for local evaluation.", + "details": "", + "category": "Feature flags", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.load_feature_flags()" + } + ] + }, + { + "id": "new_context", + "title": "new_context", + "description": "Create a new context for managing shared state. Learn more about [contexts](/docs/libraries/python#contexts).", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "fresh", + "description": "Whether to create a fresh context that doesn't inherit from parent.", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_exceptions", + "description": "Whether to automatically capture exceptions in this context. If omitted, defaults to this client's exception autocapture setting.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "with client.new_context():\n client.identify_context('')\n client.capture('event_name')" + } + ] + }, + { + "id": "scoped", + "title": "scoped", + "description": "Decorator that creates a new context for the wrapped function using this client.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "fresh", + "description": "Whether to create a fresh context that doesn't inherit from parent.", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_exceptions", + "description": "Whether to automatically capture exceptions in this context. If omitted, defaults to this client's exception autocapture setting.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set", + "title": "set", + "description": "Set properties on a person profile.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalSetArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Set with distinct id", + "code": "# Set with distinct id\nposthog.set(distinct_id='user123', properties={'name': 'Max Hedgehog'})" + } + ] + }, + { + "id": "set_context_device_id", + "title": "set_context_device_id", + "description": "Set the device ID for the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "device_id", + "description": "The device ID to associate with the current context and its children.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + } + }, + { + "id": "set_context_session", + "title": "set_context_session", + "description": "Set the session ID for the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "session_id", + "description": "The session ID to associate with the current context and its children.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + } + }, + { + "id": "set_once", + "title": "set_once", + "description": "Set properties on a person profile only if they haven't been set before.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalSetArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.set_once(distinct_id='user123', properties={'initial_signup_date': '2024-01-01'})" + } + ] + }, + { + "id": "shutdown", + "title": "shutdown", + "description": "Attempt final delivery and permanently shut down the client. Call this for final process cleanup or when disposing of a client that will not be reused. It closes capture lanes, stops background workers and the feature flag poller, and tears down integrations. Do not call it at the end of each request or serverless invocation when the client is shared; call ``flush()`` before returning instead. A shut-down client cannot be reused; create a new client if needed. Normally this method blocks until queued events have been attempted and cleanup finishes. Failed or undrainable events may be dropped and reported through logging or ``on_error``; returning does not guarantee server receipt. Event draining has no timeout. Queued spans get one final flush with up to 30 seconds waiting for an in-flight flush, then a fresh 30-second drain budget; the last HTTP request is not cut short. Any spans it cannot send are discarded with a warning, as are spans still open. Lifecycle cleanup is attempted once, and cleanup failures are logged without retry. When called directly from an SDK callback such as ``on_error``, shutdown is deferred to avoid blocking the worker that invoked the callback. If the callback must coordinate a blocking shutdown, have it signal an application-owned thread and return before that thread calls shutdown. Do not wait inside the callback for another thread or task that calls a lifecycle method.", + "details": "", + "category": null, + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.shutdown()" + } + ] + }, + { + "id": "start_span", + "title": "start_span", + "description": "Start a span for distributed tracing. Alpha. Returns a span handle. Use it as a context manager to make it the active span for the block and end it on exit (recording a raised exception on the way out); or call ``end()`` yourself for a span that cannot wrap a block. Spans started inside the block nest under it automatically. Always returns a usable handle, even when tracing is off, so calling code never branches.", + "details": "", + "category": "Tracing", + "params": [ + { + "name": "name", + "description": "A low-cardinality operation name, e.g. ``GET /users/:id``. Variable values belong in attributes, not the name.", + "isOptional": true, + "type": "str" + }, + { + "name": "kind", + "description": "``internal`` (default), ``server``, ``client``, ``producer`` or ``consumer``.", + "isOptional": true, + "type": "str" + }, + { + "name": "attributes", + "description": "Initial attributes.", + "isOptional": true, + "type": "Mapping[str, Any]" + }, + { + "name": "parent", + "description": "A span handle, or an inbound W3C ``traceparent`` header value to continue a remote trace. Defaults to the active span. A forked child starts with no active span; pass the parent span to continue a trace across a fork.", + "isOptional": false, + "type": "Span" + }, + { + "name": "tracestate", + "description": "The inbound ``tracestate`` header accompanying a ``traceparent`` string ``parent``; preserved and propagated.", + "isOptional": true, + "type": "str" + }, + { + "name": "start_time", + "description": "A ``datetime`` or epoch seconds, to backdate the span.", + "isOptional": false, + "type": "datetime" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Span" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog = Posthog(\"\", traces={\"service_name\": \"checkout-api\"})\n\nwith posthog.start_span(\"POST /checkout\", parent=request.headers.get(\"traceparent\")) as span:\n span.set_attribute(\"plan\", user.plan)\n with posthog.start_span(\"db.query\", kind=\"client\"):\n ...\n outgoing_headers = {\"traceparent\": span.traceparent()}" + } + ] + }, + { + "id": "tag", + "title": "tag", + "description": "Add a tag to the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "name", + "description": "The tag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "value", + "description": "The tag value.", + "isOptional": true, + "type": "Any" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + } + } + ] + }, + { + "id": "PostHogModule", + "title": "PostHog Module Functions", + "description": "Global functions available in the PostHog module", + "functions": [ + { + "id": "alias", + "title": "alias", + "description": "Associate user behaviour before and after they e.g. register, login, or perform some other identifying action.", + "details": "To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call. This will allow you to answer questions like \"Which marketing channels leads to users churning after a month?\" or \"What do users do on our website before signing up?\". Particularly useful for associating user behaviour before and after they e.g. register, login, or perform some other identifying action.", + "category": "Identification", + "params": [ + { + "name": "previous_id", + "description": "The unique ID of the user before", + "isOptional": true, + "type": "Number" + }, + { + "name": "distinct_id", + "description": "The current unique id", + "isOptional": true, + "type": "str" + }, + { + "name": "timestamp", + "description": "Optional timestamp for the event. UTC is preferred; non-UTC datetimes and parseable ISO timestamp strings are converted to UTC.", + "isOptional": false, + "type": "datetime" + }, + { + "name": "uuid", + "description": "Optional UUID for the event", + "isOptional": true, + "type": "str" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Alias user", + "code": "# Alias user\nfrom posthog import alias\nalias(previous_id='distinct_id', distinct_id='alias_id')" + } + ] + }, + { + "id": "capture", + "title": "capture", + "description": "Capture anything a user does within your system.", + "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.", + "category": "Events", + "params": [ + { + "name": "event", + "description": "The event name to specify the event **kwargs: Optional arguments including:", + "isOptional": true, + "type": "str" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Context and capture usage", + "code": "# Context and capture usage\nfrom posthog import new_context, identify_context, tag_context, capture\n# Enter a new context (e.g. a request/response cycle, an instance of a background job, etc)\nwith new_context():\n # Associate this context with some user, by distinct_id\n identify_context('some user')\n\n # Capture an event, associated with the context-level distinct ID ('some user')\n capture('movie started')\n\n # Capture an event associated with some other user (overriding the context-level distinct ID)\n capture('movie joined', distinct_id='some-other-user')\n\n # Capture an event with some properties\n capture('movie played', properties={'movie_id': '123', 'category': 'romcom'})\n\n # Capture an event with some properties\n capture('purchase', properties={'product_id': '123', 'category': 'romcom'})\n # Capture an event with some associated group\n capture('purchase', groups={'company': 'id:5'})\n\n # Adding a tag to the current context will cause it to appear on all subsequent events\n tag_context('some-tag', 'some-value')\n\n capture('another-event') # Will be captured with `'some-tag': 'some-value'` in the properties dict" + }, + { + "id": "example_2", + "name": "Set event properties", + "code": "# Set event properties\nfrom posthog import capture\ncapture(\n \"user_signed_up\",\n distinct_id=\"distinct_id_of_the_user\",\n properties={\n \"login_type\": \"email\",\n \"is_free_trial\": \"true\"\n }\n)" + } + ] + }, + { + "id": "capture_ai", + "title": "capture_ai", + "description": "Capture an AI event on the dedicated AI capture endpoint. Beta: the signature is stable; operational limits (per-event size cap, batching, endpoint) may change without notice. Takes the same arguments and returns the same value as `capture()`: the event UUID, or None when the event was not admitted (disabled client, or dropped by `before_send`). The event is delivered on an isolated queue with its own consumer pool and a higher per-event size cap, posting to the dedicated AI ingestion endpoint. The payload is sent as given \u2014 no redaction or truncation is applied here.", + "details": "", + "category": "Events", + "params": [ + { + "name": "event", + "description": "The event name, normally one of the `$ai_*` event names. **kwargs: Same optional arguments as `capture()`.", + "isOptional": true, + "type": "str" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import capture_ai\n\nuuid = capture_ai(\n \"$ai_generation\",\n distinct_id=\"user_123\",\n properties={\"$ai_model\": \"gpt-5\"},\n)" + } + ] + }, + { + "id": "capture_exception", + "title": "capture_exception", + "description": "Capture exceptions that happen in your code.", + "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`.", + "category": "Events", + "params": [ + { + "name": "exception", + "description": "The exception to capture. If not provided, the current exception is captured via `sys.exc_info()` **kwargs: Optional capture arguments including distinct_id, properties, timestamp, uuid, groups, flags, send_feature_flags, and disable_geoip. Overriding reserved exception properties through ``properties`` is deprecated and will stop working in the next major version.", + "isOptional": false, + "type": "BaseException" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Capture exception", + "code": "# Capture exception\nfrom posthog import capture_exception\ntry:\n risky_operation()\nexcept Exception as e:\n capture_exception(e)" + } + ] + }, + { + "id": "evaluate_flags", + "title": "evaluate_flags", + "description": "Evaluate all feature flags for a user in a single call and return a :class:`FeatureFlagEvaluations` snapshot. Branch on ``.is_enabled()`` / ``.get_flag()`` and pass the same snapshot to ``capture()`` via the ``flags`` option so events carry the exact flag values the code branched on. Prefer this over repeated ``get_feature_flag()`` calls and over ``capture(send_feature_flags=True)`` \u2014 it consolidates flag evaluation into a single ``/flags`` request per incoming request.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The user's distinct ID. If ``None``, falls back to the context distinct_id. If still unresolvable, returns an empty snapshot.", + "isOptional": false, + "type": "Number" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "If ``True``, never fall back to remote evaluation and omit flags that cannot be evaluated locally. The snapshot's ``unresolved_flags`` lists them with a reason.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys", + "description": "Optional list that scopes local evaluation, the underlying ``/flags`` request, and the returned snapshot. When omitted or ``None``, all flags are evaluated. An empty list returns an empty snapshot without evaluating flags. A requested key absent from loaded local definitions is included in one remote fallback per ``evaluate_flags`` call unless ``only_evaluate_locally`` is ``True``. If the server also does not know the key, it is omitted from the snapshot.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "Optional device ID override. If not provided, falls back to the context device_id (which may be set via tracing headers). Used by experience-continuity flags to match users across distinct_id changes.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FeatureFlagEvaluations" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import evaluate_flags, capture\nflags = evaluate_flags(\"user_123\", person_properties={\"plan\": \"enterprise\"})\nif flags.is_enabled(\"new-dashboard\"):\n render_new_dashboard()\ncapture(\"page_viewed\", distinct_id=\"user_123\", flags=flags)" + } + ] + }, + { + "id": "feature_enabled", + "title": "feature_enabled", + "description": "Use feature flags to enable or disable features for users.", + "details": "You can call `posthog.load_feature_flags()` before to make sure you're not doing unexpected requests.", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The user's distinct ID", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Groups mapping", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[bool]" + }, + "examples": [ + { + "id": "example_1", + "name": "Boolean feature flag", + "code": "# Boolean feature flag\nfrom posthog import feature_enabled, get_feature_flag_payload\nis_my_flag_enabled = feature_enabled('flag-key', 'distinct_id_of_your_user')\nif is_my_flag_enabled:\n matched_flag_payload = get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "feature_flag_definitions", + "title": "feature_flag_definitions", + "description": "Returns loaded feature flags.", + "details": "Returns loaded feature flags, if any. Helpful for debugging what flag information you have loaded.", + "category": "Feature flags", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import feature_flag_definitions\ndefinitions = feature_flag_definitions()" + } + ] + }, + { + "id": "flush", + "title": "flush", + "description": "Attempt immediate delivery of queued events and ended spans, keeping the global client reusable. Call ``flush()`` before a short-lived request or serverless invocation returns when using the shared module-level client. Use ``shutdown()`` only for final process cleanup or a client that will not be reused. Flush does not stop the background workers or feature flag poller, and does not end open spans. Returning does not guarantee server receipt: pending work can remain after a timeout or span export failure, and failed events may be dropped by consumers. Calls made directly from SDK callbacks such as ``on_error`` are deferred rather than blocking the worker that invoked the callback.", + "details": "", + "category": "Client management", + "params": [ + { + "name": "timeout_seconds", + "description": "Seconds to wait for event queues to drain, shared across event lanes. Defaults to 10 seconds. Pass ``None`` to wait without a time limit; this does not guarantee successful delivery. Queued spans flush concurrently, waiting up to this many seconds for an in-flight flush before starting a fresh drain budget. A lock wait timeout leaves the in-flight flush to finish. At least one span request is attempted after acquiring the lock, even with no budget left; retriable failures are retried after backoff while budget remains, with one last attempt at the deadline. The last span request is not cut short and is bounded by the client's ``timeout`` in seconds, so this is not a strict wall-clock limit for the whole call. With ``None``, a retriable span failure is left for a later flush rather than retried within this call.", + "isOptional": true, + "type": "float" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "import posthog\nposthog.capture('event_name', distinct_id='user_id')\n# Before a serverless invocation returns; keep the global client open.\nposthog.flush(timeout_seconds=10)" + } + ] + }, + { + "id": "get_active_span", + "title": "get_active_span", + "description": "The span that is active in the current context, or ``None``. Alpha. Only entering a span (``with posthog.start_span(...) as span:``) makes it active; a span started manually is not. Use it to propagate the trace to the next service: ``span.traceparent()`` is the header value.", + "details": "", + "category": "Tracing", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[Span]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "span = posthog.get_active_span()\nif span is not None:\n headers[\"traceparent\"] = span.traceparent()" + } + ] + }, + { + "id": "get_all_flags", + "title": "get_all_flags", + "description": "Get all flags for a given user.", + "details": "Flags are key-value pairs where the key is the flag key and the value is the flag variant, or True, or False.", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The user's distinct ID", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Groups mapping", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags", + "isOptional": true, + "type": "str" + }, + { + "name": "flag_keys_to_evaluate", + "description": "Optional list of flag keys to evaluate (evaluates all if None)", + "isOptional": true, + "type": "list[str]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[dict[str, Union[bool, str]]]" + }, + "examples": [ + { + "id": "example_1", + "name": "All flags for user", + "code": "# All flags for user\nfrom posthog import get_all_flags\nget_all_flags('distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_all_flags_and_payloads", + "title": "get_all_flags_and_payloads", + "description": "Get all feature flag values and payloads for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The user's distinct ID.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags.", + "isOptional": true, + "type": "str" + }, + { + "name": "flag_keys_to_evaluate", + "description": "Optional list of flag keys to evaluate. Evaluates all flags when omitted.", + "isOptional": true, + "type": "list[str]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FlagsAndPayloads" + } + }, + { + "id": "get_feature_flag", + "title": "get_feature_flag", + "description": "Get feature flag variant for users. Used with experiments.", + "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}}.", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The user's distinct ID", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Groups mapping from group type to group key", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties in format { group_type_name: { group_properties } }", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Union[bool, str, any]" + }, + "examples": [ + { + "id": "example_1", + "name": "Multivariate feature flag", + "code": "# Multivariate feature flag\nfrom posthog import get_feature_flag, get_feature_flag_payload\nenabled_variant = get_feature_flag('flag-key', 'distinct_id_of_your_user')\nif enabled_variant == 'variant-key':\n matched_flag_payload = get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_feature_flag_evaluation_runtime", + "title": "get_feature_flag_evaluation_runtime", + "description": "Return where a locally loaded feature flag is meant to be evaluated.", + "details": "Reads the `evaluation_runtime` each flag definition carries, so no extra request is made. Returns `None` when local evaluation has not loaded a definition for this key. A definition that carries no runtime reports `FeatureFlagEvaluationRuntime.ALL`, the default PostHog applies.", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[FeatureFlagEvaluationRuntime]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import FeatureFlagEvaluationRuntime, get_feature_flag_evaluation_runtime\nruntime = get_feature_flag_evaluation_runtime(\"my-flag\")" + } + ] + }, + { + "id": "get_feature_flag_keys_by_evaluation_runtime", + "title": "get_feature_flag_keys_by_evaluation_runtime", + "description": "Return the keys of locally loaded flags that a runtime can evaluate.", + "details": "A flag set to `FeatureFlagEvaluationRuntime.ALL` suits either runtime, so it is returned for `CLIENT` and for `SERVER`. Use this to decide which flags to hand to a browser when a backend serves flags to its own frontend.", + "category": "Feature flags", + "params": [ + { + "name": "evaluation_runtime", + "description": "", + "isOptional": true, + "type": "FeatureFlagEvaluationRuntime" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "list[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import FeatureFlagEvaluationRuntime, get_feature_flag_keys_by_evaluation_runtime\nclient_keys = get_feature_flag_keys_by_evaluation_runtime(FeatureFlagEvaluationRuntime.CLIENT)" + } + ] + }, + { + "id": "get_feature_flag_payload", + "title": "get_feature_flag_payload", + "description": "Get the payload associated with a feature flag value. Deprecated for new code. Prefer ``evaluate_flags()`` and ``flags.get_flag_payload(key)`` so flag evaluation happens once per request.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The user's distinct ID.", + "isOptional": true, + "type": "Number" + }, + { + "name": "match_value", + "description": "Optional flag value to use when selecting a payload.", + "isOptional": false, + "type": "bool" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send a $feature_flag_called event.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[object]" + } + }, + { + "id": "get_feature_flag_result", + "title": "get_feature_flag_result", + "description": "Get a FeatureFlagResult object which contains the flag result and payload. This method evaluates a feature flag and returns a FeatureFlagResult object containing: - enabled: Whether the flag is enabled - variant: The variant value if the flag has variants - payload: The payload associated with the flag (automatically deserialized from JSON) - key: The flag key - reason: Why the flag was enabled/disabled", + "details": "", + "category": null, + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The user's distinct ID.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send a $feature_flag_called event.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[FeatureFlagResult]" + } + }, + { + "id": "get_remote_config_payload", + "title": "get_remote_config_payload", + "description": "Get the payload for a remote config feature flag.", + "details": "", + "category": null, + "params": [ + { + "name": "key", + "description": "The key of the feature flag", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "get_tags", + "title": "get_tags", + "description": "Get all tags from the current context. Returns: Dict of all tags in the current context", + "details": "", + "category": "Contexts", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "dict[str, Any]" + } + }, + { + "id": "group_identify", + "title": "group_identify", + "description": "Set properties on a group.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "group_type", + "description": "Type of your group. Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "str" + }, + { + "name": "group_key", + "description": "Unique identifier of the group. Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "str" + }, + { + "name": "properties", + "description": "Properties to set on the group", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "timestamp", + "description": "Optional timestamp for the event. UTC is preferred; non-UTC datetimes and parseable ISO timestamp strings are converted to UTC.", + "isOptional": false, + "type": "datetime" + }, + { + "name": "uuid", + "description": "Optional UUID for the event", + "isOptional": true, + "type": "str" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + }, + { + "name": "distinct_id", + "description": "Optional distinct ID of the user performing the action", + "isOptional": false, + "type": "Number" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Group identify", + "code": "# Group identify\nfrom posthog import group_identify\ngroup_identify('company', 'company_id_in_your_db', {\n 'name': 'Awesome Inc.',\n 'employees': 11\n})" + } + ] + }, + { + "id": "identify_context", + "title": "identify_context", + "description": "Identify the current context with a distinct ID.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID to associate with the current context and its children", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import identify_context\nidentify_context(\"user_123\")" + } + ] + }, + { + "id": "join", + "title": "join", + "description": "Attempt to process queued events and stop the client's background workers. Use `shutdown()` directly in most cases. Failed or undrainable events may be dropped and reported through logging or ``on_error``; returning does not guarantee server receipt. Lifecycle cleanup is attempted once, and cleanup failures are logged without retry.", + "details": "", + "category": "Client management", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import join\njoin()" + } + ] + }, + { + "id": "load_feature_flags", + "title": "load_feature_flags", + "description": "Load feature flag definitions from PostHog.", + "details": "", + "category": "Feature flags", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import load_feature_flags\nload_feature_flags()" + } + ] + }, + { + "id": "new_context", + "title": "new_context", + "description": "Create a new context scope that will be active for the duration of the with block.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "fresh", + "description": "Whether to start with a fresh context (default: False)", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_exceptions", + "description": "Whether to capture exceptions raised within the context. If omitted, defaults to the relevant client's exception autocapture setting.", + "isOptional": true, + "type": "bool" + }, + { + "name": "client", + "description": "Optional Posthog client instance to use for this context (default: None)", + "isOptional": true, + "type": "Client" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import new_context, tag, capture\nwith new_context():\n tag(\"request_id\", \"123\")\n capture(\"event_name\", properties={\"property\": \"value\"})" + } + ] + }, + { + "id": "scoped", + "title": "scoped", + "description": "Decorator that creates a new context for the function.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "fresh", + "description": "Whether to start with a fresh context (default: False)", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_exceptions", + "description": "Whether to capture and track exceptions with posthog error tracking. If omitted, defaults to the global exception autocapture setting.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import scoped, tag, capture\n@scoped()\ndef process_payment(payment_id):\n tag(\"payment_id\", payment_id)\n capture(\"payment_started\")" + } + ] + }, + { + "id": "set", + "title": "set", + "description": "Set properties on a user record.", + "details": "This will overwrite previous people property values. Generally operates similar to `capture`, with distinct_id being an optional argument, defaulting to the current context's distinct ID. If there is no context-level distinct ID, and no override distinct_id is passed, this function will do nothing. Context tags are folded into $set properties, so tagging the current context and then calling `set` will cause those tags to be set on the user (unlike capture, which causes them to just be set on the event).", + "category": "Identification", + "params": [ + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalSetArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Set person properties", + "code": "# Set person properties\nfrom posthog import set\nset(distinct_id='distinct_id', properties={'name': 'Max Hedgehog'})" + } + ] + }, + { + "id": "set_capture_exception_code_variables_context", + "title": "set_capture_exception_code_variables_context", + "description": "Override code-variable capture for exceptions in the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "enabled", + "description": "Whether exceptions captured in this context should include local variable values from stack frames.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_code_variables_detect_secrets_context", + "title": "set_code_variables_detect_secrets_context", + "description": "Whether to apply entropy-based secret detection as a last-resort redaction of high-entropy values (API keys, tokens, strong passwords) in captured code variables for the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "enabled", + "description": "", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_code_variables_ignore_patterns_context", + "title": "set_code_variables_ignore_patterns_context", + "description": "Override code-variable ignore patterns for exceptions in the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "ignore_patterns", + "description": "Variable-name patterns that should be omitted entirely when code variables are captured.", + "isOptional": true, + "type": "list[str]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_code_variables_mask_patterns_context", + "title": "set_code_variables_mask_patterns_context", + "description": "Override code-variable mask patterns for exceptions in the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "mask_patterns", + "description": "Variable-name patterns whose values should be replaced with ``***`` when code variables are captured.", + "isOptional": true, + "type": "list[str]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_code_variables_mask_url_credentials_context", + "title": "set_code_variables_mask_url_credentials_context", + "description": "Whether to scrub credentials embedded in URLs/DSNs (e.g. user:pass@host) from captured code variables for the current context.", + "details": "", + "category": null, + "params": [ + { + "name": "enabled", + "description": "", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_context_device_id", + "title": "set_context_device_id", + "description": "Set the device ID for the current context, associating all feature flag requests in this or child contexts with the given device ID.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "device_id", + "description": "The device ID to associate with the current context and its children", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import set_context_device_id\nset_context_device_id(\"device_123\")" + } + ] + }, + { + "id": "set_context_session", + "title": "set_context_session", + "description": "Set the session ID for the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "session_id", + "description": "The session ID to associate with the current context and its children", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import set_context_session\nset_context_session(\"session_123\")" + } + ] + }, + { + "id": "set_once", + "title": "set_once", + "description": "Set properties on a user record, only if they do not yet exist.", + "details": "This will not overwrite previous people property values, unlike `set`. Otherwise, operates in an identical manner to `set`.", + "category": "Identification", + "params": [ + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalSetArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Set property once", + "code": "# Set property once\nfrom posthog import set_once\nset_once(distinct_id='distinct_id', properties={'initial_url': '/blog'})" + } + ] + }, + { + "id": "setup", + "title": "setup", + "description": "Create or return the global PostHog client configured by module settings. Most applications should either instantiate ``Posthog`` directly or set ``posthog.api_key``/other module settings before calling top-level helpers. ``setup()`` is called automatically by global APIs such as ``capture()``. Returns: The global ``Client`` instance. If both ``api_key`` and ``project_api_key`` are missing or blank, the client is disabled and module-level calls become no-ops.", + "details": "", + "category": "Initialization", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Client" + } + }, + { + "id": "shutdown", + "title": "shutdown", + "description": "Attempt final delivery and permanently shut down the global client. Call this for final process cleanup, not at the end of each request or serverless invocation that shares the module-level client. Use ``flush()`` before returning from those invocations instead. Shutdown closes capture lanes, stops background workers and the feature flag poller, and tears down integrations. The global client is not recreated automatically after shutdown. This normally blocks until queued events have been attempted and cleanup finishes. Failed or undrainable events may be dropped and reported through logging or ``on_error``; returning does not guarantee server receipt. Event draining has no timeout. Queued spans get a final flush with up to 30 seconds waiting for an in-flight flush, then a fresh 30-second drain budget; the last HTTP request is not cut short. Unsent queued spans and still-open spans are discarded with a warning. Lifecycle cleanup is attempted once, and cleanup failures are logged without retry. Calls made directly from SDK callbacks such as ``on_error`` are deferred to avoid deadlocking the worker. If blocking completion is required, signal an application-owned thread, return from the callback, and call ``shutdown()`` from that thread. Do not wait inside a callback for another thread or task calling a lifecycle method.", + "details": "", + "category": "Client management", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import shutdown\nshutdown()" + } + ] + }, + { + "id": "start_span", + "title": "start_span", + "description": "Start a span for distributed tracing. Alpha. Returns a span handle. Use it as a context manager to make it the active span for the block and end it on exit (recording a raised exception on the way out); or call ``end()`` yourself for a span that cannot wrap a block. Spans started inside the block nest under it automatically. Always returns a usable handle, even when tracing is off, so calling code never branches.", + "details": "", + "category": "Tracing", + "params": [ + { + "name": "name", + "description": "A low-cardinality operation name, e.g. ``GET /users/:id``. Variable values belong in attributes, not the name.", + "isOptional": true, + "type": "str" + }, + { + "name": "kind", + "description": "``internal`` (default), ``server``, ``client``, ``producer`` or ``consumer``.", + "isOptional": true, + "type": "str" + }, + { + "name": "attributes", + "description": "Initial attributes.", + "isOptional": true, + "type": "Mapping[str, Any]" + }, + { + "name": "parent", + "description": "A span handle, or an inbound W3C ``traceparent`` header value to continue a remote trace. Defaults to the active span.", + "isOptional": false, + "type": "Span" + }, + { + "name": "tracestate", + "description": "The inbound ``tracestate`` header accompanying a ``traceparent`` string ``parent``; preserved and propagated.", + "isOptional": true, + "type": "str" + }, + { + "name": "start_time", + "description": "A ``datetime`` or epoch seconds, to backdate the span.", + "isOptional": false, + "type": "datetime" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Span" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "import posthog\nposthog.traces = {\"service_name\": \"checkout-api\"}\n\nwith posthog.start_span(\"POST /checkout\", parent=request.headers.get(\"traceparent\")) as span:\n span.set_attribute(\"plan\", user.plan)\n with posthog.start_span(\"db.query\", kind=\"client\"):\n ...\n outgoing_headers = {\"traceparent\": span.traceparent()}" + } + ] + }, + { + "id": "tag", + "title": "tag", + "description": "Add a tag to the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "name", + "description": "The tag key", + "isOptional": true, + "type": "str" + }, + { + "name": "value", + "description": "The tag value", + "isOptional": true, + "type": "Any" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import tag\ntag(\"user_id\", \"123\")" + } + ] + } + ] + } + ], + "categories": [ + "Initialization", + "Identification", + "Capture", + "Error Tracking", + "Feature flags", + "Tracing", + "Contexts", + "Events", + "Client management" + ] +} \ No newline at end of file diff --git a/references/posthog-python-references-latest.json b/references/posthog-python-references-latest.json index 39752929b..ea89d2637 100644 --- a/references/posthog-python-references-latest.json +++ b/references/posthog-python-references-latest.json @@ -2,7 +2,7 @@ "id": "posthog-python", "hogRef": "0.3", "info": { - "version": "7.82.2", + "version": "7.83.0", "id": "posthog-python", "title": "PostHog Python SDK", "description": "Integrate PostHog into any python application.", @@ -83,7 +83,7 @@ "properties": [ { "name": "id", - "type": "int", + "type": "Optional[int]", "description": "Field: id" }, { @@ -93,7 +93,7 @@ }, { "name": "version", - "type": "int", + "type": "Optional[int]", "description": "Field: version" }, { From 58e8a5236f5fd5ea02ac58075b2b9bc2c1b2862a Mon Sep 17 00:00:00 2001 From: Connor Grady <50903811+connor-grady@users.noreply.github.com> Date: Tue, 3 Nov 2026 07:57:45 -0600 Subject: [PATCH 4/7] fix(mcp): inject context into mcp 2.x tool objects and test without fastmcp (#1166) * test(mcp): share the dual-major structured content reader * test(mcp): read structured content without the fastmcp camelcase shim * fix(mcp): inject context into mcp 2.x tool objects * ci: run the mcp 2.x suite without fastmcp (cherry picked from commit 0c12ca1521f6538873e00f5240cad306c9bba455) --- .github/workflows/ci.yml | 16 +++++++++--- .sampo/changesets/bold-lord-hiisi.md | 5 ++++ posthog/mcp/posthog_mcp.py | 4 +-- posthog/test/mcp/_helpers.py | 11 +++++++++ posthog/test/mcp/conftest.py | 5 ++-- posthog/test/mcp/test_output_instructions.py | 18 ++++++-------- posthog/test/mcp/test_posthog_mcp.py | 26 +++++++++++++++++++- 7 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 .sampo/changesets/bold-lord-hiisi.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a605fc14..ecbc6914c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,14 +156,22 @@ jobs: # the lockfile's mcp 1.x (also exercised incidentally by the `tests` # matrix — this leg exists as an explicit, named signal); the v2 leg # (spec 2026-07-28) swaps in mcp>=2 and standalone FastMCP 4, which uses - # the v2 registry. posthog/test/mcp/conftest.py splits collection by major. + # the v2 registry. The v2-bare leg runs mcp>=2 without FastMCP: importing + # FastMCP 4 patches camelCase properties onto mcp_types models for the + # whole process, which hides reads that plain mcp 2.x rejects. + # posthog/test/mcp/conftest.py splits collection by major. name: MCP SDK ${{ matrix.mcp-major }} (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: python-version: ['3.10', '3.14'] - mcp-major: ['v1', 'v2'] + mcp-major: ['v1', 'v2', 'v2-bare'] + include: + - mcp-major: 'v2' + mcp-packages: "'mcp>=2,<3' 'fastmcp>=4,<5'" + - mcp-major: 'v2-bare' + mcp-packages: "'mcp>=2,<3'" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -187,11 +195,11 @@ jobs: UV_PROJECT_ENVIRONMENT=$pythonLocation uv sync --extra test - name: Swap in MCP SDK v2 - if: matrix.mcp-major == 'v2' + if: matrix.mcp-major != 'v1' shell: bash run: | uv pip uninstall --python "$pythonLocation" fastmcp - uv pip install --python "$pythonLocation" 'mcp>=2,<3' 'fastmcp>=4,<5' + uv pip install --python "$pythonLocation" ${{ matrix.mcp-packages }} - name: Run MCP tests against SDK ${{ matrix.mcp-major }} run: | diff --git a/.sampo/changesets/bold-lord-hiisi.md b/.sampo/changesets/bold-lord-hiisi.md new file mode 100644 index 000000000..422365623 --- /dev/null +++ b/.sampo/changesets/bold-lord-hiisi.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +`PostHogMCP.prepare_tool_list` now adds the `context` argument to MCP SDK 2.x `Tool` objects. It only added it to dicts and 1.x `Tool` objects before, so `$mcp_intent` was never captured for 2.x tool objects. diff --git a/posthog/mcp/posthog_mcp.py b/posthog/mcp/posthog_mcp.py index b91a29eee..ee76a7443 100644 --- a/posthog/mcp/posthog_mcp.py +++ b/posthog/mcp/posthog_mcp.py @@ -771,10 +771,10 @@ def _inject_context(self, tool: Any, description: Optional[str]) -> Any: if self._is_sdk_virtual_tool(tool): return tool new_schema = add_context_parameter_to_schema( - getattr(tool, "inputSchema", None), name, description + _tool_schema(tool), name, description ) try: - tool.inputSchema = new_schema + _set_tool_schema(tool, new_schema) except Exception: # noqa: BLE001 pass return tool diff --git a/posthog/test/mcp/_helpers.py b/posthog/test/mcp/_helpers.py index cd4227c8e..adf5c537f 100644 --- a/posthog/test/mcp/_helpers.py +++ b/posthog/test/mcp/_helpers.py @@ -64,6 +64,17 @@ def events_named(source, name): return [e for e in events if e["event"] == name] +def structured_content(result): + """A ``CallToolResult``'s structured half: ``structuredContent`` on MCP SDK + 1.x models, ``structured_content`` on 2.x (same wire field). Read the field + itself rather than the camelCase spelling, which 2.x only resolves once + standalone FastMCP 4's compatibility shim has been imported.""" + for attr in ("structured_content", "structuredContent"): + if hasattr(result, attr): + return getattr(result, attr) + raise AssertionError("no structured content attribute") + + def listed_uris(response): """The uris a captured ``$mcp_resources_list`` response advertises, whether it listed static resources or templates.""" diff --git a/posthog/test/mcp/conftest.py b/posthog/test/mcp/conftest.py index e17970c23..2806b6499 100644 --- a/posthog/test/mcp/conftest.py +++ b/posthog/test/mcp/conftest.py @@ -1,8 +1,9 @@ """Split the MCP test suite by installed MCP SDK major. The suite runs against both majors in CI — the ``tests-mcp`` matrix has a -``mcp>=1.26,<2`` leg and a ``mcp>=2,<3`` leg (the main ``tests`` matrix also -exercises the v1 side incidentally). Files coupled to one +``mcp>=1.26,<2`` leg and two ``mcp>=2,<3`` legs, with and without standalone +FastMCP 4 (the main ``tests`` matrix also exercises the v1 side incidentally). +Files coupled to one major's seams import symbols the other major doesn't ship, so they are excluded from *collection* (a skip marker can't help — the failure is at import time). Version-agnostic files (units, truncation, session tokens, PostHogMCP, ids) diff --git a/posthog/test/mcp/test_output_instructions.py b/posthog/test/mcp/test_output_instructions.py index bbef3c041..ecc064598 100644 --- a/posthog/test/mcp/test_output_instructions.py +++ b/posthog/test/mcp/test_output_instructions.py @@ -14,6 +14,7 @@ can_declare_output_instructions, mirror_instructions_into_structured_content, ) +from posthog.test.mcp._helpers import structured_content def _tool(output_schema, name="demo"): @@ -204,13 +205,6 @@ def test_mirror_does_not_mutate_a_shared_model_result(): — and, through the handle, collapse unrelated clients into one session.""" import mcp.types as mcp_types - # `structuredContent` on SDK 1.x, `structured_content` on 2.x (same wire field). - def structured(result): - for attr in ("structured_content", "structuredContent"): - if hasattr(result, attr): - return getattr(result, attr) - raise AssertionError("no structured content attribute") - shared = mcp_types.CallToolResult( content=[mcp_types.TextContent(type="text", text="{}")], structuredContent={"total": 7}, @@ -220,9 +214,13 @@ def structured(result): assert delivered is True assert first is not shared # a copy, not the caller's object - assert structured(first)[MCP_INSTRUCTIONS_KEY] == {"conversation_id": "conv-A"} + assert structured_content(first)[MCP_INSTRUCTIONS_KEY] == { + "conversation_id": "conv-A" + } # the shared object the customer owns is untouched, so the next caller is clean - assert MCP_INSTRUCTIONS_KEY not in structured(shared) + assert MCP_INSTRUCTIONS_KEY not in structured_content(shared) second, _ = mirror_instructions_into_structured_content(shared, "conv-B") - assert structured(second)[MCP_INSTRUCTIONS_KEY] == {"conversation_id": "conv-B"} + assert structured_content(second)[MCP_INSTRUCTIONS_KEY] == { + "conversation_id": "conv-B" + } diff --git a/posthog/test/mcp/test_posthog_mcp.py b/posthog/test/mcp/test_posthog_mcp.py index 8f1df2d48..7d8cd66c5 100644 --- a/posthog/test/mcp/test_posthog_mcp.py +++ b/posthog/test/mcp/test_posthog_mcp.py @@ -19,6 +19,7 @@ from posthog.test.mcp._helpers import ( events_named as _events, flush_background as _flush, + structured_content, ) from posthog.version import VERSION @@ -175,6 +176,29 @@ def test_prepare_tool_list_injects_context_into_dicts(): assert "context" not in tools[0]["inputSchema"]["properties"] +@pytest.mark.parametrize("sdk_tool", [False, True]) +def test_prepare_tool_list_injects_context_into_tool_objects(sdk_tool: bool) -> None: + client, _ = make_client() + schema = {"type": "object", "properties": {"q": {"type": "string"}}} + tool = ( + Tool(name="search", inputSchema=schema) + if sdk_tool + else SimpleNamespace(name="search", input_schema=schema) + ) + schema_attribute = ( + "input_schema" if hasattr(tool, "input_schema") else "inputSchema" + ) + + prepared = client.prepare_tool_list([tool]) + + advertised = getattr(prepared[0], schema_attribute) + assert "context" in advertised["properties"] + assert "context" in advertised["required"] + assert "q" in advertised["properties"] + # the caller's schema dict is untouched + assert "context" not in schema["properties"] + + def test_prepare_tool_list_can_be_disabled(): client, _ = make_client() tools = [{"name": "search", "inputSchema": {"type": "object", "properties": {}}}] @@ -515,7 +539,7 @@ def test_prepare_tool_result_delivers_into_call_tool_result_models(wrapped): delivered = prepared.result.root if wrapped else prepared.result assert len(call_result.content) == 1 assert delivered.content[-1].text == _handle_block(prepared.conversation_id)["text"] - assert delivered.structuredContent[MCP_INSTRUCTIONS_KEY] == { + assert structured_content(delivered)[MCP_INSTRUCTIONS_KEY] == { "conversation_id": prepared.conversation_id } assert _UUID7.match(prepared.conversation_id) From ac6b689a5904b442a316b212cf0a38d20fc4ba4b Mon Sep 17 00:00:00 2001 From: "releaser-posthog-python[bot]" <262414804+releaser-posthog-python[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:59:22 +0000 Subject: [PATCH 5/7] chore: Release v7.83.1 [skip ci] (cherry picked from commit 9f5d4586083c070cf800860530a068c2f3353c08) --- .sampo/changesets/bold-lord-hiisi.md | 5 ----- openfeature-provider/CHANGELOG.md | 6 ++++++ openfeature-provider/pyproject.toml | 2 +- posthog/CHANGELOG.md | 6 ++++++ posthog/version.py | 2 +- pyproject.toml | 2 +- uv.lock | 4 ++-- 7 files changed, 17 insertions(+), 10 deletions(-) delete mode 100644 .sampo/changesets/bold-lord-hiisi.md diff --git a/.sampo/changesets/bold-lord-hiisi.md b/.sampo/changesets/bold-lord-hiisi.md deleted file mode 100644 index 422365623..000000000 --- a/.sampo/changesets/bold-lord-hiisi.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -pypi/posthog: patch ---- - -`PostHogMCP.prepare_tool_list` now adds the `context` argument to MCP SDK 2.x `Tool` objects. It only added it to dicts and 1.x `Tool` objects before, so `$mcp_intent` was never captured for 2.x tool objects. diff --git a/openfeature-provider/CHANGELOG.md b/openfeature-provider/CHANGELOG.md index 1c1793814..d665cae6f 100644 --- a/openfeature-provider/CHANGELOG.md +++ b/openfeature-provider/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to `openfeature-provider-posthog` are documented here. This file is maintained by [Sampo](https://github.com/bruits/sampo) from changesets in `.sampo/changesets/` that target `pypi/openfeature-provider-posthog`. +## 0.1.142 — 2026-11-03 + +### Patch changes + +- Updated dependencies: posthog@7.83.1 + ## 0.1.141 — 2026-11-03 ### Patch changes diff --git a/openfeature-provider/pyproject.toml b/openfeature-provider/pyproject.toml index 37c1ada54..940b97515 100644 --- a/openfeature-provider/pyproject.toml +++ b/openfeature-provider/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openfeature-provider-posthog" -version = "0.1.141" +version = "0.1.142" description = "Official PostHog provider for the OpenFeature Python SDK." readme = "README.md" authors = [{ name = "PostHog", email = "engineering@posthog.com" }] diff --git a/posthog/CHANGELOG.md b/posthog/CHANGELOG.md index 631a7caac..9d3f8ad37 100644 --- a/posthog/CHANGELOG.md +++ b/posthog/CHANGELOG.md @@ -1,5 +1,11 @@ # posthog +## 7.83.1 — 2026-11-03 + +### Patch changes + +- [0c12ca1](https://github.com/posthog/posthog-python/commit/0c12ca1521f6538873e00f5240cad306c9bba455) `PostHogMCP.prepare_tool_list` now adds the `context` argument to MCP SDK 2.x `Tool` objects. It only added it to dicts and 1.x `Tool` objects before, so `$mcp_intent` was never captured for 2.x tool objects. — Thanks @connor-grady! + ## 7.83.0 — 2026-11-03 ### Minor changes diff --git a/posthog/version.py b/posthog/version.py index 36d7c0455..3d6336f75 100644 --- a/posthog/version.py +++ b/posthog/version.py @@ -1 +1 @@ -VERSION = "7.83.0" +VERSION = "7.83.1" diff --git a/pyproject.toml b/pyproject.toml index f647d6d68..f7b25e685 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "posthog" -version = "7.83.0" +version = "7.83.1" description = "Integrate PostHog into any python application." authors = [{ name = "PostHog", email = "engineering@posthog.com" }] maintainers = [{ name = "PostHog", email = "engineering@posthog.com" }] diff --git a/uv.lock b/uv.lock index 8655cb212..f69d05711 100644 --- a/uv.lock +++ b/uv.lock @@ -2660,7 +2660,7 @@ wheels = [ [[package]] name = "openfeature-provider-posthog" -version = "0.1.141" +version = "0.1.142" source = { editable = "openfeature-provider" } dependencies = [ { name = "openfeature-sdk" }, @@ -3197,7 +3197,7 @@ wheels = [ [[package]] name = "posthog" -version = "7.83.0" +version = "7.83.1" source = { editable = "." } dependencies = [ { name = "distro" }, From 49e686ad572134d1be883effcedd15b5d4a7115a Mon Sep 17 00:00:00 2001 From: "releaser-posthog-python[bot]" <262414804+releaser-posthog-python[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:59:27 +0000 Subject: [PATCH 6/7] Update generated references (cherry picked from commit 06e56118d02ba692fbb0427d2044eb2d4d927250) --- .../posthog-python-references-7.83.1.json | 3566 +++++++++++++++++ .../posthog-python-references-latest.json | 2 +- 2 files changed, 3567 insertions(+), 1 deletion(-) create mode 100644 references/posthog-python-references-7.83.1.json diff --git a/references/posthog-python-references-7.83.1.json b/references/posthog-python-references-7.83.1.json new file mode 100644 index 000000000..a2d0c5a3e --- /dev/null +++ b/references/posthog-python-references-7.83.1.json @@ -0,0 +1,3566 @@ +{ + "id": "posthog-python", + "hogRef": "0.3", + "info": { + "version": "7.83.1", + "id": "posthog-python", + "title": "PostHog Python SDK", + "description": "Integrate PostHog into any python application.", + "slugPrefix": "posthog-python", + "specUrl": "https://github.com/PostHog/posthog-python" + }, + "types": [ + { + "id": "FeatureFlag", + "name": "FeatureFlag", + "path": "posthog.types.FeatureFlag", + "properties": [ + { + "name": "key", + "type": "str", + "description": "Field: key" + }, + { + "name": "enabled", + "type": "bool", + "description": "Field: enabled" + }, + { + "name": "variant", + "type": "Optional[str]", + "description": "Field: variant" + }, + { + "name": "reason", + "type": "Optional[FlagReason]", + "description": "Field: reason" + }, + { + "name": "metadata", + "type": "Union[FlagMetadata, LegacyFlagMetadata]", + "description": "Field: metadata" + } + ], + "example": "" + }, + { + "id": "FeatureFlagResult", + "name": "FeatureFlagResult", + "path": "posthog.types.FeatureFlagResult", + "properties": [ + { + "name": "key", + "type": "str", + "description": "Field: key" + }, + { + "name": "enabled", + "type": "bool", + "description": "Field: enabled" + }, + { + "name": "variant", + "type": "Optional[str]", + "description": "Field: variant" + }, + { + "name": "payload", + "type": "Optional[Any]", + "description": "Field: payload" + }, + { + "name": "reason", + "type": "Optional[str]", + "description": "Field: reason" + } + ], + "example": "" + }, + { + "id": "FlagMetadata", + "name": "FlagMetadata", + "path": "posthog.types.FlagMetadata", + "properties": [ + { + "name": "id", + "type": "Optional[int]", + "description": "Field: id" + }, + { + "name": "payload", + "type": "Optional[str]", + "description": "Field: payload" + }, + { + "name": "version", + "type": "Optional[int]", + "description": "Field: version" + }, + { + "name": "description", + "type": "str", + "description": "Field: description" + }, + { + "name": "has_experiment", + "type": "Optional[bool]", + "description": "Field: has_experiment" + } + ], + "example": "" + }, + { + "id": "FlagReason", + "name": "FlagReason", + "path": "posthog.types.FlagReason", + "properties": [ + { + "name": "code", + "type": "str", + "description": "Field: code" + }, + { + "name": "condition_index", + "type": "Optional[int]", + "description": "Field: condition_index" + }, + { + "name": "description", + "type": "str", + "description": "Field: description" + } + ], + "example": "" + }, + { + "id": "FlagsAndPayloads", + "name": "FlagsAndPayloads", + "path": "posthog.types.FlagsAndPayloads", + "properties": [ + { + "name": "featureFlags", + "type": "Optional[dict[str, Union[bool, str]]]", + "description": "Field: featureFlags" + }, + { + "name": "featureFlagPayloads", + "type": "Optional[dict[str, Any]]", + "description": "Field: featureFlagPayloads" + } + ], + "example": "" + }, + { + "id": "FlagsResponse", + "name": "FlagsResponse", + "path": "posthog.types.FlagsResponse", + "properties": [ + { + "name": "flags", + "type": "dict[str, FeatureFlag]", + "description": "Field: flags" + }, + { + "name": "errorsWhileComputingFlags", + "type": "bool", + "description": "Field: errorsWhileComputingFlags" + }, + { + "name": "requestId", + "type": "str", + "description": "Field: requestId" + }, + { + "name": "quotaLimit", + "type": "Optional[list[str]]", + "description": "Field: quotaLimit" + }, + { + "name": "evaluatedAt", + "type": "Optional[int]", + "description": "Field: evaluatedAt" + }, + { + "name": "minimalFlagCalledEvents", + "type": "bool", + "description": "Field: minimalFlagCalledEvents" + } + ], + "example": "" + }, + { + "id": "LegacyFlagMetadata", + "name": "LegacyFlagMetadata", + "path": "posthog.types.LegacyFlagMetadata", + "properties": [ + { + "name": "payload", + "type": "Any", + "description": "Field: payload" + } + ], + "example": "" + }, + { + "id": "SendFeatureFlagsOptions", + "name": "SendFeatureFlagsOptions", + "path": "posthog.types.SendFeatureFlagsOptions", + "properties": [ + { + "name": "should_send", + "type": "bool", + "description": "Field: should_send" + }, + { + "name": "only_evaluate_locally", + "type": "Optional[bool]", + "description": "Field: only_evaluate_locally" + }, + { + "name": "person_properties", + "type": "Optional[dict[str, Any]]", + "description": "Field: person_properties" + }, + { + "name": "group_properties", + "type": "Optional[dict[str, dict[str, Any]]]", + "description": "Field: group_properties" + }, + { + "name": "flag_keys_filter", + "type": "Optional[list[str]]", + "description": "Field: flag_keys_filter" + } + ], + "example": "" + }, + { + "id": "OptionalCaptureArgs", + "name": "OptionalCaptureArgs", + "path": "posthog.args.OptionalCaptureArgs", + "properties": [ + { + "name": "distinct_id", + "type": "NotRequired[Union[Number, str, UUID, int, any]]", + "description": "Field: distinct_id" + }, + { + "name": "properties", + "type": "NotRequired[Optional[dict[str, Any]]]", + "description": "Field: properties" + }, + { + "name": "timestamp", + "type": "NotRequired[Union[datetime, str, any]]", + "description": "Field: timestamp" + }, + { + "name": "uuid", + "type": "NotRequired[Union[str, UUID, any]]", + "description": "Field: uuid" + }, + { + "name": "groups", + "type": "NotRequired[Optional[dict[str, str]]]", + "description": "Field: groups" + }, + { + "name": "flags", + "type": "NotRequired[Optional[ForwardRef('FeatureFlagEvaluations')]]", + "description": "Field: flags" + }, + { + "name": "send_feature_flags", + "type": "NotRequired[Union[bool, SendFeatureFlagsOptions, any]]", + "description": "Field: send_feature_flags" + }, + { + "name": "disable_geoip", + "type": "NotRequired[Optional[bool]]", + "description": "Field: disable_geoip" + }, + { + "name": "_property_allowlist", + "type": "NotRequired[Optional[frozenset[str]]]", + "description": "Field: _property_allowlist" + } + ], + "example": "" + }, + { + "id": "OptionalSetArgs", + "name": "OptionalSetArgs", + "path": "posthog.args.OptionalSetArgs", + "properties": [ + { + "name": "distinct_id", + "type": "NotRequired[Union[Number, str, UUID, int, any]]", + "description": "Field: distinct_id" + }, + { + "name": "properties", + "type": "NotRequired[Optional[dict[str, Any]]]", + "description": "Field: properties" + }, + { + "name": "timestamp", + "type": "NotRequired[Union[datetime, str, any]]", + "description": "Field: timestamp" + }, + { + "name": "uuid", + "type": "NotRequired[Union[str, UUID, any]]", + "description": "Field: uuid" + }, + { + "name": "disable_geoip", + "type": "NotRequired[Optional[bool]]", + "description": "Field: disable_geoip" + } + ], + "example": "" + }, + { + "id": "SendFeatureFlagsOptions", + "name": "SendFeatureFlagsOptions", + "path": "posthog.types.SendFeatureFlagsOptions", + "properties": [ + { + "name": "should_send", + "type": "bool", + "description": "Field: should_send" + }, + { + "name": "only_evaluate_locally", + "type": "Optional[bool]", + "description": "Field: only_evaluate_locally" + }, + { + "name": "person_properties", + "type": "Optional[dict[str, Any]]", + "description": "Field: person_properties" + }, + { + "name": "group_properties", + "type": "Optional[dict[str, dict[str, Any]]]", + "description": "Field: group_properties" + }, + { + "name": "flag_keys_filter", + "type": "Optional[list[str]]", + "description": "Field: flag_keys_filter" + } + ], + "example": "" + } + ], + "classes": [ + { + "id": "PostHog", + "title": "PostHog", + "description": "This is the SDK reference for the PostHog 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. For long-running applications, create one client during application startup and reuse it for the lifetime of the process. This keeps background queues predictable and makes shutdown flushing straightforward. Multiple clients are still supported for intentional multi-project or multi-host setups.", + "functions": [ + { + "id": "__init__", + "title": "Client", + "description": "Initialize a new PostHog client instance.", + "details": "", + "category": "Initialization", + "params": [ + { + "name": "project_api_key", + "description": "PostHog project API key/token.", + "isOptional": true, + "type": "str" + }, + { + "name": "host", + "description": "PostHog host. Defaults to the US ingestion endpoint when not set. App hosts such as ``https://us.posthog.com`` are mapped to the corresponding ingestion host.", + "isOptional": false, + "type": "any" + }, + { + "name": "debug", + "description": "Enable verbose SDK logging and re-raise errors from public API methods.", + "isOptional": false, + "type": "bool" + }, + { + "name": "max_queue_size", + "description": "Maximum number of events buffered before upload.", + "isOptional": false, + "type": "int" + }, + { + "name": "send", + "description": "If False, queueing succeeds but events are not sent.", + "isOptional": false, + "type": "bool" + }, + { + "name": "on_error", + "description": "Optional callback invoked by background consumers when an upload fails. Keep it short and non-blocking. Calling lifecycle methods directly is safe and deferred, but do not start another thread or task that calls ``flush()``, ``join()``, or ``shutdown()`` and then wait for it from the callback.", + "isOptional": false, + "type": "any" + }, + { + "name": "flush_at", + "description": "Number of queued events that triggers a batch upload.", + "isOptional": false, + "type": "int" + }, + { + "name": "flush_interval", + "description": "Maximum seconds a background consumer waits before flushing a partial batch.", + "isOptional": false, + "type": "float" + }, + { + "name": "gzip", + "description": "Whether to gzip event upload payloads.", + "isOptional": false, + "type": "bool" + }, + { + "name": "max_retries", + "description": "Number of upload retries. Values below 0 are treated as 0.", + "isOptional": false, + "type": "int" + }, + { + "name": "sync_mode", + "description": "If True, send each event synchronously instead of using background worker threads. This blocks the calling thread; in asyncio applications such as FastAPI, use ``AsyncPosthog`` instead.", + "isOptional": false, + "type": "bool" + }, + { + "name": "timeout", + "description": "HTTP request timeout in seconds for event uploads.", + "isOptional": false, + "type": "float" + }, + { + "name": "thread", + "description": "Number of background consumer threads.", + "isOptional": false, + "type": "int" + }, + { + "name": "poll_interval", + "description": "Seconds between local feature flag definition refreshes.", + "isOptional": false, + "type": "int" + }, + { + "name": "personal_api_key", + "description": "Deprecated alias for ``secret_key``. Still honored for backwards compatibility; prefer ``secret_key``, which also accepts a Project Secret API Key.", + "isOptional": false, + "type": "any" + }, + { + "name": "disabled", + "description": "If True, disable captures and API requests. Useful in tests.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable server-side GeoIP enrichment. Defaults to True.", + "isOptional": false, + "type": "bool" + }, + { + "name": "is_server", + "description": "Whether events are emitted from a server-side runtime. Defaults to True; set to False when using the SDK as a client/CLI so the device OS is attributed to the person normally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "historical_migration", + "description": "Mark events as historical migration imports.", + "isOptional": false, + "type": "bool" + }, + { + "name": "feature_flags_request_timeout_seconds", + "description": "Timeout in seconds for feature flag and remote config requests.", + "isOptional": false, + "type": "float" + }, + { + "name": "feature_flags_request_max_retries", + "description": "Number of retries for feature flag requests after network, transport, or timeout failures. Defaults to 1. Set to 0 to disable retries.", + "isOptional": false, + "type": "int" + }, + { + "name": "super_properties", + "description": "Properties merged into every captured event.", + "isOptional": false, + "type": "any" + }, + { + "name": "enable_exception_autocapture", + "description": "Automatically capture uncaught exceptions.", + "isOptional": false, + "type": "bool" + }, + { + "name": "log_captured_exceptions", + "description": "Also log exceptions captured by error tracking.", + "isOptional": false, + "type": "bool" + }, + { + "name": "project_root", + "description": "Root path used to determine in-app stack frames for captured exceptions. Defaults to the current working directory.", + "isOptional": false, + "type": "any" + }, + { + "name": "privacy_mode", + "description": "For AI observability, capture usage metadata without prompt inputs or outputs.", + "isOptional": false, + "type": "bool" + }, + { + "name": "before_send", + "description": "Optional callback that can modify or drop events before upload. Return ``None`` to drop an event.", + "isOptional": false, + "type": "any" + }, + { + "name": "flag_fallback_cache_url", + "description": "Optional feature flag fallback cache URL, such as ``memory://local/?ttl=300&size=10000`` or a Redis URL.", + "isOptional": false, + "type": "any" + }, + { + "name": "enable_local_evaluation", + "description": "Whether to poll feature flag definitions for local evaluation when a personal API key is configured.", + "isOptional": false, + "type": "bool" + }, + { + "name": "flag_definition_cache_provider", + "description": "Optional external cache provider for sharing feature flag definitions across workers.", + "isOptional": true, + "type": "FlagDefinitionCacheProvider" + }, + { + "name": "capture_exception_code_variables", + "description": "Capture local variable values on exception stack frames.", + "isOptional": false, + "type": "bool" + }, + { + "name": "code_variables_mask_patterns", + "description": "Variable-name patterns to mask when capturing code variables.", + "isOptional": false, + "type": "any" + }, + { + "name": "code_variables_ignore_patterns", + "description": "Variable-name patterns to omit when capturing code variables.", + "isOptional": false, + "type": "any" + }, + { + "name": "code_variables_mask_url_credentials", + "description": "Scrub credentials embedded in URLs/DSNs (e.g. ``user:pass@host``) from captured code variables, regardless of the surrounding variable name. Defaults to True.", + "isOptional": false, + "type": "any" + }, + { + "name": "code_variables_detect_secrets", + "description": "Last-resort entropy-based detection that redacts high-entropy secret-looking values (API keys, tokens, strong passwords) sitting in innocuously-named variables, after the name and URL checks. Skips structured ids (UUIDs, ObjectIds, hashes). Defaults to True.", + "isOptional": false, + "type": "any" + }, + { + "name": "in_app_modules", + "description": "Module/package prefixes treated as in-app frames in captured exceptions.", + "isOptional": false, + "type": "UnionType[list[str], any]" + }, + { + "name": "enable_exception_autocapture_rate_limiting", + "description": "Rate limit autocaptured exceptions client-side with a token bucket per exception type. Disabled by default.", + "isOptional": false, + "type": "bool" + }, + { + "name": "exception_autocapture_bucket_size", + "description": "Maximum burst of autocaptured exceptions allowed per exception type (token bucket size, clamped to 0-100).", + "isOptional": false, + "type": "int" + }, + { + "name": "exception_autocapture_refill_rate", + "description": "Tokens restored per refill interval for each exception type's bucket.", + "isOptional": false, + "type": "int" + }, + { + "name": "exception_autocapture_refill_interval_seconds", + "description": "Seconds between token refills for autocaptured exception rate limiting.", + "isOptional": false, + "type": "int" + }, + { + "name": "capture_mode", + "description": "Capture wire protocol to use. Defaults to ``CaptureMode.V0`` (legacy ``/batch/``). Set ``CaptureMode.V1`` (or pass the string ``\"v1\"``) to opt into ``/i/v1/analytics/events``. When omitted, the ``POSTHOG_CAPTURE_MODE`` env var is consulted, then ``V0``.", + "isOptional": false, + "type": "CaptureMode" + }, + { + "name": "capture_compression", + "description": "Request-body compression for capture-v1 uploads (ignored in V0, which uses ``gzip``). ``CaptureCompression.GZIP`` or ``DEFLATE`` (or the strings ``\"gzip\"``/``\"deflate\"``). When omitted, the ``POSTHOG_CAPTURE_COMPRESSION`` env var is consulted, then the legacy ``gzip`` flag, then no compression.", + "isOptional": false, + "type": "CaptureCompression" + }, + { + "name": "secret_key", + "description": "A Personal API Key or Project Secret API Key, used to authenticate local feature flag evaluation, remote config payloads, and decrypted flag payloads. Example:: posthog.Client(project_api_key, secret_key=\"phx_...\")", + "isOptional": false, + "type": "any" + }, + { + "name": "metrics", + "description": "", + "isOptional": true, + "type": "dict" + }, + { + "name": "enable_full_ai_capture", + "description": "Route PostHog AI wrapper events through the dedicated AI capture endpoint and capture full AI content: skips string truncation and passes media (base64/data URIs) through unredacted. ``privacy_mode`` always wins. Defaults to False.", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_trace_context", + "description": "When OpenTelemetry is installed and a valid span is active at capture time, add its trace and span IDs as ``$trace_id`` and ``$span_id`` properties to events captured with ``capture()`` and ``capture_ai()``, so they can be correlated with backend traces. Explicit ``$trace_id``/``$span_id`` values passed in ``properties`` win. Exception events (``capture_exception``) always attach these IDs regardless of this setting. Defaults to False.", + "isOptional": false, + "type": "bool" + }, + { + "name": "_use_ai_lane", + "description": "", + "isOptional": false, + "type": "bool" + }, + { + "name": "_enable_multimodal_capture", + "description": "", + "isOptional": false, + "type": "bool" + }, + { + "name": "traces", + "description": "Config dict for distributed tracing: ``service_name``, ``service_version``, ``environment``, ``resource_attributes``, ``flush_interval`` (5 s), ``max_queue_size`` (2048), ``max_export_batch_size`` (512), ``max_live_spans`` (10000), ``max_span_age`` (3600 s), ``max_attributes_per_span`` (128), ``max_events_per_span`` (128), ``max_attribute_value_length`` (8192). ``before_span_send`` is a callable, or a list run in order, that receives each finished span as a dict (``trace_id``, ``span_id`` and ``parent_span_id`` are read-only) and returns it, edited, or ``None`` to drop it; a hook that raises drops the span. Tracing is off until this is provided. Spans export on a background timer even with ``sync_mode``; serverless handlers should call ``flush()`` before returning. Defaults to None.", + "isOptional": true, + "type": "dict" + }, + { + "name": "app_host", + "description": "Your PostHog app host, such as ``https://posthog.example.com``. ``emails`` sends there. Needed only when ``host`` is not PostHog Cloud, for example a reverse proxy, which must not receive the secret key.", + "isOptional": true, + "type": "str" + }, + { + "name": "email_request_timeout_seconds", + "description": "Timeout in seconds for each email send request. Defaults to 15.", + "isOptional": false, + "type": "float" + }, + { + "name": "email_request_max_retries", + "description": "Number of retries for an email send after a network error, a rate limit, or an unavailable service. Defaults to 2. Set to 0 when a job runner already retries the send.", + "isOptional": false, + "type": "int" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import Posthog\n\nposthog = Posthog('', host='')" + } + ] + }, + { + "id": "alias", + "title": "alias", + "description": "Create an alias between two distinct IDs.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "previous_id", + "description": "The previous distinct ID. Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "Number" + }, + { + "name": "distinct_id", + "description": "The new distinct ID to alias to. Falls back to the context distinct ID; the call is dropped with a warning if neither is available.", + "isOptional": true, + "type": "str" + }, + { + "name": "timestamp", + "description": "The timestamp of the event. UTC is preferred; non-UTC datetimes and parseable ISO timestamp strings are converted to UTC.", + "isOptional": false, + "type": "datetime" + }, + { + "name": "uuid", + "description": "A unique identifier for the event. If provided, it must be a valid UUID string or uuid.UUID instance; invalid values are ignored and replaced with a newly generated UUID.", + "isOptional": true, + "type": "str" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this event.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.alias(previous_id='distinct_id', distinct_id='alias_id')" + } + ] + }, + { + "id": "capture", + "title": "capture", + "description": "Captures an event manually. [Learn about capture best practices](https://posthog.com/docs/product-analytics/capture-events)", + "details": "", + "category": "Capture", + "params": [ + { + "name": "event", + "description": "The event name to capture.", + "isOptional": true, + "type": "str" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Anonymous event", + "code": "# Anonymous event\nposthog.capture('some-anon-event')" + }, + { + "id": "example_2", + "name": "Context usage", + "code": "# Context usage\nfrom posthog import identify_context, new_context\nwith new_context():\n identify_context('distinct_id_of_the_user')\n posthog.capture('user_signed_up')\n posthog.capture('user_logged_in')\n posthog.capture('some-custom-action', distinct_id='distinct_id_of_the_user')" + }, + { + "id": "example_3", + "name": "Set event properties", + "code": "# Set event properties\nposthog.capture(\n \"user_signed_up\",\n distinct_id=\"distinct_id_of_the_user\",\n properties={\n \"login_type\": \"email\",\n \"is_free_trial\": \"true\"\n }\n)" + }, + { + "id": "example_4", + "name": "Page view event", + "code": "# Page view event\nposthog.capture('$pageview', distinct_id=\"distinct_id_of_the_user\", properties={'$current_url': 'https://example.com'})" + } + ] + }, + { + "id": "capture_ai", + "title": "capture_ai", + "description": "Capture an AI event on the dedicated AI capture endpoint. Beta: the signature is stable; operational limits (per-event size cap, batching, endpoint) may change without notice. Takes the same arguments and returns the same value as `capture()`: the event UUID, or None when the event was not admitted (disabled client, or dropped by `before_send`). The event is queued on an isolated AI lane with its own consumer pool and a higher per-event size cap, posting to the dedicated AI ingestion endpoint. The payload is sent as given \u2014 no redaction or truncation is applied here.", + "details": "", + "category": "Capture", + "params": [ + { + "name": "event", + "description": "", + "isOptional": true, + "type": "str" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + } + }, + { + "id": "capture_exception", + "title": "capture_exception", + "description": "Capture an exception for error tracking. When OpenTelemetry is installed and a valid span is active, its trace and span IDs are added as ``$trace_id`` and ``$span_id`` event properties.", + "details": "", + "category": "Error Tracking", + "params": [ + { + "name": "exception", + "description": "The exception to capture.", + "isOptional": true, + "type": "BaseException" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "try:\n # Some code that might fail\n pass\nexcept Exception as e:\n posthog.capture_exception(e, 'user_distinct_id', properties=additional_properties)" + } + ] + }, + { + "id": "evaluate_flags", + "title": "evaluate_flags", + "description": "Evaluate all feature flags for a user in a single call and return a :class:`FeatureFlagEvaluations` snapshot. Branch on ``.is_enabled()`` / ``.get_flag()`` and pass the same snapshot to :meth:`capture` via the ``flags`` option so events carry the exact flag values the code branched on. Prefer this over repeated ``get_feature_flag()`` calls and over ``capture(send_feature_flags=True)`` \u2014 it consolidates flag evaluation into a single ``/flags`` request per incoming request. Local evaluation is transparent: when the poller resolves a flag, the snapshot's ``$feature_flag_called`` events are tagged ``locally_evaluated=True`` and reason ``\"Evaluated locally\"``.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The user's distinct ID. If ``None``, falls back to the context distinct_id. If still unresolvable, returns an empty snapshot.", + "isOptional": false, + "type": "Number" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "If True, never fall back to remote evaluation \u2014 flags that can't be evaluated locally are omitted from the snapshot and listed with a reason in its ``unresolved_flags``.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys", + "description": "Optional list that scopes local evaluation, the underlying ``/flags`` request, and the returned snapshot. When omitted or ``None``, all flags are evaluated. An empty list returns an empty snapshot without evaluating flags. A requested key absent from loaded local definitions is included in one remote fallback per ``evaluate_flags`` call unless ``only_evaluate_locally`` is True. If the server also does not know the key, it is omitted from the snapshot.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "Optional device ID override. If not provided, falls back to the context device_id (which may be set via tracing headers). Used by experience-continuity flags to match users across distinct_id changes.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FeatureFlagEvaluations" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "flags = posthog.evaluate_flags(\n \"user_123\",\n person_properties={\"plan\": \"enterprise\"},\n)\nif flags.is_enabled(\"new-dashboard\"):\n render_new_dashboard()\nposthog.capture(\"page_viewed\", distinct_id=\"user_123\", flags=flags)" + } + ] + }, + { + "id": "feature_enabled", + "title": "feature_enabled", + "description": "Check if a feature flag is enabled for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[bool]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user')\nif is_my_flag_enabled:\n # Do something differently for this user\n # Optional: fetch the payload\n matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "feature_flag_definitions", + "title": "feature_flag_definitions", + "description": "Return feature flag definitions loaded for local evaluation. Returns: The currently loaded feature flag definitions, or ``None`` before local evaluation has loaded definitions.", + "details": "", + "category": "Feature flags", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "flush", + "title": "flush", + "description": "Attempt immediate delivery of queued events, ended spans and autocaptured metrics, keeping the client reusable. For a client shared across requests or serverless invocations (including the module-level client), call ``flush()`` before a short-lived invocation returns. Use ``shutdown()`` instead only for final process cleanup or when disposing of a client that will not be reused. Flush does not stop the background workers or feature flag poller, and does not end open spans. Returning does not guarantee server receipt: pending work can remain after a timeout or span export failure, and failed events may be dropped by the consumers. Calls made directly from SDK callbacks such as ``on_error`` are deferred rather than blocking the worker that invoked the callback.", + "details": "", + "category": null, + "params": [ + { + "name": "timeout_seconds", + "description": "Seconds to wait for event queues to drain, shared across event lanes. Defaults to 10 seconds. Pass ``None`` to wait without a time limit; this does not guarantee successful delivery. Queued spans flush concurrently. The span exporter waits up to this many seconds for an in-flight flush, then uses a fresh drain budget once it acquires the lock. If the lock wait times out, the in-flight flush is left to finish. At least one span request is attempted after acquiring the lock, even with no budget left; retriable failures are retried after backoff while budget remains, with one last attempt at the deadline. Each span request is bounded by the client's ``timeout`` in seconds, and the last request is not cut short. This is therefore not a strict wall-clock limit for the whole call. With ``None``, a retriable span failure is left for a later flush rather than retried within this call.", + "isOptional": true, + "type": "float" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.capture('event_name', distinct_id='user_id')\n# Before a serverless invocation returns; keep the shared client open.\nposthog.flush(timeout_seconds=10)" + } + ] + }, + { + "id": "get_active_span", + "title": "get_active_span", + "description": "The span that is active in the current context, or ``None``. Alpha. Only entering a span (``with posthog.start_span(...) as span:``) makes it active; a span started manually is not. Use it to propagate the trace to the next service: ``span.traceparent()`` is the header value.", + "details": "", + "category": "Tracing", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[Span]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "span = posthog.get_active_span()\nif span is not None:\n headers[\"traceparent\"] = span.traceparent()" + } + ] + }, + { + "id": "get_all_flags", + "title": "get_all_flags", + "description": "Get all feature flags for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[dict[str, Union[bool, str]]]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.get_all_flags('distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_all_flags_and_payloads", + "title": "get_all_flags_and_payloads", + "description": "Get all feature flags and their payloads for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FlagsAndPayloads" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.get_all_flags_and_payloads('distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_feature_flag", + "title": "get_feature_flag", + "description": "Get multivariate feature flag value for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Union[bool, str, any]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user')\nif enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant\n # Do something differently for this user\n # Optional: fetch the payload\n matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_feature_flag_evaluation_runtime", + "title": "get_feature_flag_evaluation_runtime", + "description": "Return where a locally loaded feature flag is meant to be evaluated.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[FeatureFlagEvaluationRuntime]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import FeatureFlagEvaluationRuntime\n\nruntime = posthog.get_feature_flag_evaluation_runtime(\"my-flag\")\nif runtime is FeatureFlagEvaluationRuntime.SERVER:\n ..." + } + ] + }, + { + "id": "get_feature_flag_keys_by_evaluation_runtime", + "title": "get_feature_flag_keys_by_evaluation_runtime", + "description": "Return the keys of locally loaded flags that a runtime can evaluate. A flag set to ``FeatureFlagEvaluationRuntime.ALL`` suits either runtime, so it is returned for ``CLIENT`` and for ``SERVER``, and asking for ``ALL`` returns every loaded flag. Use this to decide which flags to hand to a browser when a backend serves flags to its own frontend.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "evaluation_runtime", + "description": "The runtime to match, as a ``FeatureFlagEvaluationRuntime`` or its string value.", + "isOptional": true, + "type": "FeatureFlagEvaluationRuntime" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "list[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import FeatureFlagEvaluationRuntime\n\nclient_keys = posthog.get_feature_flag_keys_by_evaluation_runtime(\n FeatureFlagEvaluationRuntime.CLIENT\n)" + } + ] + }, + { + "id": "get_feature_flag_payload", + "title": "get_feature_flag_payload", + "description": "Get the payload for a feature flag.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "match_value", + "description": "The specific flag value to get payload for.", + "isOptional": false, + "type": "bool" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Deprecated. Use get_feature_flag() instead if you need events.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[object]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user')\n\nif is_my_flag_enabled:\n # Do something differently for this user\n # Optional: fetch the payload\n matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_feature_flag_result", + "title": "get_feature_flag_result", + "description": "Get a FeatureFlagResult object which contains the flag result and payload for a key by evaluating locally or remotely depending on whether local evaluation is enabled and the flag can be locally evaluated. This also captures the `$feature_flag_called` event unless `send_feature_flag_events` is `False`.", + "details": "", + "category": null, + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to only evaluate locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[FeatureFlagResult]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "flag_result = posthog.get_feature_flag_result('flag-key', 'distinct_id_of_your_user')\nif flag_result and flag_result.get_value() == 'variant-key':\n # Do something differently for this user\n # Optional: fetch the payload\n matched_flag_payload = flag_result.payload" + } + ] + }, + { + "id": "get_feature_flags_and_payloads", + "title": "get_feature_flags_and_payloads", + "description": "Get feature flags and payloads for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FlagsAndPayloads" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "result = posthog.get_feature_flags_and_payloads('')" + } + ] + }, + { + "id": "get_feature_payloads", + "title": "get_feature_payloads", + "description": "Get feature flag payloads for a user, preserving valid serialized JSON.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "dict[str, Optional[str]]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "payloads = posthog.get_feature_payloads('')" + } + ] + }, + { + "id": "get_feature_variants", + "title": "get_feature_variants", + "description": "Get feature flag variants for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "dict[str, Union[bool, str]]" + } + }, + { + "id": "get_flags_decision", + "title": "get_flags_decision", + "description": "Get feature flags decision.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID of the user.", + "isOptional": false, + "type": "Number" + }, + { + "name": "groups", + "description": "A dictionary of group information.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "A dictionary of person properties.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "A dictionary of group properties.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this request.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys_to_evaluate", + "description": "A list of specific flag keys to evaluate. If provided, only these flags will be evaluated, improving performance.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "The device ID for this request.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FlagsResponse" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "decision = posthog.get_flags_decision('user123')" + } + ] + }, + { + "id": "get_remote_config_payload", + "title": "get_remote_config_payload", + "description": "Get the payload for a remote config feature flag.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The remote config feature flag key.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "get_tags", + "title": "get_tags", + "description": "Get all tags from the current context. Returns: Dict of all tags in the current context.", + "details": "", + "category": "Contexts", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "dict[str, Any]" + } + }, + { + "id": "group_identify", + "title": "group_identify", + "description": "Identify a group and set its properties.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "group_type", + "description": "The type of group (e.g., 'company', 'team'). Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "str" + }, + { + "name": "group_key", + "description": "The unique identifier for the group. Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "str" + }, + { + "name": "properties", + "description": "A dictionary of properties to set on the group.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "timestamp", + "description": "The timestamp of the event. UTC is preferred; non-UTC datetimes and parseable ISO timestamp strings are converted to UTC.", + "isOptional": false, + "type": "datetime" + }, + { + "name": "uuid", + "description": "A unique identifier for the event. If provided, it must be a valid UUID string or uuid.UUID instance; invalid values are ignored and replaced with a newly generated UUID.", + "isOptional": false, + "type": "str" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP for this event.", + "isOptional": true, + "type": "bool" + }, + { + "name": "distinct_id", + "description": "The distinct ID of the user performing the action.", + "isOptional": false, + "type": "Number" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.group_identify('company', 'company_id_in_your_db', {\n 'name': 'Awesome Inc.',\n 'employees': 11\n})" + } + ] + }, + { + "id": "identify_context", + "title": "identify_context", + "description": "Identify the current context with a distinct ID.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID to associate with the current context and its children.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + } + }, + { + "id": "join", + "title": "join", + "description": "Attempt to process queued events and end the consumer threads. Do not use directly, call `shutdown()` instead. Failed or undrainable events may be dropped and reported through logging or ``on_error``; returning does not guarantee server receipt. Lifecycle cleanup is attempted once, and cleanup failures are logged without retry.", + "details": "", + "category": null, + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.join()" + } + ] + }, + { + "id": "load_feature_flags", + "title": "load_feature_flags", + "description": "Load feature flags for local evaluation.", + "details": "", + "category": "Feature flags", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.load_feature_flags()" + } + ] + }, + { + "id": "new_context", + "title": "new_context", + "description": "Create a new context for managing shared state. Learn more about [contexts](/docs/libraries/python#contexts).", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "fresh", + "description": "Whether to create a fresh context that doesn't inherit from parent.", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_exceptions", + "description": "Whether to automatically capture exceptions in this context. If omitted, defaults to this client's exception autocapture setting.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "with client.new_context():\n client.identify_context('')\n client.capture('event_name')" + } + ] + }, + { + "id": "scoped", + "title": "scoped", + "description": "Decorator that creates a new context for the wrapped function using this client.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "fresh", + "description": "Whether to create a fresh context that doesn't inherit from parent.", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_exceptions", + "description": "Whether to automatically capture exceptions in this context. If omitted, defaults to this client's exception autocapture setting.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set", + "title": "set", + "description": "Set properties on a person profile.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalSetArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Set with distinct id", + "code": "# Set with distinct id\nposthog.set(distinct_id='user123', properties={'name': 'Max Hedgehog'})" + } + ] + }, + { + "id": "set_context_device_id", + "title": "set_context_device_id", + "description": "Set the device ID for the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "device_id", + "description": "The device ID to associate with the current context and its children.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + } + }, + { + "id": "set_context_session", + "title": "set_context_session", + "description": "Set the session ID for the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "session_id", + "description": "The session ID to associate with the current context and its children.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + } + }, + { + "id": "set_once", + "title": "set_once", + "description": "Set properties on a person profile only if they haven't been set before.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalSetArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.set_once(distinct_id='user123', properties={'initial_signup_date': '2024-01-01'})" + } + ] + }, + { + "id": "shutdown", + "title": "shutdown", + "description": "Attempt final delivery and permanently shut down the client. Call this for final process cleanup or when disposing of a client that will not be reused. It closes capture lanes, stops background workers and the feature flag poller, and tears down integrations. Do not call it at the end of each request or serverless invocation when the client is shared; call ``flush()`` before returning instead. A shut-down client cannot be reused; create a new client if needed. Normally this method blocks until queued events have been attempted and cleanup finishes. Failed or undrainable events may be dropped and reported through logging or ``on_error``; returning does not guarantee server receipt. Event draining has no timeout. Queued spans get one final flush with up to 30 seconds waiting for an in-flight flush, then a fresh 30-second drain budget; the last HTTP request is not cut short. Any spans it cannot send are discarded with a warning, as are spans still open. Lifecycle cleanup is attempted once, and cleanup failures are logged without retry. When called directly from an SDK callback such as ``on_error``, shutdown is deferred to avoid blocking the worker that invoked the callback. If the callback must coordinate a blocking shutdown, have it signal an application-owned thread and return before that thread calls shutdown. Do not wait inside the callback for another thread or task that calls a lifecycle method.", + "details": "", + "category": null, + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog.shutdown()" + } + ] + }, + { + "id": "start_span", + "title": "start_span", + "description": "Start a span for distributed tracing. Alpha. Returns a span handle. Use it as a context manager to make it the active span for the block and end it on exit (recording a raised exception on the way out); or call ``end()`` yourself for a span that cannot wrap a block. Spans started inside the block nest under it automatically. Always returns a usable handle, even when tracing is off, so calling code never branches.", + "details": "", + "category": "Tracing", + "params": [ + { + "name": "name", + "description": "A low-cardinality operation name, e.g. ``GET /users/:id``. Variable values belong in attributes, not the name.", + "isOptional": true, + "type": "str" + }, + { + "name": "kind", + "description": "``internal`` (default), ``server``, ``client``, ``producer`` or ``consumer``.", + "isOptional": true, + "type": "str" + }, + { + "name": "attributes", + "description": "Initial attributes.", + "isOptional": true, + "type": "Mapping[str, Any]" + }, + { + "name": "parent", + "description": "A span handle, or an inbound W3C ``traceparent`` header value to continue a remote trace. Defaults to the active span. A forked child starts with no active span; pass the parent span to continue a trace across a fork.", + "isOptional": false, + "type": "Span" + }, + { + "name": "tracestate", + "description": "The inbound ``tracestate`` header accompanying a ``traceparent`` string ``parent``; preserved and propagated.", + "isOptional": true, + "type": "str" + }, + { + "name": "start_time", + "description": "A ``datetime`` or epoch seconds, to backdate the span.", + "isOptional": false, + "type": "datetime" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Span" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "posthog = Posthog(\"\", traces={\"service_name\": \"checkout-api\"})\n\nwith posthog.start_span(\"POST /checkout\", parent=request.headers.get(\"traceparent\")) as span:\n span.set_attribute(\"plan\", user.plan)\n with posthog.start_span(\"db.query\", kind=\"client\"):\n ...\n outgoing_headers = {\"traceparent\": span.traceparent()}" + } + ] + }, + { + "id": "tag", + "title": "tag", + "description": "Add a tag to the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "name", + "description": "The tag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "value", + "description": "The tag value.", + "isOptional": true, + "type": "Any" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + } + } + ] + }, + { + "id": "PostHogModule", + "title": "PostHog Module Functions", + "description": "Global functions available in the PostHog module", + "functions": [ + { + "id": "alias", + "title": "alias", + "description": "Associate user behaviour before and after they e.g. register, login, or perform some other identifying action.", + "details": "To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call. This will allow you to answer questions like \"Which marketing channels leads to users churning after a month?\" or \"What do users do on our website before signing up?\". Particularly useful for associating user behaviour before and after they e.g. register, login, or perform some other identifying action.", + "category": "Identification", + "params": [ + { + "name": "previous_id", + "description": "The unique ID of the user before", + "isOptional": true, + "type": "Number" + }, + { + "name": "distinct_id", + "description": "The current unique id", + "isOptional": true, + "type": "str" + }, + { + "name": "timestamp", + "description": "Optional timestamp for the event. UTC is preferred; non-UTC datetimes and parseable ISO timestamp strings are converted to UTC.", + "isOptional": false, + "type": "datetime" + }, + { + "name": "uuid", + "description": "Optional UUID for the event", + "isOptional": true, + "type": "str" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Alias user", + "code": "# Alias user\nfrom posthog import alias\nalias(previous_id='distinct_id', distinct_id='alias_id')" + } + ] + }, + { + "id": "capture", + "title": "capture", + "description": "Capture anything a user does within your system.", + "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.", + "category": "Events", + "params": [ + { + "name": "event", + "description": "The event name to specify the event **kwargs: Optional arguments including:", + "isOptional": true, + "type": "str" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Context and capture usage", + "code": "# Context and capture usage\nfrom posthog import new_context, identify_context, tag_context, capture\n# Enter a new context (e.g. a request/response cycle, an instance of a background job, etc)\nwith new_context():\n # Associate this context with some user, by distinct_id\n identify_context('some user')\n\n # Capture an event, associated with the context-level distinct ID ('some user')\n capture('movie started')\n\n # Capture an event associated with some other user (overriding the context-level distinct ID)\n capture('movie joined', distinct_id='some-other-user')\n\n # Capture an event with some properties\n capture('movie played', properties={'movie_id': '123', 'category': 'romcom'})\n\n # Capture an event with some properties\n capture('purchase', properties={'product_id': '123', 'category': 'romcom'})\n # Capture an event with some associated group\n capture('purchase', groups={'company': 'id:5'})\n\n # Adding a tag to the current context will cause it to appear on all subsequent events\n tag_context('some-tag', 'some-value')\n\n capture('another-event') # Will be captured with `'some-tag': 'some-value'` in the properties dict" + }, + { + "id": "example_2", + "name": "Set event properties", + "code": "# Set event properties\nfrom posthog import capture\ncapture(\n \"user_signed_up\",\n distinct_id=\"distinct_id_of_the_user\",\n properties={\n \"login_type\": \"email\",\n \"is_free_trial\": \"true\"\n }\n)" + } + ] + }, + { + "id": "capture_ai", + "title": "capture_ai", + "description": "Capture an AI event on the dedicated AI capture endpoint. Beta: the signature is stable; operational limits (per-event size cap, batching, endpoint) may change without notice. Takes the same arguments and returns the same value as `capture()`: the event UUID, or None when the event was not admitted (disabled client, or dropped by `before_send`). The event is delivered on an isolated queue with its own consumer pool and a higher per-event size cap, posting to the dedicated AI ingestion endpoint. The payload is sent as given \u2014 no redaction or truncation is applied here.", + "details": "", + "category": "Events", + "params": [ + { + "name": "event", + "description": "The event name, normally one of the `$ai_*` event names. **kwargs: Same optional arguments as `capture()`.", + "isOptional": true, + "type": "str" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import capture_ai\n\nuuid = capture_ai(\n \"$ai_generation\",\n distinct_id=\"user_123\",\n properties={\"$ai_model\": \"gpt-5\"},\n)" + } + ] + }, + { + "id": "capture_exception", + "title": "capture_exception", + "description": "Capture exceptions that happen in your code.", + "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`.", + "category": "Events", + "params": [ + { + "name": "exception", + "description": "The exception to capture. If not provided, the current exception is captured via `sys.exc_info()` **kwargs: Optional capture arguments including distinct_id, properties, timestamp, uuid, groups, flags, send_feature_flags, and disable_geoip. Overriding reserved exception properties through ``properties`` is deprecated and will stop working in the next major version.", + "isOptional": false, + "type": "BaseException" + }, + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalCaptureArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Capture exception", + "code": "# Capture exception\nfrom posthog import capture_exception\ntry:\n risky_operation()\nexcept Exception as e:\n capture_exception(e)" + } + ] + }, + { + "id": "evaluate_flags", + "title": "evaluate_flags", + "description": "Evaluate all feature flags for a user in a single call and return a :class:`FeatureFlagEvaluations` snapshot. Branch on ``.is_enabled()`` / ``.get_flag()`` and pass the same snapshot to ``capture()`` via the ``flags`` option so events carry the exact flag values the code branched on. Prefer this over repeated ``get_feature_flag()`` calls and over ``capture(send_feature_flags=True)`` \u2014 it consolidates flag evaluation into a single ``/flags`` request per incoming request.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The user's distinct ID. If ``None``, falls back to the context distinct_id. If still unresolvable, returns an empty snapshot.", + "isOptional": false, + "type": "Number" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "If ``True``, never fall back to remote evaluation and omit flags that cannot be evaluated locally. The snapshot's ``unresolved_flags`` lists them with a reason.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "flag_keys", + "description": "Optional list that scopes local evaluation, the underlying ``/flags`` request, and the returned snapshot. When omitted or ``None``, all flags are evaluated. An empty list returns an empty snapshot without evaluating flags. A requested key absent from loaded local definitions is included in one remote fallback per ``evaluate_flags`` call unless ``only_evaluate_locally`` is ``True``. If the server also does not know the key, it is omitted from the snapshot.", + "isOptional": true, + "type": "list[str]" + }, + { + "name": "device_id", + "description": "Optional device ID override. If not provided, falls back to the context device_id (which may be set via tracing headers). Used by experience-continuity flags to match users across distinct_id changes.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FeatureFlagEvaluations" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import evaluate_flags, capture\nflags = evaluate_flags(\"user_123\", person_properties={\"plan\": \"enterprise\"})\nif flags.is_enabled(\"new-dashboard\"):\n render_new_dashboard()\ncapture(\"page_viewed\", distinct_id=\"user_123\", flags=flags)" + } + ] + }, + { + "id": "feature_enabled", + "title": "feature_enabled", + "description": "Use feature flags to enable or disable features for users.", + "details": "You can call `posthog.load_feature_flags()` before to make sure you're not doing unexpected requests.", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The user's distinct ID", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Groups mapping", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[bool]" + }, + "examples": [ + { + "id": "example_1", + "name": "Boolean feature flag", + "code": "# Boolean feature flag\nfrom posthog import feature_enabled, get_feature_flag_payload\nis_my_flag_enabled = feature_enabled('flag-key', 'distinct_id_of_your_user')\nif is_my_flag_enabled:\n matched_flag_payload = get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "feature_flag_definitions", + "title": "feature_flag_definitions", + "description": "Returns loaded feature flags.", + "details": "Returns loaded feature flags, if any. Helpful for debugging what flag information you have loaded.", + "category": "Feature flags", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import feature_flag_definitions\ndefinitions = feature_flag_definitions()" + } + ] + }, + { + "id": "flush", + "title": "flush", + "description": "Attempt immediate delivery of queued events and ended spans, keeping the global client reusable. Call ``flush()`` before a short-lived request or serverless invocation returns when using the shared module-level client. Use ``shutdown()`` only for final process cleanup or a client that will not be reused. Flush does not stop the background workers or feature flag poller, and does not end open spans. Returning does not guarantee server receipt: pending work can remain after a timeout or span export failure, and failed events may be dropped by consumers. Calls made directly from SDK callbacks such as ``on_error`` are deferred rather than blocking the worker that invoked the callback.", + "details": "", + "category": "Client management", + "params": [ + { + "name": "timeout_seconds", + "description": "Seconds to wait for event queues to drain, shared across event lanes. Defaults to 10 seconds. Pass ``None`` to wait without a time limit; this does not guarantee successful delivery. Queued spans flush concurrently, waiting up to this many seconds for an in-flight flush before starting a fresh drain budget. A lock wait timeout leaves the in-flight flush to finish. At least one span request is attempted after acquiring the lock, even with no budget left; retriable failures are retried after backoff while budget remains, with one last attempt at the deadline. The last span request is not cut short and is bounded by the client's ``timeout`` in seconds, so this is not a strict wall-clock limit for the whole call. With ``None``, a retriable span failure is left for a later flush rather than retried within this call.", + "isOptional": true, + "type": "float" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "import posthog\nposthog.capture('event_name', distinct_id='user_id')\n# Before a serverless invocation returns; keep the global client open.\nposthog.flush(timeout_seconds=10)" + } + ] + }, + { + "id": "get_active_span", + "title": "get_active_span", + "description": "The span that is active in the current context, or ``None``. Alpha. Only entering a span (``with posthog.start_span(...) as span:``) makes it active; a span started manually is not. Use it to propagate the trace to the next service: ``span.traceparent()`` is the header value.", + "details": "", + "category": "Tracing", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[Span]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "span = posthog.get_active_span()\nif span is not None:\n headers[\"traceparent\"] = span.traceparent()" + } + ] + }, + { + "id": "get_all_flags", + "title": "get_all_flags", + "description": "Get all flags for a given user.", + "details": "Flags are key-value pairs where the key is the flag key and the value is the flag variant, or True, or False.", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The user's distinct ID", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Groups mapping", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags", + "isOptional": true, + "type": "str" + }, + { + "name": "flag_keys_to_evaluate", + "description": "Optional list of flag keys to evaluate (evaluates all if None)", + "isOptional": true, + "type": "list[str]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[dict[str, Union[bool, str]]]" + }, + "examples": [ + { + "id": "example_1", + "name": "All flags for user", + "code": "# All flags for user\nfrom posthog import get_all_flags\nget_all_flags('distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_all_flags_and_payloads", + "title": "get_all_flags_and_payloads", + "description": "Get all feature flag values and payloads for a user.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "distinct_id", + "description": "The user's distinct ID.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags.", + "isOptional": true, + "type": "str" + }, + { + "name": "flag_keys_to_evaluate", + "description": "Optional list of flag keys to evaluate. Evaluates all flags when omitted.", + "isOptional": true, + "type": "list[str]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "FlagsAndPayloads" + } + }, + { + "id": "get_feature_flag", + "title": "get_feature_flag", + "description": "Get feature flag variant for users. Used with experiments.", + "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}}.", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The user's distinct ID", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Groups mapping from group type to group key", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties in format { group_type_name: { group_properties } }", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send feature flag events", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Union[bool, str, any]" + }, + "examples": [ + { + "id": "example_1", + "name": "Multivariate feature flag", + "code": "# Multivariate feature flag\nfrom posthog import get_feature_flag, get_feature_flag_payload\nenabled_variant = get_feature_flag('flag-key', 'distinct_id_of_your_user')\nif enabled_variant == 'variant-key':\n matched_flag_payload = get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')" + } + ] + }, + { + "id": "get_feature_flag_evaluation_runtime", + "title": "get_feature_flag_evaluation_runtime", + "description": "Return where a locally loaded feature flag is meant to be evaluated.", + "details": "Reads the `evaluation_runtime` each flag definition carries, so no extra request is made. Returns `None` when local evaluation has not loaded a definition for this key. A definition that carries no runtime reports `FeatureFlagEvaluationRuntime.ALL`, the default PostHog applies.", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[FeatureFlagEvaluationRuntime]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import FeatureFlagEvaluationRuntime, get_feature_flag_evaluation_runtime\nruntime = get_feature_flag_evaluation_runtime(\"my-flag\")" + } + ] + }, + { + "id": "get_feature_flag_keys_by_evaluation_runtime", + "title": "get_feature_flag_keys_by_evaluation_runtime", + "description": "Return the keys of locally loaded flags that a runtime can evaluate.", + "details": "A flag set to `FeatureFlagEvaluationRuntime.ALL` suits either runtime, so it is returned for `CLIENT` and for `SERVER`. Use this to decide which flags to hand to a browser when a backend serves flags to its own frontend.", + "category": "Feature flags", + "params": [ + { + "name": "evaluation_runtime", + "description": "", + "isOptional": true, + "type": "FeatureFlagEvaluationRuntime" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "list[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import FeatureFlagEvaluationRuntime, get_feature_flag_keys_by_evaluation_runtime\nclient_keys = get_feature_flag_keys_by_evaluation_runtime(FeatureFlagEvaluationRuntime.CLIENT)" + } + ] + }, + { + "id": "get_feature_flag_payload", + "title": "get_feature_flag_payload", + "description": "Get the payload associated with a feature flag value. Deprecated for new code. Prefer ``evaluate_flags()`` and ``flags.get_flag_payload(key)`` so flag evaluation happens once per request.", + "details": "", + "category": "Feature flags", + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The user's distinct ID.", + "isOptional": true, + "type": "Number" + }, + { + "name": "match_value", + "description": "Optional flag value to use when selecting a payload.", + "isOptional": false, + "type": "bool" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send a $feature_flag_called event.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[object]" + } + }, + { + "id": "get_feature_flag_result", + "title": "get_feature_flag_result", + "description": "Get a FeatureFlagResult object which contains the flag result and payload. This method evaluates a feature flag and returns a FeatureFlagResult object containing: - enabled: Whether the flag is enabled - variant: The variant value if the flag has variants - payload: The payload associated with the flag (automatically deserialized from JSON) - key: The flag key - reason: Why the flag was enabled/disabled", + "details": "", + "category": null, + "params": [ + { + "name": "key", + "description": "The feature flag key.", + "isOptional": true, + "type": "str" + }, + { + "name": "distinct_id", + "description": "The user's distinct ID.", + "isOptional": true, + "type": "Number" + }, + { + "name": "groups", + "description": "Mapping of group type to group key.", + "isOptional": true, + "type": "Mapping[str, Union[str, int]]" + }, + { + "name": "person_properties", + "description": "Person properties to use for evaluation.", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "group_properties", + "description": "Group properties keyed by group type.", + "isOptional": true, + "type": "dict[str, dict[str, Any]]" + }, + { + "name": "only_evaluate_locally", + "description": "Whether to evaluate only locally.", + "isOptional": false, + "type": "bool" + }, + { + "name": "send_feature_flag_events", + "description": "Whether to send a $feature_flag_called event.", + "isOptional": false, + "type": "bool" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup.", + "isOptional": true, + "type": "bool" + }, + { + "name": "device_id", + "description": "Optional device ID override for experience-continuity flags.", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[FeatureFlagResult]" + } + }, + { + "id": "get_remote_config_payload", + "title": "get_remote_config_payload", + "description": "Get the payload for a remote config feature flag.", + "details": "", + "category": null, + "params": [ + { + "name": "key", + "description": "The key of the feature flag", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "get_tags", + "title": "get_tags", + "description": "Get all tags from the current context. Returns: Dict of all tags in the current context", + "details": "", + "category": "Contexts", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "dict[str, Any]" + } + }, + { + "id": "group_identify", + "title": "group_identify", + "description": "Set properties on a group.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "group_type", + "description": "Type of your group. Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "str" + }, + { + "name": "group_key", + "description": "Unique identifier of the group. Required - the call is dropped with a warning if it is missing or empty.", + "isOptional": true, + "type": "str" + }, + { + "name": "properties", + "description": "Properties to set on the group", + "isOptional": true, + "type": "dict[str, Any]" + }, + { + "name": "timestamp", + "description": "Optional timestamp for the event. UTC is preferred; non-UTC datetimes and parseable ISO timestamp strings are converted to UTC.", + "isOptional": false, + "type": "datetime" + }, + { + "name": "uuid", + "description": "Optional UUID for the event", + "isOptional": true, + "type": "str" + }, + { + "name": "disable_geoip", + "description": "Whether to disable GeoIP lookup", + "isOptional": true, + "type": "bool" + }, + { + "name": "distinct_id", + "description": "Optional distinct ID of the user performing the action", + "isOptional": false, + "type": "Number" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Group identify", + "code": "# Group identify\nfrom posthog import group_identify\ngroup_identify('company', 'company_id_in_your_db', {\n 'name': 'Awesome Inc.',\n 'employees': 11\n})" + } + ] + }, + { + "id": "identify_context", + "title": "identify_context", + "description": "Identify the current context with a distinct ID.", + "details": "", + "category": "Identification", + "params": [ + { + "name": "distinct_id", + "description": "The distinct ID to associate with the current context and its children", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import identify_context\nidentify_context(\"user_123\")" + } + ] + }, + { + "id": "join", + "title": "join", + "description": "Attempt to process queued events and stop the client's background workers. Use `shutdown()` directly in most cases. Failed or undrainable events may be dropped and reported through logging or ``on_error``; returning does not guarantee server receipt. Lifecycle cleanup is attempted once, and cleanup failures are logged without retry.", + "details": "", + "category": "Client management", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import join\njoin()" + } + ] + }, + { + "id": "load_feature_flags", + "title": "load_feature_flags", + "description": "Load feature flag definitions from PostHog.", + "details": "", + "category": "Feature flags", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import load_feature_flags\nload_feature_flags()" + } + ] + }, + { + "id": "new_context", + "title": "new_context", + "description": "Create a new context scope that will be active for the duration of the with block.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "fresh", + "description": "Whether to start with a fresh context (default: False)", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_exceptions", + "description": "Whether to capture exceptions raised within the context. If omitted, defaults to the relevant client's exception autocapture setting.", + "isOptional": true, + "type": "bool" + }, + { + "name": "client", + "description": "Optional Posthog client instance to use for this context (default: None)", + "isOptional": true, + "type": "Client" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import new_context, tag, capture\nwith new_context():\n tag(\"request_id\", \"123\")\n capture(\"event_name\", properties={\"property\": \"value\"})" + } + ] + }, + { + "id": "scoped", + "title": "scoped", + "description": "Decorator that creates a new context for the function.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "fresh", + "description": "Whether to start with a fresh context (default: False)", + "isOptional": false, + "type": "bool" + }, + { + "name": "capture_exceptions", + "description": "Whether to capture and track exceptions with posthog error tracking. If omitted, defaults to the global exception autocapture setting.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import scoped, tag, capture\n@scoped()\ndef process_payment(payment_id):\n tag(\"payment_id\", payment_id)\n capture(\"payment_started\")" + } + ] + }, + { + "id": "set", + "title": "set", + "description": "Set properties on a user record.", + "details": "This will overwrite previous people property values. Generally operates similar to `capture`, with distinct_id being an optional argument, defaulting to the current context's distinct ID. If there is no context-level distinct ID, and no override distinct_id is passed, this function will do nothing. Context tags are folded into $set properties, so tagging the current context and then calling `set` will cause those tags to be set on the user (unlike capture, which causes them to just be set on the event).", + "category": "Identification", + "params": [ + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalSetArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Set person properties", + "code": "# Set person properties\nfrom posthog import set\nset(distinct_id='distinct_id', properties={'name': 'Max Hedgehog'})" + } + ] + }, + { + "id": "set_capture_exception_code_variables_context", + "title": "set_capture_exception_code_variables_context", + "description": "Override code-variable capture for exceptions in the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "enabled", + "description": "Whether exceptions captured in this context should include local variable values from stack frames.", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_code_variables_detect_secrets_context", + "title": "set_code_variables_detect_secrets_context", + "description": "Whether to apply entropy-based secret detection as a last-resort redaction of high-entropy values (API keys, tokens, strong passwords) in captured code variables for the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "enabled", + "description": "", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_code_variables_ignore_patterns_context", + "title": "set_code_variables_ignore_patterns_context", + "description": "Override code-variable ignore patterns for exceptions in the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "ignore_patterns", + "description": "Variable-name patterns that should be omitted entirely when code variables are captured.", + "isOptional": true, + "type": "list[str]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_code_variables_mask_patterns_context", + "title": "set_code_variables_mask_patterns_context", + "description": "Override code-variable mask patterns for exceptions in the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "mask_patterns", + "description": "Variable-name patterns whose values should be replaced with ``***`` when code variables are captured.", + "isOptional": true, + "type": "list[str]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_code_variables_mask_url_credentials_context", + "title": "set_code_variables_mask_url_credentials_context", + "description": "Whether to scrub credentials embedded in URLs/DSNs (e.g. user:pass@host) from captured code variables for the current context.", + "details": "", + "category": null, + "params": [ + { + "name": "enabled", + "description": "", + "isOptional": true, + "type": "bool" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + } + }, + { + "id": "set_context_device_id", + "title": "set_context_device_id", + "description": "Set the device ID for the current context, associating all feature flag requests in this or child contexts with the given device ID.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "device_id", + "description": "The device ID to associate with the current context and its children", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import set_context_device_id\nset_context_device_id(\"device_123\")" + } + ] + }, + { + "id": "set_context_session", + "title": "set_context_session", + "description": "Set the session ID for the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "session_id", + "description": "The session ID to associate with the current context and its children", + "isOptional": true, + "type": "str" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import set_context_session\nset_context_session(\"session_123\")" + } + ] + }, + { + "id": "set_once", + "title": "set_once", + "description": "Set properties on a user record, only if they do not yet exist.", + "details": "This will not overwrite previous people property values, unlike `set`. Otherwise, operates in an identical manner to `set`.", + "category": "Identification", + "params": [ + { + "name": "kwargs", + "description": "", + "isOptional": true, + "type": "Unpack[OptionalSetArgs]" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Optional[str]" + }, + "examples": [ + { + "id": "example_1", + "name": "Set property once", + "code": "# Set property once\nfrom posthog import set_once\nset_once(distinct_id='distinct_id', properties={'initial_url': '/blog'})" + } + ] + }, + { + "id": "setup", + "title": "setup", + "description": "Create or return the global PostHog client configured by module settings. Most applications should either instantiate ``Posthog`` directly or set ``posthog.api_key``/other module settings before calling top-level helpers. ``setup()`` is called automatically by global APIs such as ``capture()``. Returns: The global ``Client`` instance. If both ``api_key`` and ``project_api_key`` are missing or blank, the client is disabled and module-level calls become no-ops.", + "details": "", + "category": "Initialization", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Client" + } + }, + { + "id": "shutdown", + "title": "shutdown", + "description": "Attempt final delivery and permanently shut down the global client. Call this for final process cleanup, not at the end of each request or serverless invocation that shares the module-level client. Use ``flush()`` before returning from those invocations instead. Shutdown closes capture lanes, stops background workers and the feature flag poller, and tears down integrations. The global client is not recreated automatically after shutdown. This normally blocks until queued events have been attempted and cleanup finishes. Failed or undrainable events may be dropped and reported through logging or ``on_error``; returning does not guarantee server receipt. Event draining has no timeout. Queued spans get a final flush with up to 30 seconds waiting for an in-flight flush, then a fresh 30-second drain budget; the last HTTP request is not cut short. Unsent queued spans and still-open spans are discarded with a warning. Lifecycle cleanup is attempted once, and cleanup failures are logged without retry. Calls made directly from SDK callbacks such as ``on_error`` are deferred to avoid deadlocking the worker. If blocking completion is required, signal an application-owned thread, return from the callback, and call ``shutdown()`` from that thread. Do not wait inside a callback for another thread or task calling a lifecycle method.", + "details": "", + "category": "Client management", + "params": [], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "any" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import shutdown\nshutdown()" + } + ] + }, + { + "id": "start_span", + "title": "start_span", + "description": "Start a span for distributed tracing. Alpha. Returns a span handle. Use it as a context manager to make it the active span for the block and end it on exit (recording a raised exception on the way out); or call ``end()`` yourself for a span that cannot wrap a block. Spans started inside the block nest under it automatically. Always returns a usable handle, even when tracing is off, so calling code never branches.", + "details": "", + "category": "Tracing", + "params": [ + { + "name": "name", + "description": "A low-cardinality operation name, e.g. ``GET /users/:id``. Variable values belong in attributes, not the name.", + "isOptional": true, + "type": "str" + }, + { + "name": "kind", + "description": "``internal`` (default), ``server``, ``client``, ``producer`` or ``consumer``.", + "isOptional": true, + "type": "str" + }, + { + "name": "attributes", + "description": "Initial attributes.", + "isOptional": true, + "type": "Mapping[str, Any]" + }, + { + "name": "parent", + "description": "A span handle, or an inbound W3C ``traceparent`` header value to continue a remote trace. Defaults to the active span.", + "isOptional": false, + "type": "Span" + }, + { + "name": "tracestate", + "description": "The inbound ``tracestate`` header accompanying a ``traceparent`` string ``parent``; preserved and propagated.", + "isOptional": true, + "type": "str" + }, + { + "name": "start_time", + "description": "A ``datetime`` or epoch seconds, to backdate the span.", + "isOptional": false, + "type": "datetime" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "Span" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "import posthog\nposthog.traces = {\"service_name\": \"checkout-api\"}\n\nwith posthog.start_span(\"POST /checkout\", parent=request.headers.get(\"traceparent\")) as span:\n span.set_attribute(\"plan\", user.plan)\n with posthog.start_span(\"db.query\", kind=\"client\"):\n ...\n outgoing_headers = {\"traceparent\": span.traceparent()}" + } + ] + }, + { + "id": "tag", + "title": "tag", + "description": "Add a tag to the current context.", + "details": "", + "category": "Contexts", + "params": [ + { + "name": "name", + "description": "The tag key", + "isOptional": true, + "type": "str" + }, + { + "name": "value", + "description": "The tag value", + "isOptional": true, + "type": "Any" + } + ], + "showDocs": true, + "releaseTag": "public", + "returnType": { + "id": "return_type", + "name": "None" + }, + "examples": [ + { + "id": "example_1", + "name": "Example 1", + "code": "from posthog import tag\ntag(\"user_id\", \"123\")" + } + ] + } + ] + } + ], + "categories": [ + "Initialization", + "Identification", + "Capture", + "Error Tracking", + "Feature flags", + "Tracing", + "Contexts", + "Events", + "Client management" + ] +} \ No newline at end of file diff --git a/references/posthog-python-references-latest.json b/references/posthog-python-references-latest.json index ea89d2637..a2d0c5a3e 100644 --- a/references/posthog-python-references-latest.json +++ b/references/posthog-python-references-latest.json @@ -2,7 +2,7 @@ "id": "posthog-python", "hogRef": "0.3", "info": { - "version": "7.83.0", + "version": "7.83.1", "id": "posthog-python", "title": "PostHog Python SDK", "description": "Integrate PostHog into any python application.", From 9f25ef9950a12dd37bd9e2199c66c30e0cead1ca Mon Sep 17 00:00:00 2001 From: Eli Reisman Date: Tue, 3 Nov 2026 16:26:13 -0800 Subject: [PATCH 7/7] test(flags): read flag scenario events in before_send on v1 v1 passes flag properties to _enqueue separately and layers them while building the event, so a stubbed _enqueue never saw them. Reading the built event in before_send compares v1's real output with 7.82.1. Co-Authored-By: Claude Opus 5.5 --- posthog/test/released_flags_scenarios.py | 5 +++-- posthog/test/test_flags_v4_response.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/posthog/test/released_flags_scenarios.py b/posthog/test/released_flags_scenarios.py index b3cacdac1..35b3736e5 100644 --- a/posthog/test/released_flags_scenarios.py +++ b/posthog/test/released_flags_scenarios.py @@ -168,14 +168,15 @@ def flags(*args: Any, **kwargs: Any) -> Any: raise response return copy.deepcopy(response) - def enqueue(message: Dict[str, Any], *args: Any, **kwargs: Any) -> Optional[str]: + # Read the built event in before_send: v1 layers flag properties inside _enqueue. + def record(message: Dict[str, Any]) -> Optional[Dict[str, Any]]: events.append([message["event"], _flag_event_properties(message)]) return None try: with ( mock.patch("posthog.client.flags", side_effect=flags), - mock.patch.object(client, "_enqueue", side_effect=enqueue), + mock.patch.object(client, "before_send", record), warnings.catch_warnings(), ): warnings.simplefilter("ignore") diff --git a/posthog/test/test_flags_v4_response.py b/posthog/test/test_flags_v4_response.py index 662011dce..d6278b101 100644 --- a/posthog/test/test_flags_v4_response.py +++ b/posthog/test/test_flags_v4_response.py @@ -1027,14 +1027,15 @@ def _legacy_outputs(shape: str) -> Dict[str, Any]: def response(*args: Any, **kwargs: Any) -> Dict[str, Any]: return _parity_response(shape) - def enqueue(message: Dict[str, Any], *args: Any, **kwargs: Any) -> Optional[str]: + # Read the built event in before_send: v1 layers flag properties inside _enqueue. + def record(message: Dict[str, Any]) -> Optional[Dict[str, Any]]: events.append((message["event"], _flag_event_properties(message))) return None try: with ( mock.patch("posthog.client.flags", side_effect=response), - mock.patch.object(client, "_enqueue", side_effect=enqueue), + mock.patch.object(client, "before_send", record), pytest.warns(DeprecationWarning), ): outputs["get_feature_variants"] = client.get_feature_variants(DISTINCT_ID)