From 5d22534206ede6b24003fc8abede1e136d83073d Mon Sep 17 00:00:00 2001 From: Julien Bourdeau Date: Wed, 22 May 2024 17:15:10 +0200 Subject: [PATCH 1/2] feat: Introduce Deprecation tracking --- app/models/deprecation.rb | 44 +++++++++++++++++ app/services/events/post_process_service.rb | 4 ++ config/environments/development.rb | 5 +- spec/models/deprecation_spec.rb | 52 +++++++++++++++++++++ spec/spec_helper.rb | 2 + 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 app/models/deprecation.rb create mode 100644 spec/models/deprecation_spec.rb diff --git a/app/models/deprecation.rb b/app/models/deprecation.rb new file mode 100644 index 00000000000..5fe07b2cd43 --- /dev/null +++ b/app/models/deprecation.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'csv' + +class Deprecation + EXPIRE_IN = 1.month + + class << self + def report(feature_name, organization_id) + Rails.cache.write(cache_key(feature_name, organization_id, 'last_seen_at'), Time.current, expires_in: EXPIRE_IN) + Rails.cache.increment(cache_key(feature_name, organization_id, 'count'), 1, expires_in: EXPIRE_IN) + end + + def get_all(feature_name) + Organization.pluck(:id).filter_map do |organization_id| + h = get(feature_name, organization_id) + h[:last_seen_at] ? h : nil + end + end + + def get_all_as_csv(feature_name) + CSV.generate do |csv| + csv << %w[organization_id last_seen_at count] + + get_all(feature_name).each do |d| + csv << [d[:organization_id], d[:last_seen_at], d[:count]] + end + end + end + + def get(feature_name, organization_id) + last_seen_at = Rails.cache.read(cache_key(feature_name, organization_id, 'last_seen_at')) + count = Rails.cache.read(cache_key(feature_name, organization_id, 'count'), raw: true).to_i + + {organization_id:, last_seen_at:, count:} + end + + private + + def cache_key(feature_name, organization_id, suffix) + "deprecation:#{feature_name}:#{organization_id}:#{suffix}" + end + end +end diff --git a/app/services/events/post_process_service.rb b/app/services/events/post_process_service.rb index 536fd37b3e4..b4d9d9f4539 100644 --- a/app/services/events/post_process_service.rb +++ b/app/services/events/post_process_service.rb @@ -11,6 +11,10 @@ def initialize(event:) def call event.external_customer_id ||= customer&.external_id + unless event.external_subscription_id + Deprecation.report('event_missing_external_subscription_id', organization.id) + end + # NOTE: prevent subscription if more than 1 subscription is active # if multiple terminated matches the timestamp, takes the most recent if !event.external_subscription_id && subscriptions.count(&:active?) <= 1 diff --git a/config/environments/development.rb b/config/environments/development.rb index f7f67fb3b54..adcd98a86c5 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -19,15 +19,14 @@ config.consider_all_requests_local = true config.server_timing = true + config.cache_store = :redis_cache_store, {url: ENV['LAGO_REDIS_CACHE_URL'], db: '3'} + if Rails.root.join('tmp/caching-dev.txt').exist? - config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false - - config.cache_store = :null_store end config.active_storage.service = if ENV['LAGO_USE_AWS_S3'].present? && ENV['LAGO_USE_AWS_S3'] == 'true' diff --git a/spec/models/deprecation_spec.rb b/spec/models/deprecation_spec.rb new file mode 100644 index 00000000000..a861c552a69 --- /dev/null +++ b/spec/models/deprecation_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Deprecation, type: :model, cache: :redis do + let(:organization) { create(:organization) } + let(:feature_name) { 'event_legacy' } + + before do + Rails.cache.write("deprecation:#{feature_name}:#{organization.id}:last_seen_at", "2024-05-22T14:58:20.280Z") + Rails.cache.increment("deprecation:#{feature_name}:#{organization.id}:count", 101) + end + + describe '.report' do + it 'writes to cache' do + freeze_time do + described_class.report(feature_name, organization.id) + + expect(Rails.cache.read("deprecation:#{feature_name}:#{organization.id}:last_seen_at")).to eq(Time.zone.now.utc) + expect(Rails.cache.read("deprecation:#{feature_name}:#{organization.id}:count", raw: true)).to eq("102") + end + end + end + + describe '.get' do + it 'returns deprecation data for an organization' do + expect(described_class.get(feature_name, organization.id)).to eq({ + organization_id: organization.id, + last_seen_at: "2024-05-22T14:58:20.280Z", + count: 101 + }) + end + end + + describe '.get_all' do + it 'returns deprecation data for all organizations' do + expect(described_class.get_all(feature_name)).to eq([{ + organization_id: organization.id, + last_seen_at: "2024-05-22T14:58:20.280Z", + count: 101 + }]) + end + end + + describe '.get_all_as_csv' do + it 'returns deprecation data for all organizations' do + csv = "organization_id,last_seen_at,count\n" + csv += "#{organization.id},2024-05-22T14:58:20.280Z,101\n" + expect(described_class.get_all_as_csv(feature_name)).to eq(csv) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 18f1084cf6f..8b2fbd7b797 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,6 +29,8 @@ ActiveSupport::Cache.lookup_store(:memory_store) elsif example.metadata[:cache].to_sym == :null ActiveSupport::Cache.lookup_store(:null_store) + elsif example.metadata[:cache].to_sym == :redis + ActiveSupport::Cache.lookup_store(:redis_cache_store) else raise "Unknown cache store: #{example.metadata[:cache]}" end From eb02aa2a8cccf9d4bd5f9d516eabab433781faef Mon Sep 17 00:00:00 2001 From: Julien Bourdeau Date: Thu, 23 May 2024 10:36:28 +0200 Subject: [PATCH 2/2] add Redis to CI --- .github/workflows/spec.yml | 12 ++++++++++++ config/environments/test.rb | 12 +++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/spec.yml b/.github/workflows/spec.yml index 048f214983c..0b790f2142d 100644 --- a/.github/workflows/spec.yml +++ b/.github/workflows/spec.yml @@ -18,9 +18,20 @@ jobs: POSTGRES_DB: lago POSTGRES_USER: lago POSTGRES_PASSWORD: lago + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: RAILS_ENV: test DATABASE_URL: "postgres://lago:lago@localhost:5432/lago" + LAGO_REDIS_CACHE_URL: "redis://localhost:6379" RAILS_MASTER_KEY: ${{ secrets.RAILS_TEST_KEY }} SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} LAGO_API_URL: https://api.lago.dev @@ -32,6 +43,7 @@ jobs: LAGO_CLICKHOUSE_DATABASE: default LAGO_CLICKHOUSE_USERNAME: "" LAGO_CLICKHOUSE_PASSWORD: "" + steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/config/environments/test.rb b/config/environments/test.rb index 49156eea389..babd02bc23e 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -15,7 +15,6 @@ config.consider_all_requests_local = true config.action_controller.perform_caching = false - config.cache_store = :null_store config.action_dispatch.show_exceptions = false config.action_controller.allow_forgery_protection = false config.active_storage.service = :test @@ -35,4 +34,15 @@ config.active_job.queue_adapter = :test config.license_url = 'http://license.lago' + + # Configure the redis cache store but always set the null_store by default + # Use `context '...', cache: :redis` to enable the redis cache store in specs + if ENV['LAGO_REDIS_CACHE_URL'].present? + redis_store_config = { + url: ENV['LAGO_REDIS_CACHE_URL'], + ssl_params: {verify_mode: OpenSSL::SSL::VERIFY_NONE} + } + config.cache_store = :redis_cache_store, redis_store_config + end + config.cache_store = :null_store end