diff --git a/Dockerfile.dev b/Dockerfile.dev index 79a059cae..ae0c4a12a 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -20,6 +20,7 @@ RUN apk --no-cache update \ && apk --no-cache upgrade \ && apk add --no-cache \ build-base \ +python2 \ ttf-ubuntu-font-family \ git \ postgresql-dev \ diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 591819335..61d823638 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,2 +1,3 @@ //= link_tree ../images //= link_directory ../stylesheets .css +//= link_tree ../javascripts diff --git a/app/assets/javascripts/comfy/admin/cms/dashboard.js b/app/assets/javascripts/comfy/admin/cms/dashboard.js new file mode 100644 index 000000000..4d3a159a8 --- /dev/null +++ b/app/assets/javascripts/comfy/admin/cms/dashboard.js @@ -0,0 +1,39 @@ +//= require moment +//= require bootstrap-daterangepicker + +$(function() { + $('[data-toggle="tooltip"]').tooltip(); + + $('#reportrange').daterangepicker({ + opens: 'left', + locale: { + format: 'YYYY/MM/DD' + }, + startDate: $("#start_date").val() || moment().startOf('month'), + endDate: $("#end_date").val() || moment().endOf('month'), + ranges: { + [moment().format('MMMM YYYY')]: [moment().startOf('month'), moment().endOf('month')], + '3 months': [moment().startOf('month').subtract(2, 'months'), moment().endOf('month')], + '6 months': [moment().startOf('month').subtract(5, 'months'), moment().endOf('month')], + '1 year': [moment().startOf('month').subtract(11, 'months'), moment().endOf('month')] + } + }, function(start, end, label) { + $('#start_date').val(start.format('YYYY-MM-DD')); + $('#end_date').val(end.format('YYYY-MM-DD')); + $('#interval').val(label); + cb(); + $("#analytics_filter").submit(); + }); + + cb(); +}); + +function cb() { + if (!$('#start_date').val() && !$('#end_date').val()) { + $('#reportrange span').html(moment().format('MMMM YYYY')); + } else if ($('#interval').val() == 'Custom Range') { + $('#reportrange span').html(moment($("#start_date").val()).format('MMMM D, YYYY') + ' - ' + moment($("#end_date").val()).format('MMMM D, YYYY')); + } else { + $('#reportrange span').html($('#interval').val()); + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index a016c34e7..3ff838157 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -19,6 +19,7 @@ @import "./actiontext.scss"; @import 'select2/dist/css/select2.css'; @import './direct_upload.scss'; + @import 'bootstrap-daterangepicker/daterangepicker'; .alert-file_url { display: none; diff --git a/app/assets/stylesheets/dashboard.scss b/app/assets/stylesheets/dashboard.scss index 4f1963761..d6f102422 100755 --- a/app/assets/stylesheets/dashboard.scss +++ b/app/assets/stylesheets/dashboard.scss @@ -1,3 +1,175 @@ // Place all the styles related to the dashboard controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: https://sass-lang.com/ + +.vr-analytics { + color: #293845; + + &-title { + font-size: 32px; + line-height: 44px; + font-weight: bold; + } + + &-sub-title { + font-size: 24px; + line-height: 33px; + text-transform: capitalize; + } + + &-count, + &-percent-change, + &-event-label { + color: #535F6A; + font-size: 16px; + line-height: 22px; + } + + &-count { + margin-right: 10px; + + @media (min-width: 768px) { + margin-left: 24px; + } + } + + &-count-lg { + font-size: 24px; + line-height: 33px; + margin-right: 10px; + } + + &-tooltips { + height: 16px; + width: 16px; + border-radius: 50%; + background-color: #D4D7DA; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; + margin-left: 8px; + cursor: default; + } + + &-percent-change { + .positive { + color: #63C32D; + } + + .negative { + color: #E93E3E; + } + } + + &-filter-dropdown-container { + & > button { + color: #BD34D1; + font-size: 16px; + font-weight: 600; + } + } + + &-filters { + &-pages { + border: none; + outline: none; + -moz-appearance:none; /* Firefox */ + -webkit-appearance:none; /* Safari and Chrome */ + appearance:none; + background: transparent; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position-x: 98.5%; + background-position-y: 50%; + } + + &-pages, + &-ranges > span, + &-ranges > i { + color: #BD34D1; + font-size: 16px; + font-weight: 600; + cursor: pointer; + } + + @media (max-width: 767px) { + &-pages, + &-ranges { + width: 100%; + border: 1px solid #E5E7E8; + border-radius: 5px; + padding: 9px 16px; + margin-bottom: 8px; + } + } + + } + + &-card { + box-shadow: -4px 12px 24px #29384514; + border: 1px solid #E9EAEC; + border-radius: 5px; + position: relative; + + & > a { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + } + + &-section { + padding: 16px; + } + + &-resource-poster-conatiner { + margin-right: 10px; + + & > img { + height: 48px; + width: 85px; + object-fit: cover; + } + } + + &-resource-title { + font-weight: bold; + position: relative; + pointer-events: none; + z-index: 1; + + & > a { + color: #293845; + pointer-events: all; + } + } + + &-resource-duration { + color: #535F6A; + } + } + + &-section { + padding: 32px 0; + + &-header { + margin-bottom: 24px; + } + } + + &-page-visit-events { + &-donut-chart { + padding: 16px; + } + } + + + &-blank-states { + color: grey; + padding: 0 25px 40px; + font-size: 14px; + } +} \ No newline at end of file diff --git a/app/controllers/comfy/admin/api_namespaces_controller.rb b/app/controllers/comfy/admin/api_namespaces_controller.rb index a79c01619..228b13415 100755 --- a/app/controllers/comfy/admin/api_namespaces_controller.rb +++ b/app/controllers/comfy/admin/api_namespaces_controller.rb @@ -1,7 +1,7 @@ require 'will_paginate/array' class Comfy::Admin::ApiNamespacesController < Comfy::Admin::Cms::BaseController - before_action :set_api_namespace, only: %i[ show edit update destroy discard_failed_api_actions rerun_failed_api_actions export export_api_resources duplicate_with_associations duplicate_without_associations export_without_associations_as_json export_with_associations_as_json social_share_metadata api_action_workflow ] + before_action :set_api_namespace, except: %i[index new create import_as_json] before_action :ensure_authority_for_creating_api, only: %i[ new create import_as_json] before_action :ensure_authority_for_viewing_all_api, only: :index @@ -12,6 +12,7 @@ class Comfy::Admin::ApiNamespacesController < Comfy::Admin::Cms::BaseController before_action :ensure_authority_for_allow_exports_in_api, only: %i[ export export_api_resources export_without_associations_as_json export_with_associations_as_json ] before_action :ensure_authority_for_allow_duplication_in_api, only: %i[ duplicate_with_associations duplicate_without_associations ] before_action :ensure_authority_for_allow_social_share_metadata_in_api, only: %i[ social_share_metadata ] + before_action :ensure_authority_to_manage_analytics, only: :analytics_metadata before_action :ensure_authority_for_full_access_for_api_actions_only_in_api, only: %i[ api_action_workflow discard_failed_api_actions rerun_failed_api_actions ] # GET /api_namespaces or /api_namespaces.json @@ -219,6 +220,25 @@ def social_share_metadata end end + + def analytics_metadata + respond_to do |format| + if @api_namespace.update(analytics_metadata_params) + format.html do + flash[:notice] = 'Analytics Metadata successfully updated.' + redirect_to @api_namespace + end + format.json { render :show, status: :ok, location: @api_namespace } + else + format.html do + flash[:error] = @api_namespace.errors.full_messages + render :edit, status: :unprocessable_entity + end + format.json { render json: @api_namespace.errors, status: :unprocessable_entity } + end + end + end + def api_action_workflow respond_to do |format| if @api_namespace.update(api_action_workflow_params) @@ -269,4 +289,8 @@ def api_action_workflow_params def api_namespace_social_share_metadata_params params.require(:api_namespace).permit(social_share_metadata: [:title, :description, :image]) end + + def analytics_metadata_params + params.require(:api_namespace).permit(analytics_metadata: [:title, :author, :thumbnail]) + end end \ No newline at end of file diff --git a/app/controllers/comfy/admin/v2/dashboard_controller.rb b/app/controllers/comfy/admin/v2/dashboard_controller.rb new file mode 100644 index 000000000..623127db9 --- /dev/null +++ b/app/controllers/comfy/admin/v2/dashboard_controller.rb @@ -0,0 +1,53 @@ +class Comfy::Admin::V2::DashboardController < Comfy::Admin::Cms::BaseController + include AhoyEventsHelper + + before_action :ensure_authority_to_manage_analytics + + def dashboard + @start_date = params[:start_date]&.to_date || Date.today.beginning_of_month + @end_date = params[:end_date]&.to_date || Date.today.end_of_month + date_range = @start_date.beginning_of_day..@end_date.end_of_day + + @visits = Ahoy::Visit.where(started_at: @start_date.beginning_of_day..@end_date.end_of_day) + + Ahoy::Event::EVENT_CATEGORIES.values.each do |event_category| + if event_category == Ahoy::Event::EVENT_CATEGORIES[:page_visit] + events = Ahoy::Event.where(name: 'comfy-cms-page-visit').joins(:visit) + else + events = Ahoy::Event.jsonb_search(:properties, { category: event_category }).joins(:visit) + end + events = events.jsonb_search(:properties, { page_id: params[:page] }) if params[:page].present? + instance_variable_set("@previous_period_#{event_category}_events", events.where(time: previous_period(params[:interval], @start_date, @end_date))) + instance_variable_set("@#{event_category}_events", events.where(time: date_range)) + end + + # legacy and system events does not have category + # separating out 'comfy-cms-page-visit' event since we have a seprate section + @legacy_and_system_events = Ahoy::Event.where.not('properties::jsonb ? :key', key: 'category').where.not(name: 'comfy-cms-page-visit').joins(:visit) + @previous_period_legacy_and_system_events = @legacy_and_system_events.where(time: previous_period(params[:interval], @start_date, @end_date)) + @legacy_and_system_events = @legacy_and_system_events.where(time: date_range) + end + + + private + + def previous_period(interval, start_date, end_date) + today = Date.current + interval = interval || today.strftime('%B %Y') + + case interval + when "#{today.strftime('%B %Y')}" + today.prev_month.beginning_of_month.beginning_of_day..today.prev_month.end_of_month.end_of_day + when "3 months" + (start_date - 3.months).beginning_of_month.beginning_of_day..(start_date - 1.month).end_of_month.end_of_day + when "6 months" + (today - 6.months).beginning_of_month.beginning_of_day..(start_date - 1.month).end_of_month.end_of_day + when "1 year" + (today - 12.months).beginning_of_month.beginning_of_day..(start_date - 1.month).end_of_month.end_of_day + else + + days_diff = (end_date - start_date).to_i + (start_date - (days_diff + 1).days).beginning_of_day..(start_date - 1.day).end_of_day + end + end +end \ No newline at end of file diff --git a/app/helpers/api_forms_helper.rb b/app/helpers/api_forms_helper.rb index 79cbd28ec..51f9f7857 100644 --- a/app/helpers/api_forms_helper.rb +++ b/app/helpers/api_forms_helper.rb @@ -1,11 +1,14 @@ module ApiFormsHelper - def render_form(id) + def render_form(id, options = {}) # usage in cms {{ cms:helper render_form, 1 }} here 1 is the id # usage in rails = render_form @api_form.id @api_form = ApiForm.find_by(id: id) if @api_form @api_namespace = @api_form.api_namespace - render partial: 'comfy/admin/api_forms/render' + data = options['data'] || {} + + data["violet-track-form-submit"] = "true" unless data["violet-track-form-submit"].present? + render partial: 'comfy/admin/api_forms/render', locals: { data: data } end end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index 0237b66b8..d94685df8 100755 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -21,4 +21,120 @@ def session_detail_title user_visit_count = Ahoy::Visit.where(user_id: params[:id]).where('started_at <= ?', @visit.started_at).size "Visit (#{user_visit_count})" end -end + + def page_visit_chart_data(page_visit_events, start_date, end_date) + period, format = split_into(start_date, end_date) + page_visit_events.where.not('ahoy_visits.device_type': nil).group_by { |u| u.visit.device_type }.map do |key, value| + { name: key, data: Ahoy::Event.where(id: value.pluck(:id)).group_by_period(period, :time, range: start_date..end_date, format: format).count } + end + end + + def page_name(page_id) + return 'Website' if page_id.blank? + + Comfy::Cms::Page.find_by(id: page_id)&.label + end + + def visitors_chart_data(visits) + visitors_by_token = visits.group(:visitor_token).count + recurring_visitors = visitors_by_token.values.count { |v| v > 1 } + single_time_visitors = visitors_by_token.keys.count - recurring_visitors + {"Single time visitor": single_time_visitors, "Recurring visitors" => recurring_visitors } + end + + def display_percent_change(current_count, prev_count) + return if prev_count == 0 + + percent_change = percent_change(current_count, prev_count) + + raw("
0 ? 'positive' : 'negative' }\"> 0 ? 'up' : 'down' }\">#{percent_change.round(2).abs} %
") + end + + def tooltip_content(current_count, prev_count, interval, start_date, end_date) + prev_interval = previous_interval(interval, start_date, end_date) + return "There's no data from the previous #{prev_interval} to compare" if prev_count.zero? + + return "There's no change compared the previous #{prev_interval}" if current_count == prev_count + + percent_change = percent_change(current_count, prev_count) + "This is a #{percent_change.round(2).abs} % #{percent_change > 0 ? 'increase': 'decrease'} compared to the previous #{prev_interval}" + end + + def total_watch_time(video_view_events) + video_view_events.sum { |event| event.properties['watch_time'].to_i } + end + + def to_minutes(time_in_milisecond) + "#{number_with_delimiter((time_in_milisecond.to_f / (1000 * 60)).round(2) , :delimiter => ',')} min" + end + + def total_views(video_view_events) + video_view_events.select { |event| event.properties['video_start'] }.size + end + + def avg_view_duration(video_view_events) + total_watch_time(video_view_events).to_f / (total_views(video_view_events).nonzero? || 1) + end + + def avg_view_percentage(video_view_events) + view_percentage_arr = video_view_events.group_by { |event| event.properties['resource_id'] }.map do |_resource_id, events| + (events.sum { |event| event.properties['watch_time'].to_f / event.properties['total_duration'].to_f }) * 100 + end + view_percentage_arr.sum / (total_views(video_view_events).nonzero? || 1) + end + + def top_three_videos(video_view_events, previous_video_view_events) + video_view_events.group_by { |event| event.properties['resource_id'] }.map do |resource_id, events| + previous_period_event = previous_video_view_events.jsonb_search(:properties, { resource_id: resource_id }) + api_resource = ApiResource.find_by(id: resource_id) + { + total_views: total_views(events), + total_watch_time: total_watch_time(events), + previous_period_total_views: total_views(previous_period_event), + previous_period_total_watch_time: total_watch_time(previous_period_event), + resource_title: api_resource&.properties.dig(api_resource&.api_namespace.analytics_metadata&.dig("title")) || "Resource Id: #{resource_id}", + resource_author: api_resource&.properties.dig(api_resource&.api_namespace.analytics_metadata&.dig("author")), + resource_image: api_resource&.non_primitive_properties.find_by(field_type: "file", label: api_resource&.api_namespace.analytics_metadata&.dig("thumbnail"))&.file_url, + resource_id: api_resource&.id, + namespace_id: api_resource&.api_namespace.id, + duration: events.first.properties['total_duration'], + name: events.first.name + } + end.sort_by {|event| event[:total_views]}.reverse.first(3) + end + + private + def split_into(start_date, end_date) + time_in_days = (end_date - start_date).to_i + + if time_in_days <= 1 + [:hour_of_day, '%I %P'] + elsif time_in_days <= 7 + [:day, '%-d %^b %Y'] + elsif time_in_days <= 30 + [:week, '%V'] + elsif time_in_days <= 365 + [:month, '%^b %Y'] + else + [:year, '%Y'] + end + end + + def previous_interval(interval, start_date, end_date) + today = Date.current + interval = interval || today.strftime('%B %Y') + + case interval + when "3 months", "6 months", "1 year" + interval + when "#{today.strftime('%B %Y')}" + "month" + else + "#{ (end_date - start_date).to_i } days" + end + end + + def percent_change(current_count, prev_count) + ((current_count - prev_count).to_f / prev_count) * 100 + end +end \ No newline at end of file diff --git a/app/javascript/packs/violet_analytics.js b/app/javascript/packs/violet_analytics.js new file mode 100644 index 000000000..f9972ef30 --- /dev/null +++ b/app/javascript/packs/violet_analytics.js @@ -0,0 +1,120 @@ +const VIOLET_EVENT_CATEGORIES = { + page_visit: 'page_visit', + click: 'click', + form_submit: 'form_submit', + video_view: 'video_view', + section_view: 'section_view', +} +var analyticsLoaded = false; + + +$(window).on("load turbo:load", () => { + if (analyticsLoaded) { return ; } + + const pageId = $('body').data('page-id'); + const analyticsBrowserStorageKey = `violet_analytics_page_${pageId}`; + + let sectionsViewedMap = {}; + + $('[data-violet-track-section-view="true"]').each(function() { + sectionsViewedMap[this.dataset.violetEventName] = false; + }) + + sessionStorage.setItem(analyticsBrowserStorageKey, JSON.stringify({sectionsViewedMap})); + + $('[data-violet-track-click="true"]').on('click', function() { + const eventName = this.dataset.violetEventName || VIOLET_EVENT_CATEGORIES.click; + const eventLabel = this.dataset.violetEventLabel || eventName; + ahoy.track(eventName, { + category: VIOLET_EVENT_CATEGORIES.click, + label: eventLabel, + page_id: pageId, + tag: this.tagName, + href: this.href + }) + }) + + // Usage: {{ cms:helper render_form, 1, { data: { violet-track-form-submit: true, violet-event-name: 'contact_form_submit', violet-event-label: 'Contact form Submission' } } }} + $('[data-violet-track-form-submit="true"]').on('submit', function() { + const eventName = this.dataset.violetEventName || `${this.dataset.slug}-form-submit`; + const eventLabel = this.dataset.violetEventLabel || `${this.dataset.slug} Form`; + ahoy.track(eventName, { + category: VIOLET_EVENT_CATEGORIES.form_submit, + label: eventLabel, + page_id: pageId, + }) + }) + + // Usage: + // + $('[data-violet-track-video-view="true"]').each( function() { + var startTime; + const eventName = this.dataset.violetEventName || VIOLET_EVENT_CATEGORIES.video_view; + const eventLabel = this.dataset.violetEventLabel || eventName; + var resourceId = this.dataset.violetResourceId; + // Count paused and then played video as a single view. + var isFirstPlay = true; + + $(this).on("play", function() { + if (this.seeking) { return; } + + startTime = Date.now(); + }); + + $(this).on("pause ended", function(e) { + if (this.seeking) { return; } + + var watchTime = Date.now() - startTime; + ahoy.track(eventName, { + category: VIOLET_EVENT_CATEGORIES.video_view, + label: eventLabel, + page_id: pageId, + resource_id: resourceId, + video_start: isFirstPlay, + watch_time: watchTime, + total_duration: this.duration * 1000 + }) + + // replay counts as a new view event + isFirstPlay = e.type == 'ended' + }); + }) + + // track section views + $('[data-violet-track-section-view="true"]').each(function() { + trackSectionViews(this, pageId) + }) + + analyticsLoaded = true; +}) + +$(window).on('beforeunload turbo:before-visit', function() { + // trigger pause event on videos + $('[data-violet-track-video-view="true"]').each( function() { this.pause(); }) + analyticsLoaded = false; +}) + +function trackSectionViews(target, pageId) { + const eventName = target.dataset.violetEventName; + const observerOptions = { root: null, threshold: target.dataset.violetVisibilityThreshold || 0.75 }; + const analyticsBrowserStorageKey = `violet_analytics_page_${pageId}` + + const observer = new IntersectionObserver((entries) => { + const analyticsStorage = JSON.parse(sessionStorage.getItem(analyticsBrowserStorageKey)); + const entry = entries[0]; + const targetHasBeenViewed = analyticsStorage.sectionsViewedMap[eventName]; + if (entry.isIntersecting && !targetHasBeenViewed) { + ahoy.track(eventName, { + category: VIOLET_EVENT_CATEGORIES.section_view, + label: target.dataset.violetEventLabel || eventName, + page_id: pageId + }); + analyticsStorage.sectionsViewedMap[eventName] = true; + sessionStorage.setItem(analyticsBrowserStorageKey, JSON.stringify(analyticsStorage)); + } + }, observerOptions); + + observer.observe(target); +} \ No newline at end of file diff --git a/app/models/ahoy/event.rb b/app/models/ahoy/event.rb index 3eedf28bd..e8a4f62ed 100644 --- a/app/models/ahoy/event.rb +++ b/app/models/ahoy/event.rb @@ -1,5 +1,6 @@ class Ahoy::Event < ApplicationRecord include Ahoy::QueryMethods + include JsonbSearch::Searchable SYSTEM_EVENTS = { 'comfy-blog-page-visit'=> 0, @@ -13,6 +14,14 @@ class Ahoy::Event < ApplicationRecord 'api-resource-create' => 8 } + EVENT_CATEGORIES = { + page_visit: 'page_visit', + click: 'click', + video_view: 'video_view', + form_submit: 'form_submit', + section_view: 'section_view' + } + self.table_name = "ahoy_events" belongs_to :visit @@ -32,6 +41,10 @@ class Ahoy::Event < ApplicationRecord Arel.sql('distinct_name') end + def label + properties["label"] || name + end + def self.delete_specific_events_and_associated_visits(delete_events: false, event_type:) begin ActiveRecord::Base.transaction do diff --git a/app/views/comfy/admin/api_forms/_render.haml b/app/views/comfy/admin/api_forms/_render.haml index 6068b17bd..6c7a09f81 100644 --- a/app/views/comfy/admin/api_forms/_render.haml +++ b/app/views/comfy/admin/api_forms/_render.haml @@ -8,7 +8,7 @@ - recaptcha_keys_set = @api_form.show_recaptcha_v3 && ENV['RECAPTCHA_SITE_KEY_V3'].present? && ENV['RECAPTCHA_SECRET_KEY_V3'].present? unless recaptcha_keys_set - if (@api_form.show_recaptcha.blank? && @api_form.show_recaptcha_v3.blank?) || recaptcha_keys_set - = form_for :data, url: api_namespace_resource_index_path(api_namespace_id: @api_namespace.id), method: :post, html: {class: 'violet-cta-form', 'data-type': 'json', multipart: true, id: "api_form_#{@api_form.object_id}", onsubmit: 'disableForm(this); return checkRequiredRichTextFields()'}, data: { turbo: false, remote: true } do |f| + = form_for :data, url: api_namespace_resource_index_path(api_namespace_id: @api_namespace.id), method: :post, html: {class: 'violet-cta-form', 'data-type': 'json', multipart: true, id: "api_form_#{@api_form.object_id}", onsubmit: 'disableForm(this); return checkRequiredRichTextFields()'}, data: { turbo: false, remote: true, slug: @api_namespace.slug }.merge(data) do |f| = hidden_field_tag :form_id, @api_form.object_id .form-group - properties.each do |key, value| diff --git a/app/views/comfy/admin/api_namespaces/show.html.haml b/app/views/comfy/admin/api_namespaces/show.html.haml index 3f08b754e..ebf447a76 100755 --- a/app/views/comfy/admin/api_namespaces/show.html.haml +++ b/app/views/comfy/admin/api_namespaces/show.html.haml @@ -43,6 +43,9 @@ %li.nav-item %a#social-tab.nav-link{"aria-controls" => "social", "aria-selected" => "false", "data-toggle" => "tab", :href => "#social", :role => "tab"} Social Share Mapping + %li.nav-item + %a#analytics-mapping-tab.nav-link{"aria-controls" => "analytics-mapping", "aria-selected" => "false", "data-toggle" => "tab", :href => "#analytics-mapping", :role => "tab"} + Analytics Mapping .tab-content #interface.tab-pane.fade.show.active{"aria-labelledby" => "interface-tab", :role => "tabpanel"} @@ -197,6 +200,20 @@ = f.select 'api_namespace[social_share_metadata][image]', options_for_select(@image_options,(@api_namespace.social_share_metadata.present? ? @api_namespace.social_share_metadata["image"] : nil)),{include_blank: "Choose an option"}, {class: 'form-control'} .form-group = f.submit "Submit", class: 'btn btn-primary' + + #analytics-mapping.tab-pane.fade{"aria-labelledby" => "analytics-mapping-tab", :role => "tabpanel"} + = form_with(method: :patch , url: analytics_metadata_api_namespace_path(@api_namespace)) do |f| + .form-group + = f.label :title + = f.select 'api_namespace[analytics_metadata][title]', options_for_select(@api_namespace.properties.keys,(@api_namespace.analytics_metadata&.dig('title'))),{include_blank: "Choose an option"}, {class: 'form-control'} + .form-group + = f.label :author + = f.select 'api_namespace[analytics_metadata][author]', options_for_select(@api_namespace.properties.keys,(@api_namespace.analytics_metadata&.dig('author'))),{include_blank: "Choose an option"}, {class: 'form-control'} + .form-group + = f.label :thumbnail + = f.select 'api_namespace[analytics_metadata][thumbnail]', options_for_select(@image_options,(@api_namespace.analytics_metadata&.dig('thumbnail'))),{include_blank: "Choose an option"}, {class: 'form-control'} + .form-group + = f.submit "Submit", class: 'btn btn-primary' -# We show this only if the user has full_access or access related to api-resources - if has_access_to_api_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:read_api_resources_only], current_user, @api_namespace) diff --git a/app/views/comfy/admin/cms/partials/_navigation_inner.haml b/app/views/comfy/admin/cms/partials/_navigation_inner.haml index 45085acb0..043d593e6 100644 --- a/app/views/comfy/admin/cms/partials/_navigation_inner.haml +++ b/app/views/comfy/admin/cms/partials/_navigation_inner.haml @@ -3,7 +3,8 @@ = active_link_to "Email", mailbox_path, class: 'nav-link' = active_link_to "API", api_namespaces_path, class: 'nav-link' = active_link_to "App Settings", edit_web_settings_path, class: 'nav-link' - = active_link_to "Visitor Analytics", dashboard_path, class: 'nav-link' + = active_link_to "Analytics", dashboard_path, class: 'nav-link' + = active_link_to "Analytics V2", v2_dashboard_path, class: 'nav-link' = active_link_to "My Settings", edit_user_registration_path, class: 'nav-link' %li{class: 'nav-item'} %li.nav-item.dropdown diff --git a/app/views/comfy/admin/dashboard/_events_search_filters.haml b/app/views/comfy/admin/dashboard/_events_search_filters.haml index 9081eae9b..b86f68eb5 100644 --- a/app/views/comfy/admin/dashboard/_events_search_filters.haml +++ b/app/views/comfy/admin/dashboard/_events_search_filters.haml @@ -10,7 +10,7 @@ = f.search_field :visit_ip_in, value: params.dig(:q, :visit_ip_in), class: 'form-control' = f.label "IP addresses (exclude)", class: 'col-form-label' = f.search_field :visit_ip_does_not_match_all, value: params.dig(:q, :visit_ip_does_not_match_all), class: 'form-control' - .form-row.p-5 + .form-row.py-4.px-2.py-md-5.px-md-5 .col = f.label "start date", class: 'col-form-label' = f.search_field :visit_started_at_gteq, class: 'form-control', type: 'date', value: params.dig(:q, :visit_started_at_gteq) diff --git a/app/views/comfy/admin/dashboard/events_detail.haml b/app/views/comfy/admin/dashboard/events_detail.haml index 3c0f38a12..583415c8b 100644 --- a/app/views/comfy/admin/dashboard/events_detail.haml +++ b/app/views/comfy/admin/dashboard/events_detail.haml @@ -7,7 +7,7 @@ - unless Ahoy::Event::SYSTEM_EVENTS.keys.include?(params[:ahoy_event_type]) = link_to 'Delete', dashboard_destroy_event_path(ahoy_event_type: params[:ahoy_event_type]), method: :delete, data: { confirm: 'This will delete all such events and its associated visits data. Are you sure you?' }, class: 'btn btn-sm btn-danger' -%main{class: 'm-5'} +%main.my-4.mx-2.my-md-5.mx-md-5 = render partial: 'events_search_filters' %h4.mb-5.font-weight-bold @@ -34,7 +34,7 @@ = geo_chart @events.group('ahoy_visits.country').count, label: "events" .col-12.my-5 - .row + .d-flex.flex-wrap.justify-content-around .mx-5.my-3 %p By device type = pie_chart @events.where.not('ahoy_visits.device_type': nil).group('ahoy_visits.device_type').count, width: "300px", height: "300px", label: "events" diff --git a/app/views/comfy/admin/v2/dashboard/_events.html.haml b/app/views/comfy/admin/v2/dashboard/_events.html.haml new file mode 100644 index 000000000..5e1d2617e --- /dev/null +++ b/app/views/comfy/admin/v2/dashboard/_events.html.haml @@ -0,0 +1,30 @@ +%section.vr-analytics-section.vr-analytics-events + .vr-analytics-section-header + .d-md-flex.align-items-center + .vr-analytics-sub-title + = title + - if events.present? + .d-flex.align-items-center.mt-2.mt-md-0 + .vr-analytics-count + = events.count + = "total #{type}" + .vr-analytics-percent-change + = display_percent_change(events.count, previous_period_events.count) + .vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(events.count, previous_period_events.count, params[:interval], @start_date, @end_date) } + ? + + - if events.present? + .vr-analytics-section-body.d-flex.align-items-center + .vr-analytics-events-grid.row.w-100 + - previous_grouped_events = previous_period_events.group(:name).size + - events.group_by(&:label).each do |key, value| + .vr-analytics-events-grid-item.col.col-12.col-sm-6.col-md-4.col-lg-3.mb-4 + .d-flex.mr-4.align-items-center.mb-2 + .vr-analytics-count-lg + = value.count + .vr-analytics-percent-change + = display_percent_change(value.count, previous_grouped_events[key].to_i) + - if previous_grouped_events[key].to_i == 0 + .vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(value.count, 0, params[:interval], @start_date, @end_date) } + ? + = link_to key, dashboard_events_path(ahoy_event_type: value.first&.name), class: 'vr-analytics-event-label' \ No newline at end of file diff --git a/app/views/comfy/admin/v2/dashboard/dashboard.html.haml b/app/views/comfy/admin/v2/dashboard/dashboard.html.haml new file mode 100644 index 000000000..58801d9b9 --- /dev/null +++ b/app/views/comfy/admin/v2/dashboard/dashboard.html.haml @@ -0,0 +1,255 @@ += javascript_include_tag "//www.google.com/jsapi" +.vr-analytics + .page-header + .row.my-2.justify-content-between.align-items-center.navbar.navbar-expand-md.p-0 + .vr-analytics-title.col.col-6.col-md-3 + Dashboard + .vr-analytics-filter-dropdown-container.col.col-6.d-flex.d-md-none.justify-content-end + %button{"aria-controls" => "navbarSupportedContent", "aria-expanded" => "false", "aria-label" => "Toggle navigation", class:"navbar-toggler", "data-target" => "#filter-collapse", "data-toggle" => "collapse", type: "button"} + %i.fas.fa-filter.mr-2 + Filters + + .vr-analytics-filters.col.col-md-9.justify-content-end.navbar-collapse.collapse{id: 'filter-collapse'} + = form_with(url: v2_dashboard_path, method: :get, html: {id: 'analytics_filter' }) do |f| + .d-md-flex.justify-content-end.mt-3.mt-md-0 + = f.hidden_field :start_date, value: params[:start_date] + = f.hidden_field :end_date, value: params[:end_date] + = f.hidden_field :interval, value: params[:interval] + = f.select :page, options_for_select(Subdomain.current.pages.map {|page| [page.label, page.id] }, params[:page]), { prompt: "All pages"}, { class: 'vr-analytics-filters-pages', onchange: "$(this).closest('form').submit()" } + #reportrange.vr-analytics-filters-ranges.ml-md-4.d-flex.d-md-block.justify-content-between + %span + %i.fa.fa-caret-down.ml-2 + + %main{class: 'my-5'} + %section.row.vr-analytics-section.vr-analytics-page-visit-events + .col{ class: @page_visit_events.present? ? "col-lg-9 mb-4" : "col-12" } + .d-flex.justify-content-between + .vr-analytics-section-header.d-md-flex.justify-content-between.align-items-center + .vr-analytics-sub-title + = "#{page_name(params[:page])} visits" + - if @page_visit_events.present? + .d-flex.justify-content-between.align-items-center.mt-2.mt-md-0 + .vr-analytics-count + = @page_visit_events.count + total visits + .vr-analytics-percent-change + = display_percent_change(@page_visit_events.count, @previous_period_page_visit_events.count) + .vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(@page_visit_events.count, @previous_period_page_visit_events.count, params[:interval], @start_date, @end_date) } + ? + .vr-analytics-section-body + - if @page_visit_events.present? + = column_chart page_visit_chart_data(@page_visit_events, @start_date, @end_date), colors: ["#88DAE3FF", "#D785E3FF", "#F7C85CFF"], library: { plugins: { legend: { position: "top", align: "end", labels: { padding: 24, boxWidth: 8, usePointStyle: true, font: { size: 16 } } } } } + - else + .vr-analytics-blank-states + There are no page visit events within the selected date range. + %br + Use 'data-violet-track-page-visit="true"' attribute on a html attribute to track page visits. + %br + %br + %code + :escaped +
+ %br + :escaped + . + %br + :escaped + . + %br + :escaped +
+ - if @page_visit_events.present? + .col.col-lg-3 + .vr-analytics-card.vr-analytics-page-visit-events-donut-chart + %h5 Website visitors + = pie_chart visitors_chart_data(@visits), colors: ['#F7A47B', '#B5E69A'], donut: true, library: { cutout: 85, plugins: { legend: { position: "bottom", align: 'start', labels: {boxWidth: 8, usePointStyle: true, font: { size: 16 } } } } } + + + %hr.m-0 + = render partial: 'events', locals: { events: @click_events, previous_period_events: @previous_period_click_events, title: 'Clicks', type: 'clickables' } + - unless @click_events.present? + .vr-analytics-blank-states + There are no click events within the selected date range. + %br + Use 'data-violet-track-click="true"' attribute on a html element to track clicks. + %br + %br + %code + :escaped + Test + + %hr.m-0 + + %section.vr-analytics-section + .vr-analytics-section-header + .row.align-items-center + .vr-analytics-sub-title.col.col-12.col-md-2 + Watch time + + - if @video_view_events.present? + .col.col-md-10 + .row + - watch_time = total_watch_time(@video_view_events) + - previous_watch_time = total_watch_time(@previous_period_video_view_events) + .col.col-12.col-sm-4.d-flex.align-items-center.my-3.my-sm-0 + .vr-analytics-count.ml-0 + Total watch time + = to_minutes(watch_time) + .vr-analytics-percent-change + = display_percent_change(watch_time, previous_watch_time) + .vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(watch_time, previous_watch_time, params[:interval], @start_date, @end_date) } + ? + + .col.col-12.col-sm-4.d-flex.align-items-center.mb-3.mb-sm-0 + .vr-analytics-count.ml-0 + Avg. view duration + - view_duration = avg_view_duration(@video_view_events) + - previous_view_duration = avg_view_duration(@previous_period_video_view_events) + = to_minutes(view_duration) + .vr-analytics-percent-change + = display_percent_change(view_duration, previous_view_duration) + .vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(view_duration, previous_view_duration, params[:interval], @start_date, @end_date) } + ? + + .col.col-12.col-sm-4.d-flex.align-items-center.mb-3.mb-sm-0 + .vr-analytics-count.ml-0 + Avg. view percentage + - view_percent = avg_view_percentage(@video_view_events) + - previous_view_percent = avg_view_percentage(@previous_period_video_view_events) + = "#{view_percent.round(2)}%" + .vr-analytics-percent-change + = display_percent_change(view_percent, previous_view_percent) + .vr-analytics-tooltips{ data: { toggle: "tooltip", placement: "right" }, title: tooltip_content(view_percent, previous_view_percent, params[:interval], @start_date, @end_date) } + ? + - if @video_view_events.present? + .vr-analytics-section-body + .vr-analytics-event-label + - top_videos = top_three_videos(@video_view_events, @previous_period_video_view_events) + = "Top #{top_videos.count} videos" + + .row.mt-4 + - top_videos.each do |event| + .col.col-12.col-lg-5.mb-4 + .vr-analytics-card + = link_to '', dashboard_events_path(ahoy_event_type: event[:name]) + .vr-analytics-card-section + .d-flex + .vr-analytics-card-resource-poster-conatiner + %img{src: event[:resource_image]} + .vr-analytics-card-resource-title-duration + .vr-analytics-card-resource-title + = link_to event[:resource_title], api_namespace_resource_path(api_namespace_id: event[:namespace_id], id: event[:resource_id]) + .vr-analytics-card-resource-duration + = Time.at(event[:duration]/1000).strftime("%M:%S") + + %hr + .vr-analytics-card-section + .row + .col.col-6 + .d-flex.align-items-center + .vr-analytics-count-lg + = to_minutes(event[:total_watch_time]) + .vr-analytics-percent-change + = display_percent_change(event[:total_watch_time], event[:previous_period_total_watch_time]) + .vr-analytics-event-label.mt-2 + Total watch time + .col.col-6 + .d-flex.align-items-center + .vr-analytics-count-lg + = event[:total_views] + .vr-analytics-percent-change + = display_percent_change(event[:total_views], event[:previous_period_total_views]) + .vr-analytics-event-label.mt-2 + Total views + - else + .vr-analytics-blank-states + There are no video view events within the selected date range. + %br + Use 'data-violet-track-video-view="true"' attribute on a video element to track views. + %br + %br + %code + :escaped + + + %br + %br + + Please make sure 'data-violet-resource-id' is present and Analytics mapping is populated + + %hr.m-0 + = render partial: 'events', locals: { events: @form_submit_events, previous_period_events: @previous_period_form_submit_events, title: 'Form Submissions', type: 'submitables' } + - unless @form_submit_events.present? + .vr-analytics-blank-states + There are no form submission events within the selected date range. + %br + Use 'data-violet-track-from-submit="true"' attribute on a form element to track form submissions. + %br + %br + %code + :escaped + {{ cms:helper render_form, 1, { data: { violet-track-form-submit: true }}}} + %br + %br + Default event name withh me \#{api_namespace.slug}-form-submit and default event label will be \#{api_namespace.slug} Form Submit, which can be overridden with 'data-violet-event-name' and 'data-violet-event-label' respectively. + %br + %br + OR + %br + %br + %code + :escaped +
+ %br + :escaped + . + %br + :escaped + . + %br + :escaped +
+ + %hr.m-0 + = render partial: 'events', locals: { events: @section_view_events, previous_period_events: @previous_period_section_view_events, title: 'Section Views', type: 'viewables' } + - unless @section_view_events.present? + .vr-analytics-blank-states + There are no section view events within the selected date range. + %br + Use 'data-violet-track-section-view="true"' attribute on a HTML element to track section views. + %br + %br + %code + :escaped +
+ %br + :escaped + . + %br + :escaped + . + %br + :escaped +
+ + %br + %br + Make sure to adjust target visibility threshold for large sections with 'data-violet-visibility-threshold'. + %br + Threshold indicates at what percentage of the target's visibility the tracking should be executed + %br + %a{href: 'https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#thresholds'} + https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#thresholds + + %br + %br + By default, the threshold value is 0.75 + + %hr.m-0 + = render partial: 'events', locals: { events: @legacy_and_system_events, previous_period_events: @previous_period_legacy_and_system_events, title: 'Events', type: 'events' } diff --git a/app/views/layouts/comfy/admin/cms.html.haml b/app/views/layouts/comfy/admin/cms.html.haml index 500f666b1..c68dc7623 100644 --- a/app/views/layouts/comfy/admin/cms.html.haml +++ b/app/views/layouts/comfy/admin/cms.html.haml @@ -9,7 +9,7 @@ -# this is a hack until I figure out how to get chartkick working without importing the whole application js pack - if controller_name == 'dashboard' = javascript_pack_tag 'application', 'data-turbo-track': 'reload' - + = javascript_include_tag 'comfy/admin/cms/dashboard' = render 'layouts/comfy/admin/cms/head' diff --git a/app/views/layouts/comfy/admin/cms/_body.html.haml b/app/views/layouts/comfy/admin/cms/_body.html.haml index a5c9a7ac8..1c9ac9150 100644 --- a/app/views/layouts/comfy/admin/cms/_body.html.haml +++ b/app/views/layouts/comfy/admin/cms/_body.html.haml @@ -3,11 +3,12 @@ .row #cms-left.col-lg-2.bg-dark = render "layouts/comfy/admin/cms/left" - #cms-main.col-lg-8.m-auto + #cms-main{class: "col-lg-#{controller_name == 'dashboard' ? '10 ml-auto' : '8 m-auto'}"} = render "layouts/comfy/admin/cms/flash" = yield = render 'comfy/admin/cms/files/modal' if @site && !@site.new_record? - #cms-right.col-lg-2 - = render 'layouts/comfy/admin/cms/right' + - unless controller_name == 'dashboard' + #cms-right.col-lg-2 + = render 'layouts/comfy/admin/cms/right' = render "layouts/comfy/admin/cms/footer_js" \ No newline at end of file diff --git a/app/views/layouts/website.html.erb b/app/views/layouts/website.html.erb index 5a1298b10..41879f7dc 100644 --- a/app/views/layouts/website.html.erb +++ b/app/views/layouts/website.html.erb @@ -8,6 +8,7 @@ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbo-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %> + <%= javascript_pack_tag 'violet_analytics', 'data-turbo-track': 'reload' %> <% if @cms_site && @cms_layout %> @@ -20,7 +21,7 @@ <%= render partial: 'shared/flash' %> <%= render partial: 'shared/navbars/web_admin' %> - + <%= yield %> <%= render_cookies_consent_ui %> diff --git a/config/initializers/comfortable_mexican_sofa.rb b/config/initializers/comfortable_mexican_sofa.rb index d9923dc34..f331bcb19 100644 --- a/config/initializers/comfortable_mexican_sofa.rb +++ b/config/initializers/comfortable_mexican_sofa.rb @@ -17,7 +17,7 @@ def authenticate visit_id = current_visit ? current_visit.id : nil ahoy.track( "comfy-cms-page-visit", - { visit_id: visit_id, page_id: @cms_page&.id, user_id: user_id } + { visit_id: visit_id, page_id: @cms_page&.id, user_id: user_id, category: Ahoy::Event::EVENT_CATEGORIES[:page_visit] } ) end protected_paths = Comfy::Cms::Page.where(is_restricted: true).pluck(:full_path) diff --git a/config/routes.rb b/config/routes.rb index de422f2e6..235ed2dc5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,6 +20,8 @@ def self.matches?(request) post 'import_api_namespace', to: 'comfy/admin/api_namespaces#import_as_json', as: :import_as_json_api_namespaces + get 'v2/dashboard', to: 'comfy/admin/v2/dashboard#dashboard' + resources :signup_wizard resources :signin_wizard constraints SubdomainConstraint do @@ -62,6 +64,7 @@ def self.matches?(request) get 'export_with_associations_as_json' get 'export_without_associations_as_json' patch 'social_share_metadata' + patch 'analytics_metadata' patch 'api_action_workflow' end diff --git a/db/migrate/20230124014921_add_analytics_metadata_to_api_namespace.rb b/db/migrate/20230124014921_add_analytics_metadata_to_api_namespace.rb new file mode 100644 index 000000000..54280cd79 --- /dev/null +++ b/db/migrate/20230124014921_add_analytics_metadata_to_api_namespace.rb @@ -0,0 +1,5 @@ +class AddAnalyticsMetadataToApiNamespace < ActiveRecord::Migration[6.1] + def change + add_column :api_namespaces, :analytics_metadata, :jsonb + end +end diff --git a/db/schema.rb b/db/schema.rb index 07333326f..6b2ef576c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_01_09_150520) do +ActiveRecord::Schema.define(version: 2023_01_24_014921) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -189,6 +189,7 @@ t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.jsonb "social_share_metadata" + t.jsonb "analytics_metadata" t.index ["properties"], name: "index_api_namespaces_on_properties", opclass: :jsonb_path_ops, using: :gin end diff --git a/package.json b/package.json index 2d189379a..b2e4c1d56 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "@rails/webpacker": "5.2.1", "ahoy.js": "^0.3.8", "bootstrap": "^4.6.0", + "bootstrap-daterangepicker": "^3.1.0", "chart.js": "^3.3.0", "chartkick": "^4.0.4", "jquery": "^3.6.0", + "moment": "^2.29.4", "popper.js": "^1.16.1", "rails-erb-loader": "^5.5.2", "select2": "^4.1.0-rc.0", diff --git a/test/controllers/admin/comfy/api_namespaces_controller_test.rb b/test/controllers/admin/comfy/api_namespaces_controller_test.rb index 1a81b5139..5cb35af06 100755 --- a/test/controllers/admin/comfy/api_namespaces_controller_test.rb +++ b/test/controllers/admin/comfy/api_namespaces_controller_test.rb @@ -243,7 +243,7 @@ class Comfy::Admin::ApiNamespacesControllerTest < ActionDispatch::IntegrationTes stubbed_date = DateTime.new(2022, 1, 1) DateTime.stubs(:now).returns(stubbed_date) get export_api_namespace_url(api_namespace, format: :csv) - expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,#{api_namespace.social_share_metadata}\n" + expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,#{api_namespace.social_share_metadata}\nanalytics_metadata,#{api_namespace.analytics_metadata}\n" assert_response :success assert_equal response.body, expected_csv assert_equal response.header['Content-Disposition'], "attachment; filename=api_namespace_#{api_namespace.id}_#{DateTime.now.to_i}.csv" @@ -2285,7 +2285,7 @@ class Comfy::Admin::ApiNamespacesControllerTest < ActionDispatch::IntegrationTes stubbed_date = DateTime.new(2022, 1, 1) DateTime.stubs(:now).returns(stubbed_date) get export_api_namespace_url(api_namespace, format: :csv) - expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\n" + expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\nanalytics_metadata,\n" assert_response :success assert_equal response.body, expected_csv assert_equal response.header['Content-Disposition'], "attachment; filename=api_namespace_#{api_namespace.id}_#{DateTime.now.to_i}.csv" @@ -2299,7 +2299,7 @@ class Comfy::Admin::ApiNamespacesControllerTest < ActionDispatch::IntegrationTes stubbed_date = DateTime.new(2022, 1, 1) DateTime.stubs(:now).returns(stubbed_date) get export_api_namespace_url(api_namespace, format: :csv) - expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\n" + expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\nanalytics_metadata,\n" assert_response :success assert_equal response.body, expected_csv assert_equal response.header['Content-Disposition'], "attachment; filename=api_namespace_#{api_namespace.id}_#{DateTime.now.to_i}.csv" @@ -2313,7 +2313,7 @@ class Comfy::Admin::ApiNamespacesControllerTest < ActionDispatch::IntegrationTes stubbed_date = DateTime.new(2022, 1, 1) DateTime.stubs(:now).returns(stubbed_date) get export_api_namespace_url(api_namespace, format: :csv) - expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\n" + expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\nanalytics_metadata,\n" assert_response :success assert_equal response.body, expected_csv assert_equal response.header['Content-Disposition'], "attachment; filename=api_namespace_#{api_namespace.id}_#{DateTime.now.to_i}.csv" @@ -2344,7 +2344,7 @@ class Comfy::Admin::ApiNamespacesControllerTest < ActionDispatch::IntegrationTes stubbed_date = DateTime.new(2022, 1, 1) DateTime.stubs(:now).returns(stubbed_date) get export_api_namespace_url(api_namespace, format: :csv) - expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\n" + expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\nanalytics_metadata,\n" assert_response :success assert_equal response.body, expected_csv assert_equal response.header['Content-Disposition'], "attachment; filename=api_namespace_#{api_namespace.id}_#{DateTime.now.to_i}.csv" @@ -2360,7 +2360,7 @@ class Comfy::Admin::ApiNamespacesControllerTest < ActionDispatch::IntegrationTes stubbed_date = DateTime.new(2022, 1, 1) DateTime.stubs(:now).returns(stubbed_date) get export_api_namespace_url(api_namespace, format: :csv) - expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\n" + expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\nanalytics_metadata,\n" assert_response :success assert_equal response.body, expected_csv assert_equal response.header['Content-Disposition'], "attachment; filename=api_namespace_#{api_namespace.id}_#{DateTime.now.to_i}.csv" @@ -2376,7 +2376,7 @@ class Comfy::Admin::ApiNamespacesControllerTest < ActionDispatch::IntegrationTes stubbed_date = DateTime.new(2022, 1, 1) DateTime.stubs(:now).returns(stubbed_date) get export_api_namespace_url(api_namespace, format: :csv) - expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\n" + expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\nanalytics_metadata,\n" assert_response :success assert_equal response.body, expected_csv assert_equal response.header['Content-Disposition'], "attachment; filename=api_namespace_#{api_namespace.id}_#{DateTime.now.to_i}.csv" @@ -2390,7 +2390,7 @@ class Comfy::Admin::ApiNamespacesControllerTest < ActionDispatch::IntegrationTes stubbed_date = DateTime.new(2022, 1, 1) DateTime.stubs(:now).returns(stubbed_date) get export_api_namespace_url(api_namespace, format: :csv) - expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\n" + expected_csv = "id,#{api_namespace.id}\nname,namespace_with_all_types\nslug,namespace_with_all_types\nversion,1\nnull,\narray,\"[\"\"yes\"\", \"\"no\"\"]\"\nnumber,123\nobject,\"{\"\"a\"\"=>\"\"b\"\", \"\"c\"\"=>\"\"d\"\"}\"\nstring,string\nboolean,true\nrequires_authentication,false\nnamespace_type,create-read-update-delete\ncreated_at,#{api_namespace.created_at}\nupdated_at,#{api_namespace.updated_at}\nsocial_share_metadata,\nanalytics_metadata,\n" assert_response :success assert_equal response.body, expected_csv assert_equal response.header['Content-Disposition'], "attachment; filename=api_namespace_#{api_namespace.id}_#{DateTime.now.to_i}.csv" @@ -3029,4 +3029,31 @@ class Comfy::Admin::ApiNamespacesControllerTest < ActionDispatch::IntegrationTes end ######## API Accessibility Tests - END ######### + + # ANALYTICS METADATA + test 'analytics_metadata# should return success when the user has authority to manage analytics' do + @user.update!(can_manage_analytics: true) + payload = {api_namespace: {"analytics_metadata"=>{"title"=>"Array", "author"=>"String", "thumbnail"=>"picto"}}} + sign_in(@user) + + patch analytics_metadata_api_namespace_url(@api_namespace), params: payload + assert_response :redirect + + expected_message = 'Analytics Metadata successfully updated.' + assert_equal expected_message, flash[:notice] + assert_equal payload[:api_namespace]['analytics_metadata'], @api_namespace.reload.analytics_metadata + end + + test 'analytics_metadata# should return error when the user has no authority to manage analytics' do + @user.update!(can_manage_analytics: false) + payload = {api_namespace: {"analytics_metadata"=>{"title"=>"Array", "author"=>"String", "thumbnail"=>"picto"}}} + sign_in(@user) + + patch analytics_metadata_api_namespace_url(@api_namespace), params: payload + assert_response :redirect + + expected_message = 'You do not have the permission to do that. Only users who can_manage_analytics are allowed to perform that action.' + assert_equal expected_message, flash[:alert] + refute_equal payload[:api_namespace]['analytics_metadata'], @api_namespace.reload.analytics_metadata + end end diff --git a/test/controllers/admin/comfy/v2/dashboard_controller_test.rb b/test/controllers/admin/comfy/v2/dashboard_controller_test.rb new file mode 100644 index 000000000..8890699b1 --- /dev/null +++ b/test/controllers/admin/comfy/v2/dashboard_controller_test.rb @@ -0,0 +1,96 @@ +require "test_helper" + +class Comfy::Admin::V2::DashboardControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:public) + @subdomain = subdomains(:public) + @user.update(can_manage_analytics: true) + @page = comfy_cms_pages(:root) + site = Comfy::Cms::Site.first + layout = site.layouts.last + @page_2 = layout.pages.create( + site_id: site.id, + label: 'test-cms-page', + slug: 'test-cms-page', + ) + @api_resource = api_resources(:one) + end + + test "should deny #v2_dashboard if not signed in" do + get v2_dashboard_url + assert_redirected_to new_user_session_url + end + + test "should deny #v2_dashboard if not permissioned" do + sign_in(@user) + @user.update(can_manage_analytics: false) + get v2_dashboard_url + assert_response :redirect + end + + test "should get #v2_dashboard if signed in and permissioned" do + sign_in(@user) + get v2_dashboard_url + assert_response :success + end + + test "#dashboard: should set correct data" do + @subdomain.update!(tracking_enabled: true) + visit = Ahoy::Visit.first + page_visit_event_1 = visit.events.create(name: 'comfy-cms-page-visit', user_id: @user.id, time: (Time.now.beginning_of_month - 4.days), properties: {"label"=>"test_page_view", "page_id"=>@page.id, "category"=>"page_visit", "page_title"=>"lvh.me:5250"}) + click_event_1 = visit.events.create(name: 'test-link-click', user_id: @user.id, time: (Time.now.beginning_of_month - 4.days), properties: {"tag"=>"BUTTON", "label"=>"test link", "page_id"=>@page.id, "category"=>"click"}) + video_watch_event_1 = visit.events.create(name: 'test-video-watched', user_id: @user.id, time: (Time.now.beginning_of_month - 4.days), properties: {"label"=>"watched_this_video", "page_id"=>@page.id, "category"=>"video_view", "watch_time"=>11891, "resource_id"=>@api_resource.id, "video_start"=>true, "total_duration"=>11944.444}) + + page_visit_event_1_page_2 = visit.events.create(name: 'comfy-cms-page-visit', user_id: @user.id, time: Time.now, properties: {"label"=>"test_page_view", "page_id"=>@page_2.id, "category"=>"page_visit", "page_title"=>"lvh.me:5250"}) + click_event_1_page_2 = visit.events.create(name: 'test-link-click', user_id: @user.id, time: Time.now, properties: {"tag"=>"BUTTON", "label"=>"test link", "page_id"=>@page_2.id, "category"=>"click"}) + video_watch_event_1_page_2 = visit.events.create(name: 'test-video-watched', user_id: @user.id, time: Time.now, properties: {"label"=>"watched_this_video", "page_id"=>@page_2.id, "category"=>"video_view", "watch_time"=>11891, "resource_id"=>@api_resource.id, "video_start"=>true, "total_duration"=>11944.444}) + + visit_1 = visit.dup + visit_1.save! + page_visit_event_2 = visit.events.create(name: 'comfy-cms-page-visit', user_id: @user.id, time: (Time.now.beginning_of_month - 4.months), properties: {"label"=>"test_page_view", "page_id"=>@page_2.id, "category"=>"page_visit", "page_title"=>"lvh.me:5250"}) + click_event_2 = visit.events.create(name: 'test-link-click', user_id: @user.id, time: (Time.now.beginning_of_month - 4.months), properties: {"tag"=>"BUTTON", "label"=>"test link", "page_id"=>@page.id, "category"=>"click"}) + video_watch_event_2 = visit.events.create(name: 'test-video-watched', user_id: @user.id, time: (Time.now.beginning_of_month - 4.months), properties: {"label"=>"watched_this_video", "page_id"=>@page.id, "category"=>"video_view", "watch_time"=>11891, "resource_id"=>2, "video_start"=>true, "total_duration"=>11944.444}) + + @user.update(can_manage_analytics: true) + sign_in(@user) + + get v2_dashboard_url + + # default range should be current month and previous period should be last month + assert_equal Date.today.beginning_of_month, assigns(:start_date) + assert_equal Date.today.end_of_month, assigns(:end_date) + + assert_equal [page_visit_event_1_page_2.id], assigns(:page_visit_events).pluck(:id) + assert_equal [click_event_1_page_2.id], assigns(:click_events).pluck(:id) + assert_equal [video_watch_event_1_page_2.id], assigns(:video_view_events).pluck(:id) + + assert_equal [page_visit_event_1.id], assigns(:previous_period_page_visit_events).pluck(:id) + assert_equal [click_event_1.id], assigns(:previous_period_click_events).pluck(:id) + assert_equal [video_watch_event_1.id], assigns(:previous_period_video_view_events).pluck(:id) + + # When range params is present + get v2_dashboard_url, params: {start_date: (Time.now.beginning_of_month - 2.months).strftime('%Y-%m-%d'), end_date: Time.now.end_of_month.strftime('%Y-%m-%d'), interval: "3 months" } + + assert_equal (Time.now.beginning_of_month - 2.months).to_date, assigns(:start_date) + assert_equal Time.now.end_of_month.to_date, assigns(:end_date) + + assert_equal [page_visit_event_1.id, page_visit_event_1_page_2.id].sort, assigns(:page_visit_events).pluck(:id).sort + assert_equal [click_event_1.id, click_event_1_page_2.id].sort, assigns(:click_events).pluck(:id).sort + assert_equal [video_watch_event_1.id, video_watch_event_1_page_2.id].sort, assigns(:video_view_events).pluck(:id).sort + + assert_equal [page_visit_event_2.id], assigns(:previous_period_page_visit_events).pluck(:id) + assert_equal [click_event_2.id], assigns(:previous_period_click_events).pluck(:id) + assert_equal [video_watch_event_2.id], assigns(:previous_period_video_view_events).pluck(:id) + + # When page params present, it should filter by page + get v2_dashboard_url, params: {start_date: (Time.now.beginning_of_month - 2.months).strftime('%Y-%m-%d'), end_date: Time.now.end_of_month.strftime('%Y-%m-%d'), interval: "3 months", page: @page.id } + + assert_equal [page_visit_event_1.id], assigns(:page_visit_events).pluck(:id) + assert_equal [click_event_1.id], assigns(:click_events).pluck(:id) + assert_equal [video_watch_event_1.id], assigns(:video_view_events).pluck(:id) + + assert_empty assigns(:previous_period_page_visit_events) + assert_equal [click_event_2.id], assigns(:previous_period_click_events).pluck(:id) + assert_equal [video_watch_event_2.id], assigns(:previous_period_video_view_events).pluck(:id) + end +end diff --git a/test/helpers/dashboard_helper_test.rb b/test/helpers/dashboard_helper_test.rb index a3d5d8e51..1addc335f 100644 --- a/test/helpers/dashboard_helper_test.rb +++ b/test/helpers/dashboard_helper_test.rb @@ -11,4 +11,143 @@ class DashboardHelperTest < ActionView::TestCase assert_equal "private-system-url-redacted" , redact_private_urls(url_3) assert_equal url_non_redactable , redact_private_urls(url_non_redactable) end + + test 'page_visit_chart_data' do + travel_to Date.new(2023, 01, 26) do + visit = Ahoy::Visit.first + visit.update!(device_type: 'Desktop') + visit.events.create(name: 'test-page-view', user_id: 1, time: Time.now, properties: { + label: "test_page_view", + page_id: 1, + category: "page_visit", + }) + + visit.events.create(name: 'test-page-view', user_id: 1, time: 1.day.ago, properties: { + label: "test_page_view", + page_id: 1, + category: "page_visit", + }) + + visit.events.create(name: 'test-page-view', user_id: 1, time: 2.months.ago, properties: { + label: "test_page_view", + page_id: 1, + category: "page_visit", + }) + + visit.events.create(name: 'test-page-view', user_id: 1, time: 6.months.ago, properties: { + label: "test_page_view", + page_id: 1, + category: "page_visit", + }) + + visit.events.create(name: 'test-page-view', user_id: 1, time: 2.years.ago, properties: { + label: "test_page_view", + page_id: 1, + category: "page_visit", + }) + + # should split into days + assert_equal [{:name=>"Desktop", :data=>{"#{2.days.ago.strftime('%-d %^b %Y')}"=>0, "#{1.days.ago.strftime('%-d %^b %Y')}"=>1, "#{Time.now.strftime('%-d %^b %Y')}"=>1}}], page_visit_chart_data(Ahoy::Event.where(name: 'test-page-view').joins(:visit), 2.days.ago.to_date, Time.now.to_date) + + # should split into weeks + assert_equal [{:name=>"Desktop", :data=>{"#{4.weeks.ago.strftime('%V')}"=>0, "#{3.weeks.ago.strftime('%V')}"=>0, "#{2.weeks.ago.strftime('%V')}"=>0, "#{1.weeks.ago.strftime('%V')}"=>2, "#{Time.now.strftime('%V')}"=>0}}], page_visit_chart_data(Ahoy::Event.where(name: 'test-page-view').joins(:visit), Time.now.beginning_of_month.to_date, Time.now.end_of_month.to_date) + + # should split into months + assert_equal [{:name=>"Desktop", :data=>{"#{2.months.ago.strftime('%^b %Y')}"=>1, "#{1.months.ago.strftime('%^b %Y')}"=>0, "#{Time.now.strftime('%^b %Y')}"=>2}}], page_visit_chart_data(Ahoy::Event.where(name: 'test-page-view').joins(:visit), 2.months.ago.to_date, Time.now.to_date) + + # should split into years + assert_equal [{:name=>"Desktop", :data=>{"#{2.years.ago.strftime('%Y')}"=>1, "#{1.years.ago.strftime('%Y')}"=>2, "#{Time.now.strftime('%Y')}"=>2}}], page_visit_chart_data(Ahoy::Event.where(name: 'test-page-view').joins(:visit), 2.years.ago.to_date, Time.now.to_date) + end + end + + test 'page_name' do + assert_equal 'Website', page_name(nil) + + page = comfy_cms_pages(:root) + assert_equal page.label, page_name(page.id) + end + + test 'display_percent_change' do + refute display_percent_change(100, 0) + + assert_equal "
20.0 %
", display_percent_change(60, 50) + + assert_equal "
16.67 %
", display_percent_change(50, 60) + end + + test 'tooltip_content' do + assert_equal "There's no data from the previous 3 months to compare", tooltip_content(1, 0, '3 months', (Time.now.beginning_of_month - 2.months).to_date, Time.now.end_of_month.to_date) + + assert_equal "This is a 100.0 % increase compared to the previous 6 months", tooltip_content(2, 1, '6 months', (Time.now.beginning_of_month - 5.months).to_date, Time.now.end_of_month.to_date) + + assert_equal "There's no data from the previous month to compare", tooltip_content(1, 0, Date.current.strftime('%B %Y'), Time.now.beginning_of_month.to_date, Time.now.end_of_month.to_date) + + assert_equal "This is a 50.0 % decrease compared to the previous month", tooltip_content(1, 2, Date.current.strftime('%B %Y'), Time.now.beginning_of_month.to_date, Time.now.end_of_month.to_date) + + assert_equal "There's no data from the previous 10 days to compare", tooltip_content(1, 0, 'Custom Interval', (Time.now - 10.days).to_date, Time.now.to_date) + + assert_equal "This is a 100.0 % increase compared to the previous 10 days", tooltip_content(2, 1, 'Custom Range', (Time.now - 10.days).to_date, Time.now.to_date) + end + + test 'total watch time' do + mock_video_view_events + assert_equal 30000, total_watch_time([@video_watch_event_1, @video_watch_event_2]) + end + + test 'to_minutes' do + assert_equal '10,000.0 min', to_minutes(10000*1000*60) + end + + test 'total_views' do + mock_video_view_events + assert_equal 2, total_views([@video_watch_event_1, @video_watch_event_2, @video_watch_event_3]) + end + + test 'avg_view_duration' do + assert_equal 0, avg_view_duration([]) + mock_video_view_events + assert_equal 25000.0, avg_view_duration([@video_watch_event_1, @video_watch_event_2, @video_watch_event_3]) + end + + test 'avg_view_percentange' do + mock_video_view_events + assert_equal 50.0, avg_view_percentage([@video_watch_event_1, @video_watch_event_2, @video_watch_event_3]) + end + + private + + def mock_video_view_events + api_resource = api_resources(:one) + api_resource_2 = api_resources(:two) + visit = Ahoy::Visit.first + @video_watch_event_1 = visit.events.create(name: 'test-video-watched', user_id: 1, time: Time.now, properties: { + label: "watched_this_video", + page_id: 1, + category: "video_view", + watch_time: 10000, + resource_id: api_resource.id, + video_start: true, + total_duration: 40000 + }) + + @video_watch_event_2 = visit.events.create(name: 'test-video-watched', user_id: 1, time: Time.now, properties: { + label: "watched_this_video", + category: "video_view", + page_id: 1, + watch_time: 20000, + resource_id: api_resource_2.id, + video_start: true, + total_duration: 80000 + }) + + @video_watch_event_3 = visit.events.create(name: 'test-video-watched', user_id: 1, time: Time.now, properties: { + label: "watched_this_video", + category: "video_view", + page_id: 1, + watch_time: 20000, + resource_id: api_resource.id, + video_start: false, + total_duration: 40000 + }) + end end \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 022a40ed4..0aff33050 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1567,6 +1567,14 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +bootstrap-daterangepicker@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bootstrap-daterangepicker/-/bootstrap-daterangepicker-3.1.0.tgz#632e6fb2de4b6360c5c0a9d5f6adb9aace051fe8" + integrity sha512-oaQZx6ZBDo/dZNyXGVi2rx5GmFXThyQLAxdtIqjtLlYVaQUfQALl5JZMJJZzyDIX7blfy4ppZPAJ10g8Ma4d/g== + dependencies: + jquery ">=1.10" + moment "^2.9.0" + bootstrap@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7" @@ -4121,6 +4129,11 @@ jest-worker@^26.5.0: merge-stream "^2.0.0" supports-color "^7.0.0" +jquery@>=1.10: + version "3.6.3" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.3.tgz#23ed2ffed8a19e048814f13391a19afcdba160e6" + integrity sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg== + jquery@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" @@ -4667,6 +4680,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment@^2.29.4, moment@^2.9.0: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"