From 51e589e17ffc834d6a22b10a836d21896e4b032a Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Thu, 13 Feb 2025 21:04:29 +0100 Subject: [PATCH] Implement map tiles usage tracking and chart in user settings. --- CHANGELOG.md | 1 + .../api/v1/maps/tile_usage_controller.rb | 2 +- app/controllers/settings/maps_controller.rb | 7 +++ app/javascript/controllers/maps_controller.js | 5 +- app/javascript/maps/tile_monitor.js | 10 +-- app/services/maps/tile_usage/track.rb | 23 ++++++- .../reverse_geocoding/places/fetch_data.rb | 8 ++- app/views/map/index.html.erb | 1 - app/views/settings/maps/index.html.erb | 63 +++++++++++-------- config/initializers/reddis.rb | 7 --- spec/requests/api/v1/maps/tile_usage_spec.rb | 7 +-- spec/services/maps/tile_usage/track_spec.rb | 17 ++++- 12 files changed, 94 insertions(+), 57 deletions(-) delete mode 100644 config/initializers/reddis.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb1084b..c121fd47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ To set a custom tile URL, go to the user settings and set the `Maps` section to - Safe settings for user with default values. - In the user settings, you can now set a custom tile URL for the map. #429 #715 +- In the user map settings, you can now see a chart of map tiles usage. - If you have Prometheus exporter enabled, you can now see a `ruby_dawarich_map_tiles` metric in Prometheus, which shows the total number of map tiles loaded. Example: ``` diff --git a/app/controllers/api/v1/maps/tile_usage_controller.rb b/app/controllers/api/v1/maps/tile_usage_controller.rb index 43f8f070..c22778e7 100644 --- a/app/controllers/api/v1/maps/tile_usage_controller.rb +++ b/app/controllers/api/v1/maps/tile_usage_controller.rb @@ -2,7 +2,7 @@ class Api::V1::Maps::TileUsageController < ApiController def create - Maps::TileUsage::Track.new(tile_usage_params[:count].to_i).call + Maps::TileUsage::Track.new(current_api_user.id, tile_usage_params[:count].to_i).call head :ok end diff --git a/app/controllers/settings/maps_controller.rb b/app/controllers/settings/maps_controller.rb index 59beb04d..58e2fef6 100644 --- a/app/controllers/settings/maps_controller.rb +++ b/app/controllers/settings/maps_controller.rb @@ -5,6 +5,13 @@ class Settings::MapsController < ApplicationController def index @maps = current_user.safe_settings.maps + + @tile_usage = 7.days.ago.to_date.upto(Time.zone.today).map do |date| + [ + date.to_s, + Rails.cache.read("dawarich_map_tiles_usage:#{current_user.id}:#{date}") || 0 + ] + end end def update diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 53b39c20..d2f59dbb 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -34,9 +34,6 @@ import { TileMonitor } from "../maps/tile_monitor"; export default class extends Controller { static targets = ["container"]; - static values = { - monitoringEnabled: Boolean - } settingsButtonAdded = false; layerControl = null; @@ -249,7 +246,7 @@ export default class extends Controller { } // Initialize tile monitor - this.tileMonitor = new TileMonitor(this.monitoringEnabledValue, this.apiKey); + this.tileMonitor = new TileMonitor(this.apiKey); // Add tile load event handlers to each base layer Object.entries(this.baseMaps()).forEach(([name, layer]) => { diff --git a/app/javascript/maps/tile_monitor.js b/app/javascript/maps/tile_monitor.js index 3a4ff36e..0a1edc60 100644 --- a/app/javascript/maps/tile_monitor.js +++ b/app/javascript/maps/tile_monitor.js @@ -1,15 +1,11 @@ export class TileMonitor { - constructor(monitoringEnabled, apiKey) { - this.monitoringEnabled = monitoringEnabled; + constructor(apiKey) { this.apiKey = apiKey; this.tileQueue = 0; this.tileUpdateInterval = null; } startMonitoring() { - // Only start the interval if monitoring is enabled - if (!this.monitoringEnabled) return; - // Clear any existing interval if (this.tileUpdateInterval) { clearInterval(this.tileUpdateInterval); @@ -29,13 +25,11 @@ export class TileMonitor { } recordTileLoad() { - if (!this.monitoringEnabled) return; this.tileQueue += 1; } sendTileUsage() { - // Don't send if monitoring is disabled or queue is empty - if (!this.monitoringEnabled || this.tileQueue === 0) return; + if (this.tileQueue === 0) return; const currentCount = this.tileQueue; console.log('Sending tile usage batch:', currentCount); diff --git a/app/services/maps/tile_usage/track.rb b/app/services/maps/tile_usage/track.rb index 0affd754..a2ec819d 100644 --- a/app/services/maps/tile_usage/track.rb +++ b/app/services/maps/tile_usage/track.rb @@ -1,11 +1,23 @@ # frozen_string_literal: true class Maps::TileUsage::Track - def initialize(count = 1) + def initialize(user_id, count = 1) + @user_id = user_id @count = count end def call + report_to_prometheus + report_to_cache + rescue StandardError => e + Rails.logger.error("Failed to send tile usage metric: #{e.message}") + end + + private + + def report_to_prometheus + return unless DawarichSettings.prometheus_exporter_enabled? + metric_data = { type: 'counter', name: 'dawarich_map_tiles_usage', @@ -13,7 +25,12 @@ class Maps::TileUsage::Track } PrometheusExporter::Client.default.send_json(metric_data) - rescue StandardError => e - Rails.logger.error("Failed to send tile usage metric: #{e.message}") + end + + def report_to_cache + today_key = "dawarich_map_tiles_usage:#{@user_id}:#{Time.zone.today}" + + current_value = (Rails.cache.read(today_key) || 0).to_i + Rails.cache.write(today_key, current_value + @count, expires_in: 7.days) end end diff --git a/app/services/reverse_geocoding/places/fetch_data.rb b/app/services/reverse_geocoding/places/fetch_data.rb index 12186c9f..9b691d36 100644 --- a/app/services/reverse_geocoding/places/fetch_data.rb +++ b/app/services/reverse_geocoding/places/fetch_data.rb @@ -96,7 +96,13 @@ class ReverseGeocoding::Places::FetchData end def reverse_geocoded_places - data = Geocoder.search([place.latitude, place.longitude], limit: 10, distance_sort: true, radius: 10) + data = Geocoder.search( + [place.latitude, place.longitude], + limit: 10, + distance_sort: true, + radius: 1, + units: DISTANCE_UNITS + ) data.reject do |place| place.data['properties']['osm_value'].in?(IGNORED_OSM_VALUES) || diff --git a/app/views/map/index.html.erb b/app/views/map/index.html.erb index 511f12a7..9fa4a0fe 100644 --- a/app/views/map/index.html.erb +++ b/app/views/map/index.html.erb @@ -53,7 +53,6 @@ data-coordinates="<%= @coordinates %>" data-distance="<%= @distance %>" data-points_number="<%= @points_number %>" - data-maps-monitoring-enabled-value="<%= DawarichSettings.prometheus_exporter_enabled? %>" data-timezone="<%= Rails.configuration.time_zone %>">
diff --git a/app/views/settings/maps/index.html.erb b/app/views/settings/maps/index.html.erb index 2e50950d..e80d875a 100644 --- a/app/views/settings/maps/index.html.erb +++ b/app/views/settings/maps/index.html.erb @@ -22,35 +22,46 @@ Please remember, that using a custom tile URL may result in extra costs. Check your map tile provider's terms of service for more information.
-
- <%= form_for :maps, - url: settings_maps_path, - method: :patch, - autocomplete: "off", - data: { turbo_method: :patch, turbo: false }, - class: "lg:col-span-1" do |f| %> -
- <%= f.label :name %> - <%= f.text_field :name, value: @maps['name'], placeholder: 'Example: OpenStreetMap', class: "input input-bordered" %> -
+
+
+ <%= form_for :maps, + url: settings_maps_path, + method: :patch, + autocomplete: "off", + data: { turbo_method: :patch, turbo: false } do |f| %> +
+ <%= f.label :name %> + <%= f.text_field :name, value: @maps['name'], placeholder: 'Example: OpenStreetMap', class: "input input-bordered" %> +
-
- <%= f.label :url, 'URL' %> - <%= f.text_field :url, - value: @maps['url'], - autocomplete: "off", - placeholder: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - class: "input input-bordered", - data: { - map_preview_target: "urlInput", - action: "input->map-preview#updatePreview" - } %> -
+
+ <%= f.label :url, 'URL' %> + <%= f.text_field :url, + value: @maps['url'], + autocomplete: "off", + placeholder: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + class: "input input-bordered", + data: { + map_preview_target: "urlInput", + action: "input->map-preview#updatePreview" + } %> +
- <%= f.submit 'Save', class: "btn btn-primary", data: { map_preview_target: "saveButton" } %> - <% end %> + <%= f.submit 'Save', class: "btn btn-primary", data: { map_preview_target: "saveButton" } %> + <% end %> -
+

