Skip to content

Commit 0f828c1

Browse files
committed
feat!: super_options, context options and personless as the lowest option layer
1 parent a515200 commit 0f828c1

9 files changed

Lines changed: 390 additions & 35 deletions

File tree

‎posthog/__init__.py‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
5252
from posthog.contexts import (
5353
get_tags as inner_get_tags,
5454
)
55+
from posthog.contexts import (
56+
set_context_option as inner_set_context_option,
57+
)
58+
from posthog.contexts import (
59+
get_context_options as inner_get_context_options,
60+
)
5561
from posthog.exception_utils import (
5662
DEFAULT_CODE_VARIABLES_DETECT_SECRETS,
5763
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
@@ -299,6 +305,43 @@ def get_tags() -> Dict[str, Any]:
299305
return inner_get_tags()
300306

301307

308+
def set_context_option(key: str, value: Any) -> None:
309+
"""
310+
Set a capture option for every event captured within the current context.
311+
312+
Context options override ``super_options``, and an event's own ``options``
313+
override context options.
314+
315+
Args:
316+
key: The option name, such as ``"process_person_profile"``
317+
value: The option value, sent as given
318+
319+
Examples:
320+
```python
321+
from posthog import new_context, set_context_option
322+
with new_context():
323+
set_context_option("process_person_profile", False)
324+
```
325+
326+
Category:
327+
Contexts
328+
"""
329+
return inner_set_context_option(key, value)
330+
331+
332+
def get_context_options() -> Dict[str, Any]:
333+
"""
334+
Get all capture options from the current context.
335+
336+
Returns:
337+
Dict of all capture options in the current context
338+
339+
Category:
340+
Contexts
341+
"""
342+
return inner_get_context_options()
343+
344+
302345
"""Settings.
303346
304347
These module-level settings configure the legacy global PostHog client used by
@@ -340,7 +383,11 @@ def get_tags() -> Dict[str, Any]:
340383
feature_flags_request_max_retries: Number of retries for feature flag
341384
requests after network, transport, or timeout failures. Defaults to 1.
342385
Set to 0 to disable retries.
343-
super_properties: Properties merged into every captured event.
386+
super_properties: Properties for every captured event. Context tags and
387+
an event's own properties override them.
388+
super_options: Capture options for every captured event, such as
389+
``{"cookieless_mode": True}``. Context options and an event's own
390+
``options`` override them.
344391
metrics: Config dict for the ``client.metrics`` API (``service_name``,
345392
``service_version``, ``environment``, ``flush_interval``, ...). Applied
346393
when ``setup()`` builds the global client, or on a later ``setup()``
@@ -414,6 +461,7 @@ def get_tags() -> Dict[str, Any]:
414461
feature_flags_request_timeout_seconds = 3 # type: int
415462
feature_flags_request_max_retries = 1 # type: int
416463
super_properties = None # type: Optional[Dict]
464+
super_options = None # type: Optional[Dict]
417465
metrics = None # type: Optional[Dict]
418466
traces = None # type: Optional[Dict]
419467
enable_exception_autocapture = False # type: bool
@@ -1406,6 +1454,7 @@ def setup() -> Client:
14061454
feature_flags_request_timeout_seconds=feature_flags_request_timeout_seconds,
14071455
feature_flags_request_max_retries=feature_flags_request_max_retries,
14081456
super_properties=super_properties,
1457+
super_options=super_options,
14091458
metrics=metrics,
14101459
traces=traces,
14111460
# TODO: Currently this monitoring begins only when the Client is initialised (which happens when you do something with the SDK)

‎posthog/async_client.py‎

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
Client as _SyncClient,
4444
_metadata_has_experiment,
4545
_parse_flag_payload,
46+
_personless_options,
4647
add_context_tags as _add_context_tags,
4748
get_identity_state as _get_identity_state,
4849
stringify_id as _stringify_id,
@@ -55,6 +56,7 @@
5556
get_code_variables_mask_url_credentials_context,
5657
get_context_device_id as _get_context_device_id,
5758
get_context_distinct_id as _get_context_distinct_id,
59+
get_context_options as _get_context_options,
5860
get_context_session_id as _get_context_session_id,
5961
)
6062
from .exception_utils import (
@@ -120,6 +122,7 @@ def __init__(
120122
is_server: bool = True,
121123
historical_migration: bool = False,
122124
super_properties: Optional[dict[str, Any]] = None,
125+
super_options: Optional[dict[str, Any]] = None,
123126
before_send=None,
124127
log_captured_exceptions: bool = False,
125128
project_root: Optional[str] = None,
@@ -154,6 +157,7 @@ def __init__(
154157
self.is_server = is_server
155158
self.historical_migration = historical_migration
156159
self.super_properties = super_properties
160+
self.super_options = super_options
157161
self._release_id = _resolve_release_id()
158162
self.capture_compression = _resolve_capture_compression(capture_compression)
159163
self.capture_trace_context = capture_trace_context
@@ -397,6 +401,7 @@ def _prepare_event(
397401
msg: dict[str, Any],
398402
disable_geoip: Optional[bool],
399403
property_allowlist=None,
404+
derived_options: Optional[dict[str, Any]] = None,
400405
) -> tuple[Optional[dict[str, Any]], Optional[str]]:
401406
if self.disabled or not self._accepting:
402407
return None, None
@@ -423,7 +428,14 @@ def _prepare_event(
423428
if disable_geoip:
424429
properties["$geoip_disable"] = True
425430
if self.super_properties:
426-
msg["properties"] = {**properties, **self.super_properties}
431+
msg["properties"] = {**self.super_properties, **properties}
432+
# Each layer overrides the one before it: values the SDK derives, such
433+
# as personless, then super options, then context and event options.
434+
msg["options"] = {
435+
**(derived_options or {}),
436+
**_event_options(self.super_options),
437+
**(msg.get("options") or {}),
438+
}
427439
if self._release_id is not None:
428440
msg["properties"].setdefault("$release_id", self._release_id)
429441
if self.is_server:
@@ -460,16 +472,14 @@ async def _process_event(self, msg: dict[str, Any]) -> Optional[dict[str, Any]]:
460472

461473
def _build_capture_event(
462474
self, event: str, kwargs: OptionalCaptureArgs
463-
) -> tuple[dict[str, Any], Optional[bool], Any]:
475+
) -> tuple[dict[str, Any], Optional[bool], Any, dict[str, Any]]:
464476
properties = {**(kwargs.get("properties") or {}), **system_context()}
465477
if self.capture_trace_context:
466478
properties = {**_get_current_otel_span_properties(), **properties}
467479
properties = _add_context_tags(properties)
468480
assert properties is not None
469481

470482
distinct_id, personless = _get_identity_state(kwargs.get("distinct_id"))
471-
if personless and "$process_person_profile" not in properties:
472-
properties["$process_person_profile"] = False
473483
groups = kwargs.get("groups")
474484
if groups:
475485
properties["$groups"] = groups
@@ -493,22 +503,26 @@ def _build_capture_event(
493503
"distinct_id": distinct_id,
494504
"event": event,
495505
"uuid": kwargs.get("uuid"),
496-
"options": _event_options(kwargs.get("options")),
506+
"options": {
507+
**_get_context_options(),
508+
**_event_options(kwargs.get("options")),
509+
},
497510
},
498511
kwargs.get("disable_geoip"),
499512
kwargs.get("_property_allowlist"),
513+
_personless_options(personless),
500514
)
501515

502516
def capture(
503517
self, event: str, **kwargs: Unpack[OptionalCaptureArgs]
504518
) -> Optional[str]:
505519
"""Queue an event without blocking for network delivery."""
506520
try:
507-
msg, disable_geoip, property_allowlist = self._build_capture_event(
508-
event, kwargs
521+
msg, disable_geoip, property_allowlist, derived_options = (
522+
self._build_capture_event(event, kwargs)
509523
)
510524
prepared, sent_uuid = self._prepare_event(
511-
msg, disable_geoip, property_allowlist
525+
msg, disable_geoip, property_allowlist, derived_options
512526
)
513527
if prepared is None or sent_uuid is None:
514528
return None
@@ -551,11 +565,11 @@ async def capture_immediate(
551565
self._immediate_callers[current] = self._immediate_callers.get(current, 0) + 1
552566
error_batch: list[dict[str, Any]] = []
553567
try:
554-
msg, disable_geoip, property_allowlist = self._build_capture_event(
555-
event, kwargs
568+
msg, disable_geoip, property_allowlist, derived_options = (
569+
self._build_capture_event(event, kwargs)
556570
)
557571
prepared, sent_uuid = self._prepare_event(
558-
msg, disable_geoip, property_allowlist
572+
msg, disable_geoip, property_allowlist, derived_options
559573
)
560574
if prepared is None or sent_uuid is None:
561575
return None
@@ -612,7 +626,10 @@ def _build_person_properties_event(
612626
property_key: properties,
613627
"event": event,
614628
"uuid": kwargs.get("uuid"),
615-
"options": _event_options(kwargs.get("options")),
629+
"options": {
630+
**_get_context_options(),
631+
**_event_options(kwargs.get("options")),
632+
},
616633
}
617634

618635
def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:

‎posthog/client.py‎

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@
5151
get_context_device_id,
5252
get_context_distinct_id,
5353
get_context_session_id,
54+
get_context_options as _context_get_context_options,
5455
get_tags as _context_get_tags,
5556
identify_context as _context_identify_context,
5657
_scoped as _context_scoped,
5758
new_context,
5859
set_context_device_id as _context_set_context_device_id,
60+
set_context_option as _context_set_context_option,
5961
set_context_session as _context_set_context_session,
6062
tag as _context_tag,
6163
)
@@ -270,6 +272,11 @@ def _stringify_event_uuid(value) -> str:
270272
return canonical
271273

272274

275+
def _personless_options(personless: bool) -> dict[str, Any]:
276+
"""The lowest option layer: no person profile for a generated distinct ID."""
277+
return {"process_person_profile": False} if personless else {}
278+
279+
273280
def add_context_tags(properties):
274281
properties = properties or {}
275282
current_context = _get_current_context()
@@ -706,6 +713,7 @@ def __init__(
706713
feature_flags_request_timeout_seconds=3,
707714
feature_flags_request_max_retries=1,
708715
super_properties=None,
716+
super_options=None,
709717
enable_exception_autocapture=False,
710718
log_captured_exceptions=False,
711719
project_root=None,
@@ -791,7 +799,11 @@ def __init__(
791799
feature_flags_request_max_retries: Number of retries for feature flag
792800
requests after network, transport, or timeout failures. Defaults
793801
to 1. Set to 0 to disable retries.
794-
super_properties: Properties merged into every captured event.
802+
super_properties: Properties for every captured event. Context
803+
tags and an event's own properties override them.
804+
super_options: Capture options for every captured event, such as
805+
``{"cookieless_mode": True}``. Context options and an event's
806+
own ``options`` override them.
795807
enable_exception_autocapture: Automatically capture uncaught
796808
exceptions.
797809
log_captured_exceptions: Also log exceptions captured by error
@@ -999,6 +1011,7 @@ def __init__(
9991011
maximum=AI_MAX_MSG_SIZE,
10001012
)
10011013
self.super_properties = super_properties
1014+
self.super_options = super_options
10021015
# Release id from POSTHOG_RELEASE_ID, attached to every event. Resolved
10031016
# here so the env var is read once per client.
10041017
self._release_id = _resolve_release_id()
@@ -1311,6 +1324,31 @@ def get_tags(self) -> Dict[str, Any]:
13111324
"""
13121325
return _context_get_tags()
13131326

1327+
def set_context_option(self, key: str, value: Any) -> None:
1328+
"""
1329+
Set a capture option for every event captured within the current context.
1330+
1331+
Args:
1332+
key: The option name, such as ``"process_person_profile"``.
1333+
value: The option value, sent as given.
1334+
1335+
Category:
1336+
Contexts
1337+
"""
1338+
_context_set_context_option(key, value)
1339+
1340+
def get_context_options(self) -> Dict[str, Any]:
1341+
"""
1342+
Get all capture options from the current context.
1343+
1344+
Returns:
1345+
Dict of all capture options in the current context.
1346+
1347+
Category:
1348+
Contexts
1349+
"""
1350+
return _context_get_context_options()
1351+
13141352
def identify_context(self, distinct_id: str) -> None:
13151353
"""
13161354
Identify the current context with a distinct ID.
@@ -1707,7 +1745,10 @@ def _capture(
17071745
flags_snapshot = kwargs.get("flags", None)
17081746
send_feature_flags = kwargs.get("send_feature_flags", False)
17091747
disable_geoip = kwargs.get("disable_geoip", None)
1710-
options = _event_options(kwargs.get("options", None))
1748+
options = {
1749+
**_context_get_context_options(),
1750+
**_event_options(kwargs.get("options", None)),
1751+
}
17111752
# Internal, set for minimal $feature_flag_called events: a strict allowlist
17121753
# applied to the fully-enriched properties dict just before enqueueing.
17131754
property_allowlist = kwargs.get("_property_allowlist", None)
@@ -1722,9 +1763,6 @@ def _capture(
17221763

17231764
(distinct_id, personless) = get_identity_state(distinct_id)
17241765

1725-
if personless and "$process_person_profile" not in properties:
1726-
properties["$process_person_profile"] = False
1727-
17281766
msg = {
17291767
"properties": properties,
17301768
"timestamp": timestamp,
@@ -1829,7 +1867,11 @@ def _capture(
18291867
msg["properties"] = properties
18301868

18311869
return self._enqueue(
1832-
msg, disable_geoip, lane, property_allowlist=property_allowlist
1870+
msg,
1871+
disable_geoip,
1872+
lane,
1873+
property_allowlist=property_allowlist,
1874+
derived_options=_personless_options(personless),
18331875
)
18341876

18351877
def _parse_send_feature_flags(self, send_feature_flags) -> SendFeatureFlagsOptions:
@@ -1917,7 +1959,10 @@ def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
19171959
"$set": properties,
19181960
"event": "$set",
19191961
"uuid": uuid,
1920-
"options": _event_options(kwargs.get("options", None)),
1962+
"options": {
1963+
**_context_get_context_options(),
1964+
**_event_options(kwargs.get("options", None)),
1965+
},
19211966
}
19221967

19231968
return self._enqueue(msg, disable_geoip)
@@ -1967,7 +2012,10 @@ def set_once(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
19672012
"$set_once": properties,
19682013
"event": "$set_once",
19692014
"uuid": uuid,
1970-
"options": _event_options(kwargs.get("options", None)),
2015+
"options": {
2016+
**_context_get_context_options(),
2017+
**_event_options(kwargs.get("options", None)),
2018+
},
19712019
}
19722020

19732021
return self._enqueue(msg, disable_geoip)
@@ -2408,7 +2456,14 @@ def _report_capture_failure(
24082456
finally:
24092457
_on_error_state.active = False
24102458

2411-
def _enqueue(self, msg, disable_geoip, lane=None, property_allowlist=None):
2459+
def _enqueue(
2460+
self,
2461+
msg,
2462+
disable_geoip,
2463+
lane=None,
2464+
property_allowlist=None,
2465+
derived_options=None,
2466+
):
24122467
# type: (...) -> Optional[str]
24132468
"""Push a new `msg` onto a lane's queue (analytics when unspecified), return the event uuid or None."""
24142469

@@ -2445,7 +2500,15 @@ def _enqueue(self, msg, disable_geoip, lane=None, property_allowlist=None):
24452500
msg["properties"]["$geoip_disable"] = True
24462501

24472502
if self.super_properties:
2448-
msg["properties"] = {**msg["properties"], **self.super_properties}
2503+
msg["properties"] = {**self.super_properties, **msg["properties"]}
2504+
2505+
# Each layer overrides the one before it: values the SDK derives, such
2506+
# as personless, then super options, then context and event options.
2507+
msg["options"] = {
2508+
**(derived_options or {}),
2509+
**_event_options(self.super_options),
2510+
**(msg.get("options") or {}),
2511+
}
24492512

24502513
# Set after the super_properties merge so an explicit `$release_id` from
24512514
# the caller's properties or the super properties wins over the env var.

0 commit comments

Comments
 (0)