analytic 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -13
  3. data/Rakefile +12 -2
  4. data/app/assets/builds/analytic/application.css +810 -0
  5. data/app/assets/builds/analytic/application.js +54498 -0
  6. data/app/assets/builds/analytic/application.js.map +7 -0
  7. data/app/assets/config/analytic_manifest.js +0 -0
  8. data/app/controllers/analytic/application_controller.rb +6 -0
  9. data/app/controllers/analytic/dashboard_controller.rb +13 -0
  10. data/app/controllers/concerns/analytic/trackable.rb +38 -0
  11. data/app/helpers/analytic/application_helper.rb +6 -0
  12. data/app/jobs/analytic/application_job.rb +6 -0
  13. data/app/jobs/analytic/track_job.rb +23 -0
  14. data/app/mailers/analytic/application_mailer.rb +7 -0
  15. data/app/models/analytic/application_record.rb +7 -0
  16. data/app/models/analytic/dashboard.rb +65 -0
  17. data/app/models/analytic/view.rb +23 -0
  18. data/app/packs/analytic/application/index.ts +1 -0
  19. data/app/packs/analytic/application/initializers/fontawesome.ts +23 -0
  20. data/app/packs/analytic/application/initializers/index.ts +1 -0
  21. data/app/packs/analytic/entrypoints/application.tailwind.css +65 -0
  22. data/app/packs/analytic/entrypoints/application.ts +1 -0
  23. data/app/views/analytic/dashboard/show.html.erb +84 -0
  24. data/app/views/layouts/analytic/application.html.erb +42 -0
  25. data/bin/dev +3 -0
  26. data/bin/rails +11 -0
  27. data/config/routes.rb +7 -0
  28. data/db/migrate/20240805210911_create_analytic_views.rb +20 -0
  29. data/lib/analytic/config.rb +12 -0
  30. data/lib/analytic/engine.rb +7 -0
  31. data/lib/analytic/version.rb +3 -1
  32. data/lib/analytic.rb +15 -3
  33. metadata +36 -13
  34. data/lib/analytic/railtie.rb +0 -4
  35. data/lib/tasks/analytic_tasks.rake +0 -4
  36. /data/{MIT-LICENSE → LICENSE} +0 -0
