diff --git a/pkg/gav4/analytics.go b/pkg/gav4/analytics.go index 4640234..ab5499a 100644 --- a/pkg/gav4/analytics.go +++ b/pkg/gav4/analytics.go @@ -31,23 +31,28 @@ func (ga *GoogleAnalytics) Query(ctx context.Context, config *setting.Datasource return nil, fmt.Errorf("failed to read query: %w", err) } - if len(queryModel.AccountID) < 1 { - log.DefaultLogger.Error("Query", "error", "Required AccountID") - return nil, fmt.Errorf("Required AccountID") - } - - if len(queryModel.WebPropertyID) < 1 { + if len(queryModel.WebPropertyID) == 0 { log.DefaultLogger.Error("Query", "error", "Required WebPropertyID") return nil, fmt.Errorf("Required WebPropertyID") } + if len(queryModel.Dimensions) == 0 && len(queryModel.Metrics) == 0 { + log.DefaultLogger.Error("Query", "error", "Required Dimensions or Metrics") + return nil, fmt.Errorf("Required Dimensions or Metrics") + } + + if queryModel.Mode == "time series" && len(queryModel.TimeDimension) == 0 { + log.DefaultLogger.Error("Query", "error", "TimeSeries query need TimeDimension") + return nil, fmt.Errorf("TimeSeries query need TimeDimensions") + } + report, err := client.getReport(*queryModel) if err != nil { log.DefaultLogger.Error("Query", "error", err) return nil, err } - return transformReportsResponseToDataFrames(report, queryModel.RefID, queryModel.Timezone) + return transformReportsResponseToDataFrames(report, queryModel.RefID, queryModel.Timezone, queryModel.Mode) } diff --git a/pkg/gav4/client.go b/pkg/gav4/client.go index f440eca..0a90286 100644 --- a/pkg/gav4/client.go +++ b/pkg/gav4/client.go @@ -80,19 +80,20 @@ func (client *GoogleClient) getReport(query model.QueryModel) (*analyticsdata.Ru // Create the DateRange object. {StartDate: query.StartDate, EndDate: query.EndDate}, }, - Metrics: Metrics, - Dimensions: Dimensions, - Offset: offset, - OrderBys: []*analyticsdata.OrderBy{ + Metrics: Metrics, + Dimensions: Dimensions, + Offset: offset, + KeepEmptyRows: true, + } + if len(query.Dimensions) > 0 { + req.OrderBys = []*analyticsdata.OrderBy{ { Dimension: &analyticsdata.DimensionOrderBy{ - DimensionName: query.TimeDimension, + DimensionName: query.Dimensions[0], }, }, - }, - KeepEmptyRows: true, + } } - log.DefaultLogger.Debug("Doing GET request from analytics reporting", "req", req) // Call the BatchGet method and return the response. report, err := client.analyticsdata.Properties.RunReport(query.WebPropertyID, &req).Do() diff --git a/pkg/gav4/grafana.go b/pkg/gav4/grafana.go index 41555b0..c918bd9 100644 --- a/pkg/gav4/grafana.go +++ b/pkg/gav4/grafana.go @@ -75,7 +75,44 @@ func transformReportToDataFrameByDimensions(columns []*model.ColumnDefinition, r } // <--------- primary secondary ---------> -var timeDimensions []string = []string{"dateHourMinute", "gdateHour", "date", "firstSessionDate"} +var timeDimensions []string = []string{"dateHourMinute", "dateHour", "date", "firstSessionDate"} + +func transformReportToDataFramesTableMode(report *analyticsdata.RunReportResponse, refId string, timezone string) ([]*data.Frame, error) { + // return nil,nil + otherDimensions := make([]*analyticsdata.MetricHeader, 0) + for _, dimension := range report.DimensionHeaders { + otherDimensions = append([]*analyticsdata.MetricHeader{ + { + Name: dimension.Name, + Type: "STRING", + }, + }, otherDimensions...) + } + report.MetricHeaders = append(otherDimensions, report.MetricHeaders...) + + for _, row := range report.Rows { + for _, dimensionValue := range row.DimensionValues { + row.MetricValues = append([]*analyticsdata.MetricValue{ + { + Value: dimensionValue.Value, + }, + }, row.MetricValues...) + row.DimensionValues = nil + } + } + + var frames = make([]*data.Frame, 0) + columns := getColumnDefinitions(report.MetricHeaders) + frame, err := transformReportToDataFrameByDimensions(columns, report.Rows, refId, "") + if err != nil { + log.DefaultLogger.Error("transformReportToDataFrameByDimensions", "error", err.Error()) + return nil, err + } + + frames = append(frames, frame) + + return frames, nil +} func transformReportToDataFrames(report *analyticsdata.RunReportResponse, refId string, timezone string) ([]*data.Frame, error) { // return nil,nil @@ -167,10 +204,19 @@ func transformReportToDataFrames(report *analyticsdata.RunReportResponse, refId return frames, nil } -func transformReportsResponseToDataFrames(reportsResponse *analyticsdata.RunReportResponse, refId string, timezone string) (*data.Frames, error) { +func transformReportsResponseToDataFrames(reportsResponse *analyticsdata.RunReportResponse, refId string, timezone string, mode string) (*data.Frames, error) { var frames = make(data.Frames, 0) // for _, report := range reportsResponse.Rows { - frame, err := transformReportToDataFrames(reportsResponse, refId, timezone) + var transformReportToDataFramesFn func(*analyticsdata.RunReportResponse, string, string) ([]*data.Frame, error) + switch mode { + case "time series": + transformReportToDataFramesFn = transformReportToDataFrames + case "table": + transformReportToDataFramesFn = transformReportToDataFramesTableMode + default: + transformReportToDataFramesFn = transformReportToDataFramesTableMode + } + frame, err := transformReportToDataFramesFn(reportsResponse, refId, timezone) if err != nil { return nil, err } diff --git a/pkg/gav4/model.go b/pkg/gav4/model.go index f579a48..a0710d3 100644 --- a/pkg/gav4/model.go +++ b/pkg/gav4/model.go @@ -32,7 +32,9 @@ func GetQueryModel(query backend.DataQuery) (*model.QueryModel, error) { model.StartDate = query.TimeRange.From.In(timezone).Format("2006-01-02") model.EndDate = query.TimeRange.To.In(timezone).Format("2006-01-02") - model.Dimensions = append([]string{model.TimeDimension}, model.Dimensions...) + if model.TimeDimension != "" { + model.Dimensions = append([]string{model.TimeDimension}, model.Dimensions...) + } // model.TimeRange = query.TimeRange // model.MaxDataPoints = query.MaxDataPoints return model, nil diff --git a/pkg/model/models.go b/pkg/model/models.go index f655b34..cb1fd44 100644 --- a/pkg/model/models.go +++ b/pkg/model/models.go @@ -109,6 +109,7 @@ type QueryModel struct { Timezone string `json:"timezone,omitempty"` FiltersExpression string `json:"filtersExpression,omitempty"` Offset int64 `json:"offset,omitempty"` + Mode string `json:"mode,omitempty"` // Not from JSON // TimeRange backend.TimeRange `json:"-"` // MaxDataPoints int64 `json:"-"` diff --git a/src/QueryEditorCommon.tsx b/src/QueryEditorCommon.tsx new file mode 100644 index 0000000..1c31c58 --- /dev/null +++ b/src/QueryEditorCommon.tsx @@ -0,0 +1,26 @@ +import { QueryEditorProps } from '@grafana/data'; +import { DataSource } from 'DataSource'; +import { QueryEditorGA4 } from 'QueryEditorGA4'; +import { QueryEditorUA } from 'QueryEditorUA'; +import React, { PureComponent } from 'react'; +import { GADataSourceOptions, GAQuery } from 'types'; + +type Props = QueryEditorProps; + + +export class QueryEditorCommon extends PureComponent { + constructor(props: Readonly) { + super(props); + this.props.query.version = props.datasource.getGaVersion() + } + render() { + console.log("common") + const { query, datasource, onChange, onRunQuery } = this.props; + const { version } = query + if (version === "v4") { + return + } else { + return + } + } +} diff --git a/src/QueryEditorGA4.tsx b/src/QueryEditorGA4.tsx new file mode 100644 index 0000000..1ea7abe --- /dev/null +++ b/src/QueryEditorGA4.tsx @@ -0,0 +1,279 @@ +import { QueryEditorProps, SelectableValue } from '@grafana/data'; +import { + AsyncMultiSelect, + AsyncSelect, + Badge, + ButtonCascader, + CascaderOption, + HorizontalGroup, + InlineFormLabel, + InlineLabel, + Input, + RadioButtonGroup +} from '@grafana/ui'; +import { DataSource } from 'DataSource'; +import _ from 'lodash'; +import React, { PureComponent } from 'react'; +import { GADataSourceOptions, GAQuery } from 'types'; + +type Props = QueryEditorProps; + +const defaultCacheDuration = 300; +const badgeMap = { + "v4": { + "text": "GA4(alpha)", + "tootip": "experimental support" + }, +} as const +const queryMode = [ + { label: 'Time Series', value: 'time series' }, + { label: 'Table', value: 'table' }, +] as Array>; + + +export class QueryEditorGA4 extends PureComponent { + options: CascaderOption[] = [] + constructor(props: Readonly) { + super(props); + const { query } = this.props + console.log('query.mode', query.mode) + if (!query.hasOwnProperty('cacheDurationSeconds')) { + this.props.query.cacheDurationSeconds = defaultCacheDuration; + } + this.props.query.version = props.datasource.getGaVersion() + this.props.query.displayName = new Map() + this.props.datasource.getAccountSummaries().then((accountSummaries) => { + this.options = accountSummaries + this.props.onChange(this.props.query) + }) + if(query.mode === undefined || query.mode === ''){ + query.mode = 'time series' + } + } + + onMetricChange = (items: Array>) => { + const { query, onChange } = this.props; + + let metrics = [] as string[]; + items.map((item) => { + if (item.value) { + metrics.push(item.value); + } + }); + console.log(`metrics`, metrics); + + onChange({ ...query, selectedMetrics: items, metrics }); + this.willRunQuery(); + }; + + onTimeDimensionChange = (item: any) => { + const { query, onChange } = this.props; + + let timeDimension = item.value; + + console.log(`timeDimension`, timeDimension); + + onChange({ ...query, timeDimension, selectedTimeDimensions: item }); + this.willRunQuery(); + }; + + onIdSelect = (value: string[], selectedOptions: CascaderOption[]) => { + const [account, proerty, profile] = value + const { query, onChange, datasource } = this.props; + datasource.getTimezone(account, proerty, profile).then((timezone) => { + const { query, onChange } = this.props; + console.log(`timezone`, timezone); + onChange({ ...query, timezone }); + this.willRunQuery(); + }); + onChange({ ...query, accountId: account, webPropertyId: proerty, profileId: profile }); + this.willRunQuery(); + }; + + onDimensionChange = (items: Array>) => { + const { query, onChange } = this.props; + let dimensions = [] as string[]; + items.map((item) => { + if (item.value) { + dimensions.push(item.value); + } + }); + + console.log(`dimensions`, dimensions); + + onChange({ ...query, selectedDimensions: items, dimensions }); + this.willRunQuery(); + }; + + onFiltersExpressionChange = (item: any, ...t: any) => { + const { query, onChange } = this.props; + let { filtersExpression } = query; + filtersExpression = item; + + onChange({ ...query, filtersExpression }); + this.willRunQuery(); + }; + + onModeChange = (value: string) => { + const { query, onChange } = this.props; + onChange({ ...query, mode: value }); + this.willRunQuery() + } + + willRunQuery = _.debounce(() => { + const { query, onRunQuery } = this.props; + const { webPropertyId, metrics, timeDimension, mode } = query; + console.log(`willRunQuery`); + console.log(`query`, query); + if (webPropertyId && metrics && (mode === 'table' || timeDimension)) { + console.log(`onRunQuery`); + onRunQuery(); + } + }, 500); + + setDisplayName = (key: string, value = "") => { + const { query: { displayName } } = this.props + displayName.set(key, value) + } + getDisplayName = (key: string) => { + const { query: { displayName } } = this.props + if (displayName.has(key)) { + return displayName.get(key) + } + return "" + } + render() { + const { query, datasource } = this.props; + const { + accountId, + webPropertyId, + selectedTimeDimensions, + selectedMetrics, + selectedDimensions, + timezone, + filtersExpression, + mode + } = query; + console.log('GA4') + console.log('mode', mode) + const parsedWebPropertyId = webPropertyId?.split('/')[1] + return ( + <> +
+
+ + Account Select + {`Account: ${accountId || ""},Property: ${webPropertyId || ""}`} + GA timeZone}> + Timezone + + {timezone ? timezone : 'determined by profileId'} + + +
+ +
+ + The metric ga:* + + } + > + Metrics + + datasource.getMetrics(q, parsedWebPropertyId)} + placeholder={'ga:sessions'} + value={selectedMetrics} + onChange={this.onMetricChange} + backspaceRemovesValue + cacheOptions + noOptionsMessage={'Search Metrics'} + defaultOptions + menuPlacement="bottom" + isClearable + /> + + + The time dimensions At least one ga:date* is required. + + } + > + Time Dimension + + datasource.getTimeDimensions()} + placeholder={'ga:dateHour'} + value={selectedTimeDimensions} + onChange={this.onTimeDimensionChange} + backspaceRemovesValue + cacheOptions + noOptionsMessage={'Search Dimension'} + defaultOptions + menuPlacement="bottom" + isClearable + /> + + + The dimensions exclude time dimensions + + } + > + Dimensions + + datasource.getDimensionsExcludeTimeDimensions(q, parsedWebPropertyId)} + placeholder={'ga:country'} + value={selectedDimensions} + onChange={this.onDimensionChange} + backspaceRemovesValue + cacheOptions + noOptionsMessage={'Search Dimension'} + defaultOptions + menuPlacement="bottom" + isClearable + /> +
+
+ + The filter dimensions and metrics + + } + > + Filters Expressions + + this.onFiltersExpressionChange(e.currentTarget.value)} + placeholder="ga:pagePath==/path/to/page" + /> +
+
+ + Query Mode + + +
+
+ + ); + } +} + diff --git a/src/QueryEditor.tsx b/src/QueryEditorUA.tsx similarity index 99% rename from src/QueryEditor.tsx rename to src/QueryEditorUA.tsx index 2b12b0f..3f7f0e1 100644 --- a/src/QueryEditor.tsx +++ b/src/QueryEditorUA.tsx @@ -29,7 +29,7 @@ const badgeMap = { }, } as const -export class QueryEditor extends PureComponent { +export class QueryEditorUA extends PureComponent { options: CascaderOption[] = [] constructor(props: Readonly) { super(props); @@ -146,6 +146,7 @@ export class QueryEditor extends PureComponent { filtersExpression, version } = query; + console.log('UA') const parsedWebPropertyId = webPropertyId?.split('/')[1] return ( <> diff --git a/src/module.ts b/src/module.ts index 217365a..e692d13 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,9 +1,9 @@ import { DataSourcePlugin } from '@grafana/data'; +import { QueryEditorCommon } from 'QueryEditorCommon'; import { ConfigEditor } from './ConfigEditor'; import { DataSource } from './DataSource'; -import { QueryEditor } from './QueryEditor'; import { GADataSourceOptions, GAQuery } from './types'; export const plugin = new DataSourcePlugin(DataSource) .setConfigEditor(ConfigEditor) - .setQueryEditor(QueryEditor); + .setQueryEditor(QueryEditorCommon); diff --git a/src/types.ts b/src/types.ts index 41dffd2..142c343 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,7 @@ export interface GAQuery extends DataQuery { cacheDurationSeconds?: number; timezone: string; filtersExpression: string; + mode: string; } // mapping on google-key.json export interface JWT {