trackguard 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2629b3c673c2ac2c1f4e338dedc3abc7f9b572fb57e9b4f9091139809e362bc
4
- data.tar.gz: 6b4657343eecf9de2b48bea0df3f24c4463109dc6ae521dc83dad79741bbfe99
3
+ metadata.gz: 1372d974514748b7cb553b4299b14de6345339c00947e24102be03ec7ac0be0d
4
+ data.tar.gz: 142f9a999003624989fa47ad7a422a5bc76c504dbb8666a8e676d49970490775
5
5
  SHA512:
6
- metadata.gz: d8b5fc84be6801c276f5d7cb7dd0007a7b1c7d5d19d5db9858f2412396e3c99fc604e9276cc34ee0a05b4b63cc01f72fac8e176b1c2e852405813e0e484b4733
7
- data.tar.gz: 96c791bce0d1cd72230e89e34a6335e4c7dfdbe818f90dccbd466bb3e908c2b9437e820dfe6afb9ced8a605e5293f341c65dd541e603a8bd9594210009c4dfe8
6
+ metadata.gz: e3f01bd73d5701b53f8e7a3ef37ab3c3a43b016dc72e9388c8b8f9db225412380889b8fd13ae1df8783231fcdb6fde828f7f6807854c6ef9fa0ee6a4924dc774
7
+ data.tar.gz: 9ff260dc894d2cbc70e69062bb878d394be1913faf4edd61dad5b9c10c41db2e68805985832ba8f4b7762af6a6483e0b6a0d9690447993d0c3f5ea542d8eb98c
@@ -240,6 +240,7 @@
240
240
  /* ── Table ─────────────────────────────────────────────────────────── */
241
241
  .tg-table {
242
242
  width: 100%;
243
+ table-layout: fixed;
243
244
  border-collapse: collapse;
244
245
  font-size: 0.875rem;
245
246
  margin-bottom: 2rem;
@@ -269,6 +270,14 @@
269
270
  text-align: right;
270
271
  color: #9ca3af;
271
272
  font-weight: 400;
273
+ white-space: nowrap;
274
+ width: 3.5rem;
275
+ }
276
+
277
+ .tg-td--label {
278
+ overflow: hidden;
279
+ text-overflow: ellipsis;
280
+ white-space: nowrap;
272
281
  }
273
282
 
274
283
  .tg-td--break { word-break: break-all; }
@@ -15,7 +15,7 @@ module Trackguard
15
15
  private
16
16
 
17
17
  def set_trace_id
18
- @trace_id = SecureRandom.uuid
18
+ @trace_id = request.env["trackguard.trace_id"]
19
19
  end
20
20
 
21
21
  def track_page_view
@@ -31,10 +31,17 @@ module Trackguard
31
31
  trace_id: @trace_id,
32
32
  source: extract_source,
33
33
  tracking_layer: "backend",
34
- http_method: request.request_method
34
+ http_method: request.request_method,
35
+ prefetch: turbo_prefetch?
35
36
  )
36
37
  end
37
38
 
39
+ def turbo_prefetch?
40
+ request.headers["Purpose"] == "prefetch" ||
41
+ request.headers["Sec-Purpose"]&.start_with?("prefetch") ||
42
+ request.headers["X-Sec-Purpose"]&.start_with?("prefetch")
43
+ end
44
+
38
45
  def extract_source
39
46
  raw = params[:ref].presence || params[:utm_source].presence
40
47
  raw && raw.strip.downcase.first(64)
@@ -1,10 +1,18 @@
1
1
  module Trackguard
2
2
  module ApplicationHelper
3
- def trackguard_meta_tags
4
- safe_join([
5
- tag.meta(name: "trackguard-url", content: trackguard.page_views_path),
6
- tag.meta(name: "trace-id", content: @trace_id)
7
- ], "\n")
3
+ def trackguard_header_tags
4
+ tags = [ tag.meta(name: "trace-id", content: @trace_id) ]
5
+
6
+ case Trackguard.adapter
7
+ when Trackguard::Adapters::Local
8
+ tags << tag.meta(name: "trackguard-url", content: trackguard.page_views_path)
9
+ when Trackguard::Adapters::Hub
10
+ if Rails.env.production?
11
+ tags << tag.script(src: "#{Trackguard.hub_url}/track.js", data: { api_key: Trackguard.hub_api_key })
12
+ end
13
+ end
14
+
15
+ safe_join(tags, "\n")
8
16
  end
9
17
 