Tile usage

+ + <%= line_chart( + @tile_usage, + height: '200px', + xtitle: 'Days', + ytitle: 'Tiles', + suffix: ' tiles loaded' + ) %> +
+ +
"Bearer #{user.api_key}" } - expect(Maps::TileUsage::Track).to have_received(:new).with(tile_count) + expect(Maps::TileUsage::Track).to have_received(:new).with(user.id, tile_count) expect(track_service).to have_received(:call) expect(response).to have_http_status(:ok) end diff --git a/spec/services/maps/tile_usage/track_spec.rb b/spec/services/maps/tile_usage/track_spec.rb index 896810eb..678f60b1 100644 --- a/spec/services/maps/tile_usage/track_spec.rb +++ b/spec/services/maps/tile_usage/track_spec.rb @@ -5,16 +5,19 @@ require 'prometheus_exporter/client' RSpec.describe Maps::TileUsage::Track do describe '#call' do - subject(:track) { described_class.new(tile_count).call } + subject(:track) { described_class.new(user_id, tile_count).call } + let(:user_id) { 1 } let(:tile_count) { 5 } let(:prometheus_client) { instance_double(PrometheusExporter::Client) } before do allow(PrometheusExporter::Client).to receive(:default).and_return(prometheus_client) + allow(prometheus_client).to receive(:send_json) + allow(DawarichSettings).to receive(:prometheus_exporter_enabled?).and_return(true) end - it 'tracks tile usage' do + it 'tracks tile usage in prometheus' do expect(prometheus_client).to receive(:send_json).with( { type: 'counter', @@ -25,5 +28,15 @@ RSpec.describe Maps::TileUsage::Track do track end + + it 'tracks tile usage in cache' do + expect(Rails.cache).to receive(:write).with( + "dawarich_map_tiles_usage:#{user_id}:#{Time.zone.today}", + tile_count, + expires_in: 7.days + ) + + track + end end end