analytic 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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