From 84033dc39d3ceab378eb7d009c7126585d7473ff Mon Sep 17 00:00:00 2001 From: Xander Jones Date: Fri, 16 Oct 2026 15:18:18 +0100 Subject: [PATCH 1/3] feat(groups): add disableGeoip option to groupIdentify groupIdentify() puts every caller property under $group_set, so PHP users had no way to set a top-level $geoip_disable on $groupidentify events. The new disableGeoip option sets it, matching the Node, Python, and Go SDKs and the group-identify spec. Co-Authored-By: Claude Opus 5.5 --- .changeset/group-identify-disable-geoip.md | 5 ++++ lib/PostHog.php | 8 +++++- test/RequestContextTest.php | 29 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 .changeset/group-identify-disable-geoip.md diff --git a/.changeset/group-identify-disable-geoip.md b/.changeset/group-identify-disable-geoip.md new file mode 100644 index 0000000..2696244 --- /dev/null +++ b/.changeset/group-identify-disable-geoip.md @@ -0,0 +1,5 @@ +--- +"posthog-php": minor +--- + +Add a `disableGeoip` option to `PostHog::groupIdentify()`. When it is `true`, the `$groupidentify` event carries `$geoip_disable`, so PostHog skips GeoIP enrichment for it. Before this option, `groupIdentify()` put every caller-supplied property under `$group_set`, so the flag could not reach the event. diff --git a/lib/PostHog.php b/lib/PostHog.php index 748542b..9b7d09c 100644 --- a/lib/PostHog.php +++ b/lib/PostHog.php @@ -200,8 +200,10 @@ public static function identify(array $message) * groupKey: string, * properties?: array, * distinctId?: string, - * distinct_id?: string + * distinct_id?: string, + * disableGeoip?: bool * } $message Group identify payload. `distinctId`/`distinct_id` override the default synthetic ID. + * `disableGeoip` skips server-side GeoIP enrichment for this event. * @return bool Whether the groupIdentify call succeeded. * @throws Exception */ @@ -239,6 +241,10 @@ public static function groupIdentify(array $message) ) ); + if (($message["disableGeoip"] ?? false) === true) { + $msg["properties"]["\$geoip_disable"] = true; + } + return self::capture($msg); } diff --git a/test/RequestContextTest.php b/test/RequestContextTest.php index 6bdb735..dd2fa29 100644 --- a/test/RequestContextTest.php +++ b/test/RequestContextTest.php @@ -2,6 +2,7 @@ namespace PostHog\Test; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use PostHog\Client; use PostHog\PostHog; @@ -474,6 +475,34 @@ public function testGroupIdentifyAllowsSnakeCaseDistinctIdOverride(): void $this->assertSame('snake-user-123', $event['distinct_id']); } + public static function groupIdentifyDisableGeoipCases(): array + { + return [ + 'enabled' => [['disableGeoip' => true], true], + 'disabled' => [['disableGeoip' => false], false], + 'omitted' => [[], false], + ]; + } + + #[DataProvider('groupIdentifyDisableGeoipCases')] + public function testGroupIdentifyDisableGeoip(array $options, bool $expectGeoipDisabled): void + { + PostHog::groupIdentify(array_merge([ + 'groupType' => 'organization', + 'groupKey' => 'acme', + 'properties' => ['name' => 'Acme Inc.'], + ], $options)); + + $event = $this->flushAndGetEvents()[0]; + + if ($expectGeoipDisabled) { + $this->assertTrue($event['properties']['$geoip_disable']); + } else { + $this->assertArrayNotHasKey('$geoip_disable', $event['properties']); + } + $this->assertSame(['name' => 'Acme Inc.'], $event['properties']['$group_set']); + } + public function testEvaluateFlagsUsesContextDistinctIdWhenOmitted(): void { PostHog::withContext(['distinctId' => 'context-user'], function (): void { From 0e24fe05d6870ec33ee0b858095e9e85b0c7f33c Mon Sep 17 00:00:00 2001 From: Xander Jones Date: Fri, 16 Oct 2026 15:23:05 +0100 Subject: [PATCH 2/3] chore(groups): trim group_set detail from disableGeoip test and changeset Co-Authored-By: Claude Opus 5.5 --- .changeset/group-identify-disable-geoip.md | 2 +- test/RequestContextTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/group-identify-disable-geoip.md b/.changeset/group-identify-disable-geoip.md index 2696244..68c82f2 100644 --- a/.changeset/group-identify-disable-geoip.md +++ b/.changeset/group-identify-disable-geoip.md @@ -2,4 +2,4 @@ "posthog-php": minor --- -Add a `disableGeoip` option to `PostHog::groupIdentify()`. When it is `true`, the `$groupidentify` event carries `$geoip_disable`, so PostHog skips GeoIP enrichment for it. Before this option, `groupIdentify()` put every caller-supplied property under `$group_set`, so the flag could not reach the event. +Add a `disableGeoip` option to `PostHog::groupIdentify()`. When it is `true`, the `$groupidentify` event carries `$geoip_disable`, so PostHog skips GeoIP enrichment for it. diff --git a/test/RequestContextTest.php b/test/RequestContextTest.php index dd2fa29..b7793c7 100644 --- a/test/RequestContextTest.php +++ b/test/RequestContextTest.php @@ -500,7 +500,6 @@ public function testGroupIdentifyDisableGeoip(array $options, bool $expectGeoipD } else { $this->assertArrayNotHasKey('$geoip_disable', $event['properties']); } - $this->assertSame(['name' => 'Acme Inc.'], $event['properties']['$group_set']); } public function testEvaluateFlagsUsesContextDistinctIdWhenOmitted(): void From 0fb3ea0ef65d73c2450dadfab918eeecdbc8eafd Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 16 Oct 2026 11:50:37 -0700 Subject: [PATCH 3/3] feat: share GeoIP configuration across events and flag evaluation --- .changeset/group-identify-disable-geoip.md | 2 +- README.md | 1 + UPGRADING.md | 42 +++++ api/public-api.json | 42 ++++- lib/Client.php | 63 ++++++-- lib/FeatureFlagEvaluations.php | 6 +- lib/FeatureFlagEvaluationsHost.php | 4 +- lib/PostHog.php | 29 ++-- test/FakeFlagEvaluationsHost.php | 3 +- test/GeoipTest.php | 173 +++++++++++++++++++++ test/RequestContextTest.php | 28 ---- 11 files changed, 327 insertions(+), 66 deletions(-) create mode 100644 UPGRADING.md create mode 100644 test/GeoipTest.php diff --git a/.changeset/group-identify-disable-geoip.md b/.changeset/group-identify-disable-geoip.md index 68c82f2..a1d227f 100644 --- a/.changeset/group-identify-disable-geoip.md +++ b/.changeset/group-identify-disable-geoip.md @@ -2,4 +2,4 @@ "posthog-php": minor --- -Add a `disableGeoip` option to `PostHog::groupIdentify()`. When it is `true`, the `$groupidentify` event carries `$geoip_disable`, so PostHog skips GeoIP enrichment for it. +Add a client-wide disableGeoip option and per-call overrides for events and remote feature flag evaluation. Keep the default false for compatibility with existing PHP releases; set disableGeoip to true to opt into Node-style GeoIP behavior. diff --git a/README.md b/README.md index 036d06a..44a19b6 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ SDK usage examples and code snippets live in the official documentation so they - [PHP library docs](https://posthog.com/docs/libraries/php) - [Laravel integration docs](https://posthog.com/docs/libraries/laravel) +- [Upgrade notes](UPGRADING.md), including GeoIP configuration and compatibility ## Questions? diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..ab90223 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,42 @@ +# Upgrade notes + +## GeoIP configuration + +The SDK keeps `disableGeoip => false` by default for compatibility with existing +PHP releases. GeoIP enrichment remains enabled unless you explicitly disable it. +Ideally this default will change to `true` in the next major release, matching Node. + +To opt into Node's default behavior now, disable GeoIP enrichment at initialization: + +```php +PostHog::init('project-api-key', ['disableGeoip' => true]); +``` + +Events then carry `properties['$geoip_disable'] = true`, and remote feature flag +requests send `geoip_disable: true`. This prevents PostHog from attributing the +server's location to users and groups. GeoIP-based feature flag targeting can also +produce different results when you enable this option. + +The same option is available in the `Client` constructor. A per-call `disableGeoip` +option overrides the client setting for `capture()`, `identify()`, `alias()`, and +`groupIdentify()`: + +```php +PostHog::groupIdentify([ + 'groupType' => 'company', + 'groupKey' => 'acme', + 'properties' => ['name' => 'Acme'], + 'disableGeoip' => false, +]); +``` + +`evaluateFlags()` and `Client::flags()` also accept a `disableGeoip` override. +Omitting it or passing `null` inherits the client setting. A snapshot retains its +override for exposure events, including snapshots filtered with `only()` or +`onlyAccessed()`. + +Setting `disableGeoip` to `false` prevents the SDK from adding `$geoip_disable`; +it does not remove a manually supplied event property. When GeoIP is disabled, +the SDK sets `$geoip_disable` to `true` even if a caller supplied `false` in +properties. The `before_send` hook runs afterward and can still modify or remove +that property. diff --git a/api/public-api.json b/api/public-api.json index d164eae..63cfd03 100644 --- a/api/public-api.json +++ b/api/public-api.json @@ -261,6 +261,16 @@ "default": [], "defaultConstant": null, "hasDefault": true + }, + { + "name": "disableGeoip", + "type": "?bool", + "byReference": false, + "variadic": false, + "optional": true, + "default": null, + "defaultConstant": null, + "hasDefault": true } ] }, @@ -340,11 +350,11 @@ }, { "name": "disableGeoip", - "type": "bool", + "type": "?bool", "byReference": false, "variadic": false, "optional": true, - "default": false, + "default": null, "defaultConstant": null, "hasDefault": true }, @@ -456,11 +466,11 @@ }, { "name": "disableGeoip", - "type": "bool", + "type": "?bool", "byReference": false, "variadic": false, "optional": true, - "default": false, + "default": null, "defaultConstant": null, "hasDefault": true }, @@ -2677,6 +2687,16 @@ "default": false, "defaultConstant": null, "hasDefault": true + }, + { + "name": "disableGeoip", + "type": "?bool", + "byReference": false, + "variadic": false, + "optional": true, + "default": null, + "defaultConstant": null, + "hasDefault": true } ] }, @@ -2840,6 +2860,16 @@ "default": null, "defaultConstant": null, "hasDefault": false + }, + { + "name": "disableGeoip", + "type": "?bool", + "byReference": false, + "variadic": false, + "optional": true, + "default": null, + "defaultConstant": null, + "hasDefault": true } ] }, @@ -3543,11 +3573,11 @@ }, { "name": "disableGeoip", - "type": "bool", + "type": "?bool", "byReference": false, "variadic": false, "optional": true, - "default": false, + "default": null, "defaultConstant": null, "hasDefault": true }, diff --git a/lib/Client.php b/lib/Client.php index 1e99774..08cb377 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -54,6 +54,7 @@ class Client implements FeatureFlagEvaluationsHost '$lib' => true, '$lib_version' => true, '$is_server' => true, + '$geoip_disable' => true, '$release_id' => true, '$referring_domain' => true, 'utm_source' => true, @@ -260,6 +261,8 @@ class Client implements FeatureFlagEvaluationsHost * * Feature flag requests to `/flags/?v=2` retry transient curl/network errors and HTTP 502/504. * `feature_flag_request_max_retries` defaults to 1; set it to 0 to disable these retries. + * `disableGeoip` defaults to false for compatibility with existing releases. It applies to events + * and remote flag evaluation. Per-call options override this setting; false allows GeoIP enrichment. * * @param array{ * host?: string, @@ -271,6 +274,7 @@ class Client implements FeatureFlagEvaluationsHost * maximum_backoff_duration?: int, * consumer?: 'socket'|'file'|'fork_curl'|'lib_curl'|'noop', * debug?: bool, + * disableGeoip?: bool, * max_queue_size?: int, * batch_size?: int, * flush_interval_seconds?: int|float, @@ -309,6 +313,8 @@ public function __construct( $this->secretKey = StringNormalizer::normalizeOptional($secretKey ?? $personalAPIKey); $this->personalAPIKey = $this->secretKey; $this->options = $options; + // Keep the current default for compatibility; ideally flip to true in the next major release. + $this->options['disableGeoip'] = $options['disableGeoip'] ?? false; $this->debug = $options["debug"] ?? false; $this->flagDefinitionCacheProvider = self::normalizeFlagDefinitionCacheProvider( $options['flag_definition_cache_provider'] ?? null @@ -425,7 +431,8 @@ public function shutdown(): bool * uuid?: string, * flags?: FeatureFlagEvaluations, * send_feature_flags?: bool, - * sendFeatureFlags?: bool + * sendFeatureFlags?: bool, + * disableGeoip?: bool * } $message Event payload. If a top-level `uuid` is supplied it must be a valid UUID; * invalid values are replaced with a generated UUID v4. `send_feature_flags` and * `sendFeatureFlags` are deprecated; pass @@ -436,12 +443,13 @@ public function shutdown(): bool * values are still used as fallbacks when the canonical property is absent; `type` is ignored. * Integer and float timestamps are Unix epoch seconds; strings must be parseable. Null or * invalid values use the current SDK time. Timestamps are converted to the equivalent UTC - * instant before serialization. + * instant before serialization. `disableGeoip` overrides the client setting (false by default). * @return bool Whether the capture call succeeded. */ public function capture(array $message) { $flagsSnapshot = $message["flags"] ?? null; + $disableGeoip = $message['disableGeoip'] ?? null; unset($message["flags"]); $hasGroups = array_key_exists("groups", $message); @@ -492,7 +500,12 @@ public function capture(array $message) // we can without going to the server. $flags = $this->getAllFlags($message["distinct_id"], $message["groups"], [], [], true); } else { - $flags = $this->fetchFeatureVariants($message["distinct_id"], $message["groups"]); + $response = $this->flags( + $message["distinct_id"], + $message["groups"], + disableGeoip: $disableGeoip + ); + $flags = $response['featureFlags'] ?? []; } // Add all feature variants to event @@ -634,10 +647,11 @@ public function captureException( * distinctId?: string, * distinct_id?: string, * properties?: array, - * timestamp?: int|float|string|null + * timestamp?: int|float|string|null, + * disableGeoip?: bool * } $message Integer and float timestamps are Unix epoch seconds; strings must be parseable. * Null or invalid values use the current SDK time. Timestamps are converted to the equivalent - * UTC instant before serialization. + * UTC instant before serialization. `disableGeoip` overrides the client setting. * @return bool Whether the identify call succeeded. */ public function identify(array $message) @@ -868,7 +882,14 @@ private function doGetFeatureFlagResult( if (!$flagWasEvaluatedLocally && !$onlyEvaluateLocally) { try { - $response = $this->requestFlags($distinctId, $groups, $personProperties, $groupProperties, false, [$key]); + $response = $this->requestFlags( + $distinctId, + $groups, + $personProperties, + $groupProperties, + null, + [$key] + ); $errors = []; if (isset($response['errorsWhileComputingFlags']) && $response['errorsWhileComputingFlags']) { @@ -1099,7 +1120,7 @@ public function getAllFlags( * @param array $personProperties Person properties to use for flag evaluation. * @param array> $groupProperties Group properties to use for flag evaluation. * @param bool $onlyEvaluateLocally Whether to avoid a remote /flags fallback. - * @param bool $disableGeoip Whether to disable GeoIP enrichment during remote evaluation. + * @param bool|null $disableGeoip Override GeoIP enrichment; null inherits the client setting. * @param list|null $flagKeys Optional list of flag keys. When provided, only these * flags are evaluated — the underlying /flags request asks the server for just this * subset, which makes the response smaller and the request cheaper. An empty list returns @@ -1114,7 +1135,7 @@ public function evaluateFlags( array $personProperties = [], array $groupProperties = [], bool $onlyEvaluateLocally = false, - bool $disableGeoip = false, + ?bool $disableGeoip = null, ?array $flagKeys = null ): FeatureFlagEvaluations { $distinctId = $this->resolveDistinctId($distinctId); @@ -1125,6 +1146,7 @@ public function evaluateFlags( [], $groups, $this, + disableGeoip: $disableGeoip, ); } @@ -1134,6 +1156,7 @@ public function evaluateFlags( [], $groups, $this, + disableGeoip: $disableGeoip, ); } @@ -1302,6 +1325,7 @@ public function evaluateFlags( null, $errorsWhileComputing, $quotaLimited, + $disableGeoip, ); } @@ -1317,6 +1341,7 @@ public function evaluateFlags( * @param mixed $response Evaluated feature flag response for deduping. * @param array $properties Event properties for the $feature_flag_called event. * @param array $groups Group identifiers for group-based flags. + * @param bool|null $disableGeoip Override GeoIP enrichment; null inherits the client setting. * @return void */ public function captureFlagCalledIfNeeded( @@ -1324,7 +1349,8 @@ public function captureFlagCalledIfNeeded( string $key, $response, array $properties, - array $groups = [] + array $groups = [], + ?bool $disableGeoip = null ): void { $dedupElement = $distinctId . self::featureFlagResponseCacheKey($response) . self::canonicalGroupsRepr($groups); if ($this->distinctIdsFeatureFlagsReported->contains($key, $dedupElement)) { @@ -1336,6 +1362,7 @@ public function captureFlagCalledIfNeeded( 'distinct_id' => $distinctId, 'event' => '$feature_flag_called', '$groups' => $groups, + 'disableGeoip' => $disableGeoip, ]); $this->distinctIdsFeatureFlagsReported->add($key, $dedupElement); } @@ -1907,7 +1934,7 @@ private function normalizeFeatureFlags(string $response): array * @param array $groups Group identifiers. * @param array $personProperties Person properties for flag evaluation. * @param array> $groupProperties Group properties for flag evaluation. - * @param bool $disableGeoip Whether to disable GeoIP enrichment during remote evaluation. + * @param bool|null $disableGeoip Override GeoIP enrichment; null inherits the client setting. * @param list|null $flagKeys Optional list of flag keys to evaluate. * @return array The normalized feature flags response. */ @@ -1916,7 +1943,7 @@ public function flags( array $groups = array(), array $personProperties = [], array $groupProperties = [], - bool $disableGeoip = false, + ?bool $disableGeoip = null, ?array $flagKeys = null ): array { try { @@ -1943,7 +1970,7 @@ private function requestFlags( array $groups = array(), array $personProperties = [], array $groupProperties = [], - bool $disableGeoip = false, + ?bool $disableGeoip = null, ?array $flagKeys = null ): array { if (!$this->enabled) { @@ -1956,7 +1983,7 @@ private function requestFlags( 'groups' => empty($groups) ? (object) [] : $groups, 'person_properties' => empty($personProperties) ? (object) [] : $personProperties, 'group_properties' => empty($groupProperties) ? (object) [] : $groupProperties, - 'geoip_disable' => $disableGeoip, + 'geoip_disable' => $disableGeoip ?? $this->options['disableGeoip'], ); if ($flagKeys !== null) { @@ -2082,10 +2109,11 @@ private function emptyFlagsResponse(): array * distinct_id?: string, * alias: string, * properties?: array, - * timestamp?: int|float|string|null + * timestamp?: int|float|string|null, + * disableGeoip?: bool * } $message Integer and float timestamps are Unix epoch seconds; strings must be parseable. * Null or invalid values use the current SDK time. Timestamps are converted to the equivalent - * UTC instant before serialization. + * UTC instant before serialization. `disableGeoip` overrides the client setting. * @return bool Whether the alias call succeeded. */ public function alias(array $message) @@ -2363,6 +2391,11 @@ private function message($msg) $msg["properties"] = array(); } + if (($msg['disableGeoip'] ?? $this->options['disableGeoip']) === true) { + $msg['properties']['$geoip_disable'] = true; + } + unset($msg['disableGeoip']); + $legacyLibrary = $msg["library"] ?? null; $legacyLibraryVersion = $msg["library_version"] ?? null; $legacyLibraryConsumer = $msg["library_consumer"] ?? null; diff --git a/lib/FeatureFlagEvaluations.php b/lib/FeatureFlagEvaluations.php index 137d6f6..b69d493 100644 --- a/lib/FeatureFlagEvaluations.php +++ b/lib/FeatureFlagEvaluations.php @@ -26,6 +26,7 @@ class FeatureFlagEvaluations * @param array|null $accessed Flags already accessed on this snapshot. * @param bool $errorsWhileComputing Whether the remote response reported computation errors. * @param bool $quotaLimited Whether the remote response was quota limited. + * @param bool|null $disableGeoip Override GeoIP enrichment on access events; null uses the client setting. */ public function __construct( private readonly string $distinctId, @@ -37,6 +38,7 @@ public function __construct( ?array $accessed = null, private readonly bool $errorsWhileComputing = false, private readonly bool $quotaLimited = false, + private readonly ?bool $disableGeoip = null, ) { $this->accessed = $accessed ?? []; } @@ -232,7 +234,8 @@ private function recordAccess(string $key, ?EvaluatedFlagRecord $record): void $key, $response, $properties, - $this->groups + $this->groups, + $this->disableGeoip ); } @@ -253,6 +256,7 @@ private function cloneWith(array $flags): self [], $this->errorsWhileComputing, $this->quotaLimited, + $this->disableGeoip, ); } } diff --git a/lib/FeatureFlagEvaluationsHost.php b/lib/FeatureFlagEvaluationsHost.php index 12d9c15..7d30df1 100644 --- a/lib/FeatureFlagEvaluationsHost.php +++ b/lib/FeatureFlagEvaluationsHost.php @@ -24,6 +24,7 @@ interface FeatureFlagEvaluationsHost * @param mixed $response Evaluated feature flag response for deduping. * @param array $properties Event properties for the $feature_flag_called event. * @param array $groups Group identifiers for group-based flags. + * @param bool|null $disableGeoip Override GeoIP enrichment; null inherits the client setting. * @return void */ public function captureFlagCalledIfNeeded( @@ -31,7 +32,8 @@ public function captureFlagCalledIfNeeded( string $key, $response, array $properties, - array $groups + array $groups, + ?bool $disableGeoip = null ): void; /** diff --git a/lib/PostHog.php b/lib/PostHog.php index 9b7d09c..a7269e5 100644 --- a/lib/PostHog.php +++ b/lib/PostHog.php @@ -36,6 +36,8 @@ class PostHog * * Feature flag requests to `/flags/?v=2` retry transient curl/network errors and HTTP 502/504. * `feature_flag_request_max_retries` defaults to 1; set it to 0 to disable these retries. + * `disableGeoip` defaults to false for compatibility with existing releases. It applies to events + * and remote flag evaluation. Per-call options override this setting; false allows GeoIP enrichment. * * @param array{ * host?: string, @@ -47,6 +49,7 @@ class PostHog * maximum_backoff_duration?: int, * consumer?: 'socket'|'file'|'fork_curl'|'lib_curl'|'noop', * debug?: bool, + * disableGeoip?: bool, * max_queue_size?: int, * batch_size?: int, * flush_interval_seconds?: int|float, @@ -146,7 +149,8 @@ public static function captureException( * uuid?: string, * flags?: FeatureFlagEvaluations, * send_feature_flags?: bool, - * sendFeatureFlags?: bool + * sendFeatureFlags?: bool, + * disableGeoip?: bool * } $message Event payload. If a top-level `uuid` is supplied it must be a valid UUID; * invalid values are replaced with a generated UUID v4. `send_feature_flags` and * `sendFeatureFlags` are deprecated; pass @@ -157,7 +161,7 @@ public static function captureException( * values are still used as fallbacks when the canonical property is absent; `type` is ignored. * Integer and float timestamps are Unix epoch seconds; strings must be parseable. Null or * invalid values use the current SDK time. Timestamps are converted to the equivalent UTC - * instant before serialization. + * instant before serialization. `disableGeoip` overrides the client setting (false by default). * @return bool Whether the capture call succeeded. * @throws Exception */ @@ -177,10 +181,11 @@ public static function capture(array $message) * distinctId?: string, * distinct_id?: string, * properties?: array, - * timestamp?: int|float|string|null + * timestamp?: int|float|string|null, + * disableGeoip?: bool * } $message Integer and float timestamps are Unix epoch seconds; strings must be parseable. * Null or invalid values use the current SDK time. Timestamps are converted to the equivalent - * UTC instant before serialization. + * UTC instant before serialization. `disableGeoip` overrides the client setting. * @return bool Whether the identify call succeeded. * @throws Exception */ @@ -203,7 +208,7 @@ public static function identify(array $message) * distinct_id?: string, * disableGeoip?: bool * } $message Group identify payload. `distinctId`/`distinct_id` override the default synthetic ID. - * `disableGeoip` skips server-side GeoIP enrichment for this event. + * `disableGeoip` overrides the client setting (false by default); true disables GeoIP enrichment. * @return bool Whether the groupIdentify call succeeded. * @throws Exception */ @@ -234,6 +239,7 @@ public static function groupIdentify(array $message) $msg = array( "event" => "\$groupidentify", "distinctId" => $distinctId, + "disableGeoip" => $message["disableGeoip"] ?? null, "properties" => array( "\$group_type" => $message["groupType"], "\$group_key" => $message["groupKey"], @@ -241,10 +247,6 @@ public static function groupIdentify(array $message) ) ); - if (($message["disableGeoip"] ?? false) === true) { - $msg["properties"]["\$geoip_disable"] = true; - } - return self::capture($msg); } @@ -397,7 +399,7 @@ public static function getFeatureFlagPayload( * @param array $personProperties Person properties to use for flag evaluation. * @param array> $groupProperties Group properties to use for flag evaluation. * @param bool $onlyEvaluateLocally Whether to avoid a remote /flags fallback. - * @param bool $disableGeoip Whether to disable GeoIP enrichment during remote evaluation. + * @param bool|null $disableGeoip Override GeoIP enrichment; null inherits the client setting. * @param list|null $flagKeys Optional list of flag keys. When provided, only these * flags are evaluated — the underlying /flags request asks the server for just this * subset, which makes the response smaller and the request cheaper. Use this when you @@ -412,7 +414,7 @@ public static function evaluateFlags( array $personProperties = array(), array $groupProperties = array(), bool $onlyEvaluateLocally = false, - bool $disableGeoip = false, + ?bool $disableGeoip = null, ?array $flagKeys = null ): FeatureFlagEvaluations { self::checkClient(); @@ -477,10 +479,11 @@ public static function fetchFeatureVariants(string $distinctId, array $groups = * distinct_id?: string, * alias: string, * properties?: array, - * timestamp?: int|float|string|null + * timestamp?: int|float|string|null, + * disableGeoip?: bool * } $message Integer and float timestamps are Unix epoch seconds; strings must be parseable. * Null or invalid values use the current SDK time. Timestamps are converted to the equivalent - * UTC instant before serialization. + * UTC instant before serialization. `disableGeoip` overrides the client setting. * @return bool Whether the alias call succeeded. * @throws Exception */ diff --git a/test/FakeFlagEvaluationsHost.php b/test/FakeFlagEvaluationsHost.php index 3b07e23..72493b0 100644 --- a/test/FakeFlagEvaluationsHost.php +++ b/test/FakeFlagEvaluationsHost.php @@ -22,7 +22,8 @@ public function captureFlagCalledIfNeeded( string $key, $response, array $properties, - array $groups + array $groups, + ?bool $disableGeoip = null ): void { $cacheKey = $distinctId . "\0" . $key diff --git a/test/GeoipTest.php b/test/GeoipTest.php new file mode 100644 index 0000000..9543387 --- /dev/null +++ b/test/GeoipTest.php @@ -0,0 +1,173 @@ +setValue(null, null); + } + + public static function geoipCases(): array + { + return [ + 'default preserves compatibility' => [[], [], false], + 'client enabled' => [['disableGeoip' => false], [], false], + 'client disabled' => [['disableGeoip' => true], [], true], + 'call disables over default' => [[], ['disableGeoip' => true], true], + 'call enables over client' => [['disableGeoip' => true], ['disableGeoip' => false], false], + 'call disables over client' => [['disableGeoip' => false], ['disableGeoip' => true], true], + 'null inherits client' => [['disableGeoip' => false], ['disableGeoip' => null], false], + ]; + } + + #[DataProvider('geoipCases')] + public function testEventOptions(array $clientOptions, array $callOptions, bool $disabled): void + { + $http = new MockedHttpClient('app.posthog.com'); + $client = new Client('test-key', $clientOptions, $http, null, false); + PostHog::init(client: $client); + + $messages = [ + 'capture' => ['event' => 'test', 'distinctId' => 'user'], + 'identify' => ['distinctId' => 'user', 'properties' => ['plan' => 'pro']], + 'alias' => ['distinctId' => 'user', 'alias' => 'other-user'], + 'groupIdentify' => ['groupType' => 'company', 'groupKey' => 'acme', 'properties' => ['plan' => 'pro']], + ]; + foreach ($messages as $method => $message) { + $this->assertTrue(PostHog::$method(array_merge($message, $callOptions))); + } + $client->flush(); + + $events = json_decode($http->calls[0]['payload'], true)['batch']; + $this->assertCount(4, $events); + foreach ($events as $event) { + $this->assertArrayNotHasKey('disableGeoip', $event); + if ($disabled) { + $this->assertTrue($event['properties']['$geoip_disable']); + } else { + $this->assertArrayNotHasKey('$geoip_disable', $event['properties']); + } + } + $this->assertSame(['plan' => 'pro'], $events[1]['$set']); + $this->assertSame(['plan' => 'pro'], $events[3]['properties']['$group_set']); + } + + #[DataProvider('geoipCases')] + public function testFlagOptions(array $clientOptions, array $callOptions, bool $disabled): void + { + $http = new MockedHttpClient('app.posthog.com', flagsEndpointResponse: MockedResponses::FLAGS_V2_RESPONSE); + $client = new Client('test-key', $clientOptions, $http, null, false); + PostHog::init(client: $client); + + PostHog::evaluateFlags('user', ...$callOptions)->only(['simple-test'])->isEnabled('simple-test'); + $client->flags('user', ...$callOptions); + $client->flush(); + + $this->assertCount(3, $http->calls); + foreach (array_slice($http->calls, 0, 2) as $call) { + $this->assertSame($disabled, json_decode($call['payload'], true)['geoip_disable']); + } + $event = json_decode($http->calls[2]['payload'], true)['batch'][0]; + $this->assertSame('$feature_flag_called', $event['event']); + if ($disabled) { + $this->assertTrue($event['properties']['$geoip_disable']); + } else { + $this->assertArrayNotHasKey('$geoip_disable', $event['properties']); + } + } + + public function testBeforeSendCanOverrideClientSettingAndExceptionInheritsIt(): void + { + $http = new MockedHttpClient('app.posthog.com'); + $beforeSendEvent = null; + $client = new Client('test-key', [ + 'disableGeoip' => true, + 'before_send' => function (array $event) use (&$beforeSendEvent): array { + $beforeSendEvent = $event; + $event['properties']['$geoip_disable'] = false; + return $event; + }, + ], $http, null, false); + $client->captureException(new RuntimeException('test'), 'user'); + $client->flush(); + + $event = json_decode($http->calls[0]['payload'], true)['batch'][0]; + $this->assertTrue($beforeSendEvent['properties']['$geoip_disable']); + $this->assertFalse($event['properties']['$geoip_disable']); + } + + #[DataProvider('geoipCases')] + public function testCaptureFlagEnrichmentOptions(array $clientOptions, array $callOptions, bool $disabled): void + { + $http = new MockedHttpClient('app.posthog.com'); + $client = new Client('test-key', $clientOptions, $http, null, false); + $client->capture(array_merge([ + 'event' => 'test', + 'distinctId' => 'user', + 'send_feature_flags' => true, + ], $callOptions)); + $client->flush(); + + $this->assertSame($disabled, json_decode($http->calls[0]['payload'], true)['geoip_disable']); + } + + public function testMinimalExposurePreservesGeoipOption(): void + { + $response = MockedResponses::FLAGS_V2_RESPONSE; + $response['minimalFlagCalledEvents'] = true; + $response['flags']['simple-test']['metadata']['has_experiment'] = false; + $http = new MockedHttpClient('app.posthog.com', flagsEndpointResponse: $response); + $client = new Client('test-key', ['disableGeoip' => true], $http, null, false); + $client->evaluateFlags('user')->isEnabled('simple-test'); + $client->flush(); + + $event = json_decode($http->calls[1]['payload'], true)['batch'][0]; + $this->assertTrue($event['properties']['$geoip_disable']); + $this->assertArrayNotHasKey('$lib_consumer', $event['properties']); + } + + public function testLegacyFlagEvaluationInheritsClientOption(): void + { + $http = new MockedHttpClient('app.posthog.com'); + $client = new Client('test-key', ['disableGeoip' => true], $http, null, false); + $client->getFeatureFlag('simpleFlag', 'user'); + + $this->assertTrue(json_decode($http->calls[0]['payload'], true)['geoip_disable']); + } + + public static function explicitPropertyCases(): array + { + return [ + 'false option preserves true property' => [false, true, true], + 'false option preserves false property' => [false, false, false], + 'true option overrides false property' => [true, false, true], + ]; + } + + #[DataProvider('explicitPropertyCases')] + public function testExplicitPropertyPrecedence(bool $option, bool $property, bool $expected): void + { + $http = new MockedHttpClient('app.posthog.com'); + $client = new Client('test-key', [], $http, null, false); + $client->capture([ + 'event' => 'test', + 'distinctId' => 'user', + 'disableGeoip' => $option, + 'properties' => ['$geoip_disable' => $property], + ]); + $client->flush(); + + $event = json_decode($http->calls[0]['payload'], true)['batch'][0]; + $this->assertSame($expected, $event['properties']['$geoip_disable']); + } +} diff --git a/test/RequestContextTest.php b/test/RequestContextTest.php index b7793c7..6bdb735 100644 --- a/test/RequestContextTest.php +++ b/test/RequestContextTest.php @@ -2,7 +2,6 @@ namespace PostHog\Test; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use PostHog\Client; use PostHog\PostHog; @@ -475,33 +474,6 @@ public function testGroupIdentifyAllowsSnakeCaseDistinctIdOverride(): void $this->assertSame('snake-user-123', $event['distinct_id']); } - public static function groupIdentifyDisableGeoipCases(): array - { - return [ - 'enabled' => [['disableGeoip' => true], true], - 'disabled' => [['disableGeoip' => false], false], - 'omitted' => [[], false], - ]; - } - - #[DataProvider('groupIdentifyDisableGeoipCases')] - public function testGroupIdentifyDisableGeoip(array $options, bool $expectGeoipDisabled): void - { - PostHog::groupIdentify(array_merge([ - 'groupType' => 'organization', - 'groupKey' => 'acme', - 'properties' => ['name' => 'Acme Inc.'], - ], $options)); - - $event = $this->flushAndGetEvents()[0]; - - if ($expectGeoipDisabled) { - $this->assertTrue($event['properties']['$geoip_disable']); - } else { - $this->assertArrayNotHasKey('$geoip_disable', $event['properties']); - } - } - public function testEvaluateFlagsUsesContextDistinctIdWhenOmitted(): void { PostHog::withContext(['distinctId' => 'context-user'], function (): void {