Class: Report

Inherits:
Object
  • Object
show all
Includes:
Reports::Bookmarks, Reports::ConsolidatedApiRequests, Reports::ConsolidatedPageViews, Reports::ConsolidatedPageViewsBrowserDetection, Reports::DailyEngagedUsers, Reports::DauByMau, Reports::Emails, Reports::Flags, Reports::FlagsStatus, Reports::Likes, Reports::MobileVisits, Reports::ModeratorWarningPrivateMessages, Reports::ModeratorsActivity, Reports::NewContributors, Reports::NotifyModeratorsPrivateMessages, Reports::NotifyUserPrivateMessages, Reports::PostEdits, Reports::Posts, Reports::ProfileViews, Reports::Signups, Reports::SiteTraffic, Reports::StaffLogins, Reports::StorageStats, Reports::SuspiciousLogins, Reports::SystemPrivateMessages, Reports::TimeToFirstResponse, Reports::TopIgnoredUsers, Reports::TopReferredTopics, Reports::TopReferrers, Reports::TopTrafficSources, Reports::TopUploads, Reports::TopUsersByLikesReceived, Reports::TopUsersByLikesReceivedFromAVarietyOfPeople, Reports::TopUsersByLikesReceivedFromInferiorTrustLevel, Reports::TopicViewStats, Reports::Topics, Reports::TopicsWithNoResponse, Reports::TrendingSearch, Reports::TrustLevelGrowth, Reports::UserFlaggingRatio, Reports::UserToUserPrivateMessages, Reports::UserToUserPrivateMessagesWithReplies, Reports::UsersByTrustLevel, Reports::UsersByType, Reports::Visits, Reports::WebCrawlers, Reports::WebHookEventsDailyAggregate
Defined in:
app/models/report.rb

Constant Summary collapse

SCHEMA_VERSION =

Change this line each time report format change and you want to ensure cache is reset