10
18
  def trackguard_nav_links
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module Trackguard
7
+ module Hub
8
+ class SubmitBlockedRequestJob < ApplicationJob
9
+ queue_as :default
10
+
11
+ def perform(ip:, user_agent:, path:, http_method:, block_reason:)
12
+ uri = URI("#{Trackguard.hub_url}/api/backend/blocked_requests")
13
+ body = {
14
+ blocked_request: {
15
+ ip: ip, user_agent: user_agent, path: path,
16
+ http_method: http_method, block_reason: block_reason
17
+ }
18
+ }.to_json
19
+
20
+ req = Net::HTTP::Post.new(uri)
21
+ req["X-Api-Key"] = Trackguard.hub_api_key
22
+ req["Authorization"] = "Bearer #{Trackguard.hub_secret_key}"
23
+ req["Content-Type"] = "application/json"
24
+ req.body = body
25
+
26
+ opts = { use_ssl: uri.scheme == "https", open_timeout: 3, read_timeout: 5 }
27
+ response = Net::HTTP.start(uri.hostname, uri.port, **opts) { |http| http.request(req) }
28
+
29
+ unless response.is_a?(Net::HTTPSuccess)
30
+ Rails.logger.warn(
31
+ "[Trackguard::Hub::SubmitBlockedRequestJob] Unexpected response #{response.code} for path=#{path}"
32
+ )
33
+ end
34
+ rescue StandardError => e
35
+ Rails.logger.warn("[Trackguard::Hub::SubmitBlockedRequestJob] Failed to submit blocked request: #{e.message}")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module Trackguard
7
+ module Hub
8
+ class SubmitPageViewJob < ApplicationJob
9
+ queue_as :default
10
+
11
+ def perform(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:, http_method:, **)
12
+ uri = URI("#{Trackguard.hub_url}/api/backend/page_views")
13
+ body = {
14
+ page_view: {
15
+ path: path, referer: referer, session_id: session_id, trace_id: trace_id,
16
+ source: source, http_method: http_method, ip: ip, user_agent: user_agent
17
+ }
18
+ }.to_json
19
+
20
+ req = Net::HTTP::Post.new(uri)
21
+ req["X-Api-Key"] = Trackguard.hub_api_key
22
+ req["Authorization"] = "Bearer #{Trackguard.hub_secret_key}"
23
+ req["Content-Type"] = "application/json"
24
+ req.body = body
25
+
26
+ opts = { use_ssl: uri.scheme == "https", open_timeout: 3, read_timeout: 5 }
27
+ response = Net::HTTP.start(uri.hostname, uri.port, **opts) { |http| http.request(req) }
28
+
29
+ unless response.is_a?(Net::HTTPSuccess)
30
+ Rails.logger.warn(
31
+ "[Trackguard::Hub::SubmitPageViewJob] Unexpected response #{response.code} for path=#{path}"
32
+ )
33
+ end
34
+ rescue StandardError => e
35
+ Rails.logger.warn("[Trackguard::Hub::SubmitPageViewJob] Failed to submit page view: #{e.message}")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -5,20 +5,7 @@ module Trackguard
5
5
  queue_as :default
6
6
 
7
7
  def perform(ip:, user_agent:, path:, http_method:, block_reason:)
8
- visitor = Visitor.find_or_create_by!(ip: ip) do |v|
9
- v.user_agent = user_agent
10
- v.first_seen_at = Time.current
11
- v.last_seen_at = Time.current
12
- end
13
- visitor.update!(last_seen_at: Time.current, user_agent: user_agent)
14
-
15
- BlockedRequest.create!(
16
- path: path,
17
- user_agent: user_agent,
18
- http_method: http_method,
19
- block_reason: block_reason,
20
- visitor: visitor
21
- )
8
+ TrackBlockedRequest.call(ip:, user_agent:, path:, http_method:, block_reason:)
22
9
  end
23
10
  end
24
11
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trackguard
4
+ class TrackBlockedRequest < ApplicationService
5
+ def initialize(ip:, user_agent:, path:, http_method:, block_reason:, visitor_scope: {})
6
+ @ip = ip
7
+ @user_agent = user_agent
8
+ @path = path
9
+ @http_method = http_method
10
+ @block_reason = block_reason
11
+ @visitor_scope = visitor_scope
12
+ end
13
+
14
+ def call
15
+ visitor = Visitor.find_or_create_by!(ip: @ip, **@visitor_scope) do |v|
16
+ v.user_agent = @user_agent
17
+ v.first_seen_at = Time.current
18
+ v.last_seen_at = Time.current
19
+ end
20
+ visitor.update!(last_seen_at: Time.current, user_agent: @user_agent)
21
+
22
+ BlockedRequest.create!(
23
+ path: @path,
24
+ user_agent: @user_agent,
25
+ http_method: @http_method,
26
+ block_reason: @block_reason,
27
+ visitor: visitor,
28
+ **@visitor_scope
29
+ )
30
+ end
31
+ end
32
+ end
@@ -15,7 +15,7 @@
15
15
  <tbody>
