Skip to content

Commit 56e4ead

Browse files
authored
Merge pull request #10 from rameerez/feature/usage-analytics-scopes
Add usage analytics scopes for admin dashboards
2 parents 06a72eb + a0890b2 commit 56e4ead

2 files changed

Lines changed: 159 additions & 0 deletions

File tree

lib/api_keys/models/api_key.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,40 @@ def scopes=(value)
7979
scope :publishable, -> { where(key_type: "publishable") }
8080
scope :secret, -> { where.not(key_type: "publishable") }
8181

82+
# === Usage Analytics Scopes ===
83+
# These scopes help admin dashboards analyze API key usage patterns.
84+
# Useful for identifying unused keys, high-traffic keys, and stale keys that may need cleanup.
85+
86+
# Keys that have never been used (last_used_at is nil)
87+
scope :never_used, -> { where(last_used_at: nil) }
88+
89+
# Keys that have been used at least once
90+
scope :used, -> { where.not(last_used_at: nil) }
91+
92+
# Order by usage count (highest first) - useful for finding most active keys
93+
scope :by_requests, -> { order(requests_count: :desc) }
94+
95+
# Order by last used time (most recent first, nulls last)
96+
# Uses NULLS LAST for PostgreSQL compatibility; SQLite sorts nulls last by default with DESC
97+
scope :by_last_used, -> { order(Arel.sql("CASE WHEN last_used_at IS NULL THEN 1 ELSE 0 END, last_used_at DESC")) }
98+
99+
# Active keys that haven't been used within the specified period.
100+
# Useful for identifying keys that may have been abandoned or forgotten.
101+
# Excludes revoked/expired keys since those are already inactive.
102+
# @param period [ActiveSupport::Duration] The inactivity threshold (default: 30 days)
103+
scope :stale, ->(period = 30.days) {
104+
active.where("last_used_at < :threshold OR last_used_at IS NULL", threshold: period.ago)
105+
}
106+
107+
# Aliases for common admin dashboard naming conventions
108+
class << self
109+
alias_method :most_used, :by_requests
110+
alias_method :recently_used, :by_last_used
111+
end
112+
113+
# Convenience scope for 30-day stale keys (common admin filter)
114+
scope :inactive_for_30_days, -> { stale(30.days) }
115+
82116
# == Instance Methods ==
83117

84118
def revoke!

test/models/api_key_test.rb

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,131 @@ def setup
120120
assert_not_includes expired_keys, active_key
121121
end
122122