4
FILTERS =
%i[
  name
  start_date
  end_date
  category
  group
  trust_level
  file_extension
  include_subcategories
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(type) ⇒ Report

Returns a new instance of Report.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'app/models/report.rb', line 101

def initialize(type)
  @type = type
  @start_date ||= Report.default_days.days.ago.utc.beginning_of_day
  @end_date ||= Time.now.utc.end_of_day
  @prev_end_date = @start_date
  @average = false
  @percent = false
  @higher_is_better = true
  @modes = %i[table chart]
  @prev_data = nil
  @dates_filtering = true
  @available_filters = {}
  @filters = {}

  tertiary = ColorScheme.hex_for_name("tertiary") || "0088cc"
  @primary_color = rgba_color(tertiary)
  @secondary_color = rgba_color(tertiary, 0.1)
end

Instance Attribute Details

#available_filtersObject

Returns the value of attribute available_filters.



67
68
69
# File 'app/models/report.rb', line 67

def available_filters
  @available_filters
end

#averageObject

Returns the value of attribute average.



67
68
69
# File 'app/models/report.rb', line 67

def average
  @average
end

#dataObject

Returns the value of attribute data.



67
68
69
# File 'app/models/report.rb', line 67

def data
  @data
end

#dates_filteringObject

Returns the value of attribute dates_filtering.



67
68
69
# File 'app/models/report.rb', line 67

def dates_filtering
  @dates_filtering
end

#end_dateObject

Returns the value of attribute end_date.



67
68
69
# File 'app/models/report.rb', line 67

def end_date
  @end_date
end

#errorObject

Returns the value of attribute error.



67
68
69
# File 'app/models/report.rb', line 67

def error
  @error
end

#facetsObject

Returns the value of attribute facets.



67
68
69
# File 'app/models/report.rb', line 67

def facets
  @facets
end

#filtersObject

Returns the value of attribute filters.



67
68
69
# File 'app/models/report.rb', line 67

def filters
  @filters
end

#higher_is_betterObject

Returns the value of attribute higher_is_better.



67
68
69
# File 'app/models/report.rb', line 67

def higher_is_better
  @higher_is_better
end

#iconObject

Returns the value of attribute icon.



67
68
69
# File 'app/models/report.rb', line 67

def icon
  @icon
end

#labelsObject

Returns the value of attribute labels.



67
68
69
# File 'app/models/report.rb', line 67

def labels
  @labels
end

#limitObject

Returns the value of attribute limit.



67
68
69
# File 'app/models/report.rb', line 67

def limit
  @limit
end

#modesObject

Returns the value of attribute modes.



67
68
69
# File 'app/models/report.rb', line 67

def modes
  @modes
end

#percentObject

Returns the value of attribute percent.



67
68
69
# File 'app/models/report.rb', line 67

def percent
  @percent
end

#prev30DaysObject

Returns the value of attribute prev30Days.



67
68
69
# File 'app/models/report.rb', line 67

def prev30Days
  @prev30Days
end

#prev_dataObject

Returns the value of attribute prev_data.



67
68
69
# File 'app/models/report.rb', line 67

def prev_data
  @prev_data
end

#prev_periodObject

Returns the value of attribute prev_period.



67
68
69
# File 'app/models/report.rb', line 67

def prev_period
  @prev_period
end

#primary_colorObject

Returns the value of attribute primary_color.



67
68
69
# File 'app/models/report.rb', line 67

def primary_color
  @primary_color
end

#secondary_colorObject

Returns the value of attribute secondary_color.



67
68
69
# File 'app/models/report.rb', line 67

def secondary_color
  @secondary_color
end

#start_dateObject

Returns the value of attribute start_date.



67
68
69
# File 'app/models/report.rb', line 67

def start_date
  @start_date
end

#totalObject

Returns the value of attribute total.



67
68
69
# File 'app/models/report.rb', line 67

def total
  @total
end

#typeObject

Returns the value of attribute type.



67
68
69
# File 'app/models/report.rb', line 67

def type
  @type
end

Class Method Details

._get(type, opts = nil) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'app/models/report.rb', line 232

def self._get(type, opts = nil)
  opts ||= {}

  # Load the report
  report = Report.new(type)
  report.start_date = opts[:start_date] if opts[:start_date]
  report.end_date = opts[:end_date] if opts[:end_date]
  report.facets = opts[:facets] || %i[total prev30Days]
  report.limit = opts[:limit] if opts[:limit]
  report.average = opts[:average] if opts[:average]
  report.percent = opts[:percent] if opts[:percent]
  report.filters = opts[:filters] if opts[:filters]
  report.labels = Report.default_labels

  report
end

.add_counts(report, subject_class, query_column = "created_at") ⇒ Object



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'app/models/report.rb', line 383

def self.add_counts(report, subject_class, query_column = "created_at")
  if report.facets.include?(:prev_period)
    prev_data =
      subject_class.where(
        "#{query_column} >= ? and #{query_column} < ?",
        report.prev_start_date,
        report.prev_end_date,
      )

    report.prev_period = prev_data.count
  end

  report.total = subject_class.count if report.facets.include?(:total)

  if report.facets.include?(:prev30Days)
    report.prev30Days =
      subject_class.where(
        "#{query_column} >= ? and #{query_column} < ?",
        report.start_date - 30.days,
        report.start_date,
      ).count
  end
end

.add_prev_data(report, subject_class, report_method, *args) ⇒ Object



376
377
378
379
380
381
# File 'app/models/report.rb', line 376

def self.add_prev_data(report, subject_class, report_method, *args)
  if report.modes.include?(:chart) && report.facets.include?(:prev_period)
    prev_data = subject_class.public_send(report_method, *args)
    report.prev_data = prev_data.map { |k, v| { x: k, y: v } }
  end
end

.add_report(name, &block) ⇒ Object



223
224
225
# File 'app/models/report.rb', line 223

def Report.add_report(name, &block)
  singleton_class.instance_eval { define_method("report_#{name}", &block) }
end

.basic_report_about(report, subject_class, report_method, *args) ⇒ Object



368
369
370
371
372
373
374
# File 'app/models/report.rb', line 368

def self.basic_report_about(report, subject_class, report_method, *args)
  report.data = []

  subject_class
    .public_send(report_method, *args)
    .each { |date, count| report.data << { x: date, y: count } }
end

.cache(report) ⇒ Object



254
255
256
257
# File 'app/models/report.rb', line 254

def self.cache(report)
  duration = report.error == :exception ? 1.minute : 35.minutes
  Discourse.cache.write(cache_key(report), report.as_json, expires_in: duration)
end

.cache_key(report) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
# File 'app/models/report.rb', line 120

def self.cache_key(report)
  [
    "reports",
    report.type,
    report.start_date.to_date.strftime("%Y%m%d"),
    report.end_date.to_date.strftime("%Y%m%d"),
    report.facets,
    report.limit,
    report.filters.blank? ? nil : MultiJson.dump(report.filters),
    SCHEMA_VERSION,
  ].compact.map(&:to_s).join(":")
end

.clear_cache(type = nil) ⇒ Object



153
154
155
156
157
# File 'app/models/report.rb', line 153

def self.clear_cache(type = nil)
  pattern = type ? "reports:#{type}:*" : "reports:*"

  Discourse.cache.keys(pattern).each { |key| Discourse.cache.redis.del(key) }
end

.default_daysObject



90
91
92
# File 'app/models/report.rb', line 90

def self.default_days
  30
end

.default_labelsObject



94
95
96
97
98
99
# File 'app/models/report.rb', line 94

def self.default_labels
  [
    { type: :date, property: :x, title: I18n.t("reports.default.labels.day") },
    { type: :number, property: :y, title: I18n.t("reports.default.labels.count") },
  ]
end

.find(type, opts = nil) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'app/models/report.rb', line 259

def self.find(type, opts = nil)
  opts ||= {}

  begin
    report = _get(type, opts)
    report_method = :"report_#{type}"

    begin
      wrap_slow_query do
        if respond_to?(report_method)
          public_send(report_method, report)
        elsif type =~ /_reqs\z/
          req_report(report, type.split(/_reqs\z/)[0].to_sym)
        else
          return nil
        end
      end
    rescue ActiveRecord::QueryCanceled, PG::QueryCanceled => e
      report.error = :timeout
    end
  rescue Exception => e
    # In test mode, don't swallow exceptions by default to help debug errors.
    raise if Rails.env.test? && !opts[:wrap_exceptions_in_test]

    # ensures that if anything unexpected prevents us from
    # creating a report object we fail elegantly and log an error
    if !report
      Rails.logger.error("Couldn’t create report `#{type}`: <#{e.class} #{e.message}>")
      return nil
    end

    report.error = :exception

    # given reports can be added by plugins we don’t want dashboard failures
    # on report computation, however we do want to log which report is provoking
    # an error
    Rails.logger.error(
      "Error while computing report `#{report.type}`: #{e.message}\n#{e.backtrace.join("\n")}",
    )
  end

  report
end

.find_cached(type, opts = nil) ⇒ Object



249
250
251
252
# File 'app/models/report.rb', line 249

def self.find_cached(type, opts = nil)
  report = _get(type, opts)
  Discourse.cache.read(cache_key(report))
end

.legacy_page_view_requestsObject

We purposefully exclude “browser” pageviews. See ‘ConsolidatedPageViewsBrowserDetection` for browser pageviews.



340
341
342
343
344
345
346
347
348
# File 'app/models/report.rb', line 340

def self.legacy_page_view_requests
  ApplicationRequest.where(
    req_type: [
      ApplicationRequest.req_types[:page_view_crawler],
      ApplicationRequest.req_types[:page_view_anon],
      ApplicationRequest.req_types[:page_view_logged_in],
    ].flatten,
  )
end

.page_view_requestsObject

We purposefully exclude “crawler” pageviews here and by only doing browser pageviews we are excluding “other” pageviews too. This is to reflect what is shown in the “Site traffic” report by default.



354
355
356
357
358
359
360
361
# File 'app/models/report.rb', line 354

def self.page_view_requests
  ApplicationRequest.where(
    req_type: [
      ApplicationRequest.req_types[:page_view_anon_browser],
      ApplicationRequest.req_types[:page_view_logged_in_browser],
    ].flatten,
  )
end

.post_action_report(report, post_action_type) ⇒ Object



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'app/models/report.rb', line 407

def self.post_action_report(report, post_action_type)
  category_id, include_subcategories = report.add_category_filter

  report.data = []
  PostAction
    .count_per_day_for_type(
      post_action_type,
      category_id: category_id,
      include_subcategories: include_subcategories,
      start_date: report.start_date,
      end_date: report.end_date,
    )
    .each { |date, count| report.data << { x: date, y: count } }

  countable = PostAction.unscoped.where(post_action_type_id: post_action_type)
  if category_id
    if include_subcategories
      countable =
        countable.joins(post: :topic).where(
          "topics.category_id IN (?)",
          Category.subcategory_ids(category_id),
        )
    else
      countable = countable.joins(post: :topic).where("topics.category_id = ?", category_id)
    end
  end

  add_counts report, countable, "post_actions.created_at"
end

.private_messages_report(report, topic_subtype) ⇒ Object



437
438
439
440
441
442
443
444
445
446
447
448
# File 'app/models/report.rb', line 437

def self.private_messages_report(report, topic_subtype)
  report.icon = "envelope"
  subject = Topic.where("topics.user_id > 0")
  basic_report_about report,
                     subject,
                     :private_message_topics_count_per_day,
                     report.start_date,
                     report.end_date,
                     topic_subtype
  subject = Topic.private_messages.where("topics.user_id > 0").with_subtype(topic_subtype)
  add_counts report, subject, "topics.created_at"
end

.remove_report(name) ⇒ Object

Only used for testing.



228
229
230
# File 'app/models/report.rb', line 228

def Report.remove_report(name)
  singleton_class.instance_eval { remove_method("report_#{name}") }
end

.report_about(report, subject_class, report_method = :count_per_day) ⇒ Object



363
364
365
366
# File 'app/models/report.rb', line 363

def self.report_about(report, subject_class, report_method = :count_per_day)
  basic_report_about report, subject_class, report_method, report.start_date, report.end_date
  add_counts report, subject_class
end

.req_report(report, filter = nil) ⇒ Object

NOTE: Once use_legacy_pageviews is always false or no longer needed we will no longer support the page_view_anon and page_view_logged_in reports, they can be removed.



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'app/models/report.rb', line 306

def self.req_report(report, filter = nil)
  data =
    # For this report we intentionally do not want to count mobile pageviews.
    if filter == :page_view_total
      SiteSetting.use_legacy_pageviews ? legacy_page_view_requests : page_view_requests
      # This is a separate report because if people have switched over
      # to _not_ use legacy pageviews, we want to show both a Pageviews
      # and Legacy Pageviews report.
    elsif filter == :page_view_legacy_total
      legacy_page_view_requests
    else
      ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter])
    end

  report.icon = "file" if filter == :page_view_total

  report.data = []
  data
    .where("date >= ? AND date <= ?", report.start_date, report.end_date)
    .order(date: :asc)
    .group(:date)
    .sum(:count)
    .each { |date, count| report.data << { x: date, y: count } }

  report.total = data.sum(:count)

  report.prev30Days =
    data.where("date >= ? AND date < ?", (report.start_date - 31.days), report.start_date).sum(
      :count,
    )
end

.wrap_slow_query(timeout = 20_000) ⇒ Object



159
160
161
162
163
164
165
166
167
# File 'app/models/report.rb', line 159

def self.wrap_slow_query(timeout = 20_000)
  ActiveRecord::Base.connection.transaction do
    # Allows only read only transactions
    DB.exec "SET TRANSACTION READ ONLY"
    # Set a statement timeout so we can't tie up the server
    DB.exec "SET LOCAL statement_timeout = #{timeout}"
    yield
  end
end

Instance Method Details

#add_category_filterObject



141
142
143
144
145
146
147
148
149
150
151
# File 'app/models/report.rb', line 141

def add_category_filter
  category_id = filters[:category].to_i if filters[:category].present?
  add_filter("category", type: "category", default: category_id)
  return if category_id.blank?

  include_subcategories = filters[:include_subcategories]
  include_subcategories = !!ActiveRecord::Type::Boolean.new.cast(include_subcategories)
  add_filter("include_subcategories", type: "bool", default: include_subcategories)

  [category_id, include_subcategories]
end

#add_filter(name, options = {}) ⇒ Object



133
134
135
# File 'app/models/report.rb', line 133

def add_filter(name, options = {})
  available_filters[name] = options
end

#as_json(options = nil) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'app/models/report.rb', line 177

def as_json(options = nil)
  description = I18n.t("reports.#{type}.description", default: "")
  description_link = I18n.t("reports.#{type}.description_link", default: "")

  {
    type: type,
    title: I18n.t("reports.#{type}.title", default: nil),
    xaxis: I18n.t("reports.#{type}.xaxis", default: nil),
    yaxis: I18n.t("reports.#{type}.yaxis", default: nil),
    description: description.presence ? description : nil,
    description_link: description_link.presence ? description_link : nil,
    data: data,
    start_date: start_date&.iso8601,
    end_date: end_date&.iso8601,
    prev_data: self.prev_data,
    prev_start_date: prev_start_date&.iso8601,
    prev_end_date: prev_end_date&.iso8601,
    prev30Days: self.prev30Days,
    dates_filtering: self.dates_filtering,
    report_key: Report.cache_key(self),
    primary_color: self.primary_color,
    secondary_color: self.secondary_color,
    available_filters: self.available_filters.map { |k, v| { id: k }.merge(v) },
    labels: labels || Report.default_labels,
    average: self.average,
    percent: self.percent,
    higher_is_better: self.higher_is_better,
    modes: self.modes,
  }.tap do |json|
    json[:icon] = self.icon if self.icon
    json[:error] = self.error if self.error
    json[:total] = self.total if self.total
    json[:prev_period] = self.prev_period if self.prev_period
    json[:prev30Days] = self.prev30Days if self.prev30Days
    json[:limit] = self.limit if self.limit

    if type == "page_view_crawler_reqs"
      json[:related_report] = Report.find(
        "web_crawlers",
        start_date: start_date,
        end_date: end_date,
      )&.as_json
    end
  end
end

#colorsObject



455
456
457
458
459
460
461
462
463
464
# File 'app/models/report.rb', line 455

def colors
  {
    turquoise: "#1EB8D1",
    lime: "#9BC53D",
    purple: "#721D8D",
    magenta: "#E84A5F",
    brown: "#8A6916",
    yellow: "#FFCD56",
  }
end

#prev_end_dateObject



173
174
175
# File 'app/models/report.rb', line 173

def prev_end_date
  self.start_date
end

#prev_start_dateObject



169
170
171
# File 'app/models/report.rb', line 169

def prev_start_date
  self.start_date - (self.end_date - self.start_date)
end

#remove_filter(name) ⇒ Object



137
138
139
# File 'app/models/report.rb', line 137

def remove_filter(name)
  available_filters.delete(name)
end

#rgba_color(hex, opacity = 1) ⇒ Object



450
451
452
453
# File 'app/models/report.rb', line 450

def rgba_color(hex, opacity = 1)
  rgbs = hex_to_rgbs(adjust_hex(hex))
  "rgba(#{rgbs.join(",")},#{opacity})"
end