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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
CHANGELOG
=========

3.3.1 (unreleased)
3.4.0 (unreleased)
------------------

* A new `residential` property has been added to `GeoIp2\Record\Anonymizer`.
This property is an instance of the new `GeoIp2\Record\AnonymizerFeed`
class and provides residential proxy data for the network, including
`confidence`, `networkLastSeen`, and `providerName`. This may be the only
property with data even when the other anonymizer properties are unset.
This data is available from the GeoIP Insights web service.
* Updated the `GeoIp2\Model\City` constructor to handle an empty
`subdivisions` array. This provides compatibility with some third-party
databases that publish empty subdivisions arrays. Pull request by Jarek
Expand Down
13 changes: 13 additions & 0 deletions src/Record/Anonymizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ class Anonymizer implements \JsonSerializable
*/
public readonly ?string $providerName;

/**
* @var AnonymizerFeed Residential proxy data for the network. This may be the only
* property with data even when the other anonymizer properties are
* unset. This attribute is only available from the GeoIP Insights
* web service.
*/
public readonly AnonymizerFeed $residential;

/**
* @ignore
*
Expand All @@ -86,6 +94,7 @@ public function __construct(array $record)
$this->isTorExitNode = $record['is_tor_exit_node'] ?? false;
$this->networkLastSeen = $record['network_last_seen'] ?? null;
$this->providerName = $record['provider_name'] ?? null;
$this->residential = new AnonymizerFeed($record['residential'] ?? []);
}

/**
Expand Down Expand Up @@ -122,6 +131,10 @@ public function jsonSerialize(): array
if ($this->providerName !== null) {
$js['provider_name'] = $this->providerName;
}
$residential = $this->residential->jsonSerialize();
if (!empty($residential)) {
$js['residential'] = $residential;
}

return $js;
}
Expand Down
67 changes: 67 additions & 0 deletions src/Record/AnonymizerFeed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace GeoIp2\Record;

/**
* Contains data for one type of anonymizer detection, currently residential
* proxies. Additional anonymizer types may be added in the future.
*
* This record is returned by the GeoIP Insights web service.
*/
class AnonymizerFeed implements \JsonSerializable
{
/**
* @var int|null A score ranging from 1 to 99 that represents our percent confidence
* that the network is currently part of this anonymizer feed. This
* attribute is only available from the GeoIP Insights web service.
*/
public readonly ?int $confidence;

/**
* @var string|null The last day that the network was sighted in our analysis of this
* anonymizer feed, in YYYY-MM-DD format. This attribute is only
* available from the GeoIP Insights web service.
*/
public readonly ?string $networkLastSeen;

/**
* @var string|null The name of the provider associated with the network in this
* anonymizer feed. This attribute is only available from the GeoIP
* Insights web service.
*/
public readonly ?string $providerName;

/**
* @ignore
*
* @param array<string, mixed> $record
*/
public function __construct(array $record)
{
$this->confidence = $record['confidence'] ?? null;
$this->networkLastSeen = $record['network_last_seen'] ?? null;
$this->providerName = $record['provider_name'] ?? null;
}

/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$js = [];

if ($this->confidence !== null) {
$js['confidence'] = $this->confidence;
}
if ($this->networkLastSeen !== null) {
$js['network_last_seen'] = $this->networkLastSeen;
}
if ($this->providerName !== null) {
$js['provider_name'] = $this->providerName;
}

return $js;
}
}
120 changes: 120 additions & 0 deletions tests/GeoIp2/Test/Model/InsightsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public function testFull(): void
'is_tor_exit_node' => true,
'network_last_seen' => '2025-04-14',
'provider_name' => 'NordVPN',
'residential' => [
'confidence' => 82,
'network_last_seen' => '2026-05-11',
'provider_name' => 'quickshift',
],
],
'city' => [
'confidence' => 76,
Expand Down Expand Up @@ -232,6 +237,30 @@ public function testFull(): void
'$model->anonymizer->providerName is NordVPN'
);

$this->assertInstanceOf(
'GeoIp2\Record\AnonymizerFeed',
$model->anonymizer->residential,
'$model->anonymizer->residential'
);

$this->assertSame(
82,
$model->anonymizer->residential->confidence,
'$model->anonymizer->residential->confidence is 82'
);

$this->assertSame(
'2026-05-11',
$model->anonymizer->residential->networkLastSeen,
'$model->anonymizer->residential->networkLastSeen is 2026-05-11'
);

$this->assertSame(
'quickshift',
$model->anonymizer->residential->providerName,
'$model->anonymizer->residential->providerName is quickshift'
);

$this->assertSame(
15.37,
$model->traits->ipRiskSnapshot,
Expand Down Expand Up @@ -385,13 +414,77 @@ public function testFull(): void
'is_tor_exit_node' => true,
'network_last_seen' => '2025-04-14',
'provider_name' => 'NordVPN',
'residential' => [
'confidence' => 82,
'network_last_seen' => '2026-05-11',
'provider_name' => 'quickshift',
],
],
],
$model->jsonSerialize(),
'jsonSerialize returns initial array'
);
}

public function testAnonymizerResidentialOnly(): void
{
// Residential proxy data may be present even when the other
// anonymizer properties are unset, so the anonymizer object may
// be returned with only the residential key set.
$raw = [
'traits' => ['ip_address' => '6.0.42.17'],
'anonymizer' => [
'residential' => [
'confidence' => 95,
'network_last_seen' => '2026-05-14',
'provider_name' => 'novada',
],
],
];

$model = new Insights($raw, ['en']);

$this->assertNull(
$model->anonymizer->confidence,
'$model->anonymizer->confidence is null'
);

$this->assertFalse(
$model->anonymizer->isAnonymous,
'$model->anonymizer->isAnonymous is false'
);

$this->assertInstanceOf(
'GeoIp2\Record\AnonymizerFeed',
$model->anonymizer->residential,
'$model->anonymizer->residential'
);

$this->assertSame(
95,
$model->anonymizer->residential->confidence,
'$model->anonymizer->residential->confidence is 95'
);

$this->assertSame(
'2026-05-14',
$model->anonymizer->residential->networkLastSeen,
'$model->anonymizer->residential->networkLastSeen is 2026-05-14'
);

$this->assertSame(
'novada',
$model->anonymizer->residential->providerName,
'$model->anonymizer->residential->providerName is novada'
);

$this->assertSame(
$raw,
$model->jsonSerialize(),
'jsonSerialize returns only the residential key in anonymizer'
);
}

public function testEmptyObjects(): void
{
$raw = ['traits' => ['ip_address' => '5.6.7.8', 'network' => '5.6.7.0/24']];
Expand Down Expand Up @@ -458,6 +551,33 @@ public function testEmptyObjects(): void
'$model->traits'
);

$this->assertInstanceOf(
'GeoIp2\Record\Anonymizer',
$model->anonymizer,
'$model->anonymizer'
);

$this->assertInstanceOf(
'GeoIp2\Record\AnonymizerFeed',
$model->anonymizer->residential,
'$model->anonymizer->residential'
);

$this->assertNull(
$model->anonymizer->residential->confidence,
'$model->anonymizer->residential->confidence is null'
);

$this->assertNull(
$model->anonymizer->residential->networkLastSeen,
'$model->anonymizer->residential->networkLastSeen is null'
);

$this->assertNull(
$model->anonymizer->residential->providerName,
'$model->anonymizer->residential->providerName is null'
);

$this->assertSame(
$raw,
$model->jsonSerialize(),
Expand Down
Loading