diff --git a/Gemfile b/Gemfile index bfc255271..d762a257c 100644 --- a/Gemfile +++ b/Gemfile @@ -56,6 +56,7 @@ gem "sprockets-rails" gem "strong_migrations" gem "pghero" gem "pg_query", ">= 2" +gem "user_agent_parser" group :development, :test do gem "byebug", platform: :mri diff --git a/Gemfile.lock b/Gemfile.lock index 72cd6a331..3912c2419 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -541,6 +541,7 @@ GEM concurrent-ruby (~> 1.0) uber (0.1.0) uri (1.0.2) + user_agent_parser (2.18.0) useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) @@ -626,6 +627,7 @@ DEPENDENCIES syntax_tree (~> 6.2) syntax_tree-haml (~> 4.0) syntax_tree-rbs (~> 1.0.0) + user_agent_parser web-console webmock diff --git a/app/controllers/admins/graphs_controller.rb b/app/controllers/admins/graphs_controller.rb index 8cd830740..a7176cd05 100644 --- a/app/controllers/admins/graphs_controller.rb +++ b/app/controllers/admins/graphs_controller.rb @@ -16,6 +16,8 @@ def show bot_signups when "spam" spam + when "user-agents" + user_agents end render json: data end @@ -67,6 +69,27 @@ def spam end end + def user_agents + base_relation = UserAgent.where("day > ?", 2.months.ago) + base_relation + .select(:name) + .distinct + .pluck(:name) + .reject { |name| name.blank? } + .map do |name| + { + name: name, + data: + base_relation + .where(name: name) + .group(:day) + .order(day: :asc) + .pluck(Arel.sql("day, count(*)")) + .map { |d| [d.first.to_datetime.to_i * 1000, d.last] } + } + end + end + def collected_inks build CollectedInk end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4345782d5..aaad38d8e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception around_action :set_time_zone + before_action :save_user_agent if !Rails.env.development? rescue_from ActionView::MissingTemplate do |exception| @@ -17,4 +18,13 @@ def set_time_zone yield end end + + def save_user_agent + user_agent = request.user_agent + UserAgent.create( + name: UserAgentParser.parse(user_agent).family, + raw_name: user_agent, + day: Date.current + ) + end end diff --git a/app/javascript/src/admin/graphs/UserAgents.jsx b/app/javascript/src/admin/graphs/UserAgents.jsx new file mode 100644 index 000000000..fcee575c9 --- /dev/null +++ b/app/javascript/src/admin/graphs/UserAgents.jsx @@ -0,0 +1,38 @@ +import React, { useEffect, useState } from "react"; +import Highcharts from "highcharts"; +import HighchartsReact from "highcharts-react-official"; + +import { Spinner } from "../components/Spinner"; +import { getRequest } from "../../fetch"; + +export const UserAgents = () => { + const [data, setData] = useState(null); + useEffect(() => { + getRequest("/admins/graphs/user-agents.json") + .then((res) => res.json()) + .then((json) => setData(json)); + }, []); + if (data) { + const options = { + chart: { type: "spline" }, + legend: { enabled: true }, + series: data, + title: { text: "User Agents" }, + xAxis: { + type: "datetime" + }, + yAxis: { title: { text: "" } } + }; + return ( +
+ +
+ ); + } else { + return ( +
+ +
+ ); + } +}; diff --git a/app/javascript/src/admin/graphs/index.jsx b/app/javascript/src/admin/graphs/index.jsx index f943dcfdd..667f8fecb 100644 --- a/app/javascript/src/admin/graphs/index.jsx +++ b/app/javascript/src/admin/graphs/index.jsx @@ -8,6 +8,7 @@ import { CurrentlyInked } from "./CurrentlyInked"; import { UsageRecords } from "./UsageRecords"; import { BotSignUps } from "./BotSignUps"; import { Spam } from "./Spam"; +import { UserAgents } from "./UserAgents"; document.addEventListener("DOMContentLoaded", () => { const el = document.getElementById("signups-graph"); @@ -17,6 +18,14 @@ document.addEventListener("DOMContentLoaded", () => { } }); +document.addEventListener("DOMContentLoaded", () => { + const el = document.getElementById("user-agents-graph"); + if (el) { + const root = createRoot(el); + root.render(); + } +}); + document.addEventListener("DOMContentLoaded", () => { const el = document.getElementById("bot-signups-graph"); if (el) { diff --git a/app/javascript/stylesheets/admin/graphs.scss b/app/javascript/stylesheets/admin/graphs.scss index cab02a356..2d35fa6a4 100644 --- a/app/javascript/stylesheets/admin/graphs.scss +++ b/app/javascript/stylesheets/admin/graphs.scss @@ -1,6 +1,6 @@ #graphs { - .row { - box-shadow: none; + .row div { + margin-bottom: 5px; } .loader { diff --git a/app/models/user_agent.rb b/app/models/user_agent.rb new file mode 100644 index 000000000..6f4f9ab7d --- /dev/null +++ b/app/models/user_agent.rb @@ -0,0 +1,2 @@ +class UserAgent < ApplicationRecord +end diff --git a/app/views/admins/dashboards/show.html.slim b/app/views/admins/dashboards/show.html.slim index dfcd58ea5..f9df7725a 100644 --- a/app/views/admins/dashboards/show.html.slim +++ b/app/views/admins/dashboards/show.html.slim @@ -122,9 +122,11 @@ div#graphs div#currently-inked-graph div.col-sm-12.col-lg-6 div#usage-records-graph - div.col-sm-12.col-md-6.col-lg-4 + div.col-sm-12.col-lg-6 div#signups-graph - div.col-sm-12.col-md-6.col-lg-4 + div.col-sm-12.col-lg-6 div#bot-signups-graph - div.col-sm-12.col-md-6.col-lg-4 + div.col-sm-12.col-lg-6 div#spam-graph + div.col-sm-12.col-lg-6 + div#user-agents-graph diff --git a/db/migrate/20250107083051_create_user_agents.rb b/db/migrate/20250107083051_create_user_agents.rb new file mode 100644 index 000000000..d0126b8a8 --- /dev/null +++ b/db/migrate/20250107083051_create_user_agents.rb @@ -0,0 +1,12 @@ +class CreateUserAgents < ActiveRecord::Migration[8.0] + def change + create_table :user_agents do |t| + t.string :name + t.string :raw_name + t.date :day + + t.index %i[name day] + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 656b661b0..1d92016f1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -822,6 +822,39 @@ CREATE SEQUENCE public.usage_records_id_seq ALTER SEQUENCE public.usage_records_id_seq OWNED BY public.usage_records.id; +-- +-- Name: user_agents; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_agents ( + id bigint NOT NULL, + name character varying, + raw_name character varying, + day date, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: user_agents_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_agents_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_agents_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_agents_id_seq OWNED BY public.user_agents.id; + + -- -- Name: users; Type: TABLE; Schema: public; Owner: - -- @@ -1092,6 +1125,13 @@ ALTER TABLE ONLY public.reading_statuses ALTER COLUMN id SET DEFAULT nextval('pu ALTER TABLE ONLY public.usage_records ALTER COLUMN id SET DEFAULT nextval('public.usage_records_id_seq'::regclass); +-- +-- Name: user_agents id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_agents ALTER COLUMN id SET DEFAULT nextval('public.user_agents_id_seq'::regclass); + + -- -- Name: users id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1297,6 +1337,14 @@ ALTER TABLE ONLY public.usage_records ADD CONSTRAINT usage_records_pkey PRIMARY KEY (id); +-- +-- Name: user_agents user_agents_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_agents + ADD CONSTRAINT user_agents_pkey PRIMARY KEY (id); + + -- -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1657,6 +1705,13 @@ CREATE INDEX index_reading_statuses_on_user_id ON public.reading_statuses USING CREATE UNIQUE INDEX index_usage_records_on_currently_inked_id_and_used_on ON public.usage_records USING btree (currently_inked_id, used_on); +-- +-- Name: index_user_agents_on_name_and_day; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_user_agents_on_name_and_day ON public.user_agents USING btree (name, day); + + -- -- Name: index_users_on_confirmation_token; Type: INDEX; Schema: public; Owner: - -- @@ -1926,6 +1981,7 @@ ALTER TABLE ONLY public.collected_inks SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +('20250107083051'), ('20241228204210'), ('20241228202733'), ('20241211115823'), diff --git a/spec/factories/user_agents.rb b/spec/factories/user_agents.rb new file mode 100644 index 000000000..569aa0e38 --- /dev/null +++ b/spec/factories/user_agents.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :user_agent do + name { "MyString" } + raw_name { "MyString" } + day { "2025-01-07" } + end +end diff --git a/spec/models/user_agent_spec.rb b/spec/models/user_agent_spec.rb new file mode 100644 index 000000000..f58696ffa --- /dev/null +++ b/spec/models/user_agent_spec.rb @@ -0,0 +1,5 @@ +require "rails_helper" + +RSpec.describe UserAgent, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end