From 94cd69a319756db0939b73d6b3843f5732733933 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 10 Jul 2026 20:52:26 +0000 Subject: [PATCH 1/2] Add residential proxy data to anonymizer object This adds a new residential sub-object to the anonymizer object in the GeoIP2 Insights web service response. It includes a confidence score, the provider name, and the date the network was last seen for residential proxies. The residential object may be populated even when no other anonymizer attributes are set. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 8 +++ lib/maxmind/geoip2/record/anonymizer.rb | 15 ++++++ lib/maxmind/geoip2/record/anonymizer_feed.rb | 52 ++++++++++++++++++++ test/test_client.rb | 10 ++++ 4 files changed, 85 insertions(+) create mode 100644 lib/maxmind/geoip2/record/anonymizer_feed.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 93aa2c3..2dfd07f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.6.0 + +* A new `residential` object has been added to the `anonymizer` object on + `MaxMind::GeoIP2::Model::Insights`. This object contains residential + proxy data for the network, including a confidence score, the provider + name, and the date the network was last seen. This is only available + from the GeoIP2 Insights web service. + ## 1.5.1 (2026-01-19) * Re-release with a fix to the release process. This includes a bump of the diff --git a/lib/maxmind/geoip2/record/anonymizer.rb b/lib/maxmind/geoip2/record/anonymizer.rb index 073b992..162ebac 100644 --- a/lib/maxmind/geoip2/record/anonymizer.rb +++ b/lib/maxmind/geoip2/record/anonymizer.rb @@ -2,6 +2,7 @@ require 'date' require 'maxmind/geoip2/record/abstract' +require 'maxmind/geoip2/record/anonymizer_feed' module MaxMind module GeoIP2 @@ -11,6 +12,20 @@ module Record # # This record is returned by the Insights web service. class Anonymizer < Abstract + # Residential proxy data for the network. This may be populated even + # when no other anonymizer attributes are set. This property is only + # available from Insights. + # + # @return [MaxMind::GeoIP2::Record::AnonymizerFeed] + attr_reader :residential + + # @!visibility private + def initialize(record) + super + + @residential = AnonymizerFeed.new(record.nil? ? nil : record['residential']) + end + # A score ranging from 1 to 99 that represents our percent confidence # that the network is currently part of an actively used VPN service. # This property is only available from Insights. diff --git a/lib/maxmind/geoip2/record/anonymizer_feed.rb b/lib/maxmind/geoip2/record/anonymizer_feed.rb new file mode 100644 index 0000000..b46d7c0 --- /dev/null +++ b/lib/maxmind/geoip2/record/anonymizer_feed.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'date' +require 'maxmind/geoip2/record/abstract' + +module MaxMind + module GeoIP2 + module Record + # Contains data for one type of anonymizer detection, currently + # residential proxies. Additional feeds may be added in the future. + # + # This record is returned by the anonymizer object of the Insights web + # service. + class AnonymizerFeed < Abstract + # A score ranging from 1 to 99 that represents our percent confidence + # that the network is currently part of this anonymizer feed. This + # property is only available from Insights. + # + # @return [Integer, nil] + def confidence + get('confidence') + end + + # The last day that the network was sighted in our analysis of this + # anonymizer feed. This value is parsed lazily. This property is only + # available from Insights. + # + # @return [Date, nil] A Date object representing the last seen date, + # or nil if the date is not available. + def network_last_seen + return @network_last_seen if defined?(@network_last_seen) + + date_string = get('network_last_seen') + + if !date_string + return nil + end + + @network_last_seen = Date.parse(date_string) + end + + # The name of the provider associated with the network in this + # anonymizer feed. This property is only available from Insights. + # + # @return [String, nil] + def provider_name + get('provider_name') + end + end + end + end +end diff --git a/test/test_client.rb b/test/test_client.rb index c372011..1505e30 100644 --- a/test/test_client.rb +++ b/test/test_client.rb @@ -38,6 +38,11 @@ class ClientTest < Minitest::Test 'is_tor_exit_node' => false, 'network_last_seen' => '2025-10-15', 'provider_name' => 'nordvpn', + 'residential' => { + 'confidence' => 82, + 'network_last_seen' => '2026-05-11', + 'provider_name' => 'quickshift', + }, }, 'continent' => { 'code' => 'NA', @@ -109,6 +114,11 @@ def test_insights assert_equal(Date.parse('2025-10-15'), record.anonymizer.network_last_seen) assert_equal('nordvpn', record.anonymizer.provider_name) + # Test anonymizer.residential object + assert_equal(82, record.anonymizer.residential.confidence) + assert_equal(Date.parse('2026-05-11'), record.anonymizer.residential.network_last_seen) + assert_equal('quickshift', record.anonymizer.residential.provider_name) + # Test traits assert(record.traits.anycast?) assert(record.traits.residential_proxy?) From 39e1d6f220578dc00dddbf76d1d80fa0f72c5bb0 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 14 Jul 2026 14:56:17 +0000 Subject: [PATCH 2/2] Memoize nil results in lazy date parsing The network_last_seen methods memoized only successfully parsed dates. When the value was unset, the defined? guard never became true, so every call repeated the hash lookup instead of returning the cached nil. Assign the instance variable in the nil branch as well so the lookup runs at most once. This fixes the newly added AnonymizerFeed#network_last_seen along with the pre-existing instances of the same pattern in Anonymizer#network_last_seen and AnonymousPlus#network_last_seen, and updates the lazy-parsing examples in CLAUDE.md to match. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 3 +++ lib/maxmind/geoip2/model/anonymous_plus.rb | 1 + lib/maxmind/geoip2/record/anonymizer.rb | 1 + lib/maxmind/geoip2/record/anonymizer_feed.rb | 1 + 4 files changed, 6 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 954fc14..3b2921f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,6 +115,7 @@ def network_last_seen date_string = get('network_last_seen') if !date_string + @network_last_seen = nil return nil end @@ -125,6 +126,7 @@ end - Use `defined?(@variable)` to check if already parsed - Parse only once and cache in instance variable - Handle nil cases before parsing +- Memoize nil results too, so repeated calls do not re-run the lookup #### 5. **Web Service Only vs Database Models** @@ -233,6 +235,7 @@ For database models (like AnonymousPlus): date_string = get('network_last_seen') if !date_string + @network_last_seen = nil return nil end diff --git a/lib/maxmind/geoip2/model/anonymous_plus.rb b/lib/maxmind/geoip2/model/anonymous_plus.rb index 109c9a5..9377514 100644 --- a/lib/maxmind/geoip2/model/anonymous_plus.rb +++ b/lib/maxmind/geoip2/model/anonymous_plus.rb @@ -27,6 +27,7 @@ def network_last_seen date_string = get('network_last_seen') if !date_string + @network_last_seen = nil return nil end diff --git a/lib/maxmind/geoip2/record/anonymizer.rb b/lib/maxmind/geoip2/record/anonymizer.rb index 162ebac..337b4b4 100644 --- a/lib/maxmind/geoip2/record/anonymizer.rb +++ b/lib/maxmind/geoip2/record/anonymizer.rb @@ -100,6 +100,7 @@ def network_last_seen date_string = get('network_last_seen') if !date_string + @network_last_seen = nil return nil end diff --git a/lib/maxmind/geoip2/record/anonymizer_feed.rb b/lib/maxmind/geoip2/record/anonymizer_feed.rb index b46d7c0..9adafe4 100644 --- a/lib/maxmind/geoip2/record/anonymizer_feed.rb +++ b/lib/maxmind/geoip2/record/anonymizer_feed.rb @@ -33,6 +33,7 @@ def network_last_seen date_string = get('network_last_seen') if !date_string + @network_last_seen = nil return nil end