Remove telemetry

This commit is contained in:
Eugene Burmakin 2025-04-11 19:13:46 +02:00
parent 5087934ba5
commit dae1733320
21 changed files with 80 additions and 235 deletions

View file

@ -1 +1 @@
0.25.4
0.25.5

View file

@ -32,7 +32,6 @@ services:
PROMETHEUS_EXPORTER_ENABLED: false
PROMETHEUS_EXPORTER_HOST: 0.0.0.0
PROMETHEUS_EXPORTER_PORT: 9394
ENABLE_TELEMETRY: false # More on telemetry: https://dawarich.app/docs/tutorials/telemetry
dawarich_redis:
image: redis:7.4-alpine
container_name: dawarich_redis

3
.gitignore vendored
View file

@ -65,3 +65,6 @@
.dotnet/
.cursorrules
.cursormemory.md
/config/credentials/production.key
/config/credentials/production.yml.enc

View file

@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# 0.25.5 - UNRELEASED
## Removed
- Optional telemetry was removed from the app.
- Sidekiq Web UI is now protected by basic auth in non-self-hosted mode. (Needs to be tested)
# 0.25.4 - 2025-04-02
⚠️ This release includes a breaking change. ⚠️

View file

@ -1,14 +0,0 @@
# frozen_string_literal: true
class TelemetrySendingJob < ApplicationJob
queue_as :default
def perform
return unless ENV['ENABLE_TELEMETRY'] == 'true'
data = Telemetry::Gather.new.call
Rails.logger.info("Telemetry data: #{data}")
Telemetry::Send.new(data).call
end
end

View file

@ -101,7 +101,7 @@ class User < ApplicationRecord
end
def can_subscribe?
active_until&.past? && !DawarichSettings.self_hosted?
(active_until.nil? || active_until&.past?) && !DawarichSettings.self_hosted?
end
def generate_subscription_token

View file

@ -1,32 +0,0 @@
# frozen_string_literal: true
class Telemetry::Gather
def initialize(measurement: 'dawarich_usage_metrics')
@measurement = measurement
end
def call
{
measurement:,
timestamp: Time.current.to_i,
tags: { instance_id: },
fields: { dau:, app_version: }
}
end
private
attr_reader :measurement
def instance_id
@instance_id ||= Digest::SHA2.hexdigest(User.first.api_key)
end
def app_version
"\"#{APP_VERSION}\""
end
def dau
User.where(last_sign_in_at: Time.zone.today.beginning_of_day..Time.zone.today.end_of_day).count
end
end

View file

@ -1,46 +0,0 @@
# frozen_string_literal: true
class Telemetry::Send
BUCKET = 'dawarich_metrics'
ORG = 'monitoring'
def initialize(payload)
@payload = payload
end
def call
return unless ENV['ENABLE_TELEMETRY'] == 'true'
line_protocol = build_line_protocol
response = send_request(line_protocol)
handle_response(response)
end
private
attr_reader :payload
def build_line_protocol
tag_string = payload[:tags].map { |k, v| "#{k}=#{v}" }.join(',')
field_string = payload[:fields].map { |k, v| "#{k}=#{v}" }.join(',')
"#{payload[:measurement]},#{tag_string} #{field_string} #{payload[:timestamp].to_i}"
end
def send_request(line_protocol)
HTTParty.post(
"#{TELEMETRY_URL}?org=#{ORG}&bucket=#{BUCKET}&precision=s",
body: line_protocol,
headers: {
'Authorization' => "Token #{Base64.decode64(TELEMETRY_STRING)}",
'Content-Type' => 'text/plain'
}
)
end
def handle_response(response)
Rails.logger.error("InfluxDB write failed: #{response.body}") unless response.success?
response
end
end

View file

@ -7,7 +7,6 @@
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.14/dist/full.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
@ -25,7 +24,7 @@
<div class="flex flex-row gap-5 w-full px-5">
<%= yield %>
</div>
<%= render 'shared/footer' %>
<%= render SELF_HOSTED ? 'shared/footer' : 'shared/legal_footer' %>
</div>
</body>
</html>

View file

@ -16,13 +16,13 @@
Your subscription will be valid for the next <span class="text-accent"><%= days_left(current_user.active_until) %></span>.
</p>
<%= link_to 'Manage subscription', "#{ENV['SUBSCRIPTION_URL']}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-primary my-4' %>
<%= link_to 'Manage subscription', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-primary my-4' %>
<% else %>
<p class="py-6">
You are currently not subscribed to Dawarich. How about we fix that?
</p>
<%= link_to 'Manage subscription', "#{ENV['SUBSCRIPTION_URL']}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-primary my-4' %>
<%= link_to 'Manage subscription', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-primary my-4' %>
<% end %>
</div>
</div>

View file

@ -1,4 +1,4 @@
<footer class="footer footer-center p-4 bg-base-300 text-base-content">
<footer class="footer bg-base-200 text-neutral p-4">
<aside>
<p><a href="https://dawarich.app/" class="link hover:no-underline" target="_blank">Dawarich</a> 2023-<%=Time.zone.now.year %></p>
</aside>

View file

@ -0,0 +1,34 @@
<footer class="footer bg-base-200 text-neutral p-4">
<nav>
<h6 class="footer-title"><strong>Dawarich</strong></h6>
<p>
Made and hosted in 🇪🇺 Europe
</p>
<p>
Copyright © <%= Time.zone.now.year %> ZeitFlow UG
</p>
</nav>
<nav>
<h6 class="footer-title"><strong>Community</strong></h6>
<a class="hover:underline" href="https://discord.gg/pHsBjpt5J8" target="_blank">Discord</a>
<a class="hover:underline" href="https://x.com/freymakesstuff" target="_blank">X</a>
<a class="hover:underline" href="https://github.com/Freika/dawarich" target="_blank">Github</a>
<a class="hover:underline" href="https://mastodon.social/@dawarich" target="_blank">Mastodon</a>
</nav>
<nav>
<h6 class="footer-title"><strong>Docs</strong></h6>
<a class="hover:underline" href="https://dawarich.app/docs/intro" target="_blank">Tutorial</a>
<a class="hover:underline" href="https://dawarich.app/docs/tutorials/import-existing-data" target="_blank">Import existing data</a>
<a class="hover:underline" href="https://dawarich.app/docs/tutorials/export-your-data" target="_blank">Exporting data</a>
<a class="hover:underline" href="https://dawarich.app/docs/FAQ" target="_blank">FAQ</a>
<a class="hover:underline" href="https://dawarich.app/contact" target="_blank">Contact</a>
</nav>
<nav>
<h6 class="footer-title"><strong>More</strong></h6>
<a class="hover:underline" href="https://dawarich.app/privacy-policy" target="_blank">Privacy policy</a>
<a class="hover:underline" href="https://dawarich.app/terms-and-conditions" target="_blank">Terms and Conditions</a>
<a class="hover:underline" href="https://dawarich.app/refund-policy" target="_blank">Refund policy</a>
<a class="hover:underline" href="https://dawarich.app/impressum" target="_blank">Impressum</a>
<a class="hover:underline" href="https://dawarich.app/blog" target="_blank">Blog</a>
</nav>
</footer>

View file

@ -20,7 +20,7 @@
</details>
</li>
<% if user_signed_in? && current_user.can_subscribe? %>
<li><%= link_to 'Subscribe', "#{ENV['SUBSCRIPTION_URL']}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %></li>
<li><%= link_to 'Subscribe', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %></li>
<% end %>
</ul>
</div>
@ -71,7 +71,7 @@
<ul class="menu menu-horizontal bg-base-100 rounded-box px-1">
<% if user_signed_in? %>
<% if current_user.can_subscribe? %>
<li><%= link_to 'Subscribe', "#{ENV['SUBSCRIPTION_URL']}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %></li>
<li><%= link_to 'Subscribe', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %></li>
<% end %>
<div class="dropdown dropdown-end dropdown-bottom dropdown-hover"

View file

@ -7,9 +7,6 @@ DISTANCE_UNIT = ENV.fetch('DISTANCE_UNIT', 'km').to_sym
APP_VERSION = File.read('.app_version').strip
TELEMETRY_STRING = Base64.encode64('IjVFvb8j3P9-ArqhSGav9j8YcJaQiuNIzkfOPKQDk2lvKXqb8t1NSRv50oBkaKtlrB_ZRzO9NdurpMtncV_HYQ==')
TELEMETRY_URL = 'https://influxdb2.frey.today/api/v2/write'
# Reverse geocoding settings
PHOTON_API_HOST = ENV.fetch('PHOTON_API_HOST', nil)
PHOTON_API_KEY = ENV.fetch('PHOTON_API_KEY', nil)
@ -23,3 +20,4 @@ GEOAPIFY_API_KEY = ENV.fetch('GEOAPIFY_API_KEY', nil)
# /Reverse geocoding settings
SENTRY_DSN = ENV.fetch('SENTRY_DSN', nil)
MANAGER_URL = SELF_HOSTED ? nil : ENV.fetch('MANAGER_URL', raise('MANAGER_URL is not set'))

