Skip to content
Draft
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
24 changes: 24 additions & 0 deletions app/jobs/scheduled/clean_up_request_logs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Jobs
class CleanUpRequestLogs < ::Jobs::Scheduled
every 1.day

def execute(args)
clean_browser_page_views if SiteSetting.enable_page_view_logging
clean_api_request_logs if SiteSetting.enable_api_request_logging
end

private

def clean_browser_page_views
cutoff = SiteSetting.page_view_logging_retention_days.days.ago
BrowserPageView.where("created_at < ?", cutoff).delete_all
end

def clean_api_request_logs
cutoff = SiteSetting.api_request_logging_retention_days.days.ago
ApiRequestLog.where("created_at < ?", cutoff).delete_all
end
end
end
51 changes: 51 additions & 0 deletions app/models/api_request_log.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

class ApiRequestLog < ActiveRecord::Base
self.primary_key = nil

belongs_to :user, optional: true

MAX_PATH_LENGTH = 1024
MAX_ROUTE_LENGTH = 100
MAX_USER_AGENT_LENGTH = 512

def self.log!(data)
create!(
user_id: data[:current_user_id],
path: data[:path]&.slice(0, MAX_PATH_LENGTH),
route: data[:route]&.slice(0, MAX_ROUTE_LENGTH),
http_method: data[:http_method]&.slice(0, 10),
http_status: data[:status],
ip_address: data[:request_remote_ip],
user_agent: data[:user_agent]&.slice(0, MAX_USER_AGENT_LENGTH),
is_user_api: data[:is_user_api] || false,
response_time: data[:timing],
created_at: Time.current,
)
rescue => e
Discourse.warn_exception(e, message: "Failed to log API request")
end
end

# == Schema Information
#
# Table name: api_request_logs
#
# http_method :string(10)
# http_status :integer
# ip_address :inet
# is_user_api :boolean default(FALSE), not null
# path :string(1024)
# response_time :float
# route :string(100)
# user_agent :string(512)
# created_at :datetime not null
# user_id :integer
#
# Indexes
#
# index_api_request_logs_on_created_at (created_at)
# index_api_request_logs_on_http_status (http_status)
# index_api_request_logs_on_route (route)
# index_api_request_logs_on_user_id (user_id)
#
58 changes: 58 additions & 0 deletions app/models/browser_page_view.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

class BrowserPageView < ActiveRecord::Base
self.primary_key = nil

belongs_to :user, optional: true
belongs_to :topic, optional: true

MAX_SESSION_ID_LENGTH = 36
MAX_PATH_LENGTH = 1024
MAX_QUERY_STRING_LENGTH = 1024
MAX_ROUTE_NAME_LENGTH = 256
MAX_REFERRER_LENGTH = 1024
MAX_USER_AGENT_LENGTH = 512

def self.log!(data)
create!(
session_id: data[:session_id]&.slice(0, MAX_SESSION_ID_LENGTH),
user_id: data[:current_user_id],
topic_id: data[:topic_id],
path: data[:path]&.slice(0, MAX_PATH_LENGTH),
query_string: data[:query_string]&.slice(0, MAX_QUERY_STRING_LENGTH),
route_name: data[:route_name]&.slice(0, MAX_ROUTE_NAME_LENGTH),
referrer: data[:referrer]&.slice(0, MAX_REFERRER_LENGTH),
ip_address: data[:request_remote_ip],
user_agent: data[:user_agent]&.slice(0, MAX_USER_AGENT_LENGTH),
is_mobile: data[:is_mobile] || false,
created_at: Time.current,
)
rescue => e
Discourse.warn_exception(e, message: "Failed to log browser page view")
end
end

# == Schema Information
#
# Table name: browser_page_views
#
# ip_address :inet
# is_mobile :boolean default(FALSE), not null
# path :string(1024)
# query_string :string(1024)
# referrer :string(1024)
# route_name :string(256)
# user_agent :string(512)
# created_at :datetime not null
# session_id :string(36)
# topic_id :integer
# user_id :integer
#
# Indexes
#
# index_browser_page_views_on_created_at (created_at)
# index_browser_page_views_on_route_name (route_name)
# index_browser_page_views_on_session_id (session_id)
# index_browser_page_views_on_topic_id (topic_id)
# index_browser_page_views_on_user_id (user_id)
#
2 changes: 1 addition & 1 deletion config/initializers/004-message_bus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def setup_message_bus_env(env)
"Access-Control-Allow-Origin" => cors_origin,
"Access-Control-Allow-Methods" => "GET, POST",
"Access-Control-Allow-Headers" =>
"X-SILENCE-LOGGER, X-Shared-Session-Key, Dont-Chunk, Discourse-Present, Discourse-Deferred-Track-View",
"X-SILENCE-LOGGER, X-Shared-Session-Key, Dont-Chunk, Discourse-Present, Discourse-Deferred-Track-View, Discourse-Deferred-Track-View-Topic-Id, Discourse-Deferred-Track-View-Path, Discourse-Deferred-Track-View-Query-String, Discourse-Deferred-Track-View-Referrer, Discourse-Deffered-Track-View-Session-Id",
"Access-Control-Max-Age" => "7200",
}