123+
# === Usage Analytics Scopes ===
124+
# These scopes help admin dashboards analyze API key usage patterns
125+
126+
test ".never_used scope returns keys that have never been used" do
127+
used_key = ApiKeys::ApiKey.create!(owner: @user, name: "Used")
128+
used_key.update_column(:last_used_at, 1.day.ago)
129+
130+
never_used_key = ApiKeys::ApiKey.create!(owner: @user, name: "Never Used")
131+
# last_used_at is nil by default
132+
133+
never_used_keys = ApiKeys::ApiKey.never_used.to_a
134+
assert_includes never_used_keys, never_used_key
135+
assert_not_includes never_used_keys, used_key
136+
end
137+
138+
test ".used scope returns keys that have been used at least once" do
139+
used_key = ApiKeys::ApiKey.create!(owner: @user, name: "Used")
140+
used_key.update_column(:last_used_at, 1.day.ago)
141+
142+
never_used_key = ApiKeys::ApiKey.create!(owner: @user, name: "Never Used")
143+
144+
used_keys = ApiKeys::ApiKey.used.to_a
145+
assert_includes used_keys, used_key
146+
assert_not_includes used_keys, never_used_key
147+
end
148+
149+
test ".by_requests scope orders by requests_count descending" do
150+
low_usage = ApiKeys::ApiKey.create!(owner: @user, name: "Low")
151+
low_usage.update_column(:requests_count, 10)
152+
153+
high_usage = ApiKeys::ApiKey.create!(owner: @user, name: "High")
154+
high_usage.update_column(:requests_count, 1000)
155+
156+
medium_usage = ApiKeys::ApiKey.create!(owner: @user, name: "Medium")
157+
medium_usage.update_column(:requests_count, 100)
158+
159+
ordered = ApiKeys::ApiKey.by_requests.to_a
160+
assert_equal [high_usage, medium_usage, low_usage], ordered
161+
end
162+
163+
test ".by_last_used scope orders by last_used_at descending with nulls last" do
164+
old_key = ApiKeys::ApiKey.create!(owner: @user, name: "Old")
165+
old_key.update_column(:last_used_at, 7.days.ago)
166+
167+
recent_key = ApiKeys::ApiKey.create!(owner: @user, name: "Recent")
168+
recent_key.update_column(:last_used_at, 1.hour.ago)
169+
170+
never_used_key = ApiKeys::ApiKey.create!(owner: @user, name: "Never")
171+
# last_used_at is nil
172+
173+
ordered = ApiKeys::ApiKey.by_last_used.to_a
174+
# Recent should come first, then old, then never used (nulls last)
175+
assert_equal recent_key, ordered.first
176+
assert_equal old_key, ordered.second
177+
assert_equal never_used_key, ordered.last
178+
end
179+
180+
test ".stale scope returns active keys not used in specified period" do
181+
# Active key used recently - should NOT be stale
182+
recent_key = ApiKeys::ApiKey.create!(owner: @user, name: "Recent")
183+
recent_key.update_column(:last_used_at, 5.days.ago)
184+
185+
# Active key not used in 30+ days - should be stale
186+
stale_key = ApiKeys::ApiKey.create!(owner: @user, name: "Stale")
187+
stale_key.update_column(:last_used_at, 45.days.ago)
188+
189+
# Active key never used - should be stale
190+
never_used_key = ApiKeys::ApiKey.create!(owner: @user, name: "Never Used")
191+
192+
# Revoked key not used in 30+ days - should NOT be stale (already inactive)
193+
revoked_key = ApiKeys::ApiKey.create!(owner: @user, name: "Revoked")
194+
revoked_key.update_column(:last_used_at, 60.days.ago)
195+
revoked_key.revoke!
196+
197+
stale_keys = ApiKeys::ApiKey.stale(30.days).to_a
198+
assert_includes stale_keys, stale_key
199+
assert_includes stale_keys, never_used_key
200+
assert_not_includes stale_keys, recent_key
201+
assert_not_includes stale_keys, revoked_key
202+
end
203+
204+
test ".stale scope defaults to 30 days" do
205+
stale_key = ApiKeys::ApiKey.create!(owner: @user, name: "Stale")
206+
stale_key.update_column(:last_used_at, 31.days.ago)
207+
208+
recent_key = ApiKeys::ApiKey.create!(owner: @user, name: "Recent")
209+
recent_key.update_column(:last_used_at, 29.days.ago)
210+
211+
stale_keys = ApiKeys::ApiKey.stale.to_a
212+
assert_includes stale_keys, stale_key
213+
assert_not_includes stale_keys, recent_key
214+
end
215+
216+
test ".most_used is an alias for .by_requests" do
217+
low_usage = ApiKeys::ApiKey.create!(owner: @user, name: "Low")
218+
low_usage.update_column(:requests_count, 10)
219+
220+
high_usage = ApiKeys::ApiKey.create!(owner: @user, name: "High")
221+
high_usage.update_column(:requests_count, 1000)
222+
223+
assert_equal ApiKeys::ApiKey.by_requests.to_a, ApiKeys::ApiKey.most_used.to_a
224+
end
225+
226+
test ".recently_used is an alias for .by_last_used" do
227+
old_key = ApiKeys::ApiKey.create!(owner: @user, name: "Old")
228+
old_key.update_column(:last_used_at, 7.days.ago)
229+
230+
recent_key = ApiKeys::ApiKey.create!(owner: @user, name: "Recent")
231+
recent_key.update_column(:last_used_at, 1.hour.ago)
232+
233+
assert_equal ApiKeys::ApiKey.by_last_used.to_a, ApiKeys::ApiKey.recently_used.to_a
234+
end
235+
236+
test ".inactive_for_30_days scope is equivalent to .stale with 30 days" do
237+
stale_key = ApiKeys::ApiKey.create!(owner: @user, name: "Stale")
238+
stale_key.update_column(:last_used_at, 31.days.ago)
239+
240+
recent_key = ApiKeys::ApiKey.create!(owner: @user, name: "Recent")
241+
recent_key.update_column(:last_used_at, 29.days.ago)
242+
243+
assert_equal ApiKeys::ApiKey.stale(30.days).to_a, ApiKeys::ApiKey.inactive_for_30_days.to_a
244+
assert_includes ApiKeys::ApiKey.inactive_for_30_days.to_a, stale_key
245+
assert_not_includes ApiKeys::ApiKey.inactive_for_30_days.to_a, recent_key
246+
end
247+
123248
test "revoke! sets revoked_at timestamp" do
124249
api_key = ApiKeys::ApiKey.create!(owner: @user, name: "To Revoke")
125250
assert_nil api_key.revoked_at

0 commit comments

Comments
 (0)