Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,34 @@ jobs:
test:
name: Run Tests
runs-on: ubuntu-latest

strategy:
matrix:
ruby-version: ['3.2', '3.3']

steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install system dependencies (libgd)

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgd-dev pkg-config

- name: Set up Ruby ${{ matrix.ruby-version }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true

- name: Install gems
run: bundle install

- name: Run tests
env:
GH_TOKEN: fake-token-for-tests
run: bundle exec rake test

- name: Upload test coverage
uses: actions/upload-artifact@v4
with:
Expand All @@ -50,55 +52,55 @@ jobs:
lint:
name: Lint Code
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install system dependencies (libgd)

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgd-dev pkg-config

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true

- name: Install gems
run: bundle install

- name: Run rubocop
run: bundle exec rubocop || true

security:
name: Security Scan
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install system dependencies (libgd)

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgd-dev pkg-config

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true

- name: Install gems
run: bundle install

- name: Run Brakeman
run: bundle exec brakeman --no-pager --format json > brakeman.json || true

- name: Upload security report
uses: actions/upload-artifact@v4
with:
name: security-report
path: brakeman.json
path: brakeman.json
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ group :development, :test do
gem 'simplecov', '~> 0.22', require: false
gem 'rubocop', '~> 1.50', require: false
gem 'brakeman', '~> 6.0', require: false
gem 'webmock', '~> 3.19'
gem 'vcr', '~> 6.2'
end
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
t.libs << 'test'
t.libs << 'lib'
t.test_files = FileList['test/**/test_*.rb']
t.test_files = FileList['test/**/test_*.rb'].exclude('test/test_helper.rb')
t.verbose = true
t.warning = false
end
Expand Down
74 changes: 63 additions & 11 deletions lib/analytics/trend_analyzer.rb
Original file line number Diff line number Diff line change
@@ -1,34 +1,86 @@
# frozen_string_literal: true

module Analytics
class TrendAnalyzer
attr_reader :values, :dates
def initialize(values, dates)

def initialize(values, dates = nil)
@values = values.map(&:to_f)
@dates = dates
end


def total
@values.sum.round
end

def average
@values.empty? ? 0 : (@values.sum / @values.size).round
end

def wow_growth
return 0 if @values.size < 14
curr = @values.last(7).sum
prev = @values[-14..-8].sum
prev.zero? ? 0 : ((curr - prev) / prev * 100).round(1)
end

def total; @values.sum.round; end
def average; (@values.sum / @values.size).round; end


current_week = @values.last(7).sum
previous_week = @values[-14..-8].sum
return 0 if previous_week.zero?

((current_week - previous_week) / previous_week * 100).round(1)
end

def mom_growth
return 0 if @values.size < 60

current_month = @values.last(30).sum
previous_month = @values[-60..-31].sum
return 0 if previous_month.zero?

((current_month - previous_month) / previous_month * 100).round(1)
end

def growth_rate
return 0 if @values.size < 2

first = @values.first
last = @values.last
return 0 if first.zero?

periods = @values.size - 1
((last / first) ** (1.0 / periods) - 1) * 100
end

def peak_day
return nil if @values.empty?

max_value = @values.max
index = @values.index(max_value)

result = { value: max_value.round, index: index }
result[:date] = @dates[index] if @dates
result
end

def forecast(days = 30)
return [] if @values.size < 7

x = (0...@values.size).to_a
y = @values
n = x.size
sum_x = x.sum
sum_y = y.sum
sum_xy = x.zip(y).sum { |xi, yi| xi * yi }
sum_xx = x.sum { |xi| xi * xi }

slope = (n * sum_xy - sum_x * sum_y).to_f / (n * sum_xx - sum_x * sum_x)
intercept = (sum_y - slope * sum_x) / n

last_x = @values.size - 1
(1..days).map { |i| [slope * (last_x + i) + intercept, 0].max.round }
end

def moving_average(window = 7)
return [] if @values.size < window

@values.each_cons(window).map { |slice| slice.sum / window.to_f }
end
end
end
end
15 changes: 11 additions & 4 deletions lib/charts/professional_chart.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ def initialize(width: 900, height: 400, title: '', bg_color: '#ffffff', font_pat
end

def render_line_chart(values, labels, output_path)
return if values.empty? || values.all?(&:zero?)
# Validaciones
return nil if values.empty?
return nil if values.size == 1 # Necesitamos al menos 2 puntos para una línea

# Verificar si todos los valores son cero
return nil if values.all? { |v| v == 0 }

img = GD::Image.new(@width, @height)

Expand Down Expand Up @@ -82,9 +87,11 @@ def render_line_chart(values, labels, output_path)
[x.to_i, y.to_i]
end

# Area fill
area_points = [[left_margin, @height - bottom_margin]] + points + [[@width - right_margin, @height - bottom_margin]]
img.filled_polygon(area_points, area_color) if area_points.size >= 3
# Area fill (solo si hay puntos válidos)
if points.size >= 2
area_points = [[left_margin, @height - bottom_margin]] + points + [[@width - right_margin, @height - bottom_margin]]
img.filled_polygon(area_points, area_color) if area_points.size >= 3
end

# Line
points.each_cons(2) do |p1, p2|
Expand Down
23 changes: 23 additions & 0 deletions test/fixtures/sample_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
repositories:
- owner: test
name: test-repo
display_name: Test Repository
description: A test repository for metrics
color: "#4a90e2"

dashboard:
title: Test Dashboard
subtitle: Test metrics
update_frequency: Weekly

charts:
default_height: 400
default_width: 800
theme:
background: "#ffffff"
primary: "#4a90e2"

metrics:
retention_days: 365
forecast_days: 30
8 changes: 8 additions & 0 deletions test/fixtures/sample_views.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
date,count,uniques
2026-03-01,100,45
2026-03-02,120,50
2026-03-03,110,48
2026-03-04,130,55
2026-03-05,140,60
2026-03-06,125,52
2026-03-07,115,49
81 changes: 64 additions & 17 deletions test/test_analytics.rb
Original file line number Diff line number Diff line change
@@ -1,37 +1,84 @@
# frozen_string_literal: true

require_relative 'test_helper'
require_relative '../lib/analytics/trend_analyzer'

class TestAnalytics < Minitest::Test
def setup
@sample_views = [100, 110, 120, 115, 130, 125, 140, 135, 150, 145]
@sample_dates = (1..10).map { |i| Date.today - i }
@analyzer = Analytics::TrendAnalyzer.new(@sample_views, @sample_dates)
end

def test_total_calculation
total = @sample_views.sum
assert_equal 1270, total
assert_equal 1270, @analyzer.total
end

def test_average_calculation
avg = @sample_views.sum / @sample_views.size.to_f
assert_in_delta 127.0, avg, 0.1
assert_equal 127, @analyzer.average
end

def test_wow_growth
# Simulate 14+ days of data
views = [100, 110, 120, 130, 140, 150, 160, # week 1
110, 115, 125, 135, 145, 155, 165] # week 2

current_week = views.last(7).sum
previous_week = views[-14..-8].sum
growth = ((current_week - previous_week) * 100 / previous_week).round(1)

assert_in_delta 3.7, growth, 0.1
def test_empty_values_total
analyzer = Analytics::TrendAnalyzer.new([])
assert_equal 0, analyzer.total
end

def test_empty_data_handling
assert_equal 0, [].sum
assert_equal 0, [].size
def test_empty_values_average
analyzer = Analytics::TrendAnalyzer.new([])
assert_equal 0, analyzer.average
end

def test_wow_growth_with_sufficient_data
# 14+ days of data: week1 = [100,110,120,130,140,150,160] sum = 910
# week2 = [110,115,125,135,145,155,165] sum = 950
# growth = (950 - 910) / 910 * 100 = 4.4%
views = [100, 110, 120, 130, 140, 150, 160,
110, 115, 125, 135, 145, 155, 165]
analyzer = Analytics::TrendAnalyzer.new(views)
assert_in_delta 4.4, analyzer.wow_growth, 0.1
end

def test_wow_growth_with_insufficient_data
analyzer = Analytics::TrendAnalyzer.new([100, 200, 300])
assert_equal 0, analyzer.wow_growth
end

def test_mom_growth_with_sufficient_data
# 60+ days of data (simplified)
views = [10] * 30 + [15] * 30
analyzer = Analytics::TrendAnalyzer.new(views)
assert_in_delta 50.0, analyzer.mom_growth, 0.1
end

def test_growth_rate
views = [100, 110, 121] # 10% growth each period
analyzer = Analytics::TrendAnalyzer.new(views)
# (121/100)^(1/2) - 1 = 0.1 = 10%
assert_in_delta 10.0, analyzer.growth_rate, 0.5
end

def test_peak_day
peak = @analyzer.peak_day
assert_equal 150, peak[:value]
assert_equal 8, peak[:index]
end

def test_forecast_returns_array
forecast = @analyzer.forecast(5)
assert_equal 5, forecast.size
assert forecast.all? { |v| v.is_a?(Numeric) }
end

def test_forecast_with_insufficient_data
analyzer = Analytics::TrendAnalyzer.new([1, 2, 3])
assert_equal [], analyzer.forecast(5)
end

def test_moving_average
ma = @analyzer.moving_average(3)
# First MA: (100+110+120)/3 = 110
# Second: (110+120+115)/3 = 115
assert_in_delta 110.0, ma[0], 0.1
assert_in_delta 115.0, ma[1], 0.1
end
end
Loading
Loading