Expand Down
16 changes: 16 additions & 0 deletions config/site_settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4135,6 +4135,22 @@ dashboard:
hidden: true
default: false
client: true
enable_page_view_logging:
hidden: true
default: false
page_view_logging_retention_days:
hidden: true
default: 30
min: 1
max: 365
enable_api_request_logging:
hidden: true
default: false
api_request_logging_retention_days:
hidden: true
default: 30
min: 1
max: 365

experimental:
experimental_auto_grid_images:
Expand Down
25 changes: 25 additions & 0 deletions db/migrate/20251212055000_create_browser_page_views.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

class CreateBrowserPageViews < ActiveRecord::Migration[7.2]
def change
create_table :browser_page_views, id: false do |t|
t.string :session_id, limit: 36
t.integer :user_id
t.integer :topic_id
t.string :path, limit: 1024
t.string :query_string, limit: 1024
t.string :route_name, limit: 256
t.string :referrer, limit: 1024
t.inet :ip_address
t.string :user_agent, limit: 512
t.boolean :is_mobile, default: false, null: false
t.datetime :created_at, null: false
end

add_index :browser_page_views, :session_id
add_index :browser_page_views, :user_id
add_index :browser_page_views, :topic_id
add_index :browser_page_views, :route_name
add_index :browser_page_views, :created_at
end
end
23 changes: 23 additions & 0 deletions db/migrate/20251212055001_create_api_request_logs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class CreateApiRequestLogs < ActiveRecord::Migration[7.2]
def change
create_table :api_request_logs, id: false do |t|
t.integer :user_id
t.string :path, limit: 1024
t.string :route, limit: 100
t.string :http_method, limit: 10
t.integer :http_status
t.inet :ip_address
t.string :user_agent, limit: 512
t.boolean :is_user_api, default: false, null: false
t.float :response_time
t.datetime :created_at, null: false
end

add_index :api_request_logs, :user_id
add_index :api_request_logs, :route
add_index :api_request_logs, :http_status
add_index :api_request_logs, :created_at
end
end
51 changes: 50 additions & 1 deletion frontend/discourse/app/instance-initializers/message-bus.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,26 @@ const LONG_POLL_AFTER_UNSEEN_TIME = 1200000; // 20 minutes

let _sendDeferredPageview = false;
let _deferredViewTopicId = null;
let _deferredViewPath = null;
let _deferredViewQueryString = null;
let _deferredViewReferrer = null;
let _deferredViewRouteName = null;

export function sendDeferredPageview() {
export function sendDeferredPageview(routeName = null) {
_sendDeferredPageview = true;
_deferredViewPath = window.location.pathname.slice(0, 1024);
_deferredViewRouteName = routeName?.toString().slice(0, 256) || null;

const search = window.location.search;
if (search && search.length > 1) {
_deferredViewQueryString = search.slice(1, 1025).replace(/[\r\n]/g, "");
} else {
_deferredViewQueryString = null;
}

_deferredViewReferrer = document.referrer
? document.referrer.slice(0, 1024).replace(/[\r\n]/g, "")
: null;
}

function mbAjax(messageBus, opts) {
Expand All @@ -31,14 +48,41 @@ function mbAjax(messageBus, opts) {

if (_sendDeferredPageview) {
opts.headers["Discourse-Deferred-Track-View"] = "true";
// we can gather this implicitly, but for clarity of logging
// sending it explicitly is beneficial
opts.headers["Discourse-Deferred-Track-View-Session-Id"] =
messageBus.clientId;

if (_deferredViewTopicId) {
opts.headers["Discourse-Deferred-Track-View-Topic-Id"] =
_deferredViewTopicId;
}

if (_deferredViewPath) {
opts.headers["Discourse-Deferred-Track-View-Path"] = _deferredViewPath;
}

if (_deferredViewQueryString) {
opts.headers["Discourse-Deferred-Track-View-Query-String"] =
_deferredViewQueryString;
}

if (_deferredViewReferrer) {
opts.headers["Discourse-Deferred-Track-View-Referrer"] =
_deferredViewReferrer;
}

if (_deferredViewRouteName) {
opts.headers["Discourse-Deferred-Track-View-Route-Name"] =
_deferredViewRouteName;
}

_sendDeferredPageview = false;
_deferredViewTopicId = null;
_deferredViewPath = null;
_deferredViewQueryString = null;
_deferredViewReferrer = null;
_deferredViewRouteName = null;
}

const oldComplete = opts.complete;
Expand Down Expand Up @@ -102,6 +146,11 @@ export default {
_deferredViewTopicId = router.currentRoute.parent.params.id;
}

// Set the route name for deferred page view tracking
if (router.currentRouteName) {
_deferredViewRouteName = router.currentRouteName;
}

clearInterval(interval);
messageBus.start();
}
Expand Down
Loading
Loading