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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
CHANGELOG
=========

5.2.0 (unreleased)
------------------

* A new `residential` field has been added to the `Anonymizer` record. This
is an `AnonymizerFeed` record containing `confidence`, `networkLastSeen`,
and `providerName` fields with residential proxy data for the network.
`residential` may be populated even when none of the other fields on
`Anonymizer` are set. The `AnonymizerFeed` record is intended to be reused
for additional feeds, such as VPNs, mobile networks, and hosting or
datacenter providers, that may be added in the future.

5.1.0 (2026-05-12)
------------------

Expand Down
64 changes: 61 additions & 3 deletions src/main/java/com/maxmind/geoip2/record/Anonymizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
* @param providerName The name of the VPN provider (e.g., NordVPN, SurfShark, etc.) associated
* with the network. This is only available from the GeoIP Insights
* web service.
* @param residential Residential proxy data for the network. This may be populated even when
* none of the other fields on this record are set. This is only available
* from the GeoIP Insights web service.
*/
public record Anonymizer(
@JsonProperty("confidence")
Expand Down Expand Up @@ -58,14 +61,69 @@ public record Anonymizer(
LocalDate networkLastSeen,

@JsonProperty("provider_name")
String providerName
String providerName,

@JsonProperty("residential")
AnonymizerFeed residential
) implements JsonSerializable {

/**
* Compact canonical constructor that sets a default for a null {@code residential} value.
*/
public Anonymizer {
residential = residential != null ? residential : new AnonymizerFeed();
}

/**
* Constructs an {@code Anonymizer} record with {@code null} values for all the nullable
* fields and {@code false} for all boolean fields.
* fields and {@code false} for all boolean fields. The {@code residential} field defaults
* to an empty {@code AnonymizerFeed} rather than {@code null}.
*/
public Anonymizer() {
this(null, false, false, false, false, false, false, null, null);
this(null, false, false, false, false, false, false, null, null, null);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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.

Doc (minor): the no-arg constructor's JavaDoc says it "Constructs an Anonymizer record with null values for all the nullable fields" — but the new compact constructor now converts a null residential to a non-null empty AnonymizerFeed, so after new Anonymizer(), residential() is non-null. A maintainer trusting the JavaDoc might branch on residential() == null, which is never true. Suggest noting that residential defaults to an empty AnonymizerFeed.

— Claude, on behalf of Will

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Claude, on behalf of Greg: Good catch — fixed in c468a20. The no-arg constructor's JavaDoc now notes that residential defaults to an empty AnonymizerFeed rather than null, so it's clear that residential() == null is never true after new Anonymizer(). Only the JavaDoc changed; no behavior was touched.

}

/**
* Constructs an {@code Anonymizer} record without the {@code residential} field.
*
* @param confidence the confidence that the network is an actively used VPN service
* @param isAnonymous whether the IP address belongs to any sort of anonymous network
* @param isAnonymousVpn whether the IP address is registered to an anonymous VPN provider
* @param isHostingProvider whether the IP address belongs to a hosting or VPN provider
* @param isPublicProxy whether the IP address belongs to a public proxy
* @param isResidentialProxy whether the IP address is on a suspected anonymizing network
* and belongs to a residential ISP
* @param isTorExitNode whether the IP address is a Tor exit node
* @param networkLastSeen the last day that the network was sighted in our analysis of
* anonymized networks
* @param providerName the name of the VPN provider associated with the network
* @deprecated Use the canonical constructor that also accepts the {@code residential}
* field. This constructor is provided for backward compatibility and will be
* removed in version 6.0.0.
*/
@Deprecated(since = "5.2.0", forRemoval = true)
public Anonymizer(
Integer confidence,
boolean isAnonymous,
boolean isAnonymousVpn,
boolean isHostingProvider,
boolean isPublicProxy,
boolean isResidentialProxy,
boolean isTorExitNode,
LocalDate networkLastSeen,
String providerName
) {
this(
confidence,
isAnonymous,
isAnonymousVpn,
isHostingProvider,
isPublicProxy,
isResidentialProxy,
isTorExitNode,
networkLastSeen,
providerName,
null
);
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/maxmind/geoip2/record/AnonymizerFeed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.maxmind.geoip2.record;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.maxmind.geoip2.JsonSerializable;
import java.time.LocalDate;

/**
* <p>
* Contains data for one type of anonymizer detection, currently residential proxies. Additional
* feeds, such as VPNs, mobile networks, and hosting or datacenter providers, may be added to the
* {@link Anonymizer} record in the future using this same record type.
* </p>
* <p>
* This record is returned by the GeoIP Insights web service.
* </p>
*
* @param confidence A score ranging from 1 to 99 that represents our percent confidence that
* the network is currently part of this anonymizer feed. This is only
* available from the GeoIP Insights web service.
* @param networkLastSeen The last day that the network was sighted in our analysis of this
* anonymizer feed. This is only available from the GeoIP Insights web
* service.
* @param providerName The name of the provider associated with the network in this anonymizer
* feed. This is only available from the GeoIP Insights web service.
*/
public record AnonymizerFeed(
@JsonProperty("confidence")
Integer confidence,

@JsonProperty("network_last_seen")
LocalDate networkLastSeen,

@JsonProperty("provider_name")
String providerName
) implements JsonSerializable {

/**
* Constructs an {@code AnonymizerFeed} record with {@code null} values for all fields.
*/
public AnonymizerFeed() {
this(null, null, null);
}
}
20 changes: 20 additions & 0 deletions src/test/java/com/maxmind/geoip2/model/InsightsResponseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.ConnectionTypeResponse.ConnectionType;
import com.maxmind.geoip2.record.Anonymizer;
import com.maxmind.geoip2.record.AnonymizerFeed;
import com.maxmind.geoip2.record.Location;
import com.maxmind.geoip2.record.MaxMind;
import com.maxmind.geoip2.record.Postal;
Expand Down Expand Up @@ -206,6 +207,25 @@ public void testAnonymizer() {
anonymizer.providerName(),
"anonymizer.providerName() does not return NordVPN"
);

AnonymizerFeed residential = anonymizer.residential();

assertNotNull(residential, "anonymizer.residential() returns null");
assertEquals(
Integer.valueOf(82),
residential.confidence(),
"anonymizer.residential().confidence() does not return 82"
);
assertEquals(
LocalDate.parse("2026-05-11"),
residential.networkLastSeen(),
"anonymizer.residential().networkLastSeen() does not return 2026-05-11"
);
assertEquals(
"quickshift",
residential.providerName(),
"anonymizer.residential().providerName() does not return quickshift"
);
}

@Test
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/com/maxmind/geoip2/model/JsonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public void testInsightsSerialization() throws IOException {
.put("is_tor_exit_node", true)
.put("network_last_seen", "2024-12-31")
.put("provider_name", "NordVPN")
.startObjectField("residential")
.put("confidence", 82)
.put("network_last_seen", "2026-05-11")
.put("provider_name", "quickshift")
.end()
.end()
.startObjectField("country")
.startObjectField("names")
Expand Down
7 changes: 6 additions & 1 deletion src/test/resources/test-data/insights0.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@
"is_residential_proxy": true,
"is_tor_exit_node": true,
"network_last_seen": "2024-12-31",
"provider_name": "NordVPN"
"provider_name": "NordVPN",
"residential": {
"confidence": 82,
"network_last_seen": "2026-05-11",
"provider_name": "quickshift"
}
}
}
7 changes: 6 additions & 1 deletion src/test/resources/test-data/insights1.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@
"is_residential_proxy": true,
"is_tor_exit_node": true,
"network_last_seen": "2024-12-31",
"provider_name": "NordVPN"
"provider_name": "NordVPN",
"residential": {
"confidence": 82,
"network_last_seen": "2026-05-11",
"provider_name": "quickshift"
}
}
}
Loading