Implement map tiles usage tracking and chart in user settings.

This commit is contained in:
Eugene Burmakin 2025-02-13 21:04:29 +01:00
parent 838d85c62e
commit 51e589e17f
12 changed files with 94 additions and 57 deletions

View file

@ -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. - 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 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: - 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:
``` ```

View file

@ -2,7 +2,7 @@
class Api::V1::Maps::TileUsageController < ApiController class Api::V1::Maps::TileUsageController < ApiController
def create 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 head :ok
end end

View file

@ -5,6 +5,13 @@ class Settings::MapsController < ApplicationController
def index def index
@maps = current_user.safe_settings.maps @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 end
def update def update

View file

@ -34,9 +34,6 @@ import { TileMonitor } from "../maps/tile_monitor";
export default class extends Controller { export default class extends Controller {
static targets = ["container"]; static targets = ["container"];
static values = {
monitoringEnabled: Boolean
}
settingsButtonAdded = false; settingsButtonAdded = false;
layerControl = null; layerControl = null;
@ -249,7 +246,7 @@ export default class extends Controller {
} }
// Initialize tile monitor // 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 // Add tile load event handlers to each base layer
Object.entries(this.baseMaps()).forEach(([name, layer]) => { Object.entries(this.baseMaps()).forEach(([name, layer]) => {

View file

@ -1,15 +1,11 @@
export class TileMonitor { export class TileMonitor {
constructor(monitoringEnabled, apiKey) { constructor(apiKey) {
this.monitoringEnabled = monitoringEnabled;
this.apiKey = apiKey; this.apiKey = apiKey;
this.tileQueue = 0; this.tileQueue = 0;
this.tileUpdateInterval = null; this.tileUpdateInterval = null;
} }
startMonitoring() { startMonitoring() {
// Only start the interval if monitoring is enabled
if (!this.monitoringEnabled) return;
// Clear any existing interval // Clear any existing interval
if (this.tileUpdateInterval) { if (this.tileUpdateInterval) {
clearInterval(this.tileUpdateInterval); clearInterval(this.tileUpdateInterval);
@ -29,13 +25,11 @@ export class TileMonitor {
} }
recordTileLoad() { recordTileLoad() {
if (!this.monitoringEnabled) return;
this.tileQueue += 1; this.tileQueue += 1;
} }
sendTileUsage() { sendTileUsage() {
// Don't send if monitoring is disabled or queue is empty if (this.tileQueue === 0) return;
if (!this.monitoringEnabled || this.tileQueue === 0) return;
const currentCount = this.tileQueue; const currentCount = this.tileQueue;
console.log('Sending tile usage batch:', currentCount); console.log('Sending tile usage batch:', currentCount);

View file

@ -1,11 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
class Maps::TileUsage::Track class Maps::TileUsage::Track
def initialize(count = 1) def initialize(user_id, count = 1)
@user_id = user_id
@count = count @count = count
end end
def call 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 = { metric_data = {
type: 'counter', type: 'counter',
name: 'dawarich_map_tiles_usage', name: 'dawarich_map_tiles_usage',
@ -13,7 +25,12 @@ class Maps::TileUsage::Track
} }
PrometheusExporter::Client.default.send_json(metric_data) PrometheusExporter::Client.default.send_json(metric_data)
rescue StandardError => e end
Rails.logger.error("Failed to send tile usage metric: #{e.message}")
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
end end

View file

@ -96,7 +96,13 @@ class ReverseGeocoding::Places::FetchData
end end
def reverse_geocoded_places 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| data.reject do |place|
place.data['properties']['osm_value'].in?(IGNORED_OSM_VALUES) || place.data['properties']['osm_value'].in?(IGNORED_OSM_VALUES) ||

View file

@ -53,7 +53,6 @@
data-coordinates="<%= @coordinates %>" data-coordinates="<%= @coordinates %>"
data-distance="<%= @distance %>" data-distance="<%= @distance %>"
data-points_number="<%= @points_number %>" data-points_number="<%= @points_number %>"
data-maps-monitoring-enabled-value="<%= DawarichSettings.prometheus_exporter_enabled? %>"
data-timezone="<%= Rails.configuration.time_zone %>"> data-timezone="<%= Rails.configuration.time_zone %>">
<div data-maps-target="container" class="h-[25rem] rounded-lg w-full min-h-screen"> <div data-maps-target="container" class="h-[25rem] rounded-lg w-full min-h-screen">
<div id="fog" class="fog"></div> <div id="fog" class="fog"></div>

View file

@ -22,35 +22,46 @@
<span>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.</span> <span>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.</span>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-5" data-controller="map-preview"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-5" data-controller="map-preview">
<%= form_for :maps, <div class="flex flex-col gap-4">
url: settings_maps_path, <%= form_for :maps,
method: :patch, url: settings_maps_path,
autocomplete: "off", method: :patch,
data: { turbo_method: :patch, turbo: false }, autocomplete: "off",
class: "lg:col-span-1" do |f| %> data: { turbo_method: :patch, turbo: false } do |f| %>
<div class="form-control my-2"> <div class="form-control my-2">
<%= f.label :name %> <%= f.label :name %>
<%= f.text_field :name, value: @maps['name'], placeholder: 'Example: OpenStreetMap', class: "input input-bordered" %> <%= f.text_field :name, value: @maps['name'], placeholder: 'Example: OpenStreetMap', class: "input input-bordered" %>
</div> </div>
<div class="form-control my-2"> <div class="form-control my-2">
<%= f.label :url, 'URL' %> <%= f.label :url, 'URL' %>
<%= f.text_field :url, <%= f.text_field :url,
value: @maps['url'], value: @maps['url'],
autocomplete: "off", autocomplete: "off",
placeholder: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', placeholder: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
class: "input input-bordered", class: "input input-bordered",
data: { data: {
map_preview_target: "urlInput", map_preview_target: "urlInput",
action: "input->map-preview#updatePreview" action: "input->map-preview#updatePreview"
} %> } %>
</div> </div>
<%= f.submit 'Save', class: "btn btn-primary", data: { map_preview_target: "saveButton" } %> <%= f.submit 'Save', class: "btn btn-primary", data: { map_preview_target: "saveButton" } %>
<% end %> <% end %>
<div class="lg:col-span-2" style="height: 500px;"> <h2 class="text-lg font-bold">Tile usage</h2>
<%= line_chart(
@tile_usage,
height: '200px',
xtitle: 'Days',
ytitle: 'Tiles',
suffix: ' tiles loaded'
) %>
</div>
<div style="height: 500px;">
<div <div
data-map-preview-target="mapContainer" data-map-preview-target="mapContainer"
class="w-full h-full rounded-lg border" class="w-full h-full rounded-lg border"

View file

@ -1,7 +0,0 @@
# frozen_string_literal: true
module Reddis
def self.client
@client ||= Redis.new(url: ENV['REDIS_URL'])
end
end

View file

@ -6,21 +6,20 @@ RSpec.describe 'Api::V1::Maps::TileUsage', type: :request do
describe 'POST /api/v1/maps/tile_usage' do describe 'POST /api/v1/maps/tile_usage' do
let(:tile_count) { 5 } let(:tile_count) { 5 }
let(:track_service) { instance_double(Maps::TileUsage::Track) } let(:track_service) { instance_double(Maps::TileUsage::Track) }
let(:user) { create(:user) }
before do before do
allow(Maps::TileUsage::Track).to receive(:new).with(tile_count).and_return(track_service) allow(Maps::TileUsage::Track).to receive(:new).with(user.id, tile_count).and_return(track_service)
allow(track_service).to receive(:call) allow(track_service).to receive(:call)
end end
context 'when user is authenticated' do context 'when user is authenticated' do
let(:user) { create(:user) }
it 'tracks tile usage' do it 'tracks tile usage' do
post '/api/v1/maps/tile_usage', post '/api/v1/maps/tile_usage',
params: { tile_usage: { count: tile_count } }, params: { tile_usage: { count: tile_count } },
headers: { 'Authorization' => "Bearer #{user.api_key}" } headers: { 'Authorization' => "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(track_service).to have_received(:call)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
end end

View file

@ -5,16 +5,19 @@ require 'prometheus_exporter/client'
RSpec.describe Maps::TileUsage::Track do RSpec.describe Maps::TileUsage::Track do
describe '#call' 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(:tile_count) { 5 }
let(:prometheus_client) { instance_double(PrometheusExporter::Client) } let(:prometheus_client) { instance_double(PrometheusExporter::Client) }
before do before do
allow(PrometheusExporter::Client).to receive(:default).and_return(prometheus_client) 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 end
it 'tracks tile usage' do it 'tracks tile usage in prometheus' do
expect(prometheus_client).to receive(:send_json).with( expect(prometheus_client).to receive(:send_json).with(
{ {
type: 'counter', type: 'counter',
@ -25,5 +28,15 @@ RSpec.describe Maps::TileUsage::Track do
track track
end 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
end end