mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Implement map tiles usage tracking and chart in user settings.
This commit is contained in:
parent
838d85c62e
commit
51e589e17f
12 changed files with 94 additions and 57 deletions
|
|
@ -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:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]) => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) ||
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Reddis
|
|
||||||
def self.client
|
|
||||||
@client ||= Redis.new(url: ENV['REDIS_URL'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue