Skip to content
代码片段 群组 项目
提交 3c860b36 编辑于 作者: Elwyn Benson's avatar Elwyn Benson 提交者: Robert Hunt
浏览文件

Remove extra handling of bad data in SingleStats

Malformed query / response should trigger an error instead of silently
failing and falling back to 0
上级 2e3d57c9
No related branches found
No related tags found
无相关合并请求
...@@ -2,18 +2,19 @@ import { CubejsApi, HttpTransport } from '@cubejs-client/core'; ...@@ -2,18 +2,19 @@ import { CubejsApi, HttpTransport } from '@cubejs-client/core';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { pikadayToString } from '~/lib/utils/datetime_utility'; import { pikadayToString } from '~/lib/utils/datetime_utility';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import {
EVENTS_TABLE_NAME,
SESSIONS_TABLE_NAME,
} from 'ee/analytics/analytics_dashboards/constants';
// This can be any value because the cube proxy adds the real API token. // This can be any value because the cube proxy adds the real API token.
const CUBE_API_TOKEN = '1'; const CUBE_API_TOKEN = '1';
const PRODUCT_ANALYTICS_CUBE_PROXY = '/api/v4/projects/:id/product_analytics/request'; const PRODUCT_ANALYTICS_CUBE_PROXY = '/api/v4/projects/:id/product_analytics/request';
const DEFAULT_COUNT_KEY = 'TrackedEvents.count';
// Filter measurement types must be lowercase // Filter measurement types must be lowercase
export const DATE_RANGE_FILTER_DIMENSIONS = { export const DATE_RANGE_FILTER_DIMENSIONS = {
sessions: 'Sessions.startAt', snowplowtrackedevents: `${EVENTS_TABLE_NAME}.derivedTstamp`,
trackedevents: 'TrackedEvents.utcTime', snowplowsessions: `${SESSIONS_TABLE_NAME}.startAt`,
snowplowtrackedevents: 'SnowplowTrackedEvents.derivedTstamp',
snowplowsessions: 'SnowplowSessions.startAt',
}; };
const convertToCommonChartFormat = (resultSet) => { const convertToCommonChartFormat = (resultSet) => {
...@@ -68,11 +69,7 @@ const convertToSingleValue = (resultSet, query) => { ...@@ -68,11 +69,7 @@ const convertToSingleValue = (resultSet, query) => {
const [measure] = query?.measures ?? []; const [measure] = query?.measures ?? [];
const [row] = resultSet.rawData(); const [row] = resultSet.rawData();
if (!row) { return row[measure] ?? 0;
return 0;
}
return row[measure ?? DEFAULT_COUNT_KEY] ?? Object.values(row)[0] ?? 0;
}; };
const buildDateRangeFilter = (query, queryOverrides, { startDate, endDate }) => { const buildDateRangeFilter = (query, queryOverrides, { startDate, endDate }) => {
......
...@@ -31,16 +31,6 @@ export default { ...@@ -31,16 +31,6 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
instructions() {
return {
install: INSTALL_NPM_PACKAGE,
import: IMPORT_NPM_PACKAGE,
init: INIT_TRACKING,
htmlSetup: HTML_SCRIPT_SETUP,
};
},
},
i18n: { i18n: {
sdkClientsTitle: s__('ProductAnalytics|SDK clients'), sdkClientsTitle: s__('ProductAnalytics|SDK clients'),
sdkHost: s__('ProductAnalytics|SDK host'), sdkHost: s__('ProductAnalytics|SDK host'),
...@@ -68,6 +58,10 @@ export default { ...@@ -68,6 +58,10 @@ export default {
), ),
}, },
BROWSER_SDK_DOCS_URL, BROWSER_SDK_DOCS_URL,
INSTALL_NPM_PACKAGE,
IMPORT_NPM_PACKAGE,
INIT_TRACKING,
HTML_SCRIPT_SETUP,
}; };
</script> </script>
...@@ -80,13 +74,13 @@ export default { ...@@ -80,13 +74,13 @@ export default {
<h5 class="gl-mb-5">{{ $options.i18n.jsModuleTitle }}</h5> <h5 class="gl-mb-5">{{ $options.i18n.jsModuleTitle }}</h5>
<strong class="gl-display-block gl-mb-3">{{ $options.i18n.addNpmPackage }}</strong> <strong class="gl-display-block gl-mb-3">{{ $options.i18n.addNpmPackage }}</strong>
<pre class="gl-mb-5">{{ instructions.install }}</pre> <pre class="gl-mb-5">{{ $options.INSTALL_NPM_PACKAGE }}</pre>
<strong class="gl-display-block gl-mt-5 gl-mb-3">{{ <strong class="gl-display-block gl-mt-5 gl-mb-3">{{
$options.i18n.importNpmPackage $options.i18n.importNpmPackage
}}</strong> }}</strong>
<pre class="gl-mb-5">{{ instructions.import }}</pre> <pre class="gl-mb-5">{{ $options.IMPORT_NPM_PACKAGE }}</pre>
<strong class="gl-display-block gl-mt-5 gl-mb-3">{{ $options.i18n.initNpmPackage }}</strong> <strong class="gl-display-block gl-mt-5 gl-mb-3">{{ $options.i18n.initNpmPackage }}</strong>
<pre class="gl-mb-5"><gl-sprintf :message="instructions.init"> <pre class="gl-mb-5"><gl-sprintf :message="$options.INIT_TRACKING">
<template #appId><span>{{ trackingKey }}</span></template> <template #appId><span>{{ trackingKey }}</span></template>
<template #host><span>{{ collectorHost }}</span></template> <template #host><span>{{ collectorHost }}</span></template>
</gl-sprintf></pre> </gl-sprintf></pre>
...@@ -97,7 +91,7 @@ export default { ...@@ -97,7 +91,7 @@ export default {
<strong class="gl-display-block gl-mb-3">{{ <strong class="gl-display-block gl-mb-3">{{
$options.i18n.htmlScriptTagDescription $options.i18n.htmlScriptTagDescription
}}</strong> }}</strong>
<pre class="gl-mb-5"><gl-sprintf :message="instructions.htmlSetup"> <pre class="gl-mb-5"><gl-sprintf :message="$options.HTML_SCRIPT_SETUP">
<template #appId><span>{{ trackingKey }}</span></template> <template #appId><span>{{ trackingKey }}</span></template>
<template #host><span>{{ collectorHost }}</span></template> <template #host><span>{{ collectorHost }}</span></template>
</gl-sprintf></pre> </gl-sprintf></pre>
......
import { CubejsApi, HttpTransport, __setMockLoad } from '@cubejs-client/core'; import { CubejsApi, HttpTransport, __setMockLoad } from '@cubejs-client/core';
import { fetch } from 'ee/analytics/analytics_dashboards/data_sources/cube_analytics'; import { fetch } from 'ee/analytics/analytics_dashboards/data_sources/cube_analytics';
import { pikadayToString } from '~/lib/utils/datetime_utility'; import { pikadayToString } from '~/lib/utils/datetime_utility';
import { mockResultSet, mockFilters, mockTableWithLinksResultSet } from '../../mock_data'; import {
mockResultSet,
mockFilters,
mockTableWithLinksResultSet,
mockResultSetWithNullValues,
} from '../../mock_data';
const mockLoad = jest.fn().mockImplementation(() => mockResultSet); const mockLoad = jest.fn().mockImplementation(() => mockResultSet);
...@@ -48,97 +53,103 @@ describe('Cube Analytics Data Source', () => { ...@@ -48,97 +53,103 @@ describe('Cube Analytics Data Source', () => {
}); });
describe('formats the data', () => { describe('formats the data', () => {
it('returns the expected data format for line charts', async () => { describe('charts', () => {
const result = await fetch({ projectId, visualizationType, query }); it('returns the expected data format for line charts', async () => {
const result = await fetch({ projectId, visualizationType, query });
expect(result[0]).toMatchObject({
data: [ expect(result[0]).toMatchObject({
['2022-11-09T00:00:00.000', 55], data: [
['2022-11-10T00:00:00.000', 14], ['2022-11-09T00:00:00.000', 55],
], ['2022-11-10T00:00:00.000', 14],
name: 'pageview, SnowplowTrackedEvents Count', ],
name: 'pageview, SnowplowTrackedEvents Count',
});
}); });
});
it('returns the expected data format for column charts', async () => { it('returns the expected data format for column charts', async () => {
const result = await fetch({ projectId, visualizationType: 'ColumnChart', query }); const result = await fetch({ projectId, visualizationType: 'ColumnChart', query });
expect(result[0]).toMatchObject({ expect(result[0]).toMatchObject({
data: [ data: [
['2022-11-09T00:00:00.000', 55], ['2022-11-09T00:00:00.000', 55],
['2022-11-10T00:00:00.000', 14], ['2022-11-10T00:00:00.000', 14],
], ],
name: 'pageview, SnowplowTrackedEvents Count', name: 'pageview, SnowplowTrackedEvents Count',
});
}); });
}); });
it('returns the expected data format for data tables', async () => { describe('data tables', () => {
const result = await fetch({ projectId, visualizationType: 'DataTable', query }); it('returns the expected data format for', async () => {
const result = await fetch({ projectId, visualizationType: 'DataTable', query });
expect(result[0]).toMatchObject({ expect(result[0]).toMatchObject({
count: '55', count: '55',
event_type: 'pageview', event_type: 'pageview',
utc_time: '2022-11-09T00:00:00.000', utc_time: '2022-11-09T00:00:00.000',
});
}); });
});
it('returns the expected data format for data tables when links config is defined', async () => {
mockLoad.mockImplementationOnce(() => mockTableWithLinksResultSet);
const result = await fetch({ it('returns the expected data format when links config is defined', async () => {
projectId, mockLoad.mockImplementationOnce(() => mockTableWithLinksResultSet);
visualizationType: 'DataTable',
query: { const result = await fetch({
measures: ['SnowplowTrackedEvents.pageViewsCount'], projectId,
dimensions: ['SnowplowTrackedEvents.docPath', 'SnowplowTrackedEvents.url'], visualizationType: 'DataTable',
}, query: {
visualizationOptions: { measures: ['SnowplowTrackedEvents.pageViewsCount'],
links: [ dimensions: ['SnowplowTrackedEvents.docPath', 'SnowplowTrackedEvents.url'],
{ },
text: 'SnowplowTrackedEvents.docPath', visualizationOptions: {
href: 'SnowplowTrackedEvents.url', links: [
}, {
], text: 'SnowplowTrackedEvents.docPath',
}, href: 'SnowplowTrackedEvents.url',
}); },
],
expect(result[0]).toMatchObject({ },
page_views_count: '1', });
doc_path: {
text: '/foo', expect(result[0]).toMatchObject({
href: 'https://example.com/foo', page_views_count: '1',
}, doc_path: {
text: '/foo',
href: 'https://example.com/foo',
},
});
}); });
}); });
it('returns the expected data format for single stats', async () => { describe('single stats', () => {
const result = await fetch({ projectId, visualizationType: 'SingleStat', query }); it('returns the expected data format', async () => {
const result = await fetch({ projectId, visualizationType: 'SingleStat', query });
expect(result).toBe('36'); expect(result).toBe('36');
});
it('returns the expected data format for single stats with custom measure', async () => {
const override = { measures: ['SnowplowTrackedEvents.url'] };
const result = await fetch({
projectId,
visualizationType: 'SingleStat',
query,
queryOverrides: override,
}); });
expect(result).toBe('https://example.com/us'); it('returns the expected data format with custom measure', async () => {
}); const override = { measures: ['SnowplowTrackedEvents.url'] };
const result = await fetch({
projectId,
visualizationType: 'SingleStat',
query,
queryOverrides: override,
});
it('returns the expected data format for single stats when the measure is unknown', async () => { expect(result).toBe('https://example.com/us');
const override = { measures: ['unknown'] };
const result = await fetch({
projectId,
visualizationType: 'SingleStat',
query,
queryOverrides: override,
}); });
expect(result).toBe('en-US'); it('returns 0 when the measure is null', async () => {
mockLoad.mockImplementationOnce(() => mockResultSetWithNullValues);
const result = await fetch({
projectId,
visualizationType: 'SingleStat',
query,
});
expect(result).toBe(0);
});
}); });
}); });
}); });
......
...@@ -306,6 +306,16 @@ export const mockTableWithLinksResultSet = { ...@@ -306,6 +306,16 @@ export const mockTableWithLinksResultSet = {
], ],
}; };
export const mockResultSetWithNullValues = {
rawData: () => [
{
'SnowplowTrackedEvents.userLanguage': null,
'SnowplowTrackedEvents.count': null,
'SnowplowTrackedEvents.url': null,
},
],
};
export const mockFilters = { export const mockFilters = {
startDate: new Date('2015-01-01'), startDate: new Date('2015-01-01'),
endDate: new Date('2016-01-01'), endDate: new Date('2016-01-01'),
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册