File without changes
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class DashboardController < ApplicationController
5
+ include Analytic::Trackable
6
+ before_action { analytic_track! }
7
+
8
+ # GET /
9
+ def show
10
+ @dashboard = Dashboard.new(period: params[:period])
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ module Trackable
5
+ # @param params [Hash]
6
+ def analytic_track!(params: {})
7
+ Analytic::View.create!(
8
+ timestamp: Time.current,
9
+ session_id: analytic_session_id,
10
+ visitor_id: analytic_visitor_id,
11
+ host: request.host,
12
+ path: request.path,
13
+ params:
14
+ )
15
+ end
16
+
17
+ # @param params [Hash]
18
+ def analytic_enqueue_track_job!(params: {})
19
+ Analytic::TrackJob.perform_later(
20
+ session_id: analytic_session_id,
21
+ visitor_id: analytic_visitor_id,
22
+ host: request.host,
23
+ path: request.path,
24
+ params:
25
+ )
26
+ end
27
+
28
+ # @return [String]
29
+ def analytic_session_id
30
+ session[:analytic_session_id] ||= SecureRandom.uuid
31
+ end
32
+
33
+ # @return [String]
34
+ def analytic_visitor_id
35
+ cookies.permanent[:analytic_visitor_id] ||= SecureRandom.uuid
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ module ApplicationHelper
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class TrackJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ # @param timestamp [Timestamp]
8
+ # @param session_id [String]
9
+ # @param visitor_id [String]
10
+ # @param host [String]
11
+ # @param path [String]
12
+ # @param params [Hash]
13
+ def perform(session_id:, visitor_id:, host:, path:, timestamp: Time.current, params: {})
14
+ Analytic::View.create!(
15
+ session_id:,
16
+ visitor_id:,
17
+ host:,
18
+ path:,
19
+ params:
20
+ )
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class ApplicationMailer < ActionMailer::Base
5
+ layout 'mailer'
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class Dashboard
5
+ PAGES_LIMIT = 8
6
+
7
+ # @param period [String] today, yesterday, week, month, year
8
+ def initialize(period:)
9
+ @period = period
10
+ end
11
+
12
+ # @return [Integer]
13
+ delegate :count, to: :views
14
+
15
+ # @return [Integer]
16
+ delegate :distinct_visitors_count, to: :views
17
+
18
+ # @return [Integer]
19
+ delegate :distinct_sessions_count, to: :views
20
+
21
+ # @return [String]
22
+ def name
23
+ if @period
24
+ "#{@period.capitalize} | Dashboard"
25
+ else
26
+ 'Dashboard'
27
+ end
28
+ end
29
+
30
+ # @return [Array<Array(String, String, Integer)>]
31
+ def pages
32
+ views
33
+ .group(:host)
34
+ .group(:path)
35
+ .order(count: :desc)
36
+ .limit(PAGES_LIMIT)
37
+ .pluck(:host, :path, Arel.sql('COUNT(*) AS count'))
38
+ end
39
+
40
+ protected
41
+
42
+ # @return [Analytic::View::ActiveRecord_Relation]
43
+ def views
44
+ @views ||= Analytic::View.within(range)
45
+ end
46
+
47
+ # @return [Range<Time>]
48
+ def range
49
+ return unless @period
50
+
51
+ case @period
52
+ when 'today' then timezone.today.all_day
53
+ when 'yesterday' then timezone.yesterday.all_day
54
+ when 'week' then timezone.now.all_week
55
+ when 'month' then timezone.now.all_month
56
+ when 'year' then timezone.now.all_year
57
+ end
58
+ end
59
+
60
+ # @return [ActiveSupport::TimeZone]
61
+ def timezone
62
+ Analytic.config.timezone || Time.zone
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class View < ApplicationRecord
5
+ validates :timestamp, presence: true
6
+ validates :visitor_id, presence: true
7
+ validates :session_id, presence: true
8
+ validates :path, presence: true
9
+ validates :host, presence: true
10
+
11
+ scope :within, ->(range) { where(timestamp: range) if range }
12
+
13
+ # @return [Integer]
14
+ def self.distinct_visitors_count
15
+ count('DISTINCT "analytic_views"."visitor_id"')
16
+ end
17
+
18
+ # @return [Integer]
19
+ def self.distinct_sessions_count
20
+ count('DISTINCT "analytic_views"."session_id"')
21
+ end
22
+ end
23
+ end
@@ -0,0 +1 @@
1
+ import "./initializers";
@@ -0,0 +1,23 @@
1
+ import "@fortawesome/fontawesome-free";
2
+
3
+ import { library } from "@fortawesome/fontawesome-svg-core";
4
+
5
+ import { faClock } from "@fortawesome/free-solid-svg-icons/faClock";
6
+ import { faEye } from "@fortawesome/free-solid-svg-icons/faEye";
7
+ import { faFileCode } from "@fortawesome/free-solid-svg-icons/faFileCode";
8
+ import { faGauge } from "@fortawesome/free-solid-svg-icons/faGauge";
9
+ import { faGlobe } from "@fortawesome/free-solid-svg-icons/faGlobe";
10
+ import { faHouse } from "@fortawesome/free-solid-svg-icons/faHouse";
11
+ import { faSliders } from "@fortawesome/free-solid-svg-icons/faSliders";
12
+ import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers";
13
+
14
+ library.add(
15
+ faClock,
16
+ faEye,
17
+ faGauge,
18
+ faGlobe,
19
+ faHouse,
20
+ faSliders,
21
+ faUsers,
22
+ faFileCode
23
+ );
@@ -0,0 +1 @@
1
+ import "./fontawesome";
@@ -0,0 +1,65 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer components {
6
+ .pill {
7
+ @apply bg-slate-200 text-slate-800 hover:text-white hover:bg-indigo-600 rounded-full px-3 py-2;
8
+ }
9
+
10
+ .card {
11
+ @apply shadow-md rounded bg-white;
12
+ }
13
+
14
+ .card__header {
15
+ @apply px-5 py-4 border-b border-slate-200;
16
+ }
17
+
18
+ .card__footer {
19
+ @apply px-5 py-4 border-t border-slate-200;
20
+ }
21
+
22
+ .card__content {
23
+ @apply px-5 py-4
24
+ }
25
+
26
+ .card__title {
27
+ @apply uppercase text-slate-600 font-semibold text-sm flex gap-2 items-center;
28
+ }
29
+
30
+ .card__value {
31
+ @apply uppercase text-slate-800 font-extrabold text-lg;
32
+ }
33
+
34
+ .table {
35
+ @apply w-full divide-y divide-slate-200;
36
+ }
37
+
38
+ .table tbody {
39
+ @apply divide-y divide-slate-200;
40
+ }
41
+
42
+ .table td {
43
+ @apply px-3 py-2 text-left;
44
+ }
45
+
46
+ .table th {
47
+ @apply px-3 py-2 text-left;
48
+ }
49
+
50
+ .table td:first-child {
51
+ @apply pl-0;
52
+ }
53
+
54
+ .table td:last-child {
55
+ @apply pr-0;
56
+ }
57
+
58
+ .table th:first-child {
59
+ @apply pl-0;
60
+ }
61
+
62
+ .table th:last-child {
63
+ @apply pr-0;
64
+ }
65
+ }
@@ -0,0 +1 @@
1
+ import "../application";
@@ -0,0 +1,84 @@
1
+ <%- provide(:title, @dashboard.name) %>
2
+
3
+ <div class="space-y-4">
4
+ <div class="flex gap-2">
5
+ <%= link_to 'Today', analytic.dashboard_path(period: 'today'), class: 'pill' %>
6
+ <%= link_to 'Yesterday', analytic.dashboard_path(period: 'yesterday'), class: 'pill' %>
7
+ <%= link_to 'Week', analytic.dashboard_path(period: 'week'), class: 'pill' %>
8
+ <%= link_to 'Month', analytic.dashboard_path(period: 'month'), class: 'pill' %>
9
+ <%= link_to 'Year', analytic.dashboard_path(period: 'year'), class: 'pill' %>
10
+ </div>
11
+
12
+ <div class="grid grid-flow-col justify-stretch gap-4">
13
+ <div class="card">
14
+ <div class="card__header">
15
+ <div class="card__title">
16
+ <%= tag.i class: "fa-solid fa-users" %>
17
+ Visitors
18
+ </div>
19
+ </div>
20
+ <div class="card__content">
21
+ <div class="card__value">
22
+ <%= number_with_delimiter @dashboard.distinct_visitors_count %>
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="card">
28
+ <div class="card__header">
29
+ <div class="card__title">
30
+ <%= tag.i class: "fa-solid fa-globe" %>
31
+ Sessions
32
+ </div>
33
+ </div>
34
+ <div class="card__content">
35
+ <div class="card__value">
36
+ <%= number_with_delimiter @dashboard.distinct_sessions_count %>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="card">
42
+ <div class="card__header">
43
+ <div class="card__title">
44
+ <%= tag.i class: "fa-solid fa-eye" %>
45
+ Views
46
+ </div>
47
+ </div>
48
+ <div class="card__content">
49
+ <div class="card__value">
50
+ <%= number_with_delimiter @dashboard.count %>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="card">
57
+ <div class="card__header">
58
+ <div class="card__title">
59
+ <%= tag.i class: "fa-solid fa-file-code" %>
60
+ Pages
61
+ </div>
62
+ </div>
63
+ <div class="card__content">
64
+ <table class="table">
65
+ <thead>
66
+ <tr>
67
+ <th scope="col">Host</th>
68
+ <th scope="col">Path</th>
69
+ <th scope="col">Views</th>
70
+ </tr>
71
+ </thead>
72
+ <tbody>
73
+ <%- @dashboard.pages.each do |(host, path, views)| -%>
74
+ <tr>
75
+ <td><%= host %></td>
76
+ <td><%= path %></td>
77
+ <td><%= number_with_delimiter views %></td>
78
+ </tr>
79
+ <%- end -%>
80
+ </tbody>
81
+ </table>
82
+ </div>
83
+ </div>
84
+ </div>
@@ -0,0 +1,42 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= yield(:title) %> | Analytic</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <%= javascript_include_tag 'analytic/application' %>
8
+ <%= stylesheet_link_tag 'analytic/application' %>
9
+ <%= %>
10
+ </head>
11
+ <body class="bg-slate-50">
12
+
13
+ <header class="bg-indigo-600 text-white py-2">
14
+ <div class="container mx-auto px-4">
15
+ <div class="flex justify-between">
16
+ <nav class="flex gap-2">
17
+ <%= link_to analytic.root_path, class: "flex gap-2 rounded items-center px-3 py-2 font-semibold hover:bg-indigo-800" do %>
18
+ <%= tag.i class: "fa-solid fa-gauge" %>
19
+ Analytic
20
+ <% end %>
21
+ </nav>
22
+ <nav class="flex gap-2">
23
+ <%= link_to analytic.root_path, class: "flex gap-2 rounded items-center px-3 py-2 hover:bg-indigo-800" do %>
24
+ <%= tag.i class: "fa-solid fa-house" %>
25
+ Dashboard
26
+ <% end %>
27
+ </nav>
28
+ </div>
29
+ </div>
30
+ </header>
31
+
32
+ <main class="container mx-auto px-4 py-8">
33
+ <%= yield %>
34
+ </main>
35
+
36
+ <footer class="container mx-auto px-4 text-center">
37
+ <div class="flex gap-2 justify-center">
38
+ <%= time_tag(Time.current) %>
39
+ </div>
40
+ </footer>
41
+ </body>
42
+ </html>
data/bin/dev ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ foreman start -f Procfile.dev
data/bin/rails ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENGINE_ROOT = File.expand_path("..", __dir__)
4
+ ENGINE_PATH = File.expand_path("../lib/analytic/engine", __dir__)
5
+ APP_PATH = File.expand_path("../spec/dummy/config/application", __dir__)
6
+
7
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
8
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
9
+
10
+ require "rails/all"
11
+ require "rails/engine/commands"
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Analytic::Engine.routes.draw do
4
+ root to: 'dashboard#show'
5
+
6
+ get '/dashboard/(period/:period)', to: 'dashboard#show', as: :dashboard
7
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAnalyticViews < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :analytic_views do |t|
6
+ t.uuid :visitor_id, null: false
7
+ t.uuid :session_id, null: false
8
+ t.string :path, null: false
9
+ t.string :host, null: false
10
+ t.jsonb :params, null: false, default: {}
11
+ t.datetime :timestamp, null: false
12
+
13
+ t.timestamps
14
+
15
+ t.index %i[timestamp path]
16
+ t.index %i[timestamp visitor_id]
17
+ t.index %i[timestamp session_id]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class Config
5
+ # @return [String]
6
+ attr_accessor :timezone
7
+
8
+ def initialize
9
+ @timezone = Time.zone
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Analytic
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Analytic
6
+ end
7
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Analytic
2
- VERSION = "0.1.0"
4
+ VERSION = '0.2.0'
3
5
  end
