Skip to content

Commit a359bd3

Browse files
authored
Merge pull request #319 from maxmind/greg/stf-997-add-residential-to-anonymizer
Add residential property to Anonymizer record
2 parents dc46114 + be47d40 commit a359bd3

4 files changed

Lines changed: 207 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
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 residential proxy data for the network, including
10+
`confidence`, `networkLastSeen`, and `providerName`. This may be the only
11+
property with data even when the other anonymizer properties are unset.
12+
This data is available from the GeoIP Insights web service.
713
* Updated the `GeoIp2\Model\City` constructor to handle an empty
814
`subdivisions` array. This provides compatibility with some third-party
915
databases that publish empty subdivisions arrays. Pull request by Jarek

src/Record/Anonymizer.php

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

73+
/**
74+
* @var AnonymizerFeed Residential proxy data for the network. This may be the only
75+
* property with data even when the other anonymizer properties are
76+
* unset. This attribute is only available from the GeoIP Insights
77+
* web service.
78+
*/
79+
public readonly AnonymizerFeed $residential;
80+
7381
/**
7482
* @ignore
7583
*
@@ -86,6 +94,7 @@ public function __construct(array $record)
8694
$this->isTorExitNode = $record['is_tor_exit_node'] ?? false;
8795
$this->networkLastSeen = $record['network_last_seen'] ?? null;
8896
$this->providerName = $record['provider_name'] ?? null;
97+
$this->residential = new AnonymizerFeed($record['residential'] ?? []);
8998
}
9099

91100
/**
@@ -122,6 +131,10 @@ public function jsonSerialize(): array
122131
if ($this->providerName !== null) {
123132
$js['provider_name'] = $this->providerName;
124133
}
134+
$residential = $this->residential->jsonSerialize();
135+
if (!empty($residential)) {
136+
$js['residential'] = $residential;
137+
}
125138

126139
return $js;
127140
}

src/Record/AnonymizerFeed.php

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

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+
// Residential proxy data may be present even when the other
432+
// anonymizer properties are unset, so the anonymizer object may
433+
// be returned 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)