Add graphs page to Product Analytics

Show few sample graphs for users based on analytics
events of last 30 days
Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parent 395ea91e
import Vue from 'vue';
import ActivityChart from './components/activity_chart.vue';
export default () => {
const containers = document.querySelectorAll('.js-project-analytics-chart');
if (!containers) {
return false;
}
return containers.forEach(container => {
const { chartData } = container.dataset;
const formattedData = JSON.parse(chartData);
return new Vue({
el: container,
provide: {
formattedData,
},
components: {
ActivityChart,
},
render(createElement) {
return createElement('activity-chart');
},
});
});
};
<script>
import { s__ } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
export default {
i18n: {
noDataMsg: s__(
'ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already.',
),
},
components: {
GlColumnChart,
},
inject: {
formattedData: {
default: {},
},
},
computed: {
seriesData() {
return {
full: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
};
},
},
};
</script>
<template>
<div class="gl-xs-w-full">
<gl-column-chart
v-if="formattedData.keys"
:data="seriesData"
:x-axis-title="__('Value')"
:y-axis-title="__('Number of events')"
:x-axis-type="'category'"
/>
<p v-else data-testid="noActivityChartData">
{{ $options.i18n.noDataMsg }}
</p>
</div>
</template>
import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle';
document.addEventListener('DOMContentLoaded', () => initActivityCharts());
...@@ -16,6 +16,19 @@ class Projects::ProductAnalyticsController < Projects::ApplicationController ...@@ -16,6 +16,19 @@ class Projects::ProductAnalyticsController < Projects::ApplicationController
@event = product_analytics_events.try(:first) @event = product_analytics_events.try(:first)
end end
def graphs
@graphs = []
@timerange = 30
requested_graphs = %w(platform os_timezone br_lang doc_charset)
requested_graphs.each do |graph|
@graphs << ProductAnalytics::BuildGraphService
.new(project, { graph: graph, timerange: @timerange })
.execute
end
end
private private
def product_analytics_events def product_analytics_events
......
...@@ -20,6 +20,10 @@ class ProductAnalyticsEvent < ApplicationRecord ...@@ -20,6 +20,10 @@ class ProductAnalyticsEvent < ApplicationRecord
where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1) where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1)
} }
def self.count_by_graph(graph, days)
group(graph).timerange(days).count
end
def as_json_wo_empty def as_json_wo_empty
as_json.compact as_json.compact
end end
......
# frozen_string_literal: true
module ProductAnalytics
class BuildGraphService
def initialize(project, params)
@project = project
@params = params
end
def execute
graph = @params[:graph].to_sym
timerange = @params[:timerange].days
results = product_analytics_events.count_by_graph(graph, timerange)
{
id: graph,
keys: results.keys,
values: results.values
}
end
private
def product_analytics_events
@project.product_analytics_events
end
end
end
- graph = local_assigns.fetch(:graph)
%h3
= graph[:id]
.js-project-analytics-chart{ "data-chart-data": graph.to_json, "data-chart-id": graph[:id] }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
%ul.nav-links %ul.nav-links
= nav_link(path: 'product_analytics#index') do = nav_link(path: 'product_analytics#index') do
= link_to _('Events'), project_product_analytics_path(@project) = link_to _('Events'), project_product_analytics_path(@project)
= nav_link(path: 'product_analytics#graphs') do
= link_to 'Graphs', graphs_project_product_analytics_path(@project)
= nav_link(path: 'product_analytics#test') do = nav_link(path: 'product_analytics#test') do
= link_to _('Test'), test_project_product_analytics_path(@project) = link_to _('Test'), test_project_product_analytics_path(@project)
= nav_link(path: 'product_analytics#setup') do = nav_link(path: 'product_analytics#setup') do
......
- page_title _('Product Analytics')
= render 'links'
%p
= _('Showing graphs based on events of the last %{timerange} days.') % { timerange: @timerange }
- @graphs.each_slice(2) do |pair|
.row.append-bottom-10
- pair.each do |graph|
.col-md-6{ id: graph[:id] }
= render 'graph', graph: graph
- page_title 'Product Analytics' - page_title _('Product Analytics')
= render 'links' = render 'links'
......
= render "links" - page_title _('Product Analytics')
= render 'links'
%p %p
= _('Copy the code below to implement tracking in your application:') = _('Copy the code below to implement tracking in your application:')
......
- page_title _('Product Analytics')
= render 'links' = render 'links'
%p %p
......
...@@ -310,6 +310,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -310,6 +310,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
collection do collection do
get :setup get :setup
get :test get :test
get :graphs
end end
end end
......
...@@ -16406,6 +16406,9 @@ msgstr "" ...@@ -16406,6 +16406,9 @@ msgstr ""
msgid "Number of employees" msgid "Number of employees"
msgstr "" msgstr ""
msgid "Number of events"
msgstr ""
msgid "Number of events for this project: %{total_count}." msgid "Number of events for this project: %{total_count}."
msgstr "" msgstr ""
...@@ -17926,6 +17929,9 @@ msgstr "" ...@@ -17926,6 +17929,9 @@ msgstr ""
msgid "Product Analytics" msgid "Product Analytics"
msgstr "" msgstr ""
msgid "ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already."
msgstr ""
msgid "Productivity" msgid "Productivity"
msgstr "" msgstr ""
...@@ -22003,6 +22009,9 @@ msgstr "" ...@@ -22003,6 +22009,9 @@ msgstr ""
msgid "Showing all issues" msgid "Showing all issues"
msgstr "" msgstr ""
msgid "Showing graphs based on events of the last %{timerange} days."
msgstr ""
msgid "Showing last %{size} of log -" msgid "Showing last %{size} of log -"
msgstr "" msgstr ""
......
...@@ -66,6 +66,27 @@ RSpec.describe Projects::ProductAnalyticsController do ...@@ -66,6 +66,27 @@ RSpec.describe Projects::ProductAnalyticsController do
end end
end end
describe 'GET #graphs' do
it 'renders graphs with 200 status code' do
get :graphs, params: project_params
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:graphs)
end
context 'feature flag disabled' do
before do
stub_feature_flags(product_analytics: false)
end
it 'returns not found' do
get :graphs, params: project_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
private private
def project_params(opts = {}) def project_params(opts = {})
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Product Analytics > Graphs' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
before do
project.add_maintainer(user)
sign_in(user)
end
it 'shows graphs', :js do
create(:product_analytics_event, project: project)
visit(graphs_project_product_analytics_path(project))
expect(page).to have_content('Showing graphs based on events')
expect(page).to have_content('platform')
expect(page).to have_content('os_timezone')
expect(page).to have_content('br_lang')
expect(page).to have_content('doc_charset')
end
end
import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import ActivityChart from '~/analytics/product_analytics/components/activity_chart.vue';
describe('Activity Chart Bundle', () => {
let wrapper;
function mountComponent({ provide }) {
wrapper = shallowMount(ActivityChart, {
provide: {
formattedData: {},
...provide,
},
});
}
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findChart = () => wrapper.find(GlColumnChart);
const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]');
describe('Activity Chart', () => {
it('renders an warning message with no data', () => {
mountComponent({ provide: { formattedData: {} } });
expect(findNoData().exists()).toBe(true);
});
it('renders a chart with data', () => {
mountComponent({
provide: { formattedData: { keys: ['key1', 'key2'], values: [5038, 2241] } },
});
expect(findNoData().exists()).toBe(false);
expect(findChart().exists()).toBe(true);
});
});
});
...@@ -21,4 +21,18 @@ RSpec.describe ProductAnalyticsEvent, type: :model do ...@@ -21,4 +21,18 @@ RSpec.describe ProductAnalyticsEvent, type: :model do
it { expect(described_class.timerange(7.days)).to match_array([event_1, event_2]) } it { expect(described_class.timerange(7.days)).to match_array([event_1, event_2]) }
it { expect(described_class.timerange(30.days)).to match_array([event_1, event_2, event_3]) } it { expect(described_class.timerange(30.days)).to match_array([event_1, event_2, event_3]) }
end end
describe '.count_by_graph' do
let_it_be(:events) do
[
create(:product_analytics_event, platform: 'web'),
create(:product_analytics_event, platform: 'web'),
create(:product_analytics_event, platform: 'app'),
create(:product_analytics_event, platform: 'mobile', collector_tstamp: Time.zone.now - 10.days)
]
end
it { expect(described_class.count_by_graph('platform', 7.days)).to eq({ 'app' => 1, 'web' => 2 }) }
it { expect(described_class.count_by_graph('platform', 30.days)).to eq({ 'app' => 1, 'mobile' => 1, 'web' => 2 }) }
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ProductAnalytics::BuildGraphService do
let_it_be(:project) { create(:project) }
let_it_be(:events) do
[
create(:product_analytics_event, project: project, platform: 'web'),
create(:product_analytics_event, project: project, platform: 'web'),
create(:product_analytics_event, project: project, platform: 'app'),
create(:product_analytics_event, project: project, platform: 'mobile'),
create(:product_analytics_event, project: project, platform: 'mobile', collector_tstamp: Time.zone.now - 60.days)
]
end
let(:params) { { graph: 'platform', timerange: 5 } }
subject { described_class.new(project, params).execute }
it 'returns a valid graph hash' do
expect(subject[:id]).to eq(:platform)
expect(subject[:keys]).to eq(%w(app mobile web))
expect(subject[:values]).to eq([1, 1, 2])
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment