Skip to content

Commit be47d40

Browse files
oschwaldclaude
andcommitted
Add residential property to Anonymizer record
Add a residential sub-object to the anonymizer object with confidence, networkLastSeen, and providerName. Residential proxy data may be present even when the other anonymizer properties are unset, 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 be47d40

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)