lookout-ahoy 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +99 -0
- data/Rakefile +24 -0
- data/app/assets/images/lookout/apple-touch-icon.png +0 -0
- data/app/assets/images/lookout/favicon-16x16.png +0 -0
- data/app/assets/images/lookout/favicon-32x32.png +0 -0
- data/app/assets/images/lookout/logo.png +0 -0
- data/app/assets/images/lookout/safari-pinned-tab.png +0 -0
- data/app/assets/images/lookout/safari-pinned-tab.svg +199 -0
- data/app/assets/javascript/lookout/application.js +2 -0
- data/app/assets/javascript/lookout/controllers/application.js +9 -0
- data/app/assets/javascript/lookout/controllers/application_controller.js +33 -0
- data/app/assets/javascript/lookout/controllers/combobox_controller.js +371 -0
- data/app/assets/javascript/lookout/controllers/details_modal_controller.js +18 -0
- data/app/assets/javascript/lookout/controllers/dropdown_label_controller.js +39 -0
- data/app/assets/javascript/lookout/controllers/filter/item_controller.js +12 -0
- data/app/assets/javascript/lookout/controllers/filter_form_controller.js +13 -0
- data/app/assets/javascript/lookout/controllers/filter_modal_controller.js +45 -0
- data/app/assets/javascript/lookout/controllers/frame_link_controller.js +20 -0
- data/app/assets/javascript/lookout/controllers/funnel_chart_controller.js +159 -0
- data/app/assets/javascript/lookout/controllers/index.js +4 -0
- data/app/assets/javascript/lookout/controllers/interval_controller.js +15 -0
- data/app/assets/javascript/lookout/controllers/line_chart_controller.js +251 -0
- data/app/assets/javascript/lookout/controllers/predicate_select_controller.js +10 -0
- data/app/assets/javascript/lookout/controllers/properties_controller.js +8 -0
- data/app/assets/javascript/lookout/controllers/property_filter_controller.js +45 -0
- data/app/assets/javascript/lookout/controllers/realtime_controller.js +30 -0
- data/app/assets/javascript/lookout/controllers/sparkline_controller.js +64 -0
- data/app/assets/javascript/lookout/controllers/tile_controller.js +33 -0
- data/app/assets/javascript/lookout/controllers/toggle_controller.js +17 -0
- data/app/assets/javascript/lookout/helpers/chart_utils.js +156 -0
- data/app/assets/javascript/lookout/helpers/countries.js +2261 -0
- data/app/assets/javascript/lookout/helpers/number_formatters.js +55 -0
- data/app/assets/manifest/lookout/manifest.js +2 -0
- data/app/components/lookout/combobox_component.html.erb +33 -0
- data/app/components/lookout/combobox_component.rb +13 -0
- data/app/components/lookout/comparison_link_component.html.erb +17 -0
- data/app/components/lookout/comparison_link_component.rb +44 -0
- data/app/components/lookout/dropdown_button_component.html.erb +16 -0
- data/app/components/lookout/dropdown_button_component.rb +14 -0
- data/app/components/lookout/dropdown_link_component.html.erb +17 -0
- data/app/components/lookout/dropdown_link_component.rb +19 -0
- data/app/components/lookout/filter/dropdown_component.html.erb +50 -0
- data/app/components/lookout/filter/dropdown_component.rb +51 -0
- data/app/components/lookout/filter/modal_component.html.erb +16 -0
- data/app/components/lookout/filter/modal_component.rb +13 -0
- data/app/components/lookout/filter/select_component.html.erb +25 -0
- data/app/components/lookout/filter/select_component.rb +64 -0
- data/app/components/lookout/filter/tag_component.html.erb +13 -0
- data/app/components/lookout/filter/tag_component.rb +14 -0
- data/app/components/lookout/filter/tag_container_component.html.erb +4 -0
- data/app/components/lookout/filter/tag_container_component.rb +6 -0
- data/app/components/lookout/previous_next_component.html.erb +8 -0
- data/app/components/lookout/previous_next_component.rb +11 -0
- data/app/components/lookout/stats/comparable_container_component.html.erb +25 -0
- data/app/components/lookout/stats/comparable_container_component.rb +103 -0
- data/app/components/lookout/stats/container_component.html.erb +23 -0
- data/app/components/lookout/stats/container_component.rb +28 -0
- data/app/components/lookout/sticky_nav_component.html.erb +32 -0
- data/app/components/lookout/sticky_nav_component.rb +24 -0
- data/app/components/lookout/table_component.html.erb +16 -0
- data/app/components/lookout/table_component.rb +48 -0
- data/app/components/lookout/tables/devices_table_component.rb +11 -0
- data/app/components/lookout/tables/dynamic_table.rb +13 -0
- data/app/components/lookout/tables/dynamic_table_component.rb +207 -0
- data/app/components/lookout/tables/goals_table_component.rb +17 -0
- data/app/components/lookout/tables/header_component.html.erb +6 -0
- data/app/components/lookout/tables/header_component.rb +18 -0
- data/app/components/lookout/tables/headers/header_component.html.erb +5 -0
- data/app/components/lookout/tables/headers/header_component.rb +16 -0
- data/app/components/lookout/tables/properties_table_component.rb +27 -0
- data/app/components/lookout/tables/row_component.html.erb +4 -0
- data/app/components/lookout/tables/rows/row_component.html.erb +6 -0
- data/app/components/lookout/tables/rows/row_component.rb +40 -0
- data/app/components/lookout/tile_component.html.erb +24 -0
- data/app/components/lookout/tile_component.rb +24 -0
- data/app/components/lookout/tooltip_component.html.erb +3 -0
- data/app/components/lookout/tooltip_component.rb +18 -0
- data/app/controllers/lookout/application_controller.rb +83 -0
- data/app/controllers/lookout/campaigns_controller.rb +19 -0
- data/app/controllers/lookout/devices_controller.rb +20 -0
- data/app/controllers/lookout/entry_pages_controller.rb +19 -0
- data/app/controllers/lookout/exit_pages_controller.rb +19 -0
- data/app/controllers/lookout/exports_controller.rb +14 -0
- data/app/controllers/lookout/filters/base_controller.rb +15 -0
- data/app/controllers/lookout/filters/goals_controller.rb +9 -0
- data/app/controllers/lookout/filters/locations_controller.rb +11 -0
- data/app/controllers/lookout/filters/operating_systems/names_controller.rb +13 -0
- data/app/controllers/lookout/filters/operating_systems/versions_controller.rb +13 -0
- data/app/controllers/lookout/filters/pages/actions_controller.rb +13 -0
- data/app/controllers/lookout/filters/pages/entry_pages_controller.rb +14 -0
- data/app/controllers/lookout/filters/pages/exit_pages_controller.rb +15 -0
- data/app/controllers/lookout/filters/properties/names_controller.rb +29 -0
- data/app/controllers/lookout/filters/properties/values_controller.rb +15 -0
- data/app/controllers/lookout/filters/screens_controller.rb +11 -0
- data/app/controllers/lookout/filters/sources_controller.rb +11 -0
- data/app/controllers/lookout/filters/utms_controller.rb +10 -0
- data/app/controllers/lookout/funnels_controller.rb +8 -0
- data/app/controllers/lookout/goals_controller.rb +7 -0
- data/app/controllers/lookout/locations/cities_controller.rb +22 -0
- data/app/controllers/lookout/locations/countries_controller.rb +22 -0
- data/app/controllers/lookout/locations/maps_controller.rb +24 -0
- data/app/controllers/lookout/locations/regions_controller.rb +22 -0
- data/app/controllers/lookout/properties_controller.rb +73 -0
- data/app/controllers/lookout/realtimes_controller.rb +7 -0
- data/app/controllers/lookout/roots_controller.rb +6 -0
- data/app/controllers/lookout/sources_controller.rb +21 -0
- data/app/controllers/lookout/stats/base_controller.rb +148 -0
- data/app/controllers/lookout/stats/bounce_rates_controller.rb +12 -0
- data/app/controllers/lookout/stats/total_pageviews_controller.rb +10 -0
- data/app/controllers/lookout/stats/total_visits_controller.rb +10 -0
- data/app/controllers/lookout/stats/unique_visitors_controller.rb +11 -0
- data/app/controllers/lookout/stats/views_per_visits_controller.rb +11 -0
- data/app/controllers/lookout/stats/visit_durations_controller.rb +10 -0
- data/app/controllers/lookout/stats_controller.rb +7 -0
- data/app/controllers/lookout/top_pages_controller.rb +20 -0
- data/app/decorators/lookout/application_decorator.rb +58 -0
- data/app/decorators/lookout/campaign_decorator.rb +27 -0
- data/app/decorators/lookout/city_decorator.rb +24 -0
- data/app/decorators/lookout/country_decorator.rb +38 -0
- data/app/decorators/lookout/device_decorator.rb +27 -0
- data/app/decorators/lookout/entry_page_decorator.rb +7 -0
- data/app/decorators/lookout/exit_page_decorator.rb +7 -0
- data/app/decorators/lookout/page_decorator.rb +27 -0
- data/app/decorators/lookout/region_decorator.rb +28 -0
- data/app/decorators/lookout/source_decorator.rb +27 -0
- data/app/decorators/lookout/top_page_decorator.rb +7 -0
- data/app/helpers/lookout/application_helper.rb +124 -0
- data/app/models/concerns/lookout/compare_mode.rb +19 -0
- data/app/models/concerns/lookout/limitable.rb +17 -0
- data/app/models/concerns/lookout/range_options.rb +8 -0
- data/app/models/lookout/comparison_mode.rb +72 -0
- data/app/models/lookout/export.rb +48 -0
- data/app/models/lookout/filter_parser.rb +82 -0
- data/app/models/lookout/range_from_params.rb +78 -0
- data/app/models/lookout/rangeable.rb +7 -0
- data/app/models/lookout/widget.rb +15 -0
- data/app/presenters/lookout/dashboard_presenter.rb +53 -0
- data/app/presenters/lookout/funnel_presenter.rb +75 -0
- data/app/presenters/lookout/goals_presenter.rb +72 -0
- data/app/queries/concerns/lookout/comparable_queries.rb +25 -0
- data/app/queries/concerns/lookout/comparable_query.rb +152 -0
- data/app/queries/concerns/lookout/lazy_comparable_query.rb +42 -0
- data/app/queries/lookout/application_query.rb +186 -0
- data/app/queries/lookout/campaign_query.rb +14 -0
- data/app/queries/lookout/city_query.rb +14 -0
- data/app/queries/lookout/country_query.rb +10 -0
- data/app/queries/lookout/device_query.rb +10 -0
- data/app/queries/lookout/entry_pages_query.rb +18 -0
- data/app/queries/lookout/event_query.rb +42 -0
- data/app/queries/lookout/exit_pages_query.rb +19 -0
- data/app/queries/lookout/region_query.rb +14 -0
- data/app/queries/lookout/source_query.rb +11 -0
- data/app/queries/lookout/stats/average_views_per_visit_query.rb +20 -0
- data/app/queries/lookout/stats/average_visit_duration_query.rb +34 -0
- data/app/queries/lookout/stats/base_query.rb +18 -0
- data/app/queries/lookout/stats/bounce_rates_query.rb +33 -0
- data/app/queries/lookout/stats/total_pageviews_query.rb +9 -0
- data/app/queries/lookout/stats/total_visitors_query.rb +9 -0
- data/app/queries/lookout/stats/unique_visitors_query.rb +9 -0
- data/app/queries/lookout/stats/views_per_visit_query.rb +17 -0
- data/app/queries/lookout/stats/visit_duration_query.rb +19 -0
- data/app/queries/lookout/top_page_query.rb +13 -0
- data/app/queries/lookout/visit_query.rb +42 -0
- data/app/views/lookout/campaigns/index.html+details.erb +4 -0
- data/app/views/lookout/campaigns/index.html.erb +3 -0
- data/app/views/lookout/devices/_table.html.erb +2 -0
- data/app/views/lookout/devices/index.html+details.erb +4 -0
- data/app/views/lookout/devices/index.html.erb +3 -0
- data/app/views/lookout/entry_pages/index.html+details.erb +4 -0
- data/app/views/lookout/entry_pages/index.html.erb +3 -0
- data/app/views/lookout/exit_pages/index.html+details.erb +4 -0
- data/app/views/lookout/exit_pages/index.html.erb +3 -0
- data/app/views/lookout/funnels/index.html.erb +7 -0
- data/app/views/lookout/funnels/show.html.erb +15 -0
- data/app/views/lookout/goals/index.html.erb +4 -0
- data/app/views/lookout/layouts/application.html.erb +144 -0
- data/app/views/lookout/layouts/shared/_tile_loader.html.erb +5 -0
- data/app/views/lookout/layouts/shared/_widget_disabled.html+details.erb +3 -0
- data/app/views/lookout/layouts/shared/_widget_disabled.html.erb +3 -0
- data/app/views/lookout/locations/cities/index.html+details.erb +4 -0
- data/app/views/lookout/locations/cities/index.html.erb +3 -0
- data/app/views/lookout/locations/countries/index.html+details.erb +5 -0
- data/app/views/lookout/locations/countries/index.html.erb +3 -0
- data/app/views/lookout/locations/maps/_simple_map.html.erb +26 -0
- data/app/views/lookout/locations/maps/show.html.erb +106 -0
- data/app/views/lookout/locations/regions/index.html+details.erb +4 -0
- data/app/views/lookout/locations/regions/index.html.erb +3 -0
- data/app/views/lookout/properties/_form.html.erb +6 -0
- data/app/views/lookout/properties/index.html.erb +3 -0
- data/app/views/lookout/properties/show.html.erb +6 -0
- data/app/views/lookout/realtimes/show.html.erb +9 -0
- data/app/views/lookout/roots/_filters.html.erb +80 -0
- data/app/views/lookout/roots/show.html.erb +191 -0
- data/app/views/lookout/sources/index.html+details.erb +4 -0
- data/app/views/lookout/sources/index.html.erb +3 -0
- data/app/views/lookout/stats/base/index.html.erb +40 -0
- data/app/views/lookout/stats/show.html.erb +15 -0
- data/app/views/lookout/top_pages/index.html+details.erb +4 -0
- data/app/views/lookout/top_pages/index.html.erb +3 -0
- data/config/routes.rb +69 -0
- data/lib/generators/lookout/install_generator.rb +31 -0
- data/lib/generators/lookout/migration_generator.rb +21 -0
- data/lib/generators/lookout/templates/config.rb.tt +185 -0
- data/lib/generators/lookout/templates/migration.rb.tt +7 -0
- data/lib/lookout/active_record.rb +108 -0
- data/lib/lookout/ahoy/event_methods.rb +75 -0
- data/lib/lookout/ahoy/visit_methods.rb +24 -0
- data/lib/lookout/configuration.rb +58 -0
- data/lib/lookout/database_adapter.rb +168 -0
- data/lib/lookout/engine.rb +47 -0
- data/lib/lookout/filter_configuration/filter.rb +16 -0
- data/lib/lookout/filter_configuration/filter_collection.rb +48 -0
- data/lib/lookout/filters_configuration.rb +77 -0
- data/lib/lookout/funnels.rb +44 -0
- data/lib/lookout/goals.rb +51 -0
- data/lib/lookout/period_collection.rb +115 -0
- data/lib/lookout/predicate_label.rb +7 -0
- data/lib/lookout/railtie.rb +9 -0
- data/lib/lookout/version.rb +3 -0
- data/lib/lookout.rb +78 -0
- metadata +673 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module Querying
|
|
3
|
+
delegate :with, to: :all
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
module WithMerger
|
|
7
|
+
def merge
|
|
8
|
+
super
|
|
9
|
+
merge_withs
|
|
10
|
+
relation
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def merge_withs
|
|
16
|
+
relation.recursive_with = true if other.recursive_with?
|
|
17
|
+
other_values = other.with_values.reject { |value| relation.with_values.include?(value) }
|
|
18
|
+
relation.with!(*other_values) if other_values.any?
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Relation
|
|
23
|
+
class Merger
|
|
24
|
+
prepend WithMerger
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def with(opts, *rest)
|
|
28
|
+
spawn.with!(opts, *rest)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def with!(opts, *rest)
|
|
32
|
+
if opts == :recursive
|
|
33
|
+
self.recursive_with = true
|
|
34
|
+
self.with_values += rest
|
|
35
|
+
else
|
|
36
|
+
self.with_values += [opts] + rest
|
|
37
|
+
end
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def with_values
|
|
42
|
+
@values[:with] || []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def with_values=(values)
|
|
46
|
+
raise ImmutableRelation if @loaded
|
|
47
|
+
|
|
48
|
+
@values[:with] = values
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def recursive_with?
|
|
52
|
+
@values[:recursive_with]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def recursive_with=(value)
|
|
56
|
+
raise ImmutableRelation if @loaded
|
|
57
|
+
|
|
58
|
+
@values[:recursive_with] = value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def build_arel(*args)
|
|
64
|
+
arel = super
|
|
65
|
+
build_with(arel) if @values[:with]
|
|
66
|
+
arel
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_with(arel) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
|
70
|
+
return if with_values.empty?
|
|
71
|
+
|
|
72
|
+
with_statements = with_values.map do |with_value|
|
|
73
|
+
case with_value
|
|
74
|
+
when String then Arel::Nodes::SqlLiteral.new(with_value)
|
|
75
|
+
when Arel::Nodes::As then with_value
|
|
76
|
+
when Hash then build_with_value_from_hash(with_value)
|
|
77
|
+
when Array then build_with_value_from_array(with_value)
|
|
78
|
+
else
|
|
79
|
+
raise ArgumentError, "Unsupported argument type: #{with_value} #{with_value.class}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
recursive_with? ? arel.with(:recursive, with_statements) : arel.with(with_statements)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_with_value_from_array(array)
|
|
87
|
+
unless array.map(&:class).uniq == [Arel::Nodes::As]
|
|
88
|
+
raise ArgumentError, "Unsupported argument type: #{array} #{array.class}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
array
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_with_value_from_hash(hash) # rubocop:disable Metrics/MethodLength
|
|
95
|
+
hash.map do |name, value|
|
|
96
|
+
table = Arel::Table.new(name)
|
|
97
|
+
expression = case value
|
|
98
|
+
when String then Arel::Nodes::SqlLiteral.new("(#{value})")
|
|
99
|
+
when ActiveRecord::Relation then value.arel
|
|
100
|
+
when Arel::SelectManager, Arel::Nodes::Union then value
|
|
101
|
+
else
|
|
102
|
+
raise ArgumentError, "Unsupported argument type: #{value} #{value.class}"
|
|
103
|
+
end
|
|
104
|
+
Arel::Nodes::As.new(table, expression)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Lookout
|
|
2
|
+
module Ahoy
|
|
3
|
+
module EventMethods
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
scope :page_view, -> { where("name = '#{Lookout.config.event[:view_name]}'") }
|
|
8
|
+
|
|
9
|
+
ransacker :route do |_parent|
|
|
10
|
+
Arel.sql(Lookout.config.event[:url_column])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
ransacker :entry_page do |parent|
|
|
14
|
+
Arel.sql("entry_pages.url")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
ransacker :exit_page do |parent|
|
|
18
|
+
Arel.sql("exit_pages.url")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
scope :with_entry_pages, -> {
|
|
22
|
+
with(entry_pages: self.select("MIN(#{table_name}.id) as min_id, #{Arel.sql("#{Lookout.config.event.url_column} AS url")}").where(name: Lookout.config.event[:view_name]).group("#{table_name}.properties")).joins("INNER JOIN entry_pages ON entry_pages.min_id = #{table_name}.id")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
scope :with_exit_pages, -> {
|
|
26
|
+
with(exit_pages: self.select("MAX(#{table_name}.id) as max_id, #{Arel.sql("#{Lookout.config.event.url_column} AS url")}")
|
|
27
|
+
.where(name: Lookout.config.event[:view_name]).group("#{table_name}.properties"))
|
|
28
|
+
.joins("INNER JOIN exit_pages ON exit_pages.max_id = #{table_name}.id")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
scope :with_routes, -> { where(Lookout.config.event[:url_exists]) }
|
|
32
|
+
|
|
33
|
+
scope :with_url, -> {
|
|
34
|
+
select(Arel.sql("#{Lookout.config.event.url_column} AS url"))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
scope :distinct_url, -> {
|
|
38
|
+
distinct(Arel.sql("#{Lookout.config.event.url_column}"))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
scope :with_property_values, ->(value) {
|
|
42
|
+
where(Lookout::DatabaseAdapter.json_key_exists("properties", value))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ransacker :properties, args: [:parent, :ransacker_args] do |parent, args|
|
|
46
|
+
Lookout::DatabaseAdapter.ransacker_json_extract(parent, args)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
ransacker :goal,
|
|
50
|
+
formatter: ->(value) {
|
|
51
|
+
::Arel::Nodes::SqlLiteral.new(
|
|
52
|
+
::Lookout.config.goals[value].event_query.call.select(:id).to_sql
|
|
53
|
+
)
|
|
54
|
+
} do |parent|
|
|
55
|
+
parent.table[:id]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class_methods do
|
|
60
|
+
def ransackable_attributes(auth_object = nil)
|
|
61
|
+
super + [ "action", "controller", "id", "name", "page", "properties", "time", "url", "user_id", "visit_id", "goal"] + self._ransackers.keys
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def ransackable_scopes(auth_object = nil)
|
|
65
|
+
super + [:with_property_values, :property_value_i_cont]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ransackable_associations(auth_object = nil)
|
|
69
|
+
super + [:visit]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Lookout
|
|
2
|
+
module Ahoy
|
|
3
|
+
module VisitMethods
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
ransacker :ref_domain do
|
|
8
|
+
expr = Lookout::DatabaseAdapter.domain_from("#{self.table_name}.referring_domain")
|
|
9
|
+
Arel.sql("(#{expr})")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def ransackable_attributes(auth = nil)
|
|
15
|
+
columns_hash.keys + ["ref_domain"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ransackable_associations(auth = nil)
|
|
19
|
+
super + ["events"]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'lookout/period_collection'
|
|
2
|
+
require 'lookout/filters_configuration'
|
|
3
|
+
|
|
4
|
+
module Lookout
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :view_name, :theme, :realtime_interval, :disabled_widgets, :return_path, :return_copy
|
|
7
|
+
attr_reader :goals, :funnels, :cache, :ranges, :event, :models, :filters, :predicate_labels
|
|
8
|
+
def initialize
|
|
9
|
+
@goals = GoalCollection.new
|
|
10
|
+
@funnels = FunnelCollection.new
|
|
11
|
+
@theme = "dark"
|
|
12
|
+
@return_path = "/"
|
|
13
|
+
@return_copy = "← Back"
|
|
14
|
+
@ranges = ::Lookout::PeriodCollection.load_default
|
|
15
|
+
@cache = ActiveSupport::OrderedOptions.new.tap do |option|
|
|
16
|
+
option.enabled = false
|
|
17
|
+
option.store = Rails.cache
|
|
18
|
+
option.ttl = 1.minute
|
|
19
|
+
end
|
|
20
|
+
@models = ActiveSupport::OrderedOptions.new.tap do |option|
|
|
21
|
+
option.event = "::Ahoy::Event"
|
|
22
|
+
option.visit = "::Ahoy::Visit"
|
|
23
|
+
end
|
|
24
|
+
@event = ActiveSupport::OrderedOptions.new.tap do |option|
|
|
25
|
+
option.view_name = "$view"
|
|
26
|
+
# Dynamically generate SQL based on database adapter
|
|
27
|
+
table_name = @models.event.parameterize.tableize
|
|
28
|
+
option.url_column = DatabaseAdapter.build_url_column(table_name)
|
|
29
|
+
option.url_exists = DatabaseAdapter.build_url_exists(table_name)
|
|
30
|
+
end
|
|
31
|
+
@filters = FiltersConfiguration.load_default
|
|
32
|
+
@predicate_labels = {
|
|
33
|
+
eq: 'equals',
|
|
34
|
+
not_eq: 'not equals',
|
|
35
|
+
cont: 'contains',
|
|
36
|
+
in: 'in',
|
|
37
|
+
not_in: 'not in',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@realtime_interval = 30.seconds
|
|
41
|
+
@disabled_widgets = []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def goal(id, &block)
|
|
45
|
+
instance = Goal.new
|
|
46
|
+
instance.id = id
|
|
47
|
+
instance.instance_exec(&block)
|
|
48
|
+
@goals.register(instance)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def funnel(id, &block)
|
|
52
|
+
instance = Funnel.new
|
|
53
|
+
instance.id = id
|
|
54
|
+
instance.instance_exec(&block)
|
|
55
|
+
@funnels.register(instance)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
module Lookout
|
|
2
|
+
module DatabaseAdapter
|
|
3
|
+
class << self
|
|
4
|
+
# Detect the database adapter being used
|
|
5
|
+
def adapter_name
|
|
6
|
+
@adapter_name ||= detect_adapter
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Reset cached adapter name (useful for testing)
|
|
10
|
+
def reset!
|
|
11
|
+
@adapter_name = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def postgresql?
|
|
15
|
+
adapter_name == :postgresql
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def sqlite?
|
|
19
|
+
adapter_name == :sqlite
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Generate SQL for concatenating strings
|
|
23
|
+
def concat(*args)
|
|
24
|
+
if postgresql?
|
|
25
|
+
"CONCAT(#{args.join(', ')})"
|
|
26
|
+
else # sqlite
|
|
27
|
+
args.join(' || ')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Generate SQL for extracting JSON text value
|
|
32
|
+
# Usage: json_extract_text('properties', 'controller')
|
|
33
|
+
def json_extract_text(column, key)
|
|
34
|
+
if postgresql?
|
|
35
|
+
"#{column}->>'#{key}'"
|
|
36
|
+
else # sqlite
|
|
37
|
+
"JSON_EXTRACT(#{column}, '$.#{key}')"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Generate SQL for checking if JSON key exists
|
|
42
|
+
def json_key_exists(column, key)
|
|
43
|
+
if postgresql?
|
|
44
|
+
"JSONB_EXISTS(#{column}, '#{key}')"
|
|
45
|
+
else # sqlite
|
|
46
|
+
"JSON_TYPE(#{column}, '$.#{key}') IS NOT NULL"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Generate SQL for getting all JSON object keys
|
|
51
|
+
# Returns a query that produces rows with a 'keys' column
|
|
52
|
+
def json_object_keys(column)
|
|
53
|
+
if postgresql?
|
|
54
|
+
"jsonb_object_keys(#{column})"
|
|
55
|
+
else # sqlite
|
|
56
|
+
# SQLite doesn't have a built-in function for this
|
|
57
|
+
# We'll need to use json_each which returns key/value pairs
|
|
58
|
+
# This will be used in a subquery context
|
|
59
|
+
"json_each.key"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Generate the FROM clause for getting JSON keys
|
|
64
|
+
# For SQLite, we need to use json_each()
|
|
65
|
+
def json_keys_from_clause(table_name, column)
|
|
66
|
+
if postgresql?
|
|
67
|
+
table_name
|
|
68
|
+
else # sqlite
|
|
69
|
+
"#{table_name}, json_each(#{table_name}.#{column})"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Generate SQL for JSON contains check (for ransacker)
|
|
74
|
+
def json_infix_operator
|
|
75
|
+
if postgresql?
|
|
76
|
+
'->>'
|
|
77
|
+
else # sqlite
|
|
78
|
+
# We'll handle this differently in SQLite
|
|
79
|
+
# Return a marker that we can detect and handle specially
|
|
80
|
+
'JSON_EXTRACT'
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Generate SQL for building URL from controller/action in properties
|
|
85
|
+
# table_name: the table name (e.g., 'ahoy_events')
|
|
86
|
+
def build_url_column(table_name)
|
|
87
|
+
controller = json_extract_text("#{table_name}.properties", 'controller')
|
|
88
|
+
action = json_extract_text("#{table_name}.properties", 'action')
|
|
89
|
+
|
|
90
|
+
if postgresql?
|
|
91
|
+
"CONCAT(#{controller}, '#', #{action})"
|
|
92
|
+
else # sqlite
|
|
93
|
+
"#{controller} || '#' || #{action}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Generate SQL for checking if URL properties exist
|
|
98
|
+
def build_url_exists(table_name)
|
|
99
|
+
controller_exists = json_key_exists("#{table_name}.properties", 'controller')
|
|
100
|
+
action_exists = json_key_exists("#{table_name}.properties", 'action')
|
|
101
|
+
|
|
102
|
+
"#{controller_exists} AND #{action_exists}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# For SQLite JSON_EXTRACT in ransacker, we need special handling
|
|
106
|
+
def ransacker_json_extract(parent, key)
|
|
107
|
+
if postgresql?
|
|
108
|
+
Arel::Nodes::InfixOperation.new('->>', parent.table[:properties], Arel::Nodes.build_quoted(key))
|
|
109
|
+
else # sqlite
|
|
110
|
+
Arel::Nodes::NamedFunction.new(
|
|
111
|
+
'JSON_EXTRACT',
|
|
112
|
+
[parent.table[:properties], Arel::Nodes.build_quoted("$.#{key}")]
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Extract domain from a referrer/URL column
|
|
118
|
+
# column can be a bare column (referring_domain) or qualified (table.referring_domain)
|
|
119
|
+
def domain_from(column)
|
|
120
|
+
if postgresql?
|
|
121
|
+
# Use regex form supported by Postgres
|
|
122
|
+
"substring(#{column} from '(?:.*://)?(?:www\\.)?([^/?]*)')"
|
|
123
|
+
else
|
|
124
|
+
# SQLite: derive domain using instr/substr
|
|
125
|
+
# Step 1: strip scheme
|
|
126
|
+
base = "CASE WHEN instr(#{column}, '://') > 0 THEN substr(#{column}, instr(#{column}, '://') + 3) ELSE #{column} END"
|
|
127
|
+
# Step 2: strip leading www.
|
|
128
|
+
no_www = "CASE WHEN substr(#{base}, 1, 4) = 'www.' THEN substr(#{base}, 5) ELSE #{base} END"
|
|
129
|
+
# Step 3: take up to first '/'
|
|
130
|
+
"CASE WHEN instr(#{no_www}, '/') > 0 THEN substr(#{no_www}, 1, instr(#{no_www}, '/') - 1) ELSE #{no_www} END"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Cast a number to decimal/real for percentage calculations
|
|
135
|
+
def numeric_cast(expression)
|
|
136
|
+
if postgresql?
|
|
137
|
+
"#{expression}::numeric"
|
|
138
|
+
else # sqlite
|
|
139
|
+
"CAST(#{expression} AS REAL)"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Calculate percentage: (numerator / denominator) * 100
|
|
144
|
+
def percentage_calculation(numerator, denominator)
|
|
145
|
+
if postgresql?
|
|
146
|
+
"(#{numerator}/#{denominator}::numeric) * 100"
|
|
147
|
+
else # sqlite
|
|
148
|
+
"(CAST(#{numerator} AS REAL) / #{denominator}) * 100"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def detect_adapter
|
|
155
|
+
name = ActiveRecord::Base.connection_db_config.adapter.to_s
|
|
156
|
+
case name
|
|
157
|
+
when /postg/i
|
|
158
|
+
:postgresql
|
|
159
|
+
when /sqlite/i
|
|
160
|
+
:sqlite
|
|
161
|
+
else
|
|
162
|
+
raise "Unsupported database adapter: #{name}. Lookout supports PostgreSQL and SQLite only."
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'ahoy'
|
|
2
|
+
require 'turbo-rails'
|
|
3
|
+
require 'stimulus-rails'
|
|
4
|
+
require 'ransack'
|
|
5
|
+
require 'view_component'
|
|
6
|
+
require 'groupdate'
|
|
7
|
+
require 'pagy'
|
|
8
|
+
require 'zip'
|
|
9
|
+
|
|
10
|
+
module Ransack
|
|
11
|
+
module Nodes
|
|
12
|
+
class Condition
|
|
13
|
+
|
|
14
|
+
# allows for sql from a formatter
|
|
15
|
+
# see https://github.com/activerecord-hackery/ransack/issues/702
|
|
16
|
+
def casted_array?(predicate)
|
|
17
|
+
return unless predicate.is_a?(Arel::Nodes::Casted)
|
|
18
|
+
|
|
19
|
+
predicate.value.is_a?(Array)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Ransack.configure do |config|
|
|
27
|
+
config.add_predicate 'json_cont', arel_predicate: 'contains', formatter: proc { |v| JSON.parse(v) }
|
|
28
|
+
config.add_predicate 'json_eq', arel_predicate: 'eq', formatter: proc { |v| JSON.parse(v) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module Lookout
|
|
32
|
+
class Engine < Rails::Engine
|
|
33
|
+
isolate_namespace ::Lookout
|
|
34
|
+
|
|
35
|
+
initializer "lookout.precompile" do |app|
|
|
36
|
+
if app.config.respond_to?(:assets)
|
|
37
|
+
app.config.assets.paths << Lookout::Engine.root.join("app/javascript")
|
|
38
|
+
app.config.assets.paths << Lookout::Engine.root.join("app/images")
|
|
39
|
+
app.config.assets.precompile << "lookout/application.js"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
ActiveSupport.on_load(:active_record) do
|
|
44
|
+
require "lookout/active_record"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Lookout
|
|
2
|
+
class FilterConfiguration
|
|
3
|
+
class Filter
|
|
4
|
+
attr_reader :column, :label, :url, :predicates, :multiple, :position
|
|
5
|
+
|
|
6
|
+
def initialize(label:, column:, url:, predicates: [:in, :not_in], multiple: true, position: nil)
|
|
7
|
+
@column = column
|
|
8
|
+
@label = label
|
|
9
|
+
@url = url
|
|
10
|
+
@predicates = predicates
|
|
11
|
+
@multiple = multiple
|
|
12
|
+
@position = position
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Lookout
|
|
2
|
+
class FilterConfiguration
|
|
3
|
+
class FilterCollection
|
|
4
|
+
def initialize(label)
|
|
5
|
+
@label = label
|
|
6
|
+
@registry = []
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def filter(label:, column:, url:, predicates: [:in, :not_in], multiple: true, position: nil)
|
|
10
|
+
position ||= @registry.size
|
|
11
|
+
if item = find(column)
|
|
12
|
+
@registry.delete(item)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@registry << FilterConfiguration::Filter.new(label: label, column: column, url: url, predicates: predicates, multiple: multiple, position: position)
|
|
16
|
+
@registry = @registry.sort_by { |filter| filter.position }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each(&block)
|
|
20
|
+
@registry.each(&block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def modal_name
|
|
24
|
+
"#{@label.parameterize.underscore}Modal"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def find(column)
|
|
28
|
+
@registry.find { |filter| filter.column == column.to_sym }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete(name)
|
|
32
|
+
@registry.delete_if { |filter| filter.column == name }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def filters
|
|
36
|
+
@registry
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def [](name)
|
|
40
|
+
find(name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def include?(name)
|
|
44
|
+
find(name).present?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require 'lookout/filter_configuration/filter'
|
|
2
|
+
require 'lookout/filter_configuration/filter_collection'
|
|
3
|
+
|
|
4
|
+
module Lookout
|
|
5
|
+
class FiltersConfiguration
|
|
6
|
+
def self.load_default
|
|
7
|
+
new.tap do |config|
|
|
8
|
+
config.register("Page") do
|
|
9
|
+
filter column: :route, label: "Route", url: :filters_actions_path, predicates: [:in, :not_in]
|
|
10
|
+
filter column: :entry_page, label: "Entry Page", url: :filters_entry_pages_path, predicates: [:in, :not_in]
|
|
11
|
+
filter column: :exit_page, label: "Exit Page", url: :filters_exit_pages_path, predicates: [:in, :not_in]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
config.register("Geography") do
|
|
15
|
+
filter column: :country, label: "Country", url: :filters_locations_countries_path, predicates: [:in, :not_in]
|
|
16
|
+
filter column: :region, label: "Region", url: :filters_locations_regions_path, predicates: [:in, :not_in]
|
|
17
|
+
filter column: :city, label: "City", url: :filters_locations_cities_path, predicates: [:in, :not_in]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
config.register("Source") do
|
|
21
|
+
filter column: :referring_domain, label: "Source", url: :filters_sources_path, predicates: [:in, :not_in]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
config.register("Screen size") do
|
|
25
|
+
filter column: :device_type, label: "Screen size", url: :filters_screens_path, predicates: [:in, :not_in]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
config.register("Operating System") do
|
|
29
|
+
filter column: :os, label: "OS Name", url: :filters_names_path, predicates: [:in, :not_in]
|
|
30
|
+
filter column: :os_version, label: "OS Version", url: :filters_versions_path, predicates: [:in, :not_in]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
config.register("UTM Tags") do
|
|
34
|
+
filter column: :utm_medium, label: "UTM Medium", url: :filters_utm_mediums_path, predicates: [:in, :not_in, :cont]
|
|
35
|
+
filter column: :utm_source, label: "UTM Source", url: :filters_utm_sources_path, predicates: [:in, :not_in, :cont]
|
|
36
|
+
filter column: :utm_campaign, label: "UTM Campaign", url: :filters_utm_campaigns_path, predicates: [:in, :not_in, :cont]
|
|
37
|
+
filter column: :utm_term, label: "UTM Term", url: :filters_utm_terms_path, predicates: [:in, :not_in, :cont]
|
|
38
|
+
filter column: :utm_content, label: "UTM Content", url: :filters_utm_contents_path, predicates: [:in, :not_in, :cont]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
config.register("Goal") do
|
|
42
|
+
filter column: :goal, label: "Goal", url: :filters_goals_path, predicates: [:in]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize
|
|
48
|
+
@registry = {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def register(label, &block)
|
|
52
|
+
item = FilterConfiguration::FilterCollection.new(label)
|
|
53
|
+
item.instance_exec(&block)
|
|
54
|
+
@registry[label] = item
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def [](val)
|
|
58
|
+
@registry[val]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def delete(name)
|
|
62
|
+
@registry.delete(name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reset
|
|
66
|
+
@registry = {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def each(&block)
|
|
70
|
+
@registry.each(&block)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def detect(&block)
|
|
74
|
+
@registry.detect(&block)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Lookout
|
|
2
|
+
class Funnel
|
|
3
|
+
attr_accessor :id
|
|
4
|
+
attr_reader :goals
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@id = nil
|
|
8
|
+
@label = nil
|
|
9
|
+
@goals = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def goal(id)
|
|
13
|
+
@goals << Lookout.config.goals[id]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def label(value)
|
|
17
|
+
@label = value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def title
|
|
21
|
+
@label
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class FunnelCollection
|
|
26
|
+
include Enumerable
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@funnels = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def register(funnel)
|
|
33
|
+
@funnels[funnel.id] = funnel
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def each(&block)
|
|
37
|
+
@funnels.each(&block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def [](value)
|
|
41
|
+
@funnels[value.to_sym]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|