16
16
  <% rows.each do |label, count| %>
17
17
  <tr>
18
- <td class="tg-td"><%= label %></td>
18
+ <td class="tg-td tg-td--label"><%= label %></td>
19
19
  <td class="tg-td tg-td--num"><%= count %></td>
20
20
  </tr>
21
21
  <% end %>
data/config/importmap.rb CHANGED
@@ -1 +1,3 @@
1
- pin_all_from File.expand_path("../app/assets/javascripts/controllers", __dir__), under: "controllers"
1
+ if Trackguard.adapter.is_a?(Trackguard::Adapters::Local)
2
+ pin_all_from File.expand_path("../app/assets/javascripts/controllers", __dir__), under: "controllers"
3
+ end
@@ -9,7 +9,7 @@ module Trackguard
9
9
  def flagged_visitor?(ip) = raise NotImplementedError, "#{self.class}#flagged_visitor?"
10
10
 
11
11
  def track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:, tracking_layer:,
12
- http_method:)
12
+ http_method:, prefetch: false)
13
13
  return if blocked_user_agent?(user_agent)
14
14
  return if blocked_path?(path)
15
15
  return if path.blank? || path.start_with?(Trackguard.admin_path)
@@ -17,7 +17,7 @@ module Trackguard
17
17
  perform_track_page_view(
18
18
  path: path, ip: ip, user_agent: user_agent, referer: referer,
19
19
  session_id: session_id, trace_id: trace_id, source: source,
20
- tracking_layer: tracking_layer, http_method: http_method
20
+ tracking_layer: tracking_layer, http_method: http_method, prefetch: prefetch
21
21
  )
22
22
  end
23
23
 
@@ -28,7 +28,7 @@ module Trackguard
28
28
  protected
29
29
 
30
30
  def perform_track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:,
31
- tracking_layer:, http_method:)
31
+ tracking_layer:, http_method:, prefetch: false)
32
32
  raise NotImplementedError, "#{self.class}#perform_track_page_view"
33
33
  end
34
34
  end
@@ -31,14 +31,22 @@ module Trackguard
31
31
  end
32
32
 
33
33
  def track_blocked_request(ip:, user_agent:, path:, http_method:, block_reason:)
34
- # placeholder: hub adapter does not persist blocked requests locally
34
+ Trackguard::Hub::SubmitBlockedRequestJob.perform_later(
35
+ ip: ip, user_agent: user_agent, path: path, http_method: http_method, block_reason: block_reason
36
+ )
35
37
  end
36
38
 
37
39
  protected
38
40
 
39
41
  def perform_track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:,
40
- tracking_layer:, http_method:)
41
- # placeholder: hub tracking not yet implemented
42
+ tracking_layer:, http_method:, prefetch: false)
43
+ return if prefetch
44
+
45
+ Trackguard::Hub::SubmitPageViewJob.perform_later(
46
+ path: path, ip: ip, user_agent: user_agent, referer: referer,
47
+ session_id: session_id, trace_id: trace_id, source: source,
48
+ tracking_layer: tracking_layer, http_method: http_method
49
+ )
42
50
  end
43
51
 
44
52
  private
@@ -55,22 +63,8 @@ module Trackguard
55
63
  end
56
64
 
57
65
  def fetch_rules_from_hub
58
- uri = URI("#{Trackguard.hub_url}/api/rules")
59
- request = Net::HTTP::Get.new(uri)
60
- request["Authorization"] = "Bearer #{Trackguard.hub_secret_key}"
61
- request["Accept"] = "application/json"
62
-
63
- etag = Rails.cache.read(ETAG_KEY)
64
- request["If-None-Match"] = etag if etag
65
-
66
- response = Net::HTTP.start(
67
- uri.hostname, uri.port,
68
- use_ssl: uri.scheme == "https",
69
- open_timeout: 3,
70
- read_timeout: 5
71
- ) do |http|
72
- http.request(request)
73
- end
66
+ uri = URI("#{Trackguard.hub_url}/api/backend/rules")
67
+ response = execute_http(uri, build_request(uri))
74
68
 
75
69
  return Rails.cache.read(STALE_KEY) || {} if response.is_a?(Net::HTTPNotModified)
76
70
 
@@ -80,6 +74,21 @@ module Trackguard
80
74
  Rails.cache.write(ETAG_KEY, response["ETag"], expires_in: 24.hours) if response["ETag"]
81
75
  fresh
82
76
  end
