Skip to content

Commit 646a8aa

Browse files
Make JA4s have names + drop unused index (#1453)
* Make JA4s have names + drop unused index * Update Swagger specs
1 parent cb9f9ca commit 646a8aa

7 files changed

Lines changed: 91 additions & 9 deletions

File tree

app/controllers/concerns/api/admin/v1/user_utilities.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def user_heartbeats
307307
total_count = query.count
308308
source_types = Heartbeat.source_types.invert
309309
rows = query.order(time: :asc).limit(limit).offset(offset).pluck(*HEARTBEAT_RESPONSE_COLUMNS)
310-
ja4s_by_id = Ja4.where(id: rows.filter_map(&:last).uniq).pluck(:id, :fingerprint).to_h
310+
ja4s_by_id = Ja4.where(id: rows.filter_map(&:last).uniq).index_by(&:id)
311311
heartbeats = rows.map do |id, time, lineno, cursorpos, is_write, project, language, entity, branch, category, editor, machine, user_agent, ip_address, lines, source_type, ja4_id|
312312
{
313313
id: id,
@@ -324,7 +324,7 @@ def user_heartbeats
324324
machine: machine,
325325
user_agent: user_agent,
326326
ip_address: ip_address,
327-
ja4: ja4s_by_id[ja4_id],
327+
ja4: ja4s_by_id[ja4_id]&.then { |ja4| { fingerprint: ja4.fingerprint, name: ja4.name } },
328328
lines: lines,
329329
source_type: source_types[source_type] || source_type
330330
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddNameToJa4s < ActiveRecord::Migration[8.1]
2+
def change
3+
add_column :ja4s, :name, :text, if_not_exists: true
4+
end
5+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
class DropUnusedHeartbeatIndexes < ActiveRecord::Migration[8.1]
2+
disable_ddl_transaction!
3+
4+
def up
5+
remove_index :heartbeats,
6+
name: :index_heartbeats_on_time_imported,
7+
algorithm: :concurrently,
8+
if_exists: true
9+
10+
remove_index :heartbeats,
11+
name: :index_heartbeats_on_last_language_user_id,
12+
algorithm: :concurrently,
13+
if_exists: true
14+
end
15+
16+
def down
17+
add_index :heartbeats, :time,
18+
name: :index_heartbeats_on_time_imported,
19+
where: "source_type != 0",
20+
algorithm: :concurrently,
21+
if_not_exists: true
22+
23+
add_index :heartbeats, :user_id,
24+
name: :index_heartbeats_on_last_language_user_id,
25+
where: "language = '<<LAST_LANGUAGE>>'",
26+
algorithm: :concurrently,
27+
if_not_exists: true
28+
end
29+
end

db/schema.rb

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/requests/api/admin/v1/admin_user_utils_spec.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,14 @@
177177
machine: { type: :string, nullable: true, example: 'Orpheus-MacBook-Pro' },
178178
user_agent: { type: :string, nullable: true, example: 'wakatime/v1.115.2 (darwin-24.6.0) go1.23 vscode/1.96.0' },
179179
ip_address: { type: :string, nullable: true, example: '203.0.113.7' },
180-
ja4: { type: :string, nullable: true, example: 't13d1516h2_8daaf6152771_02713d6af862' },
180+
ja4: {
181+
type: :object,
182+
nullable: true,
183+
properties: {
184+
fingerprint: { type: :string, example: 't13d1516h2_8daaf6152771_02713d6af862' },
185+
name: { type: :string, nullable: true, example: 'Go net/http' }
186+
}
187+
},
181188
lines: { type: :integer, nullable: true, example: 350 },
182189
source_type: { type: :string, example: 'direct_entry' }
183190
}
@@ -195,7 +202,7 @@
195202
entity: 'app/models/user.rb',
196203
time: Time.current.to_f,
197204
source_type: :direct_entry,
198-
ja4: Ja4.create!(fingerprint: 't13d1516h2_8daaf6152771_02713d6af862')
205+
ja4: Ja4.create!(fingerprint: 't13d1516h2_8daaf6152771_02713d6af862', name: 'Go net/http')
199206
)
200207
u
201208
end
@@ -210,7 +217,10 @@
210217
let(:limit) { 10 }
211218
let(:offset) { 0 }
212219
run_test! do |response|
213-
expect(JSON.parse(response.body).dig('heartbeats', 0, 'ja4')).to eq('t13d1516h2_8daaf6152771_02713d6af862')
220+
expect(JSON.parse(response.body).dig('heartbeats', 0, 'ja4')).to eq(
221+
'fingerprint' => 't13d1516h2_8daaf6152771_02713d6af862',
222+
'name' => 'Go net/http'
223+
)
214224
end
215225
end
216226

swagger/admin/swagger.yaml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1850,9 +1850,16 @@ paths:
18501850
nullable: true
18511851
example: 203.0.113.7
18521852
ja4:
1853-
type: string
1853+
type: object
18541854
nullable: true
1855-
example: t13d1516h2_8daaf6152771_02713d6af862
1855+
properties:
1856+
fingerprint:
1857+
type: string
1858+
example: t13d1516h2_8daaf6152771_02713d6af862
1859+
name:
1860+
type: string
1861+
nullable: true
1862+
example: Go net/http
18561863
lines:
18571864
type: integer
18581865
nullable: true
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
require "test_helper"
2+
3+
class Api::Admin::V1::AdminControllerTest < ActionDispatch::IntegrationTest
4+
test "user heartbeats returns ja4 fingerprint and name" do
5+
admin = User.create!(timezone: "UTC", admin_level: :superadmin)
6+
key = admin.admin_api_keys.create!(name: "test")
7+
user = User.create!(timezone: "UTC", username: "admin_heartbeats_ja4")
8+
ja4 = Ja4.create!(fingerprint: "t13d1312h2_f57a46bbacb6_ab7e3b40a677", name: "Go net/http")
9+
10+
user.heartbeats.create!(
11+
time: Time.current.to_i,
12+
project: "test-project",
13+
entity: "test.rb",
14+
source_type: :direct_entry,
15+
ja4: ja4
16+
)
17+
18+
get "/api/admin/v1/user/heartbeats", params: { user_id: user.id }, headers: auth_headers(key)
19+
20+
assert_response :success
21+
response_ja4 = response.parsed_body.fetch("heartbeats").first.fetch("ja4")
22+
assert_equal "t13d1312h2_f57a46bbacb6_ab7e3b40a677", response_ja4.fetch("fingerprint")
23+
assert_equal "Go net/http", response_ja4.fetch("name")
24+
end
25+
26+
private
27+
28+
def auth_headers(key)
29+
{ "Authorization" => ActionController::HttpAuthentication::Token.encode_credentials(key.token) }
30+
end
31+
end

0 commit comments

Comments
 (0)