data/lib/analytic.rb CHANGED
@@ -1,6 +1,18 @@
1
- require "analytic/version"
2
- require "analytic/railtie"
1
+ # frozen_string_literal: true
2
+
3
+ require 'analytic/config'
4
+ require 'analytic/version'
5
+ require 'analytic/engine'
3
6
 
4
7
  module Analytic
5
- # Your code goes here...
8
+ # @return [Analytic::Config]
9
+ def self.config
10
+ @config ||= Config.new
11
+ end
12
+
13
+ # @yield [config]
14
+ # @yieldparam config [Analytic::Config]
15
+ def self.configure
16
+ yield config
17
+ end
6
18
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: analytic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-04 00:00:00.000000000 Z
11
+ date: 2024-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,35 +16,58 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 7.1.3.4
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 7.1.3.4
27
- description: An easy way to track analytics in your app.
26
+ version: '0'
27
+ description: Analytic
28
28
  email:
29
29
  - kevin@ksylvest.com
30
30
  executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
- - MIT-LICENSE
34
+ - LICENSE
35
35
  - README.md
36
36
  - Rakefile
37
+ - app/assets/builds/analytic/application.css
38
+ - app/assets/builds/analytic/application.js
39
+ - app/assets/builds/analytic/application.js.map
40
+ - app/assets/config/analytic_manifest.js
41
+ - app/controllers/analytic/application_controller.rb
42
+ - app/controllers/analytic/dashboard_controller.rb
43
+ - app/controllers/concerns/analytic/trackable.rb
44
+ - app/helpers/analytic/application_helper.rb
45
+ - app/jobs/analytic/application_job.rb
46
+ - app/jobs/analytic/track_job.rb
47
+ - app/mailers/analytic/application_mailer.rb
48
+ - app/models/analytic/application_record.rb
49
+ - app/models/analytic/dashboard.rb
50
+ - app/models/analytic/view.rb
51
+ - app/packs/analytic/application/index.ts
52
+ - app/packs/analytic/application/initializers/fontawesome.ts
53
+ - app/packs/analytic/application/initializers/index.ts
54
+ - app/packs/analytic/entrypoints/application.tailwind.css
55
+ - app/packs/analytic/entrypoints/application.ts
56
+ - app/views/analytic/dashboard/show.html.erb
57
+ - app/views/layouts/analytic/application.html.erb
58
+ - bin/dev
59
+ - bin/rails
60
+ - config/routes.rb
61
+ - db/migrate/20240805210911_create_analytic_views.rb
37
62
  - lib/analytic.rb