77
+
78
+ def build_request(uri)
79
+ req = Net::HTTP::Get.new(uri)
80
+ req["Authorization"] = "Bearer #{Trackguard.hub_secret_key}"
81
+ req["X-Api-Key"] = Trackguard.hub_api_key
82
+ req["Accept"] = "application/json"
83
+ etag = Rails.cache.read(ETAG_KEY)
84
+ req["If-None-Match"] = etag if etag
85
+ req
86
+ end
87
+
88
+ def execute_http(uri, request)
89
+ opts = { use_ssl: uri.scheme == "https", open_timeout: 3, read_timeout: 5 }
90
+ Net::HTTP.start(uri.hostname, uri.port, **opts) { |http| http.request(request) }
91
+ end
83
92
  end
84
93
  end
85
94
  end
@@ -32,7 +32,7 @@ module Trackguard
32
32
  protected
33
33
 
34
34
  def perform_track_page_view(path:, ip:, user_agent:, referer:, session_id:, trace_id:, source:,
35
- tracking_layer:, http_method:)
35
+ tracking_layer:, http_method:, prefetch: false)
36
36
  TrackPageViewJob.perform_later(
37
37
  path: path,
38
38
  ip: ip,
@@ -6,7 +6,6 @@ module Trackguard
6
6
 
7
7
  config.to_prepare do
8
8
  ActionController::Base.helper Trackguard::ApplicationHelper
9
- ActionController::Base.helper Trackguard::HubHelper
10
9
  end
11
10
 
12
11
  initializer "trackguard.assets" do |app|
@@ -17,6 +16,10 @@ module Trackguard
17
16
  app.config.importmap.paths << root.join("config/importmap.rb") if app.config.respond_to?(:importmap)
18
17
  end
19
18
 
19
+ initializer "trackguard.trace_id_middleware" do |app|
20
+ app.middleware.use Trackguard::TraceIdMiddleware
21
+ end
22
+
20
23
  config.after_initialize do
21
24
  Trackguard::RackAttack.configure
22
25
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trackguard
4
+ class TraceIdMiddleware
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ env["trackguard.trace_id"] = SecureRandom.uuid
11
+ @app.call(env)
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,3 @@
1
1
  module Trackguard
2
- VERSION = "0.28.0".freeze
2
+ VERSION = "0.29.0".freeze
3
3
  end
data/lib/trackguard.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "trackguard/version"
4
4
  require "trackguard/engine"
5
5
  require "trackguard/rack_attack"
6
+ require "trackguard/trace_id_middleware"
6
7
  require "trackguard/adapters/base"
7
8
  require "trackguard/adapters/local"
8
9
  require "trackguard/adapters/hub"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trackguard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.0
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Krzysztof Rygielski
@@ -58,8 +58,9 @@ files:
58
58
  - app/controllers/trackguard/admin/whitelisted_ips_controller.rb
59
59
  - app/controllers/trackguard/page_views_controller.rb
60
60
  - app/helpers/trackguard/application_helper.rb
61
- - app/helpers/trackguard/hub_helper.rb
62
61
  - app/jobs/trackguard/detect_suspicious_visitors_job.rb
62
+ - app/jobs/trackguard/hub/submit_blocked_request_job.rb
63
+ - app/jobs/trackguard/hub/submit_page_view_job.rb
63
64
  - app/jobs/trackguard/track_blocked_request_job.rb
64
65
  - app/jobs/trackguard/track_page_view_job.rb
65
66
  - app/models/trackguard/blocked_path.rb
@@ -71,6 +72,7 @@ files:
71
72
  - app/models/trackguard/whitelisted_ip.rb
72
73
  - app/services/trackguard/analytics_query.rb
73
74
  - app/services/trackguard/application_service.rb
75
+ - app/services/trackguard/track_blocked_request.rb
74
76
  - app/services/trackguard/track_page_view.rb
75
77
  - app/views/layouts/trackguard/admin.html.erb
76
78
  - app/views/trackguard/admin/_nav.html.erb
@@ -98,6 +100,7 @@ files:
98
100
  - lib/trackguard/adapters/local.rb
99
101
  - lib/trackguard/engine.rb
100
102
  - lib/trackguard/rack_attack.rb
103
+ - lib/trackguard/trace_id_middleware.rb
101
104
  - lib/trackguard/version.rb
102
105
  - trackguard.gemspec
103
106
  homepage: https://github.com/riggy/trackguard
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Trackguard
4
- module HubHelper
5
- def trackguard_hub_js_tag
6
- return unless Trackguard.adapter.is_a?(Trackguard::Adapters::Hub)
7
-
8
- tag.script(src: "https://app.trackguard.dev/track.js", data: { api_key: Trackguard.hub_api_key })
9
- end
10
- end
11
- end