Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/group-identify-disable-geoip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-php": minor
---

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.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
42 changes: 42 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 36 additions & 6 deletions api/public-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
},
Expand Down Expand Up @@ -340,11 +350,11 @@
},
{
"name": "disableGeoip",
"type": "bool",
"type": "?bool",
"byReference": false,
"variadic": false,
"optional": true,
"default": false,
"default": null,
"defaultConstant": null,
"hasDefault": true
},
Expand Down Expand Up @@ -456,11 +466,11 @@
},
{
"name": "disableGeoip",
"type": "bool",
"type": "?bool",
"byReference": false,
"variadic": false,
"optional": true,
"default": false,
"default": null,
"defaultConstant": null,
"hasDefault": true
},
Expand Down Expand Up @@ -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
}
]
},
Expand Down Expand Up @@ -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
}
]
},
Expand Down Expand Up @@ -3543,11 +3573,11 @@
},
{
"name": "disableGeoip",
"type": "bool",
"type": "?bool",
"byReference": false,
"variadic": false,
"optional": true,
"default": false,
"default": null,
"defaultConstant": null,
"hasDefault": true
},
Expand Down
63 changes: 48 additions & 15 deletions lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -634,10 +647,11 @@ public function captureException(
* distinctId?: string,
* distinct_id?: string,
* properties?: array<string, mixed>,
* 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)
Expand Down Expand Up @@ -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']) {
Expand Down Expand Up @@ -1099,7 +1120,7 @@ public function getAllFlags(
* @param array<string, mixed> $personProperties Person properties to use for flag evaluation.
* @param array<string, array<string, mixed>> $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<string>|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
Expand All @@ -1114,7 +1135,7 @@ public function evaluateFlags(
array $personProperties = [],
array $groupProperties = [],
bool $onlyEvaluateLocally = false,
bool $disableGeoip = false,
?bool $disableGeoip = null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 · Preserve compatibility with existing Client method overrides

Existing subclasses overriding Client::evaluateFlags() with bool $disableGeoip = false now fail at class loading: PHP rejects the narrower parameter because the parent accepts null. The same break affects overrides of Client::flags(). This makes the minor SDK update fatal for these applications; preserve the inherited signatures or reserve this change for a major release.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We acknowledge this breaks compatibility for existing subclasses overriding these methods. We’re accepting that break in this minor release as an explicit tradeoff: nullable parameters let omitted arguments inherit the client-wide GeoIP setting while preserving an explicit false override. Keeping bool $disableGeoip = false would lose that distinction. Given that subclassing these SDK methods is an uncommon usage pattern, we prefer consistent configuration behavior here. Affected subclasses will need to update their overrides to ?bool $disableGeoip = null.

?array $flagKeys = null
): FeatureFlagEvaluations {
$distinctId = $this->resolveDistinctId($distinctId);
Expand All @@ -1125,6 +1146,7 @@ public function evaluateFlags(
[],
$groups,
$this,
disableGeoip: $disableGeoip,
);
}

Expand All @@ -1134,6 +1156,7 @@ public function evaluateFlags(
[],
$groups,
$this,
disableGeoip: $disableGeoip,
);
}

Expand Down Expand Up @@ -1302,6 +1325,7 @@ public function evaluateFlags(
null,
$errorsWhileComputing,
$quotaLimited,
$disableGeoip,
);
}

Expand All @@ -1317,14 +1341,16 @@ public function evaluateFlags(
* @param mixed $response Evaluated feature flag response for deduping.
* @param array<string, mixed> $properties Event properties for the $feature_flag_called event.
* @param array<string, mixed> $groups Group identifiers for group-based flags.
* @param bool|null $disableGeoip Override GeoIP enrichment; null inherits the client setting.
* @return void
*/
public function captureFlagCalledIfNeeded(
string $distinctId,
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)) {
Expand All @@ -1336,6 +1362,7 @@ public function captureFlagCalledIfNeeded(
'distinct_id' => $distinctId,
'event' => '$feature_flag_called',
'$groups' => $groups,
'disableGeoip' => $disableGeoip,
]);
$this->distinctIdsFeatureFlagsReported->add($key, $dedupElement);
}
Expand Down Expand Up @@ -1907,7 +1934,7 @@ private function normalizeFeatureFlags(string $response): array
* @param array<string, mixed> $groups Group identifiers.
* @param array<string, mixed> $personProperties Person properties for flag evaluation.
* @param array<string, array<string, mixed>> $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<string>|null $flagKeys Optional list of flag keys to evaluate.
* @return array<string, mixed> The normalized feature flags response.
*/
Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -2082,10 +2109,11 @@ private function emptyFlagsResponse(): array
* distinct_id?: string,
* alias: string,
* properties?: array<string, mixed>,
* 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)
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading