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 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
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def network_last_seen
date_string = get('network_last_seen')

if !date_string
@network_last_seen = nil
return nil
end

Expand All @@ -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**

Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/maxmind/geoip2/model/anonymous_plus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def network_last_seen
date_string = get('network_last_seen')

if !date_string
@network_last_seen = nil
return nil
end

Expand Down
16 changes: 16 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
# 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
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

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: Declining as a style preference — the explicit form reads more clearly than record&.[](...) with a string argument, and functionally they are identical.


# 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 Expand Up @@ -85,6 +100,7 @@ def network_last_seen
date_string = get('network_last_seen')

if !date_string
@network_last_seen = nil
return nil
end

Expand Down
53 changes: 53 additions & 0 deletions lib/maxmind/geoip2/record/anonymizer_feed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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
@network_last_seen = nil
return nil
end

@network_last_seen = Date.parse(date_string)
end
Comment on lines +30 to +41

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

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: This matches the lazy-parse pattern used by the existing record classes (see Anonymizer#network_last_seen and Model::AnonymousPlus), which deliberately re-read when the value is unset. Keeping the new class consistent with those; the repeated get on a nil value is negligible.

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: Correction to the earlier reply — we decided to take this suggestion after all, and to apply it consistently: commit 39e1d6f fixes the nil-memoization in all three instances of the pattern (the new AnonymizerFeed#network_last_seen, plus the pre-existing Anonymizer#network_last_seen and AnonymousPlus#network_last_seen), and updates the repo development docs to prescribe the nil-memoizing form.


# 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