38
- - lib/analytic/railtie.rb
63
+ - lib/analytic/config.rb
64
+ - lib/analytic/engine.rb
39
65
  - lib/analytic/version.rb
40
- - lib/tasks/analytic_tasks.rake
41
66
  homepage: https://github.com/ksylvest/analytic
42
67
  licenses:
43
68
  - MIT
44
69
  metadata:
45
- homepage_uri: https://github.com/ksylvest/analytic
46
- source_code_uri: https://github.com/ksylvest/analytic
47
- changelog_uri: https://github.com/ksylvest/analytic
70
+ rubygems_mfa_required: 'true'
48
71
  post_install_message:
49
72
  rdoc_options: []
50
73
  require_paths:
@@ -53,7 +76,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
53
76
  requirements:
54
77
  - - ">="
55
78
  - !ruby/object:Gem::Version
56
- version: '0'
79
+ version: 3.3.0
57
80
  required_rubygems_version: !ruby/object:Gem::Requirement
58
81
  requirements:
59
82
  - - ">="
@@ -63,5 +86,5 @@ requirements: []
63
86
  rubygems_version: 3.5.11
64
87
  signing_key:
65
88
  specification_version: 4
66
- summary: An easy way to track analytics in your app.
89
+ summary: Analytic
67
90
  test_files: []
@@ -1,4 +0,0 @@
1
- module Analytic
2
- class Railtie < ::Rails::Railtie
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :analytic do
3
- # # Task goes here
4
- # end
File without changes