View file

@ -6,7 +6,22 @@ Rails.application.routes.draw do
mount ActionCable.server => '/cable'
mount Rswag::Api::Engine => '/api-docs'
mount Rswag::Ui::Engine => '/api-docs'
authenticate :user, ->(u) { u.admin? && DawarichSettings.self_hosted? } do
Sidekiq::Web.use(Rack::Auth::Basic) do |username, password|
ActiveSupport::SecurityUtils.secure_compare(
::Digest::SHA256.hexdigest(username),
::Digest::SHA256.hexdigest(ENV['SIDEKIQ_USERNAME'])
) &
ActiveSupport::SecurityUtils.secure_compare(
::Digest::SHA256.hexdigest(password),
::Digest::SHA256.hexdigest(ENV['SIDEKIQ_PASSWORD'])
)
end
authenticate :user, lambda { |u|
(u.admin? && DawarichSettings.self_hosted?) ||
(u.admin? && ENV['SIDEKIQ_USERNAME'].present? && ENV['SIDEKIQ_PASSWORD'].present?)
} do
mount Sidekiq::Web => '/sidekiq'
end

View file

@ -1,44 +1,7 @@
# frozen_string_literal: true
class CreateTelemetryNotification < ActiveRecord::Migration[7.2]
def up
# TODO: Remove
# User.find_each do |user|
# Notifications::Create.new(
# user:, kind: :info, title: 'Telemetry enabled', content: notification_content
# ).call
# end
end
def up; end
def down
raise ActiveRecord::IrreversibleMigration
end
private
def notification_content
<<~CONTENT
<p>With the release 0.19.2, Dawarich now can collect usage some metrics and send them to InfluxDB.</p>
<br>
<p>Before this release, the only metrics that could be somehow tracked by developers (only <a href="https://github.com/Freika" class="underline">Freika</a>, as of now) were the number of stars on GitHub and the overall number of docker images being pulled, across all versions of Dawarich, non-splittable by version. New in-app telemetry will allow us to track more granular metrics, allowing me to make decisions based on facts, not just guesses.</p>
<br>
<p>I'm aware about the privacy concerns, so I want to be very transparent about what data is being sent and how it's used.</p>
<br>
<p>Data being sent:</p>
<br>
<ul class="list-disc">
<li>Number of DAU (Daily Active Users)</li>
<li>App version</li>
<li>Instance ID (unique identifier of the Dawarich instance built by hashing the api key of the first user in the database)</li>
</ul>
<br>
<p>The data is being sent to a InfluxDB instance hosted by me and won't be shared with anyone.</p>
<br>
<p>Basically this set of metrics allows me to see how many people are using Dawarich and what versions they are using. No other data is being sent, nor it gives me any knowledge about individual users or their data or activity.</p>
<br>
<p>The telemetry is enabled by default, but it <strong class="text-info underline">can be disabled</strong> by setting <code>DISABLE_TELEMETRY</code> env var to <code>true</code>. The dataset might change in the future, but any changes will be documented here in the changelog and in every release as well as on the <a href="https://dawarich.app/docs/tutorials/telemetry" class="underline">telemetry page</a> of the website docs.</p>
<br>
<p>You can read more about it in the <a href="https://github.com/Freika/dawarich/releases/tag/0.19.2" class="underline">release page</a>.</p>
CONTENT
end
def down; end
end

View file

@ -69,8 +69,8 @@ services:
PROMETHEUS_EXPORTER_ENABLED: false
PROMETHEUS_EXPORTER_HOST: 0.0.0.0
PROMETHEUS_EXPORTER_PORT: 9394
ENABLE_TELEMETRY: false # More on telemetry: https://dawarich.app/docs/tutorials/telemetry
SELF_HOSTED: "true"
RAILS_MASTER_KEY: ${RAILS_MASTER_KEY}
logging:
driver: "json-file"
options:
@ -122,8 +122,8 @@ services:
PROMETHEUS_EXPORTER_ENABLED: false
PROMETHEUS_EXPORTER_HOST: dawarich_app
PROMETHEUS_EXPORTER_PORT: 9394
ENABLE_TELEMETRY: false # More on telemetry: https://dawarich.app/docs/tutorials/telemetry
SELF_HOSTED: "true"
RAILS_MASTER_KEY: ${RAILS_MASTER_KEY}
logging:
driver: "json-file"
options:

