Skip to content

Commit 8f738eb

Browse files
oschwaldclaude
andcommitted
Add residential property to Anonymizer record
Surface the full-feed GeoIP Residential Proxy database in the anonymizer object as a residential sub-object with confidence, networkLastSeen, and providerName. The feed is a superset of the residential proxies in Anonymous Plus, so the anonymizer object may now be returned with only this property set. The new GeoIp2\Record\AnonymizerFeed class is written to be reused if the server adds sibling sub-objects (vpn, mobile, datacenter) with the same shape. STF-997 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent dc46114 commit 8f738eb

4 files changed

Lines changed: 210 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
CHANGELOG
22
=========
33

4-
3.3.1 (unreleased)
4+
3.4.0 (unreleased)
55
------------------
66

7+
* A new `residential` property has been added to `GeoIp2\Record\Anonymizer`.
8+
This property is an instance of the new `GeoIp2\Record\AnonymizerFeed`
9+
class and provides `confidence`, `networkLastSeen`, and `providerName`
10+
data sourced from the full-feed GeoIP Residential Proxy database. Because
11+
this feed is a superset of the residential proxies in Anonymous Plus, the
12+
`anonymizer` object may now be returned with only this property set. This
13+
data is available from the GeoIP Insights web service.
714
* Updated the `GeoIp2\Model\City` constructor to handle an empty
815
`subdivisions` array. This provides compatibility with some third-party
916
databases that publish empty subdivisions arrays. Pull request by Jarek

src/Record/Anonymizer.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ class Anonymizer implements \JsonSerializable
7070
*/
7171
public readonly ?string $providerName;
7272

73+
/**
74+
* @var AnonymizerFeed Data about the network from the full-feed GeoIP
75+
* Residential Proxy database. Because this feed is a superset of the
76+
* residential proxies in Anonymous Plus, this may be the only property
77+
* with data on the anonymizer object. This attribute is only available
78+
* from the GeoIP Insights web service.
79+
*/
80+
public readonly AnonymizerFeed $residential;
81+
7382
/**
7483
* @ignore
7584
*
@@ -86,6 +95,7 @@ public function __construct(array $record)
8695
$this->isTorExitNode = $record['is_tor_exit_node'] ?? false;
8796
$this->networkLastSeen = $record['network_last_seen'] ?? null;
8897
$this->providerName = $record['provider_name'] ?? null;
98+
$this->residential = new AnonymizerFeed($record['residential'] ?? []);
8999
}
90100

91101
/**
@@ -122,6 +132,10 @@ public function jsonSerialize(): array
122132
if ($this->providerName !== null) {
123133
$js['provider_name'] = $this->providerName;
124134
}
135+
$residential = $this->residential->jsonSerialize();
136+
if (!empty($residential)) {
137+
$js['residential'] = $residential;
138+
}
125139

126140
return $js;
127141
}

src/Record/AnonymizerFeed.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GeoIp2\Record;
6+
7+
/**
8+
* Contains data for a network record from one of MaxMind's full-feed
9+
* anonymizer databases. Currently, this is only used for the `residential`
10+
* property on {@see Anonymizer}, which is sourced from the GeoIP
11+
* Residential Proxy database.
12+
*
13+
* This record is returned by the GeoIP Insights web service.
14+
*/
15+
class AnonymizerFeed implements \JsonSerializable
16+
{
17+
/**
18+
* @var int|null A confidence score from 1-99 indicating MaxMind's confidence in this
19+
* record. This attribute is only available from the GeoIP Insights web
20+
* service.
21+
*/
22+
public readonly ?int $confidence;
23+
24+
/**
25+
* @var string|null The date the network was last seen in the feed in YYYY-MM-DD
26+
* format. This attribute is only available from the GeoIP Insights web
27+
* service.
28+
*/
29+
public readonly ?string $networkLastSeen;
30+
31+
/**
32+
* @var string|null The name of the provider associated with the network. This
33+
* attribute is only available from the GeoIP Insights web service.
34+
*/
35+
public readonly ?string $providerName;
36+
37+
/**
38+
* @ignore
39+
*
40+
* @param array<string, mixed> $record
41+
*/
42+
public function __construct(array $record)
43+
{
44+
$this->confidence = $record['confidence'] ?? null;
45+
$this->networkLastSeen = $record['network_last_seen'] ?? null;
46+
$this->providerName = $record['provider_name'] ?? null;
47+
}
48+
49+
/**
50+
* @return array<string, mixed>
51+
*/
52+
public function jsonSerialize(): array
53+
{
54+
$js = [];
55+
56+
if ($this->confidence !== null) {
57+
$js['confidence'] = $this->confidence;
58+
}
59+
if ($this->networkLastSeen !== null) {
60+
$js['network_last_seen'] = $this->networkLastSeen;
61+
}
62+
if ($this->providerName !== null) {
63+
$js['provider_name'] = $this->providerName;
64+
}
65+
66+
return $js;
67+
}
68+
}

