Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 data from the
GeoIP Residential Proxy database, including confidence scoring, provider
name detection, and network last seen date for residential proxies. 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
Expand Down
15 changes: 15 additions & 0 deletions lib/maxmind/geoip2/record/anonymizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'date'
require 'maxmind/geoip2/record/abstract'
require 'maxmind/geoip2/record/anonymizer_feed'

module MaxMind
module GeoIP2
Expand All @@ -11,6 +12,20 @@ module Record
#
# This record is returned by the Insights web service.
class Anonymizer < Abstract
# Data about the residential proxy status of the IP address, as
# sourced from the GeoIP Residential Proxy database. 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
Comment on lines +23 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since this project requires Ruby 3.2+, we can use the safe navigation operator (&.) to safely access 'residential' on record if it is not nil. This is much more idiomatic and concise than the ternary operator check.

        def initialize(record)
          super

          @residential = AnonymizerFeed.new(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.
Expand Down
52 changes: 52 additions & 0 deletions lib/maxmind/geoip2/record/anonymizer_feed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require 'date'
require 'maxmind/geoip2/record/abstract'

module MaxMind
module GeoIP2
module Record
# Contains data from an anonymizer data feed, such as the residential
# proxy feed.
#
# 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 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
Comment on lines +30 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In Ruby, defined?(@ivar) returns true if the instance variable has been initialized, even if its value is nil. However, because @network_last_seen is not assigned when date_string is nil/falsy, subsequent calls to network_last_seen will repeatedly call get('network_last_seen') instead of returning the cached nil value.

We can optimize this and make it more idiomatic by assigning the result of the ternary operator directly to @network_last_seen.

        def network_last_seen
          return @network_last_seen if defined?(@network_last_seen)

          date_string = get('network_last_seen')
          @network_last_seen = date_string ? Date.parse(date_string) : nil
        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
10 changes: 10 additions & 0 deletions test/test_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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?)
Expand Down
Loading