mirror of
https://github.com/discourse/discourse.git
synced 2025-02-28 06:30:12 +08:00
FEATURE: search logs page (#5313)
This commit is contained in:
parent
7ecc15cad1
commit
3831663fea
@ -0,0 +1,4 @@
|
|||||||
|
export default Ember.Controller.extend({
|
||||||
|
loading: false,
|
||||||
|
period: "all"
|
||||||
|
});
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ajax } from 'discourse/lib/ajax';
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
renderTemplate() {
|
||||||
|
this.render('admin/templates/logs/search-logs', {into: 'adminLogs'});
|
||||||
|
},
|
||||||
|
|
||||||
|
queryParams: {
|
||||||
|
period: {
|
||||||
|
refreshModel: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
model(params) {
|
||||||
|
this._params = params;
|
||||||
|
return ajax('/admin/logs/search_logs.json', { data: { period: params.period } }).then(search_logs => {
|
||||||
|
return search_logs.map(sl => Ember.Object.create(sl));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
const params = this._params;
|
||||||
|
controller.setProperties({ model, period: params.period });
|
||||||
|
}
|
||||||
|
});
|
@ -66,6 +66,7 @@ export default function() {
|
|||||||
this.route('screenedEmails', { path: '/screened_emails' });
|
this.route('screenedEmails', { path: '/screened_emails' });
|
||||||
this.route('screenedIpAddresses', { path: '/screened_ip_addresses' });
|
this.route('screenedIpAddresses', { path: '/screened_ip_addresses' });
|
||||||
this.route('screenedUrls', { path: '/screened_urls' });
|
this.route('screenedUrls', { path: '/screened_urls' });
|
||||||
|
this.route('searchLogs', { path: '/search_logs' });
|
||||||
this.route('adminWatchedWords', { path: '/watched_words', resetNamespace: true}, function() {
|
this.route('adminWatchedWords', { path: '/watched_words', resetNamespace: true}, function() {
|
||||||
this.route('index', { path: '/' } );
|
this.route('index', { path: '/' } );
|
||||||
this.route('action', { path: '/action/:action_id' } );
|
this.route('action', { path: '/action/:action_id' } );
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
{{nav-item route='adminLogs.screenedIpAddresses' label='admin.logs.screened_ips.title'}}
|
{{nav-item route='adminLogs.screenedIpAddresses' label='admin.logs.screened_ips.title'}}
|
||||||
{{nav-item route='adminLogs.screenedUrls' label='admin.logs.screened_urls.title'}}
|
{{nav-item route='adminLogs.screenedUrls' label='admin.logs.screened_urls.title'}}
|
||||||
{{nav-item route='adminWatchedWords' label='admin.watched_words.title'}}
|
{{nav-item route='adminWatchedWords' label='admin.watched_words.title'}}
|
||||||
|
{{nav-item route='adminLogs.searchLogs' label='admin.logs.search_logs.title'}}
|
||||||
{{#if currentUser.admin}}
|
{{#if currentUser.admin}}
|
||||||
{{nav-item path='/logs' label='admin.logs.logster.title'}}
|
{{nav-item path='/logs' label='admin.logs.logster.title'}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
36
app/assets/javascripts/admin/templates/logs/search-logs.hbs
Normal file
36
app/assets/javascripts/admin/templates/logs/search-logs.hbs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<p>
|
||||||
|
{{period-chooser period=period}}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{{#conditional-loading-spinner condition=loading}}
|
||||||
|
{{#if model.length}}
|
||||||
|
|
||||||
|
<div class='table search-logs-list'>
|
||||||
|
<div class="heading-container">
|
||||||
|
<div class="col heading term">{{i18n 'admin.logs.search_logs.term'}}</div>
|
||||||
|
<div class="col heading">{{i18n 'admin.logs.search_logs.searches'}}</div>
|
||||||
|
<div class="col heading">{{i18n 'admin.logs.search_logs.click_through'}}</div>
|
||||||
|
<div class="col heading topic">{{i18n 'admin.logs.search_logs.most_viewed_topic'}}</div>
|
||||||
|
<div class="col heading">{{i18n 'admin.logs.search_logs.unique'}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#each model as |item|}}
|
||||||
|
<div class="admin-list-item">
|
||||||
|
<div class="col term">{{item.term}}</div>
|
||||||
|
<div class="col">{{item.searches}}</div>
|
||||||
|
<div class="col">{{item.click_through}}</div>
|
||||||
|
<div class="col topic">
|
||||||
|
{{#if item.clicked_topic_id}}
|
||||||
|
<a href='{{unbound item.topic_url}}'>{{item.topic_title}}</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="col">{{item.unique}}</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
{{i18n 'search.no_results'}}
|
||||||
|
{{/if}}
|
||||||
|
{{/conditional-loading-spinner}}
|
@ -1328,7 +1328,7 @@ table.api-keys {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.staff-actions, .screened-emails, .screened-urls, .screened-ip-addresses, .permalinks, .web-hook-events {
|
.staff-actions, .screened-emails, .screened-urls, .screened-ip-addresses, .permalinks, .search-logs-list, .web-hook-events {
|
||||||
|
|
||||||
border-bottom: dotted 1px dark-light-choose($primary-low-mid, $secondary);
|
border-bottom: dotted 1px dark-light-choose($primary-low-mid, $secondary);
|
||||||
|
|
||||||
@ -1354,6 +1354,23 @@ table.api-keys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-logs-list{
|
||||||
|
.col {
|
||||||
|
text-align: center;
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col.term {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col.topic {
|
||||||
|
width: 35%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.log-details-modal {
|
.log-details-modal {
|
||||||
.modal-tab {
|
.modal-tab {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
|
8
app/controllers/admin/search_logs_controller.rb
Normal file
8
app/controllers/admin/search_logs_controller.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class Admin::SearchLogsController < Admin::AdminController
|
||||||
|
|
||||||
|
def index
|
||||||
|
period = params[:period] || "all"
|
||||||
|
render_serialized(SearchLog.trending(period.to_sym), SearchLogsSerializer)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -1,6 +1,7 @@
|
|||||||
require_dependency 'enum'
|
require_dependency 'enum'
|
||||||
|
|
||||||
class SearchLog < ActiveRecord::Base
|
class SearchLog < ActiveRecord::Base
|
||||||
|
belongs_to :topic, foreign_key: :clicked_topic_id
|
||||||
validates_presence_of :term, :ip_address
|
validates_presence_of :term, :ip_address
|
||||||
|
|
||||||
def self.search_types
|
def self.search_types
|
||||||
@ -48,6 +49,32 @@ class SearchLog < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.trending(period = :all)
|
||||||
|
SearchLog.select("term,
|
||||||
|
COUNT(*) AS searches,
|
||||||
|
SUM(CASE
|
||||||
|
WHEN clicked_topic_id IS NOT NULL THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END) AS click_through,
|
||||||
|
MODE() WITHIN GROUP (ORDER BY clicked_topic_id) AS clicked_topic_id,
|
||||||
|
COUNT(DISTINCT ip_address) AS unique")
|
||||||
|
.where('created_at > ?', start_of(period))
|
||||||
|
.group(:term)
|
||||||
|
.order('COUNT(*) DESC')
|
||||||
|
.limit(100).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.start_of(period)
|
||||||
|
case period
|
||||||
|
when :yearly then 1.year.ago
|
||||||
|
when :monthly then 1.month.ago
|
||||||
|
when :quarterly then 3.months.ago
|
||||||
|
when :weekly then 1.week.ago
|
||||||
|
when :daily then 1.day.ago
|
||||||
|
else 1000.years.ago
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.clean_up
|
def self.clean_up
|
||||||
search_id = SearchLog.order(:id).offset(SiteSetting.search_query_log_max_size).limit(1).pluck(:id)
|
search_id = SearchLog.order(:id).offset(SiteSetting.search_query_log_max_size).limit(1).pluck(:id)
|
||||||
if search_id.present?
|
if search_id.present?
|
||||||
|
17
app/serializers/search_logs_serializer.rb
Normal file
17
app/serializers/search_logs_serializer.rb
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
class SearchLogsSerializer < ApplicationSerializer
|
||||||
|
attributes :term,
|
||||||
|
:searches,
|
||||||
|
:click_through,
|
||||||
|
:clicked_topic_id,
|
||||||
|
:topic_title,
|
||||||
|
:topic_url,
|
||||||
|
:unique
|
||||||
|
|
||||||
|
def topic_title
|
||||||
|
object&.topic&.title
|
||||||
|
end
|
||||||
|
|
||||||
|
def topic_url
|
||||||
|
object&.topic&.url
|
||||||
|
end
|
||||||
|
end
|
@ -3184,6 +3184,13 @@ en:
|
|||||||
roll_up:
|
roll_up:
|
||||||
text: "Roll up"
|
text: "Roll up"
|
||||||
title: "Creates new subnet ban entries if there are at least 'min_ban_entries_for_roll_up' entries."
|
title: "Creates new subnet ban entries if there are at least 'min_ban_entries_for_roll_up' entries."
|
||||||
|
search_logs:
|
||||||
|
title: "Search Logs"
|
||||||
|
term: "Term"
|
||||||
|
searches: "Searches"
|
||||||
|
click_through: "Click Through"
|
||||||
|
most_viewed_topic: "Most Viewed Topic"
|
||||||
|
unique: "Unique"
|
||||||
logster:
|
logster:
|
||||||
title: "Error Logs"
|
title: "Error Logs"
|
||||||
|
|
||||||
|
@ -174,6 +174,7 @@ Discourse::Application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
post "watched_words/upload" => "watched_words#upload"
|
post "watched_words/upload" => "watched_words#upload"
|
||||||
|
resources :search_logs, only: [:index]
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/logs" => "staff_action_logs#index"
|
get "/logs" => "staff_action_logs#index"
|
||||||
|
@ -156,6 +156,41 @@ RSpec.describe SearchLog, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "trending" do
|
||||||
|
before do
|
||||||
|
SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1')
|
||||||
|
SearchLog.log(term: 'php', search_type: :header, ip_address: '127.0.0.1')
|
||||||
|
SearchLog.log(term: 'java', search_type: :header, ip_address: '127.0.0.1')
|
||||||
|
SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1', user_id: Fabricate(:user).id)
|
||||||
|
SearchLog.log(term: 'swift', search_type: :header, ip_address: '127.0.0.1')
|
||||||
|
SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.2')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "considers time period" do
|
||||||
|
expect(SearchLog.trending.count).to eq(4)
|
||||||
|
|
||||||
|
SearchLog.where(term: 'swift').update_all(created_at: 1.year.ago)
|
||||||
|
expect(SearchLog.trending(:monthly).count).to eq(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "correctly returns trending data" do
|
||||||
|
top_trending = SearchLog.trending.first
|
||||||
|
expect(top_trending.term).to eq("ruby")
|
||||||
|
expect(top_trending.searches).to eq(3)
|
||||||
|
expect(top_trending.unique).to eq(2)
|
||||||
|
expect(top_trending.click_through).to eq(0)
|
||||||
|
expect(top_trending.clicked_topic_id).to eq(nil)
|
||||||
|
|
||||||
|
popular_topic = Fabricate(:topic)
|
||||||
|
not_so_popular_topic = Fabricate(:topic)
|
||||||
|
SearchLog.where(term: 'ruby', ip_address: '127.0.0.1').update_all(clicked_topic_id: popular_topic.id)
|
||||||
|
SearchLog.where(term: 'ruby', ip_address: '127.0.0.2').update_all(clicked_topic_id: not_so_popular_topic.id)
|
||||||
|
top_trending = SearchLog.trending.first
|
||||||
|
expect(top_trending.click_through).to eq(3)
|
||||||
|
expect(top_trending.clicked_topic_id).to eq(popular_topic.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "clean_up" do
|
context "clean_up" do
|
||||||
|
|
||||||
it "will remove old logs" do
|
it "will remove old logs" do
|
||||||
|
35
spec/requests/admin/search_logs_spec.rb
Normal file
35
spec/requests/admin/search_logs_spec.rb
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Admin::SearchLogsController do
|
||||||
|
let(:admin) { Fabricate(:admin) }
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1')
|
||||||
|
end
|
||||||
|
|
||||||
|
context "#index" do
|
||||||
|
it "raises an error if you aren't logged in" do
|
||||||
|
expect do
|
||||||
|
get '/admin/logs/search_logs.json'
|
||||||
|
end.to raise_error(ActionController::RoutingError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error if you aren't an admin" do
|
||||||
|
sign_in(user)
|
||||||
|
expect do
|
||||||
|
get '/admin/logs/search_logs.json'
|
||||||
|
end.to raise_error(ActionController::RoutingError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should work if you are an admin" do
|
||||||
|
sign_in(admin)
|
||||||
|
get '/admin/logs/search_logs.json'
|
||||||
|
|
||||||
|
expect(response).to be_success
|
||||||
|
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json[0]['term']).to eq('ruby')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
test/javascripts/acceptance/admin-search-logs-test.js.es6
Normal file
10
test/javascripts/acceptance/admin-search-logs-test.js.es6
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { acceptance } from "helpers/qunit-helpers";
|
||||||
|
acceptance("Admin - Search Logs", { loggedIn: true });
|
||||||
|
|
||||||
|
QUnit.test("show search logs", assert => {
|
||||||
|
visit("/admin/logs/search_logs");
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok($('div.table.search-logs-list').length, "has the div class");
|
||||||
|
assert.ok(exists('.search-logs-list .admin-list-item .col'), "has a list of search logs");
|
||||||
|
});
|
||||||
|
});
|
@ -391,6 +391,12 @@ export default function() {
|
|||||||
return response(200, result);
|
return response(200, result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.get('/admin/logs/search_logs.json', () => {
|
||||||
|
return response(200, [
|
||||||
|
{"term":"foobar","searches":35,"click_through":6,"clicked_topic_id":1550,"topic_title":"Foo Bar Topic Title","topic_url":"http://discourse.example.com/t/foo-bar-topic-title/1550","unique":16}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
this.get('/onebox', request => {
|
this.get('/onebox', request => {
|
||||||
if (request.queryParams.url === 'http://www.example.com/has-title.html' ||
|
if (request.queryParams.url === 'http://www.example.com/has-title.html' ||
|
||||||
request.queryParams.url === 'http://www.example.com/has-title-and-a-url-that-is-more-than-80-characters-because-thats-good-for-seo-i-guess.html') {
|
request.queryParams.url === 'http://www.example.com/has-title-and-a-url-that-is-more-than-80-characters-because-thats-good-for-seo-i-guess.html') {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user