tests/GeoIp2/Test/Model/InsightsTest.php

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public function testFull(): void
2727
'is_tor_exit_node' => true,
2828
'network_last_seen' => '2025-04-14',
2929
'provider_name' => 'NordVPN',
30+
'residential' => [
31+
'confidence' => 82,
32+
'network_last_seen' => '2026-05-11',
33+
'provider_name' => 'quickshift',
34+
],
3035
],
3136
'city' => [
3237
'confidence' => 76,
@@ -232,6 +237,30 @@ public function testFull(): void
232237
'$model->anonymizer->providerName is NordVPN'
233238
);
234239

240+
$this->assertInstanceOf(
241+
'GeoIp2\Record\AnonymizerFeed',
242+
$model->anonymizer->residential,
243+
'$model->anonymizer->residential'
244+
);
245+
246+
$this->assertSame(
247+
82,
248+
$model->anonymizer->residential->confidence,
249+
'$model->anonymizer->residential->confidence is 82'
250+
);
251+
252+
$this->assertSame(
253+
'2026-05-11',
254+
$model->anonymizer->residential->networkLastSeen,
255+
'$model->anonymizer->residential->networkLastSeen is 2026-05-11'
256+
);
257+
258+
$this->assertSame(
259+
'quickshift',
260+
$model->anonymizer->residential->providerName,
261+
'$model->anonymizer->residential->providerName is quickshift'
262+
);
263+
235264
$this->assertSame(
236265
15.37,
237266
$model->traits->ipRiskSnapshot,
@@ -385,13 +414,77 @@ public function testFull(): void
385414
'is_tor_exit_node' => true,
386415
'network_last_seen' => '2025-04-14',
387416
'provider_name' => 'NordVPN',
417+
'residential' => [
418+
'confidence' => 82,
419+
'network_last_seen' => '2026-05-11',
420+
'provider_name' => 'quickshift',
421+
],
388422
],
389423
],
390424
$model->jsonSerialize(),
391425
'jsonSerialize returns initial array'
392426
);
393427
}
394428

429+
public function testAnonymizerResidentialOnly(): void
430+
{
431+
// The full-feed Residential Proxy database is a superset of the
432+
// residential proxies in Anonymous Plus, so the anonymizer object
433+
// may be present with only the residential key set.
434+
$raw = [
435+
'traits' => ['ip_address' => '6.0.42.17'],
436+
'anonymizer' => [
437+
'residential' => [
438+
'confidence' => 95,
439+
'network_last_seen' => '2026-05-14',
440+
'provider_name' => 'novada',
441+
],
442+
],
443+
];
444+
445+
$model = new Insights($raw, ['en']);
446+
447+
$this->assertNull(
448+
$model->anonymizer->confidence,
449+
'$model->anonymizer->confidence is null'
450+
);
451+
452+
$this->assertFalse(
453+
$model->anonymizer->isAnonymous,
454+
'$model->anonymizer->isAnonymous is false'
455+
);
456+
457+
$this->assertInstanceOf(
458+
'GeoIp2\Record\AnonymizerFeed',
459+
$model->anonymizer->residential,
460+
'$model->anonymizer->residential'
461+
);
462+
463+
$this->assertSame(
464+
95,
465+
$model->anonymizer->residential->confidence,
466+
'$model->anonymizer->residential->confidence is 95'
467+
);
468+
469+
$this->assertSame(
470+
'2026-05-14',
471+
$model->anonymizer->residential->networkLastSeen,
472+
'$model->anonymizer->residential->networkLastSeen is 2026-05-14'
473+
);
474+
475+
$this->assertSame(
476+
'novada',
477+
$model->anonymizer->residential->providerName,
478+
'$model->anonymizer->residential->providerName is novada'
479+
);
480+
481+
$this->assertSame(
482+
$raw,
483+
$model->jsonSerialize(),
484+
'jsonSerialize returns only the residential key in anonymizer'
485+
);
486+
}
487+
395488
public function testEmptyObjects(): void
396489
{
397490
$raw = ['traits' => ['ip_address' => '5.6.7.8', 'network' => '5.6.7.0/24']];
@@ -458,6 +551,33 @@ public function testEmptyObjects(): void
458551
'$model->traits'
459552
);
460553

554+
$this->assertInstanceOf(
555+
'GeoIp2\Record\Anonymizer',
556+
$model->anonymizer,
557+
'$model->anonymizer'
558+
);
559+
560+
$this->assertInstanceOf(
561+
'GeoIp2\Record\AnonymizerFeed',
562+
$model->anonymizer->residential,
563+
'$model->anonymizer->residential'
564+
);
565+
566+
$this->assertNull(
567+
$model->anonymizer->residential->confidence,
568+
'$model->anonymizer->residential->confidence is null'
569+
);
570+
571+
$this->assertNull(
572+
$model->anonymizer->residential->networkLastSeen,
573+
'$model->anonymizer->residential->networkLastSeen is null'
574+
);
575+
576+
$this->assertNull(
577+
$model->anonymizer->residential->providerName,
578+
'$model->anonymizer->residential->providerName is null'
579+
);
580+
461581
$this->assertSame(
462582
$raw,
463583
$model->jsonSerialize(),

0 commit comments

Comments
 (0)