View file

@ -14,17 +14,19 @@ if [ -n "$DATABASE_URL" ]; then
DATABASE_PORT=$(echo $DATABASE_URL | awk -F[@/:] '{print $5}')
DATABASE_USERNAME=$(echo $DATABASE_URL | awk -F[:/@] '{print $4}')
DATABASE_PASSWORD=$(echo $DATABASE_URL | awk -F[:/@] '{print $5}')
DATABASE_NAME=$(echo $DATABASE_URL | awk -F[@/] '{print $5}')
else
# Use existing environment variables
DATABASE_HOST=${DATABASE_HOST}
DATABASE_PORT=${DATABASE_PORT}
DATABASE_USERNAME=${DATABASE_USERNAME}
DATABASE_PASSWORD=${DATABASE_PASSWORD}
DATABASE_NAME=${DATABASE_NAME}
fi
# Wait for the database to become available
echo "⏳ Waiting for database to be ready..."
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c '\q'; do
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c '\q'; do
>&2 echo "Postgres is unavailable - retrying..."
sleep 2
done

View file

@ -29,16 +29,16 @@ rm -f $APP_PATH/tmp/pids/server.pid
# Wait for the database to become available
echo "⏳ Waiting for database to be ready..."
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c '\q'; do
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c '\q'; do
>&2 echo "Postgres is unavailable - retrying..."
sleep 2
done
echo "✅ PostgreSQL is ready!"
# Create database if it doesn't exist
if ! PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'" | grep -q 1; then
if ! PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'" | grep -q 1; then
echo "Creating database $DATABASE_NAME..."
bundle exec rails db:create
PGPASSWORD=$DATABASE_PASSWORD createdb -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" "$DATABASE_NAME"
fi
# Run database migrations

View file

@ -1,39 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe TelemetrySendingJob, type: :job do
describe '#perform' do
let(:gather_service) { instance_double(Telemetry::Gather) }
let(:send_service) { instance_double(Telemetry::Send) }
let(:telemetry_data) { { some: 'data' } }
before do
allow(Telemetry::Gather).to receive(:new).and_return(gather_service)
allow(gather_service).to receive(:call).and_return(telemetry_data)
allow(Telemetry::Send).to receive(:new).with(telemetry_data).and_return(send_service)
allow(send_service).to receive(:call)
end
context 'with default env' do
it 'does not send telemetry data' do
described_class.perform_now
expect(send_service).not_to have_received(:call)
end
end
context 'when ENABLE_TELEMETRY is set to true' do
before do
stub_const('ENV', ENV.to_h.merge('ENABLE_TELEMETRY' => 'true'))
end
it 'gathers telemetry data and sends it' do
described_class.perform_now
expect(gather_service).to have_received(:call)
expect(send_service).to have_received(:call)
end
end
end
end

View file

@ -1,45 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Telemetry::Gather do
let!(:user) { create(:user, last_sign_in_at: Time.zone.today) }
describe '#call' do
subject(:gather) { described_class.new.call }
it 'returns a hash with measurement, timestamp, tags, and fields' do
expect(gather).to include(:measurement, :timestamp, :tags, :fields)
end
it 'includes the correct measurement' do
expect(gather[:measurement]).to eq('dawarich_usage_metrics')
end
it 'includes the current timestamp' do
expect(gather[:timestamp]).to be_within(1).of(Time.current.to_i)
end
it 'includes the correct instance_id in tags' do
expect(gather[:tags][:instance_id]).to eq(Digest::SHA2.hexdigest(user.api_key))
end
it 'includes the correct app_version in fields' do
expect(gather[:fields][:app_version]).to eq("\"#{APP_VERSION}\"")
end
it 'includes the correct dau in fields' do
expect(gather[:fields][:dau]).to eq(1)
end
context 'with a custom measurement' do
let(:measurement) { 'custom_measurement' }
subject(:gather) { described_class.new(measurement:).call }
it 'includes the correct measurement' do
expect(gather[:measurement]).to eq('custom_measurement')
end
end
end
end