mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(Google Analytics Node): Overhaul for google analytics node
This commit is contained in:
parent
e810966a3b
commit
736e700902
|
@ -1,299 +1,25 @@
|
||||||
import { IExecuteFunctions } from 'n8n-core';
|
import { INodeTypeBaseDescription, IVersionedNodeType, VersionedNodeType } from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import { GoogleAnalyticsV1 } from './v1/GoogleAnalyticsV1.node';
|
||||||
IDataObject,
|
import { GoogleAnalyticsV2 } from './v2/GoogleAnalyticsV2.node';
|
||||||
ILoadOptionsFunctions,
|
|
||||||
INodeExecutionData,
|
|
||||||
INodePropertyOptions,
|
|
||||||
INodeType,
|
|
||||||
INodeTypeDescription,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { reportFields, reportOperations } from './ReportDescription';
|
export class GoogleAnalytics extends VersionedNodeType {
|
||||||
|
constructor() {
|
||||||
import { userActivityFields, userActivityOperations } from './UserActivityDescription';
|
const baseDescription: INodeTypeBaseDescription = {
|
||||||
|
|
||||||
import { googleApiRequest, googleApiRequestAllItems, merge, simplify } from './GenericFunctions';
|
|
||||||
|
|
||||||
import moment from 'moment-timezone';
|
|
||||||
|
|
||||||
import { IData } from './Interfaces';
|
|
||||||
|
|
||||||
export class GoogleAnalytics implements INodeType {
|
|
||||||
description: INodeTypeDescription = {
|
|
||||||
displayName: 'Google Analytics',
|
displayName: 'Google Analytics',
|
||||||
name: 'googleAnalytics',
|
name: 'googleAnalytics',
|
||||||
icon: 'file:analytics.svg',
|
icon: 'file:analytics.svg',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Use the Google Analytics API',
|
description: 'Use the Google Analytics API',
|
||||||
defaults: {
|
defaultVersion: 2,
|
||||||
name: 'Google Analytics',
|
|
||||||
},
|
|
||||||
inputs: ['main'],
|
|
||||||
outputs: ['main'],
|
|
||||||
credentials: [
|
|
||||||
{
|
|
||||||
name: 'googleAnalyticsOAuth2',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
displayName: 'Resource',
|
|
||||||
name: 'resource',
|
|
||||||
type: 'options',
|
|
||||||
noDataExpression: true,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Report',
|
|
||||||
value: 'report',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'User Activity',
|
|
||||||
value: 'userActivity',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'report',
|
|
||||||
},
|
|
||||||
//-------------------------------
|
|
||||||
// Reports Operations
|
|
||||||
//-------------------------------
|
|
||||||
...reportOperations,
|
|
||||||
...reportFields,
|
|
||||||
|
|
||||||
//-------------------------------
|
|
||||||
// User Activity Operations
|
|
||||||
//-------------------------------
|
|
||||||
...userActivityOperations,
|
|
||||||
...userActivityFields,
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
methods = {
|
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||||
loadOptions: {
|
1: new GoogleAnalyticsV1(baseDescription),
|
||||||
// Get all the dimensions to display them to user so that he can
|
2: new GoogleAnalyticsV2(baseDescription),
|
||||||
// select them easily
|
|
||||||
async getDimensions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
|
||||||
const returnData: INodePropertyOptions[] = [];
|
|
||||||
const { items: dimensions } = await googleApiRequest.call(
|
|
||||||
this,
|
|
||||||
'GET',
|
|
||||||
'',
|
|
||||||
{},
|
|
||||||
{},
|
|
||||||
'https://www.googleapis.com/analytics/v3/metadata/ga/columns',
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const dimesion of dimensions) {
|
|
||||||
if (
|
|
||||||
dimesion.attributes.type === 'DIMENSION' &&
|
|
||||||
dimesion.attributes.status !== 'DEPRECATED'
|
|
||||||
) {
|
|
||||||
returnData.push({
|
|
||||||
name: dimesion.attributes.uiName,
|
|
||||||
value: dimesion.id,
|
|
||||||
description: dimesion.attributes.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
returnData.sort((a, b) => {
|
|
||||||
const aName = a.name.toLowerCase();
|
|
||||||
const bName = b.name.toLowerCase();
|
|
||||||
if (aName < bName) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (aName > bName) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return returnData;
|
|
||||||
},
|
|
||||||
// Get all the views to display them to user so that he can
|
|
||||||
// select them easily
|
|
||||||
async getViews(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
|
||||||
const returnData: INodePropertyOptions[] = [];
|
|
||||||
const { items } = await googleApiRequest.call(
|
|
||||||
this,
|
|
||||||
'GET',
|
|
||||||
'',
|
|
||||||
{},
|
|
||||||
{},
|
|
||||||
'https://www.googleapis.com/analytics/v3/management/accounts/~all/webproperties/~all/profiles',
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
returnData.push({
|
|
||||||
name: item.name,
|
|
||||||
value: item.id,
|
|
||||||
description: item.websiteUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return returnData;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
super(nodeVersions, baseDescription);
|
||||||
const items = this.getInputData();
|
|
||||||
const returnData: INodeExecutionData[] = [];
|
|
||||||
const resource = this.getNodeParameter('resource', 0);
|
|
||||||
const operation = this.getNodeParameter('operation', 0);
|
|
||||||
|
|
||||||
let method = '';
|
|
||||||
const qs: IDataObject = {};
|
|
||||||
let endpoint = '';
|
|
||||||
let responseData;
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
try {
|
|
||||||
if (resource === 'report') {
|
|
||||||
if (operation === 'get') {
|
|
||||||
//https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet
|
|
||||||
method = 'POST';
|
|
||||||
endpoint = '/v4/reports:batchGet';
|
|
||||||
const viewId = this.getNodeParameter('viewId', i) as string;
|
|
||||||
const returnAll = this.getNodeParameter('returnAll', 0);
|
|
||||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
|
||||||
const simple = this.getNodeParameter('simple', i) as boolean;
|
|
||||||
|
|
||||||
const body: IData = {
|
|
||||||
viewId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (additionalFields.useResourceQuotas) {
|
|
||||||
qs.useResourceQuotas = additionalFields.useResourceQuotas;
|
|
||||||
}
|
|
||||||
if (additionalFields.dateRangesUi) {
|
|
||||||
const dateValues = (additionalFields.dateRangesUi as IDataObject)
|
|
||||||
.dateRanges as IDataObject;
|
|
||||||
if (dateValues) {
|
|
||||||
const start = dateValues.startDate as string;
|
|
||||||
const end = dateValues.endDate as string;
|
|
||||||
Object.assign(body, {
|
|
||||||
dateRanges: [
|
|
||||||
{
|
|
||||||
startDate: moment(start).utc().format('YYYY-MM-DD'),
|
|
||||||
endDate: moment(end).utc().format('YYYY-MM-DD'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (additionalFields.metricsUi) {
|
|
||||||
const metrics = (additionalFields.metricsUi as IDataObject)
|
|
||||||
.metricValues as IDataObject[];
|
|
||||||
body.metrics = metrics;
|
|
||||||
}
|
|
||||||
if (additionalFields.dimensionUi) {
|
|
||||||
const dimensions = (additionalFields.dimensionUi as IDataObject)
|
|
||||||
.dimensionValues as IDataObject[];
|
|
||||||
if (dimensions) {
|
|
||||||
body.dimensions = dimensions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (additionalFields.dimensionFiltersUi) {
|
|
||||||
const dimensionFilters = (additionalFields.dimensionFiltersUi as IDataObject)
|
|
||||||
.filterValues as IDataObject[];
|
|
||||||
if (dimensionFilters) {
|
|
||||||
dimensionFilters.forEach((filter) => (filter.expressions = [filter.expressions]));
|
|
||||||
body.dimensionFilterClauses = { filters: dimensionFilters };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (additionalFields.includeEmptyRows) {
|
|
||||||
Object.assign(body, { includeEmptyRows: additionalFields.includeEmptyRows });
|
|
||||||
}
|
|
||||||
if (additionalFields.hideTotals) {
|
|
||||||
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
|
||||||
}
|
|
||||||
if (additionalFields.hideValueRanges) {
|
|
||||||
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (returnAll) {
|
|
||||||
responseData = await googleApiRequestAllItems.call(
|
|
||||||
this,
|
|
||||||
'reports',
|
|
||||||
method,
|
|
||||||
endpoint,
|
|
||||||
{ reportRequests: [body] },
|
|
||||||
qs,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
responseData = await googleApiRequest.call(
|
|
||||||
this,
|
|
||||||
method,
|
|
||||||
endpoint,
|
|
||||||
{ reportRequests: [body] },
|
|
||||||
qs,
|
|
||||||
);
|
|
||||||
responseData = responseData.reports;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (simple) {
|
|
||||||
responseData = simplify(responseData);
|
|
||||||
} else if (returnAll && responseData.length > 1) {
|
|
||||||
responseData = merge(responseData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (resource === 'userActivity') {
|
|
||||||
if (operation === 'search') {
|
|
||||||
//https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/userActivity/search
|
|
||||||
method = 'POST';
|
|
||||||
endpoint = '/v4/userActivity:search';
|
|
||||||
const viewId = this.getNodeParameter('viewId', i);
|
|
||||||
const userId = this.getNodeParameter('userId', i);
|
|
||||||
const returnAll = this.getNodeParameter('returnAll', 0);
|
|
||||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
|
||||||
const body: IDataObject = {
|
|
||||||
viewId,
|
|
||||||
user: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (additionalFields.activityTypes) {
|
|
||||||
Object.assign(body, { activityTypes: additionalFields.activityTypes });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (returnAll) {
|
|
||||||
responseData = await googleApiRequestAllItems.call(
|
|
||||||
this,
|
|
||||||
'sessions',
|
|
||||||
method,
|
|
||||||
endpoint,
|
|
||||||
body,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
body.pageSize = this.getNodeParameter('limit', 0);
|
|
||||||
responseData = await googleApiRequest.call(this, method, endpoint, body);
|
|
||||||
responseData = responseData.sessions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const executionData = this.helpers.constructExecutionMetaData(
|
|
||||||
this.helpers.returnJsonArray(responseData),
|
|
||||||
{ itemData: { item: i } },
|
|
||||||
);
|
|
||||||
returnData.push(...executionData);
|
|
||||||
} catch (error) {
|
|
||||||
if (this.continueOnFail()) {
|
|
||||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
|
||||||
this.helpers.returnJsonArray({ error: error.message }),
|
|
||||||
{ itemData: { item: i } },
|
|
||||||
);
|
|
||||||
returnData.push(...executionErrorData);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.prepareOutputData(returnData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import { OptionsWithUri } from 'request';
|
import { OptionsWithUri } from 'request';
|
||||||
|
|
||||||
import { IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions } from 'n8n-core';
|
import { IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions } from 'n8n-core';
|
||||||
|
|
||||||
import { IDataObject, NodeApiError } from 'n8n-workflow';
|
import { IDataObject, NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
export async function googleApiRequest(
|
export async function googleApiRequest(
|
||||||
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
|
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
|
||||||
method: string,
|
method: string,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
|
body: IDataObject = {},
|
||||||
body: any = {},
|
|
||||||
qs: IDataObject = {},
|
qs: IDataObject = {},
|
||||||
uri?: string,
|
uri?: string,
|
||||||
option: IDataObject = {},
|
option: IDataObject = {},
|
||||||
): Promise<any> {
|
) {
|
||||||
|
const baseURL = 'https://analyticsreporting.googleapis.com';
|
||||||
|
|
||||||
let options: OptionsWithUri = {
|
let options: OptionsWithUri = {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
@ -22,11 +21,12 @@ export async function googleApiRequest(
|
||||||
method,
|
method,
|
||||||
body,
|
body,
|
||||||
qs,
|
qs,
|
||||||
uri: uri || `https://analyticsreporting.googleapis.com${endpoint}`,
|
uri: uri || `${baseURL}${endpoint}`,
|
||||||
json: true,
|
json: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
options = Object.assign({}, options, option);
|
options = Object.assign({}, options, option);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Object.keys(body).length === 0) {
|
if (Object.keys(body).length === 0) {
|
||||||
delete options.body;
|
delete options.body;
|
||||||
|
@ -34,10 +34,17 @@ export async function googleApiRequest(
|
||||||
if (Object.keys(qs).length === 0) {
|
if (Object.keys(qs).length === 0) {
|
||||||
delete options.qs;
|
delete options.qs;
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
|
||||||
return await this.helpers.requestOAuth2.call(this, 'googleAnalyticsOAuth2', options);
|
return await this.helpers.requestOAuth2.call(this, 'googleAnalyticsOAuth2', options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new NodeApiError(this.getNode(), error);
|
const errorData = (error.message || '').split(' - ')[1] as string;
|
||||||
|
if (errorData) {
|
||||||
|
const parsedError = JSON.parse(errorData.trim());
|
||||||
|
const [message, ...rest] = parsedError.error.message.split('\n');
|
||||||
|
const description = rest.join('\n');
|
||||||
|
const httpCode = parsedError.error.code;
|
||||||
|
throw new NodeApiError(this.getNode(), error, { message, description, httpCode });
|
||||||
|
}
|
||||||
|
throw new NodeApiError(this.getNode(), error, { message: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,19 +53,18 @@ export async function googleApiRequestAllItems(
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
method: string,
|
method: string,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
|
body: IDataObject = {},
|
||||||
body: any = {},
|
|
||||||
query: IDataObject = {},
|
query: IDataObject = {},
|
||||||
uri?: string,
|
uri?: string,
|
||||||
): Promise<any> {
|
) {
|
||||||
const returnData: IDataObject[] = [];
|
const returnData: IDataObject[] = [];
|
||||||
|
|
||||||
let responseData;
|
let responseData;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
responseData = await googleApiRequest.call(this, method, endpoint, body, query, uri);
|
responseData = await googleApiRequest.call(this, method, endpoint, body, query, uri);
|
||||||
if (body.reportRequests && Array.isArray(body.reportRequests)) {
|
if (body.reportRequests && Array.isArray(body.reportRequests)) {
|
||||||
body.reportRequests[0].pageToken = responseData[propertyName][0].nextPageToken;
|
(body.reportRequests as IDataObject[])[0].pageToken =
|
||||||
|
responseData[propertyName][0].nextPageToken;
|
||||||
} else {
|
} else {
|
||||||
body.pageToken = responseData.nextPageToken;
|
body.pageToken = responseData.nextPageToken;
|
||||||
}
|
}
|
||||||
|
@ -74,26 +80,32 @@ export async function googleApiRequestAllItems(
|
||||||
export function simplify(responseData: any | [any]) {
|
export function simplify(responseData: any | [any]) {
|
||||||
const response = [];
|
const response = [];
|
||||||
for (const {
|
for (const {
|
||||||
columnHeader: { dimensions },
|
columnHeader: { dimensions, metricHeader },
|
||||||
data: { rows },
|
data: { rows },
|
||||||
} of responseData) {
|
} of responseData) {
|
||||||
if (rows === undefined) {
|
if (rows === undefined) {
|
||||||
// Do not error if there is no data
|
// Do not error if there is no data
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const metrics = metricHeader.metricHeaderEntries.map((entry: { name: string }) => entry.name);
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const data: IDataObject = {};
|
const data: IDataObject = {};
|
||||||
if (dimensions) {
|
if (dimensions) {
|
||||||
for (let i = 0; i < dimensions.length; i++) {
|
for (let i = 0; i < dimensions.length; i++) {
|
||||||
data[dimensions[i]] = row.dimensions[i];
|
data[dimensions[i]] = row.dimensions[i];
|
||||||
data.total = row.metrics[0].values.join(',');
|
for (const [index, metric] of metrics.entries()) {
|
||||||
|
data[metric] = row.metrics[0].values[index];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
data.total = row.metrics[0].values.join(',');
|
for (const [index, metric] of metrics.entries()) {
|
||||||
|
data[metric] = row.metrics[0].values[index];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
response.push(data);
|
response.push(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,305 @@
|
||||||
|
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||||
|
import { IExecuteFunctions } from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodePropertyOptions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeBaseDescription,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { reportFields, reportOperations } from './ReportDescription';
|
||||||
|
import { userActivityFields, userActivityOperations } from './UserActivityDescription';
|
||||||
|
import { googleApiRequest, googleApiRequestAllItems, merge, simplify } from './GenericFunctions';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
import { IData } from './Interfaces';
|
||||||
|
|
||||||
|
const versionDescription: INodeTypeDescription = {
|
||||||
|
displayName: 'Google Analytics',
|
||||||
|
name: 'googleAnalytics',
|
||||||
|
icon: 'file:analytics.svg',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
|
description: 'Use the Google Analytics API',
|
||||||
|
defaults: {
|
||||||
|
name: 'Google Analytics',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'googleAnalyticsOAuth2',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Report',
|
||||||
|
value: 'report',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User Activity',
|
||||||
|
value: 'userActivity',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'report',
|
||||||
|
},
|
||||||
|
//-------------------------------
|
||||||
|
// Reports Operations
|
||||||
|
//-------------------------------
|
||||||
|
...reportOperations,
|
||||||
|
...reportFields,
|
||||||
|
|
||||||
|
//-------------------------------
|
||||||
|
// User Activity Operations
|
||||||
|
//-------------------------------
|
||||||
|
...userActivityOperations,
|
||||||
|
...userActivityFields,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GoogleAnalyticsV1 implements INodeType {
|
||||||
|
description: INodeTypeDescription;
|
||||||
|
|
||||||
|
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||||
|
this.description = {
|
||||||
|
...baseDescription,
|
||||||
|
...versionDescription,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
loadOptions: {
|
||||||
|
// Get all the dimensions to display them to user so that he can
|
||||||
|
// select them easily
|
||||||
|
async getDimensions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const { items: dimensions } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'https://www.googleapis.com/analytics/v3/metadata/ga/columns',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const dimesion of dimensions) {
|
||||||
|
if (
|
||||||
|
dimesion.attributes.type === 'DIMENSION' &&
|
||||||
|
dimesion.attributes.status !== 'DEPRECATED'
|
||||||
|
) {
|
||||||
|
returnData.push({
|
||||||
|
name: dimesion.attributes.uiName,
|
||||||
|
value: dimesion.id,
|
||||||
|
description: dimesion.attributes.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
returnData.sort((a, b) => {
|
||||||
|
const aName = a.name.toLowerCase();
|
||||||
|
const bName = b.name.toLowerCase();
|
||||||
|
if (aName < bName) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (aName > bName) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
},
|
||||||
|
// Get all the views to display them to user so that he can
|
||||||
|
// select them easily
|
||||||
|
async getViews(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const { items } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'https://www.googleapis.com/analytics/v3/management/accounts/~all/webproperties/~all/profiles',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
returnData.push({
|
||||||
|
name: item.name,
|
||||||
|
value: item.id,
|
||||||
|
description: item.websiteUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return returnData;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
const returnData: INodeExecutionData[] = [];
|
||||||
|
const resource = this.getNodeParameter('resource', 0);
|
||||||
|
const operation = this.getNodeParameter('operation', 0);
|
||||||
|
|
||||||
|
let method = '';
|
||||||
|
const qs: IDataObject = {};
|
||||||
|
let endpoint = '';
|
||||||
|
let responseData;
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
try {
|
||||||
|
if (resource === 'report') {
|
||||||
|
if (operation === 'get') {
|
||||||
|
//https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet
|
||||||
|
method = 'POST';
|
||||||
|
endpoint = '/v4/reports:batchGet';
|
||||||
|
const viewId = this.getNodeParameter('viewId', i) as string;
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', 0);
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||||
|
const simple = this.getNodeParameter('simple', i) as boolean;
|
||||||
|
|
||||||
|
const body: IData = {
|
||||||
|
viewId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (additionalFields.useResourceQuotas) {
|
||||||
|
qs.useResourceQuotas = additionalFields.useResourceQuotas;
|
||||||
|
}
|
||||||
|
if (additionalFields.dateRangesUi) {
|
||||||
|
const dateValues = (additionalFields.dateRangesUi as IDataObject)
|
||||||
|
.dateRanges as IDataObject;
|
||||||
|
if (dateValues) {
|
||||||
|
const start = dateValues.startDate as string;
|
||||||
|
const end = dateValues.endDate as string;
|
||||||
|
Object.assign(body, {
|
||||||
|
dateRanges: [
|
||||||
|
{
|
||||||
|
startDate: moment(start).utc().format('YYYY-MM-DD'),
|
||||||
|
endDate: moment(end).utc().format('YYYY-MM-DD'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.metricsUi) {
|
||||||
|
const metrics = (additionalFields.metricsUi as IDataObject)
|
||||||
|
.metricValues as IDataObject[];
|
||||||
|
body.metrics = metrics;
|
||||||
|
}
|
||||||
|
if (additionalFields.dimensionUi) {
|
||||||
|
const dimensions = (additionalFields.dimensionUi as IDataObject)
|
||||||
|
.dimensionValues as IDataObject[];
|
||||||
|
if (dimensions) {
|
||||||
|
body.dimensions = dimensions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (additionalFields.dimensionFiltersUi) {
|
||||||
|
const dimensionFilters = (additionalFields.dimensionFiltersUi as IDataObject)
|
||||||
|
.filterValues as IDataObject[];
|
||||||
|
if (dimensionFilters) {
|
||||||
|
dimensionFilters.forEach((filter) => (filter.expressions = [filter.expressions]));
|
||||||
|
body.dimensionFilterClauses = { filters: dimensionFilters };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.includeEmptyRows) {
|
||||||
|
Object.assign(body, { includeEmptyRows: additionalFields.includeEmptyRows });
|
||||||
|
}
|
||||||
|
if (additionalFields.hideTotals) {
|
||||||
|
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
||||||
|
}
|
||||||
|
if (additionalFields.hideValueRanges) {
|
||||||
|
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnAll) {
|
||||||
|
responseData = await googleApiRequestAllItems.call(
|
||||||
|
this,
|
||||||
|
'reports',
|
||||||
|
method,
|
||||||
|
endpoint,
|
||||||
|
{ reportRequests: [body] },
|
||||||
|
qs,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
responseData = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
method,
|
||||||
|
endpoint,
|
||||||
|
{ reportRequests: [body] },
|
||||||
|
qs,
|
||||||
|
);
|
||||||
|
responseData = responseData.reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simple) {
|
||||||
|
responseData = simplify(responseData);
|
||||||
|
} else if (returnAll && responseData.length > 1) {
|
||||||
|
responseData = merge(responseData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resource === 'userActivity') {
|
||||||
|
if (operation === 'search') {
|
||||||
|
//https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/userActivity/search
|
||||||
|
method = 'POST';
|
||||||
|
endpoint = '/v4/userActivity:search';
|
||||||
|
const viewId = this.getNodeParameter('viewId', i);
|
||||||
|
const userId = this.getNodeParameter('userId', i);
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', 0);
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||||
|
const body: IDataObject = {
|
||||||
|
viewId,
|
||||||
|
user: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (additionalFields.activityTypes) {
|
||||||
|
Object.assign(body, { activityTypes: additionalFields.activityTypes });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnAll) {
|
||||||
|
responseData = await googleApiRequestAllItems.call(
|
||||||
|
this,
|
||||||
|
'sessions',
|
||||||
|
method,
|
||||||
|
endpoint,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body.pageSize = this.getNodeParameter('limit', 0);
|
||||||
|
responseData = await googleApiRequest.call(this, method, endpoint, body);
|
||||||
|
responseData = responseData.sessions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const executionData = this.helpers.constructExecutionMetaData(
|
||||||
|
this.helpers.returnJsonArray(responseData),
|
||||||
|
{ itemData: { item: i } },
|
||||||
|
);
|
||||||
|
returnData.push(...executionData);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||||
|
this.helpers.returnJsonArray({ error: error.message }),
|
||||||
|
{ itemData: { item: i } },
|
||||||
|
);
|
||||||
|
returnData.push(...executionErrorData);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.prepareOutputData(returnData);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
|
||||||
export interface IData {
|
export interface IData {
|
||||||
viewId: string;
|
viewId: string;
|
||||||
dimensions?: IDimension[];
|
dimensions?: IDimension[];
|
||||||
|
@ -6,6 +8,7 @@ export interface IData {
|
||||||
};
|
};
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
metrics?: IMetric[];
|
metrics?: IMetric[];
|
||||||
|
dateRanges?: IDataObject[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDimension {
|
export interface IDimension {
|
|
@ -0,0 +1,28 @@
|
||||||
|
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||||
|
import { IExecuteFunctions } from 'n8n-core';
|
||||||
|
import {
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeBaseDescription,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { listSearch, loadOptions } from './methods';
|
||||||
|
import { router } from './actions/router';
|
||||||
|
import { versionDescription } from './actions/versionDescription';
|
||||||
|
|
||||||
|
export class GoogleAnalyticsV2 implements INodeType {
|
||||||
|
description: INodeTypeDescription;
|
||||||
|
|
||||||
|
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||||
|
this.description = {
|
||||||
|
...baseDescription,
|
||||||
|
...versionDescription,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
methods = { loadOptions, listSearch };
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
return router.call(this);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { AllEntities, Entity } from 'n8n-workflow';
|
||||||
|
|
||||||
|
type GoogleAnalyticsMap = {
|
||||||
|
userActivity: 'search';
|
||||||
|
report: ReportBasedOnProperty;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GoogleAnalytics = AllEntities<GoogleAnalyticsMap>;
|
||||||
|
|
||||||
|
export type GoogleAnalyticsUserActivity = Entity<GoogleAnalyticsMap, 'userActivity'>;
|
||||||
|
export type GoogleAnalyticReport = Entity<GoogleAnalyticsMap, 'report'>;
|
||||||
|
|
||||||
|
export type ReportBasedOnProperty = 'getga4' | 'getuniversal';
|
|
@ -0,0 +1,488 @@
|
||||||
|
import { INodeProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const dimensionDropdown: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Dimension',
|
||||||
|
name: 'listName',
|
||||||
|
type: 'options',
|
||||||
|
default: 'date',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Browser',
|
||||||
|
value: 'browser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Campaign',
|
||||||
|
value: 'campaignName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'City',
|
||||||
|
value: 'city',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Country',
|
||||||
|
value: 'country',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date',
|
||||||
|
value: 'date',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Device Category',
|
||||||
|
value: 'deviceCategory',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Item Name',
|
||||||
|
value: 'itemName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Language',
|
||||||
|
value: 'language',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Page Location',
|
||||||
|
value: 'pageLocation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Source / Medium',
|
||||||
|
value: 'sourceMedium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
name: 'Other dimensions…',
|
||||||
|
value: 'other',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name or ID',
|
||||||
|
name: 'name',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDimensionsGA4',
|
||||||
|
loadOptionsDependsOn: ['propertyId.value'],
|
||||||
|
},
|
||||||
|
default: 'date',
|
||||||
|
description:
|
||||||
|
'The name of the dimension. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['other'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const metricDropdown: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Metric',
|
||||||
|
name: 'listName',
|
||||||
|
type: 'options',
|
||||||
|
default: 'totalUsers',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: '1 Day Active Users',
|
||||||
|
value: 'active1DayUsers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '28 Day Active Users',
|
||||||
|
value: 'active28DayUsers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '7 Day Active Users',
|
||||||
|
value: 'active7DayUsers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Checkouts',
|
||||||
|
value: 'checkouts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Events',
|
||||||
|
value: 'eventCount',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Page Views',
|
||||||
|
value: 'screenPageViews',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Session Duration',
|
||||||
|
value: 'userEngagementDuration',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sessions',
|
||||||
|
value: 'sessions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sessions per User',
|
||||||
|
value: 'sessionsPerUser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Total Users',
|
||||||
|
value: 'totalUsers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
name: 'Other metrics…',
|
||||||
|
value: 'other',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
name: 'Custom metric…',
|
||||||
|
value: 'custom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name or ID',
|
||||||
|
name: 'name',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getMetricsGA4',
|
||||||
|
loadOptionsDependsOn: ['propertyId.value'],
|
||||||
|
},
|
||||||
|
default: 'totalUsers',
|
||||||
|
hint: 'If expression is specified, name can be any string that you would like',
|
||||||
|
description:
|
||||||
|
'The name of the metric. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['other'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: 'custom_metric',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['custom'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const dimensionsFilterExpressions: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Expression',
|
||||||
|
name: 'expression',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
placeholder: 'Add Expression',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'String Filter',
|
||||||
|
name: 'stringFilter',
|
||||||
|
values: [
|
||||||
|
...dimensionDropdown,
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Case Sensitive',
|
||||||
|
name: 'caseSensitive',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Match Type',
|
||||||
|
name: 'matchType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'EXACT',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Begins With',
|
||||||
|
value: 'BEGINS_WITH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Contains Value',
|
||||||
|
value: 'CONTAINS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ends With',
|
||||||
|
value: 'ENDS_WITH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Exact Match',
|
||||||
|
value: 'EXACT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Full Match for the Regular Expression',
|
||||||
|
value: 'FULL_REGEXP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Partial Match for the Regular Expression',
|
||||||
|
value: 'PARTIAL_REGEXP',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'In List Filter',
|
||||||
|
name: 'inListFilter',
|
||||||
|
values: [
|
||||||
|
...dimensionDropdown,
|
||||||
|
{
|
||||||
|
displayName: 'Values',
|
||||||
|
name: 'values',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
hint: 'Comma separated list of values. Must be non-empty.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Case Sensitive',
|
||||||
|
name: 'caseSensitive',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Numeric Filter',
|
||||||
|
name: 'numericFilter',
|
||||||
|
values: [
|
||||||
|
...dimensionDropdown,
|
||||||
|
{
|
||||||
|
displayName: 'Value Type',
|
||||||
|
name: 'valueType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'doubleValue',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Double Value',
|
||||||
|
value: 'doubleValue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Integer Value',
|
||||||
|
value: 'int64Value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
default: 'EQUAL',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Equal',
|
||||||
|
value: 'EQUAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Greater Than',
|
||||||
|
value: 'GREATER_THAN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Greater than or Equal',
|
||||||
|
value: 'GREATER_THAN_OR_EQUAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Less Than',
|
||||||
|
value: 'LESS_THAN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Less than or Equal',
|
||||||
|
value: 'LESS_THAN_OR_EQUAL',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const dimensionFilterField: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Dimensions Filters',
|
||||||
|
name: 'dimensionFiltersUI',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: {},
|
||||||
|
placeholder: 'Add Filter',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Filter Expressions',
|
||||||
|
name: 'filterExpressions',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Filter Expression Type',
|
||||||
|
name: 'filterExpressionType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'andGroup',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'And Group',
|
||||||
|
value: 'andGroup',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Or Group',
|
||||||
|
value: 'orGroup',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...dimensionsFilterExpressions,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const metricsFilterExpressions: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Expression',
|
||||||
|
name: 'expression',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
placeholder: 'Add Expression',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Between Filter',
|
||||||
|
name: 'betweenFilter',
|
||||||
|
values: [
|
||||||
|
...metricDropdown,
|
||||||
|
{
|
||||||
|
displayName: 'Value Type',
|
||||||
|
name: 'valueType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'doubleValue',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Double Value',
|
||||||
|
value: 'doubleValue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Integer Value',
|
||||||
|
value: 'int64Value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'From Value',
|
||||||
|
name: 'fromValue',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'To Value',
|
||||||
|
name: 'toValue',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Numeric Filter',
|
||||||
|
name: 'numericFilter',
|
||||||
|
values: [
|
||||||
|
...metricDropdown,
|
||||||
|
{
|
||||||
|
displayName: 'Value Type',
|
||||||
|
name: 'valueType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'doubleValue',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Double Value',
|
||||||
|
value: 'doubleValue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Integer Value',
|
||||||
|
value: 'int64Value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
default: 'EQUAL',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Equal',
|
||||||
|
value: 'EQUAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Greater Than',
|
||||||
|
value: 'GREATER_THAN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Greater than or Equal',
|
||||||
|
value: 'GREATER_THAN_OR_EQUAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Less Than',
|
||||||
|
value: 'LESS_THAN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Less than or Equal',
|
||||||
|
value: 'LESS_THAN_OR_EQUAL',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const metricsFilterField: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Metrics Filters',
|
||||||
|
name: 'metricsFiltersUI',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: {},
|
||||||
|
placeholder: 'Add Filter',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Filter Expressions',
|
||||||
|
name: 'filterExpressions',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Filter Expression Type',
|
||||||
|
name: 'filterExpressionType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'andGroup',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'And Group',
|
||||||
|
value: 'andGroup',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Or Group',
|
||||||
|
value: 'orGroup',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...metricsFilterExpressions,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { INodeProperties } from 'n8n-workflow';
|
||||||
|
import * as getga4 from './get.ga4.operation';
|
||||||
|
import * as getuniversal from './get.universal.operation';
|
||||||
|
|
||||||
|
export { getga4, getuniversal };
|
||||||
|
|
||||||
|
export const description: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
description: 'Return the analytics data',
|
||||||
|
action: 'Get a report',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'get',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Property Type',
|
||||||
|
name: 'propertyType',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
description:
|
||||||
|
'Google Analytics 4 is the latest version. Universal Analytics is an older version that is not fully functional after the end of June 2023.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Google Analytics 4',
|
||||||
|
value: 'ga4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Universal Analytics',
|
||||||
|
value: 'universal',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'ga4',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...getga4.description,
|
||||||
|
...getuniversal.description,
|
||||||
|
];
|
|
@ -0,0 +1,620 @@
|
||||||
|
import { IExecuteFunctions } from 'n8n-core';
|
||||||
|
import { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||||
|
import {
|
||||||
|
checkDuplicates,
|
||||||
|
defaultEndDate,
|
||||||
|
defaultStartDate,
|
||||||
|
prepareDateRange,
|
||||||
|
processFilters,
|
||||||
|
simplifyGA4,
|
||||||
|
} from '../../helpers/utils';
|
||||||
|
import { googleApiRequest, googleApiRequestAllItems } from '../../transport';
|
||||||
|
import {
|
||||||
|
dimensionDropdown,
|
||||||
|
dimensionFilterField,
|
||||||
|
metricDropdown,
|
||||||
|
metricsFilterField,
|
||||||
|
} from './FiltersDescription';
|
||||||
|
|
||||||
|
export const description: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Property',
|
||||||
|
name: 'propertyId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'list', value: '' },
|
||||||
|
required: true,
|
||||||
|
description: 'The Property of Google Analytics',
|
||||||
|
hint: "If this doesn't work, try changing the 'Property Type' field above",
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
placeholder: 'Select a property...',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchProperties',
|
||||||
|
searchFilterRequired: false,
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'https://analytics.google.com/analytics/...',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '.*analytics\\.google\\.com\\/analytics.*\\/p([0-9]{1,})(?:\\/.*|)*',
|
||||||
|
errorMessage: 'Not a valid Google Analytics URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extractValue: {
|
||||||
|
type: 'regex',
|
||||||
|
regex: '.*analytics\\.google\\.com\\/analytics.*\\/p([0-9]{1,})(?:\\/.*|)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '123456',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '[0-9]{1,}',
|
||||||
|
errorMessage: 'Not a valid Google Analytics Property ID',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
url: '=https://analytics.google.com/analytics/web/#/p{{$value}}/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Date Range',
|
||||||
|
name: 'dateRange',
|
||||||
|
type: 'options',
|
||||||
|
required: true,
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Last 7 Days',
|
||||||
|
value: 'last7days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Last 30 Days',
|
||||||
|
value: 'last30days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Today',
|
||||||
|
value: 'today',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Yesterday',
|
||||||
|
value: 'yesterday',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Last Complete Calendar Week',
|
||||||
|
value: 'lastCalendarWeek',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Last Complete Calendar Month',
|
||||||
|
value: 'lastCalendarMonth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Custom',
|
||||||
|
value: 'custom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'last7days',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Start',
|
||||||
|
name: 'startDate',
|
||||||
|
type: 'dateTime',
|
||||||
|
required: true,
|
||||||
|
default: defaultStartDate(),
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
dateRange: ['custom'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'End',
|
||||||
|
name: 'endDate',
|
||||||
|
type: 'dateTime',
|
||||||
|
required: true,
|
||||||
|
default: defaultEndDate(),
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
dateRange: ['custom'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Metrics',
|
||||||
|
name: 'metricsGA4',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: { metricValues: [{ listName: 'totalUsers' }] },
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
placeholder: 'Add Metric',
|
||||||
|
description:
|
||||||
|
'The quantitative measurements of a report. For example, the metric eventCount is the total number of events. Requests are allowed up to 10 metrics.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Values',
|
||||||
|
name: 'metricValues',
|
||||||
|
values: [
|
||||||
|
...metricDropdown,
|
||||||
|
{
|
||||||
|
displayName: 'Expression',
|
||||||
|
name: 'expression',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description:
|
||||||
|
'A mathematical expression for derived metrics. For example, the metric Event count per user is eventCount/totalUsers.',
|
||||||
|
placeholder: 'e.g. eventCount/totalUsers',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['custom'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Invisible',
|
||||||
|
name: 'invisible',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['custom'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'Whether a metric is invisible in the report response. If a metric is invisible, the metric will not produce a column in the response, but can be used in metricFilter, orderBys, or a metric expression.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
displayName: 'Dimensions to split by',
|
||||||
|
name: 'dimensionsGA4',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: { dimensionValues: [{ listName: 'date' }] },
|
||||||
|
// default: {},
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
placeholder: 'Add Dimension',
|
||||||
|
description:
|
||||||
|
'Dimensions are attributes of your data. For example, the dimension city indicates the city from which an event originates. Dimension values in report responses are strings; for example, the city could be "Paris" or "New York". Requests are allowed up to 9 dimensions.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Values',
|
||||||
|
name: 'dimensionValues',
|
||||||
|
values: [...dimensionDropdown],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['get'],
|
||||||
|
resource: ['report'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to return all results or only up to a given limit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['get'],
|
||||||
|
resource: ['report'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
returnAll: [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 1000,
|
||||||
|
},
|
||||||
|
default: 50,
|
||||||
|
description: 'Max number of results to return',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-simplify
|
||||||
|
displayName: 'Simplify Output',
|
||||||
|
name: 'simple',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['get'],
|
||||||
|
resource: ['report'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: true,
|
||||||
|
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['ga4'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Currency Code',
|
||||||
|
name: 'currencyCode',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description:
|
||||||
|
'A currency code in ISO4217 format, such as "AED", "USD", "JPY". If the field is empty, the report uses the property\'s default currency.',
|
||||||
|
},
|
||||||
|
...dimensionFilterField,
|
||||||
|
{
|
||||||
|
displayName: 'Metric Aggregation',
|
||||||
|
name: 'metricAggregations',
|
||||||
|
type: 'multiOptions',
|
||||||
|
default: [],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'MAXIMUM',
|
||||||
|
value: 'MAXIMUM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MINIMUM',
|
||||||
|
value: 'MINIMUM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TOTAL',
|
||||||
|
value: 'TOTAL',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/simple': [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...metricsFilterField,
|
||||||
|
{
|
||||||
|
displayName: 'Keep Empty Rows',
|
||||||
|
name: 'keepEmptyRows',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether false or unspecified, each row with all metrics equal to 0 will not be returned. If true, these rows will be returned if they are not separately removed by a filter.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Order By',
|
||||||
|
name: 'orderByUI',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: {},
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
placeholder: 'Add Order',
|
||||||
|
description: 'Specifies how rows are ordered in the response',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Metric Order By',
|
||||||
|
name: 'metricOrderBy',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Descending',
|
||||||
|
name: 'desc',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether true, sorts by descending order',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Metric Name or ID',
|
||||||
|
name: 'metricName',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getMetricsGA4',
|
||||||
|
loadOptionsDependsOn: ['propertyId.value'],
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description:
|
||||||
|
'Sorts by metric values. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Dimmension Order By',
|
||||||
|
name: 'dimmensionOrderBy',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Descending',
|
||||||
|
name: 'desc',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether true, sorts by descending order',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Dimmension Name or ID',
|
||||||
|
name: 'dimensionName',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDimensionsGA4',
|
||||||
|
loadOptionsDependsOn: ['propertyId.value'],
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description:
|
||||||
|
'Sorts by metric values. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Order Type',
|
||||||
|
name: 'orderType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'ORDER_TYPE_UNSPECIFIED',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Alphanumeric',
|
||||||
|
value: 'ALPHANUMERIC',
|
||||||
|
description: 'Alphanumeric sort by Unicode code point',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Case Insensitive Alphanumeric',
|
||||||
|
value: 'CASE_INSENSITIVE_ALPHANUMERIC',
|
||||||
|
description:
|
||||||
|
'Case insensitive alphanumeric sort by lower case Unicode code point',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Numeric',
|
||||||
|
value: 'NUMERIC',
|
||||||
|
description: 'Dimension values are converted to numbers before sorting',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Unspecified',
|
||||||
|
value: 'ORDER_TYPE_UNSPECIFIED',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return Property Quota',
|
||||||
|
name: 'returnPropertyQuota',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
"Whether to return the current state of this Analytics Property's quota. Quota is returned in PropertyQuota.",
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/simple': [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function execute(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
index: number,
|
||||||
|
): Promise<INodeExecutionData[]> {
|
||||||
|
//migration guide: https://developers.google.com/analytics/devguides/migration/api/reporting-ua-to-ga4#core_reporting
|
||||||
|
const propertyId = this.getNodeParameter('propertyId', index, undefined, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as string;
|
||||||
|
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', 0);
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', index);
|
||||||
|
const dateRange = this.getNodeParameter('dateRange', index) as string;
|
||||||
|
const metricsGA4 = this.getNodeParameter('metricsGA4', index, {}) as IDataObject;
|
||||||
|
const dimensionsGA4 = this.getNodeParameter('dimensionsGA4', index, {}) as IDataObject;
|
||||||
|
const simple = this.getNodeParameter('simple', index) as boolean;
|
||||||
|
|
||||||
|
let responseData: IDataObject[] = [];
|
||||||
|
|
||||||
|
const qs: IDataObject = {};
|
||||||
|
const body: IDataObject = {
|
||||||
|
dateRanges: prepareDateRange.call(this, dateRange, index),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metricsGA4.metricValues) {
|
||||||
|
const metrics = (metricsGA4.metricValues as IDataObject[]).map((metric) => {
|
||||||
|
switch (metric.listName) {
|
||||||
|
case 'other':
|
||||||
|
return { name: metric.name };
|
||||||
|
case 'custom':
|
||||||
|
const newMetric = {
|
||||||
|
name: metric.name,
|
||||||
|
expression: metric.expression,
|
||||||
|
invisible: metric.invisible,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newMetric.invisible === false) {
|
||||||
|
delete newMetric.invisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newMetric.expression === '') {
|
||||||
|
delete newMetric.expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMetric;
|
||||||
|
default:
|
||||||
|
return { name: metric.listName };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (metrics.length) {
|
||||||
|
checkDuplicates.call(this, metrics, 'name', 'metrics');
|
||||||
|
body.metrics = metrics;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dimensionsGA4.dimensionValues) {
|
||||||
|
const dimensions = (dimensionsGA4.dimensionValues as IDataObject[]).map((dimension) => {
|
||||||
|
switch (dimension.listName) {
|
||||||
|
case 'other':
|
||||||
|
return { name: dimension.name };
|
||||||
|
default:
|
||||||
|
return { name: dimension.listName };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (dimensions.length) {
|
||||||
|
checkDuplicates.call(this, dimensions, 'name', 'dimensions');
|
||||||
|
body.dimensions = dimensions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.currencyCode) {
|
||||||
|
body.currencyCode = additionalFields.currencyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.dimensionFiltersUI) {
|
||||||
|
const { filterExpressionType, expression } = (
|
||||||
|
additionalFields.dimensionFiltersUI as IDataObject
|
||||||
|
).filterExpressions as IDataObject;
|
||||||
|
if (expression) {
|
||||||
|
body.dimensionFilter = {
|
||||||
|
[filterExpressionType as string]: {
|
||||||
|
expressions: processFilters(expression as IDataObject),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.metricsFiltersUI) {
|
||||||
|
const { filterExpressionType, expression } = (additionalFields.metricsFiltersUI as IDataObject)
|
||||||
|
.filterExpressions as IDataObject;
|
||||||
|
if (expression) {
|
||||||
|
body.metricFilter = {
|
||||||
|
[filterExpressionType as string]: {
|
||||||
|
expressions: processFilters(expression as IDataObject),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.metricAggregations) {
|
||||||
|
body.metricAggregations = additionalFields.metricAggregations;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.keepEmptyRows) {
|
||||||
|
body.keepEmptyRows = additionalFields.keepEmptyRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.orderByUI) {
|
||||||
|
let orderBys: IDataObject[] = [];
|
||||||
|
const metricOrderBy = (additionalFields.orderByUI as IDataObject)
|
||||||
|
.metricOrderBy as IDataObject[];
|
||||||
|
const dimmensionOrderBy = (additionalFields.orderByUI as IDataObject)
|
||||||
|
.dimmensionOrderBy as IDataObject[];
|
||||||
|
if (metricOrderBy) {
|
||||||
|
orderBys = orderBys.concat(
|
||||||
|
metricOrderBy.map((order) => {
|
||||||
|
return {
|
||||||
|
desc: order.desc,
|
||||||
|
metric: {
|
||||||
|
metricName: order.metricName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (dimmensionOrderBy) {
|
||||||
|
orderBys = orderBys.concat(
|
||||||
|
dimmensionOrderBy.map((order) => {
|
||||||
|
return {
|
||||||
|
desc: order.desc,
|
||||||
|
dimension: {
|
||||||
|
dimensionName: order.dimensionName,
|
||||||
|
orderType: order.orderType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
body.orderBys = orderBys;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.returnPropertyQuota) {
|
||||||
|
body.returnPropertyQuota = additionalFields.returnPropertyQuota;
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = 'POST';
|
||||||
|
const endpoint = `/v1beta/properties/${propertyId}:runReport`;
|
||||||
|
|
||||||
|
if (returnAll) {
|
||||||
|
responseData = await googleApiRequestAllItems.call(this, '', method, endpoint, body, qs);
|
||||||
|
} else {
|
||||||
|
body.limit = this.getNodeParameter('limit', 0);
|
||||||
|
responseData = [await googleApiRequest.call(this, method, endpoint, body, qs)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseData?.length && simple) {
|
||||||
|
responseData = simplifyGA4(responseData[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionData = this.helpers.constructExecutionMetaData(
|
||||||
|
this.helpers.returnJsonArray(responseData),
|
||||||
|
{ itemData: { item: index } },
|
||||||
|
);
|
||||||
|
|
||||||
|
return executionData;
|
||||||
|
}
|
|
@ -0,0 +1,725 @@
|
||||||
|
import { IExecuteFunctions } from 'n8n-core';
|
||||||
|
import { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||||
|
import { IData, IDimension, IMetric } from '../../helpers/Interfaces';
|
||||||
|
import {
|
||||||
|
checkDuplicates,
|
||||||
|
defaultEndDate,
|
||||||
|
defaultStartDate,
|
||||||
|
merge,
|
||||||
|
prepareDateRange,
|
||||||
|
simplify,
|
||||||
|
} from '../../helpers/utils';
|
||||||
|
import { googleApiRequest, googleApiRequestAllItems } from '../../transport';
|
||||||
|
|
||||||
|
const dimensionDropdown: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Dimension',
|
||||||
|
name: 'listName',
|
||||||
|
type: 'options',
|
||||||
|
default: 'ga:date',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Browser',
|
||||||
|
value: 'ga:browser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Campaign',
|
||||||
|
value: 'ga:campaign',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'City',
|
||||||
|
value: 'ga:city',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Country',
|
||||||
|
value: 'ga:country',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date',
|
||||||
|
value: 'ga:date',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Device Category',
|
||||||
|
value: 'ga:deviceCategory',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Item Name',
|
||||||
|
value: 'ga:productName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Language',
|
||||||
|
value: 'ga:language',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Page',
|
||||||
|
value: 'ga:pagePath',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Source / Medium',
|
||||||
|
value: 'ga:sourceMedium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
name: 'Other dimensions…',
|
||||||
|
value: 'other',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name or ID',
|
||||||
|
name: 'name',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDimensions',
|
||||||
|
loadOptionsDependsOn: ['viewId.value'],
|
||||||
|
},
|
||||||
|
default: 'ga:date',
|
||||||
|
description:
|
||||||
|
'Name of the dimension to fetch, for example ga:browser. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['other'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const description: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'View',
|
||||||
|
name: 'viewId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'list', value: '' },
|
||||||
|
required: true,
|
||||||
|
description: 'The View of Google Analytics',
|
||||||
|
hint: "If this doesn't work, try changing the 'Property Type' field above",
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
placeholder: 'Select a view...',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchViews',
|
||||||
|
searchFilterRequired: false,
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'https://analytics.google.com/analytics/...',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '.*analytics.google.com/analytics.*p[0-9]{1,}.*',
|
||||||
|
errorMessage: 'Not a valid Google Analytics URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extractValue: {
|
||||||
|
type: 'regex',
|
||||||
|
regex: '.*analytics.google.com/analytics.*p([0-9]{1,})',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '123456',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '[0-9]{1,}',
|
||||||
|
errorMessage: 'Not a valid Google Analytics View ID',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Date Range',
|
||||||
|
name: 'dateRange',
|
||||||
|
type: 'options',
|
||||||
|
required: true,
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Last 7 Days',
|
||||||
|
value: 'last7days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Last 30 Days',
|
||||||
|
value: 'last30days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Today',
|
||||||
|
value: 'today',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Yesterday',
|
||||||
|
value: 'yesterday',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Last Complete Calendar Week',
|
||||||
|
value: 'lastCalendarWeek',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Last Complete Calendar Month',
|
||||||
|
value: 'lastCalendarMonth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Custom',
|
||||||
|
value: 'custom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'last7days',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Start',
|
||||||
|
name: 'startDate',
|
||||||
|
type: 'dateTime',
|
||||||
|
required: true,
|
||||||
|
default: defaultStartDate(),
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
dateRange: ['custom'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'End',
|
||||||
|
name: 'endDate',
|
||||||
|
type: 'dateTime',
|
||||||
|
required: true,
|
||||||
|
default: defaultEndDate(),
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
dateRange: ['custom'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Metrics',
|
||||||
|
name: 'metricsUA',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: { metricValues: [{ listName: 'ga:users' }] },
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
placeholder: 'Add metric',
|
||||||
|
description: 'Metrics in the request',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Metric',
|
||||||
|
name: 'metricValues',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Metric',
|
||||||
|
name: 'listName',
|
||||||
|
type: 'options',
|
||||||
|
default: 'ga:users',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Checkouts',
|
||||||
|
value: 'ga:productCheckouts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Events',
|
||||||
|
value: 'ga:totalEvents',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Page Views',
|
||||||
|
value: 'ga:pageviews',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Session Duration',
|
||||||
|
value: 'ga:sessionDuration',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sessions',
|
||||||
|
value: 'ga:sessions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sessions per User',
|
||||||
|
value: 'ga:sessionsPerUser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Total Users',
|
||||||
|
value: 'ga:users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
name: 'Other metrics…',
|
||||||
|
value: 'other',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
name: 'Custom metric…',
|
||||||
|
value: 'custom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name or ID',
|
||||||
|
name: 'name',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getMetrics',
|
||||||
|
loadOptionsDependsOn: ['viewId.value'],
|
||||||
|
},
|
||||||
|
default: 'ga:users',
|
||||||
|
hint: 'If expression is specified, name can be any string that you would like',
|
||||||
|
description:
|
||||||
|
'The name of the metric. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['other'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: 'custom_metric',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['custom'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Expression',
|
||||||
|
name: 'expression',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. ga:totalRefunds/ga:users',
|
||||||
|
description:
|
||||||
|
'Learn more about Google Analytics <a href="https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet#Metric">metric expressions</a>',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['custom'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Formatting Type',
|
||||||
|
name: 'formattingType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'INTEGER',
|
||||||
|
description: 'Specifies how the metric expression should be formatted',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Currency',
|
||||||
|
value: 'CURRENCY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Float',
|
||||||
|
value: 'FLOAT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Integer',
|
||||||
|
value: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Percent',
|
||||||
|
value: 'PERCENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Time',
|
||||||
|
value: 'TIME',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
listName: ['custom'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
displayName: 'Dimensions to split by',
|
||||||
|
name: 'dimensionsUA',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: { dimensionValues: [{ listName: 'ga:date' }] },
|
||||||
|
// default: {},
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
placeholder: 'Add Dimension',
|
||||||
|
description:
|
||||||
|
'Dimensions are attributes of your data. For example, the dimension ga:city indicates the city, for example, "Paris" or "New York", from which a session originates.',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Values',
|
||||||
|
name: 'dimensionValues',
|
||||||
|
values: [...dimensionDropdown],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['get'],
|
||||||
|
resource: ['report'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to return all results or only up to a given limit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['get'],
|
||||||
|
resource: ['report'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
returnAll: [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 1000,
|
||||||
|
},
|
||||||
|
default: 50,
|
||||||
|
description: 'Max number of results to return',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-simplify
|
||||||
|
displayName: 'Simplify Output',
|
||||||
|
name: 'simple',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['get'],
|
||||||
|
resource: ['report'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: true,
|
||||||
|
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['report'],
|
||||||
|
operation: ['get'],
|
||||||
|
propertyType: ['universal'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Dimension Filters',
|
||||||
|
name: 'dimensionFiltersUi',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: {},
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
placeholder: 'Add Dimension Filter',
|
||||||
|
description: 'Dimension Filters in the request',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Filters',
|
||||||
|
name: 'filterValues',
|
||||||
|
values: [
|
||||||
|
...dimensionDropdown,
|
||||||
|
// https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet#Operator
|
||||||
|
{
|
||||||
|
displayName: 'Operator',
|
||||||
|
name: 'operator',
|
||||||
|
type: 'options',
|
||||||
|
default: 'EXACT',
|
||||||
|
description: 'Operator to use in combination with value',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Begins With',
|
||||||
|
value: 'BEGINS_WITH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ends With',
|
||||||
|
value: 'ENDS_WITH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Equals (Number)',
|
||||||
|
value: 'NUMERIC_EQUAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Exactly Matches',
|
||||||
|
value: 'EXACT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Greater Than (Number)',
|
||||||
|
value: 'NUMERIC_GREATER_THAN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Less Than (Number)',
|
||||||
|
value: 'NUMERIC_LESS_THAN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Partly Matches',
|
||||||
|
value: 'PARTIAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Regular Expression',
|
||||||
|
value: 'REGEXP',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'expressions',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: '',
|
||||||
|
description:
|
||||||
|
'String or <a href="https://support.google.com/analytics/answer/1034324?hl=en">regular expression</a> to match against',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Hide Totals',
|
||||||
|
name: 'hideTotals',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether to hide the total of all metrics for all the matching rows, for every date range',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/simple': [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Hide Value Ranges',
|
||||||
|
name: 'hideValueRanges',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to hide the minimum and maximum across all matching rows',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/simple': [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Include Empty Rows',
|
||||||
|
name: 'includeEmptyRows',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether the response exclude rows if all the retrieved metrics are equal to zero',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Use Resource Quotas',
|
||||||
|
name: 'useResourceQuotas',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to enable resource based quotas',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'/simple': [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function execute(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
index: number,
|
||||||
|
): Promise<INodeExecutionData[]> {
|
||||||
|
//https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet
|
||||||
|
// const viewId = this.getNodeParameter('viewId', index) as string;
|
||||||
|
const viewId = this.getNodeParameter('viewId', index, undefined, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as string;
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', 0);
|
||||||
|
const dateRange = this.getNodeParameter('dateRange', index) as string;
|
||||||
|
const metricsUA = this.getNodeParameter('metricsUA', index) as IDataObject;
|
||||||
|
const dimensionsUA = this.getNodeParameter('dimensionsUA', index) as IDataObject;
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', index);
|
||||||
|
const simple = this.getNodeParameter('simple', index) as boolean;
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
const qs: IDataObject = {};
|
||||||
|
const body: IData = {
|
||||||
|
viewId,
|
||||||
|
dateRanges: prepareDateRange.call(this, dateRange, index),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metricsUA.metricValues) {
|
||||||
|
const metrics = (metricsUA.metricValues as IDataObject[]).map((metric) => {
|
||||||
|
switch (metric.listName) {
|
||||||
|
case 'other':
|
||||||
|
return {
|
||||||
|
alias: metric.name,
|
||||||
|
expression: metric.name,
|
||||||
|
};
|
||||||
|
case 'custom':
|
||||||
|
const newMetric = {
|
||||||
|
alias: metric.name,
|
||||||
|
expression: metric.expression,
|
||||||
|
formattingType: metric.formattingType,
|
||||||
|
};
|
||||||
|
return newMetric;
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
alias: metric.listName,
|
||||||
|
expression: metric.listName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (metrics.length) {
|
||||||
|
checkDuplicates.call(this, metrics, 'alias', 'metrics');
|
||||||
|
body.metrics = metrics as IMetric[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dimensionsUA.dimensionValues) {
|
||||||
|
const dimensions = (dimensionsUA.dimensionValues as IDataObject[]).map((dimension) => {
|
||||||
|
switch (dimension.listName) {
|
||||||
|
case 'other':
|
||||||
|
return { name: dimension.name };
|
||||||
|
default:
|
||||||
|
return { name: dimension.listName };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (dimensions.length) {
|
||||||
|
checkDuplicates.call(this, dimensions, 'name', 'dimensions');
|
||||||
|
body.dimensions = dimensions as IDimension[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.useResourceQuotas) {
|
||||||
|
qs.useResourceQuotas = additionalFields.useResourceQuotas;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.dimensionFiltersUi) {
|
||||||
|
const dimensionFilters = (additionalFields.dimensionFiltersUi as IDataObject)
|
||||||
|
.filterValues as IDataObject[];
|
||||||
|
if (dimensionFilters) {
|
||||||
|
dimensionFilters.forEach((filter) => {
|
||||||
|
filter.expressions = [filter.expressions];
|
||||||
|
switch (filter.listName) {
|
||||||
|
case 'other':
|
||||||
|
filter.dimensionName = filter.name;
|
||||||
|
delete filter.name;
|
||||||
|
delete filter.listName;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filter.dimensionName = filter.listName;
|
||||||
|
delete filter.listName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
body.dimensionFilterClauses = { filters: dimensionFilters };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalFields.includeEmptyRows) {
|
||||||
|
Object.assign(body, { includeEmptyRows: additionalFields.includeEmptyRows });
|
||||||
|
}
|
||||||
|
if (additionalFields.hideTotals) {
|
||||||
|
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
||||||
|
}
|
||||||
|
if (additionalFields.hideValueRanges) {
|
||||||
|
Object.assign(body, { hideTotals: additionalFields.hideTotals });
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = 'POST';
|
||||||
|
const endpoint = '/v4/reports:batchGet';
|
||||||
|
|
||||||
|
if (returnAll) {
|
||||||
|
responseData = await googleApiRequestAllItems.call(
|
||||||
|
this,
|
||||||
|
'reports',
|
||||||
|
method,
|
||||||
|
endpoint,
|
||||||
|
{ reportRequests: [body] },
|
||||||
|
qs,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body.pageSize = this.getNodeParameter('limit', 0);
|
||||||
|
responseData = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
method,
|
||||||
|
endpoint,
|
||||||
|
{ reportRequests: [body] },
|
||||||
|
qs,
|
||||||
|
);
|
||||||
|
responseData = responseData.reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simple) {
|
||||||
|
responseData = simplify(responseData);
|
||||||
|
} else if (returnAll && responseData.length > 1) {
|
||||||
|
responseData = merge(responseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionData = this.helpers.constructExecutionMetaData(
|
||||||
|
this.helpers.returnJsonArray(responseData),
|
||||||
|
{ itemData: { item: index } },
|
||||||
|
);
|
||||||
|
|
||||||
|
return executionData;
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { IExecuteFunctions } from 'n8n-core';
|
||||||
|
import { INodeExecutionData, NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { GoogleAnalytics, ReportBasedOnProperty } from './node.type';
|
||||||
|
import * as userActivity from './userActivity/UserActivity.resource';
|
||||||
|
import * as report from './report/Report.resource';
|
||||||
|
|
||||||
|
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
const returnData: INodeExecutionData[] = [];
|
||||||
|
const resource = this.getNodeParameter<GoogleAnalytics>('resource', 0) as string;
|
||||||
|
const operation = this.getNodeParameter('operation', 0);
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
const googleAnalytics = {
|
||||||
|
resource,
|
||||||
|
operation,
|
||||||
|
} as GoogleAnalytics;
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
try {
|
||||||
|
switch (googleAnalytics.resource) {
|
||||||
|
case 'report':
|
||||||
|
const propertyType = this.getNodeParameter('propertyType', 0) as string;
|
||||||
|
const operationBasedOnProperty =
|
||||||
|
`${googleAnalytics.operation}${propertyType}` as ReportBasedOnProperty;
|
||||||
|
responseData = await report[operationBasedOnProperty].execute.call(this, i);
|
||||||
|
break;
|
||||||
|
case 'userActivity':
|
||||||
|
responseData = await userActivity[googleAnalytics.operation].execute.call(this, i);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known`);
|
||||||
|
}
|
||||||
|
|
||||||
|
returnData.push(...responseData);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||||
|
this.helpers.returnJsonArray({ error: error.message }),
|
||||||
|
{ itemData: { item: i } },
|
||||||
|
);
|
||||||
|
returnData.push(...executionErrorData);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.prepareOutputData(returnData);
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { INodeProperties } from 'n8n-workflow';
|
||||||
|
import * as search from './search.operation';
|
||||||
|
|
||||||
|
export { search };
|
||||||
|
|
||||||
|
export const description: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['userActivity'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Search',
|
||||||
|
value: 'search',
|
||||||
|
description: 'Return user activity data',
|
||||||
|
action: 'Search user activity data',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'search',
|
||||||
|
},
|
||||||
|
...search.description,
|
||||||
|
];
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { IExecuteFunctions } from 'n8n-core';
|
||||||
|
import { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||||
|
import { googleApiRequest, googleApiRequestAllItems } from '../../transport';
|
||||||
|
|
||||||
|
export const description: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'View Name or ID',
|
||||||
|
name: 'viewId',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getViews',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['userActivity'],
|
||||||
|
operation: ['search'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
placeholder: '123456',
|
||||||
|
description:
|
||||||
|
'The view from Google Analytics. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
hint: "If there's nothing here, try changing the 'Property type' field above",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'User ID',
|
||||||
|
name: 'userId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['userActivity'],
|
||||||
|
operation: ['search'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
placeholder: '123456',
|
||||||
|
description: 'ID of a user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['search'],
|
||||||
|
resource: ['userActivity'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to return all results or only up to a given limit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['search'],
|
||||||
|
resource: ['userActivity'],
|
||||||
|
returnAll: [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
maxValue: 500,
|
||||||
|
},
|
||||||
|
default: 100,
|
||||||
|
description: 'Max number of results to return',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['search'],
|
||||||
|
resource: ['userActivity'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Activity Types',
|
||||||
|
name: 'activityTypes',
|
||||||
|
type: 'multiOptions',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Ecommerce',
|
||||||
|
value: 'ECOMMERCE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Event',
|
||||||
|
value: 'EVENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Goal',
|
||||||
|
value: 'GOAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pageview',
|
||||||
|
value: 'PAGEVIEW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Screenview',
|
||||||
|
value: 'SCREENVIEW',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
description: 'Type of activites requested',
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function execute(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
index: number,
|
||||||
|
): Promise<INodeExecutionData[]> {
|
||||||
|
//https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/userActivity/search
|
||||||
|
const viewId = this.getNodeParameter('viewId', index);
|
||||||
|
const userId = this.getNodeParameter('userId', index);
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', 0);
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', index);
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
const body: IDataObject = {
|
||||||
|
viewId,
|
||||||
|
user: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (additionalFields.activityTypes) {
|
||||||
|
Object.assign(body, { activityTypes: additionalFields.activityTypes });
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = 'POST';
|
||||||
|
const endpoint = '/v4/userActivity:search';
|
||||||
|
|
||||||
|
if (returnAll) {
|
||||||
|
responseData = await googleApiRequestAllItems.call(this, 'sessions', method, endpoint, body);
|
||||||
|
} else {
|
||||||
|
body.pageSize = this.getNodeParameter('limit', 0);
|
||||||
|
responseData = await googleApiRequest.call(this, method, endpoint, body);
|
||||||
|
responseData = responseData.sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionData = this.helpers.constructExecutionMetaData(
|
||||||
|
this.helpers.returnJsonArray(responseData),
|
||||||
|
{ itemData: { item: index } },
|
||||||
|
);
|
||||||
|
|
||||||
|
return executionData;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||||
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
import * as userActivity from './userActivity/UserActivity.resource';
|
||||||
|
import * as report from './report/Report.resource';
|
||||||
|
|
||||||
|
export const versionDescription: INodeTypeDescription = {
|
||||||
|
displayName: 'Google Analytics',
|
||||||
|
name: 'googleAnalytics',
|
||||||
|
icon: 'file:analytics.svg',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 2,
|
||||||
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
|
description: 'Use the Google Analytics API',
|
||||||
|
defaults: {
|
||||||
|
name: 'Google Analytics',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'googleAnalyticsOAuth2',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Report',
|
||||||
|
value: 'report',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User Activity',
|
||||||
|
value: 'userActivity',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'report',
|
||||||
|
},
|
||||||
|
...report.description,
|
||||||
|
...userActivity.description,
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export interface IData {
|
||||||
|
viewId: string;
|
||||||
|
dimensions?: IDimension[];
|
||||||
|
dimensionFilterClauses?: {
|
||||||
|
filters: IDimensionFilter[];
|
||||||
|
};
|
||||||
|
pageSize?: number;
|
||||||
|
metrics?: IMetric[];
|
||||||
|
dateRanges?: IDataObject[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDimension {
|
||||||
|
name?: string;
|
||||||
|
histogramBuckets?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDimensionFilter {
|
||||||
|
dimensionName?: string;
|
||||||
|
operator?: string;
|
||||||
|
expressions?: string[];
|
||||||
|
}
|
||||||
|
export interface IMetric {
|
||||||
|
expression?: string;
|
||||||
|
alias?: string;
|
||||||
|
formattingType?: string;
|
||||||
|
}
|
255
packages/nodes-base/nodes/Google/Analytics/v2/helpers/utils.ts
Normal file
255
packages/nodes-base/nodes/Google/Analytics/v2/helpers/utils.ts
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
import { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-core';
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
INodeListSearchItems,
|
||||||
|
INodePropertyOptions,
|
||||||
|
NodeOperationError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
// tslint:disable-next-line:no-any
|
||||||
|
export function simplify(responseData: any | [any]) {
|
||||||
|
const returnData = [];
|
||||||
|
for (const {
|
||||||
|
columnHeader: { dimensions, metricHeader },
|
||||||
|
data: { rows },
|
||||||
|
} of responseData) {
|
||||||
|
if (rows === undefined) {
|
||||||
|
// Do not error if there is no data
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const metrics = metricHeader.metricHeaderEntries.map((entry: { name: string }) => entry.name);
|
||||||
|
for (const row of rows) {
|
||||||
|
const rowDimensions: IDataObject = {};
|
||||||
|
const rowMetrics: IDataObject = {};
|
||||||
|
if (dimensions) {
|
||||||
|
for (let i = 0; i < dimensions.length; i++) {
|
||||||
|
rowDimensions[dimensions[i]] = row.dimensions[i];
|
||||||
|
for (const [index, metric] of metrics.entries()) {
|
||||||
|
rowMetrics[metric] = row.metrics[0].values[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [index, metric] of metrics.entries()) {
|
||||||
|
rowMetrics[metric] = row.metrics[0].values[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
returnData.push({ ...rowDimensions, ...rowMetrics });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line:no-any
|
||||||
|
export function merge(responseData: [any]) {
|
||||||
|
const response: { columnHeader: IDataObject; data: { rows: [] } } = {
|
||||||
|
columnHeader: responseData[0].columnHeader,
|
||||||
|
data: responseData[0].data,
|
||||||
|
};
|
||||||
|
const allRows = [];
|
||||||
|
for (const {
|
||||||
|
data: { rows },
|
||||||
|
} of responseData) {
|
||||||
|
allRows.push(...rows);
|
||||||
|
}
|
||||||
|
response.data.rows = allRows as [];
|
||||||
|
return [response];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function simplifyGA4(response: IDataObject) {
|
||||||
|
if (!response.rows) return [];
|
||||||
|
const dimensionHeaders = ((response.dimensionHeaders as IDataObject[]) || []).map(
|
||||||
|
(header) => header.name as string,
|
||||||
|
);
|
||||||
|
const metricHeaders = ((response.metricHeaders as IDataObject[]) || []).map(
|
||||||
|
(header) => header.name as string,
|
||||||
|
);
|
||||||
|
const returnData: IDataObject[] = [];
|
||||||
|
|
||||||
|
(response.rows as IDataObject[]).forEach((row) => {
|
||||||
|
if (!row) return;
|
||||||
|
const rowDimensions: IDataObject = {};
|
||||||
|
const rowMetrics: IDataObject = {};
|
||||||
|
dimensionHeaders.forEach((dimension, index) => {
|
||||||
|
rowDimensions[dimension] = (row.dimensionValues as IDataObject[])[index].value;
|
||||||
|
});
|
||||||
|
metricHeaders.forEach((metric, index) => {
|
||||||
|
rowMetrics[metric] = (row.metricValues as IDataObject[])[index].value;
|
||||||
|
});
|
||||||
|
returnData.push({ ...rowDimensions, ...rowMetrics });
|
||||||
|
});
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processFilters(expression: IDataObject): IDataObject[] {
|
||||||
|
const processedFilters: IDataObject[] = [];
|
||||||
|
|
||||||
|
Object.entries(expression).forEach((entry) => {
|
||||||
|
const [filterType, filters] = entry;
|
||||||
|
|
||||||
|
(filters as IDataObject[]).forEach((filter) => {
|
||||||
|
let fieldName = '';
|
||||||
|
switch (filter.listName) {
|
||||||
|
case 'other':
|
||||||
|
fieldName = filter.name as string;
|
||||||
|
delete filter.name;
|
||||||
|
break;
|
||||||
|
case 'custom':
|
||||||
|
fieldName = filter.name as string;
|
||||||
|
delete filter.name;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fieldName = filter.listName as string;
|
||||||
|
}
|
||||||
|
delete filter.listName;
|
||||||
|
|
||||||
|
if (filterType === 'inListFilter') {
|
||||||
|
filter.values = (filter.values as string).split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterType === 'numericFilter') {
|
||||||
|
filter.value = {
|
||||||
|
[filter.valueType as string]: filter.value,
|
||||||
|
};
|
||||||
|
delete filter.valueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterType === 'betweenFilter') {
|
||||||
|
filter.fromValue = {
|
||||||
|
[filter.valueType as string]: filter.fromValue,
|
||||||
|
};
|
||||||
|
filter.toValue = {
|
||||||
|
[filter.valueType as string]: filter.toValue,
|
||||||
|
};
|
||||||
|
delete filter.valueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedFilters.push({
|
||||||
|
filter: {
|
||||||
|
fieldName,
|
||||||
|
[filterType]: filter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return processedFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareDateRange(
|
||||||
|
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||||
|
period: string,
|
||||||
|
itemIndex: number,
|
||||||
|
) {
|
||||||
|
const dateRanges: IDataObject[] = [];
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case 'today':
|
||||||
|
dateRanges.push({
|
||||||
|
startDate: DateTime.local().startOf('day').toISODate(),
|
||||||
|
endDate: DateTime.now().toISODate(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'yesterday':
|
||||||
|
dateRanges.push({
|
||||||
|
startDate: DateTime.local().startOf('day').minus({ days: 1 }).toISODate(),
|
||||||
|
endDate: DateTime.local().endOf('day').minus({ days: 1 }).toISODate(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'lastCalendarWeek':
|
||||||
|
const begginingOfLastWeek = DateTime.local().startOf('week').minus({ weeks: 1 }).toISODate();
|
||||||
|
const endOfLastWeek = DateTime.local().endOf('week').minus({ weeks: 1 }).toISODate();
|
||||||
|
dateRanges.push({
|
||||||
|
startDate: begginingOfLastWeek,
|
||||||
|
endDate: endOfLastWeek,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'lastCalendarMonth':
|
||||||
|
const begginingOfLastMonth = DateTime.local()
|
||||||
|
.startOf('month')
|
||||||
|
.minus({ months: 1 })
|
||||||
|
.toISODate();
|
||||||
|
const endOfLastMonth = DateTime.local().endOf('month').minus({ months: 1 }).toISODate();
|
||||||
|
dateRanges.push({
|
||||||
|
startDate: begginingOfLastMonth,
|
||||||
|
endDate: endOfLastMonth,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'last7days':
|
||||||
|
dateRanges.push({
|
||||||
|
startDate: DateTime.now().minus({ days: 7 }).toISODate(),
|
||||||
|
endDate: DateTime.now().toISODate(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'last30days':
|
||||||
|
dateRanges.push({
|
||||||
|
startDate: DateTime.now().minus({ days: 30 }).toISODate(),
|
||||||
|
endDate: DateTime.now().toISODate(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'custom':
|
||||||
|
const start = DateTime.fromISO(this.getNodeParameter('startDate', itemIndex, '') as string);
|
||||||
|
const end = DateTime.fromISO(this.getNodeParameter('endDate', itemIndex, '') as string);
|
||||||
|
|
||||||
|
if (start > end) {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
this.getNode(),
|
||||||
|
`Parameter Start: ${start.toISO()} cannot be after End: ${end.toISO()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dateRanges.push({
|
||||||
|
startDate: start.toISODate(),
|
||||||
|
endDate: end.toISODate(),
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NodeOperationError(
|
||||||
|
this.getNode(),
|
||||||
|
`The period '${period}' is not supported, to specify own period use 'custom' option`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dateRanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultStartDate = () => DateTime.now().startOf('day').minus({ days: 8 }).toISO();
|
||||||
|
|
||||||
|
export const defaultEndDate = () => DateTime.now().startOf('day').minus({ days: 1 }).toISO();
|
||||||
|
|
||||||
|
export function checkDuplicates(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
data: IDataObject[],
|
||||||
|
key: string,
|
||||||
|
type: string,
|
||||||
|
) {
|
||||||
|
const fields = data.map((item) => item[key] as string);
|
||||||
|
const duplicates = fields.filter((field, i) => fields.indexOf(field) !== i);
|
||||||
|
const unique = Array.from(new Set(duplicates));
|
||||||
|
if (unique.length) {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
this.getNode(),
|
||||||
|
`A ${type} is specified more than once (${unique.join(', ')})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortLoadOptions(data: INodePropertyOptions[] | INodeListSearchItems[]) {
|
||||||
|
const returnData = [...data];
|
||||||
|
returnData.sort((a, b) => {
|
||||||
|
const aName = a.name.toLowerCase();
|
||||||
|
const bName = b.name.toLowerCase();
|
||||||
|
if (aName < bName) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (aName > bName) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * as loadOptions from './loadOptions';
|
||||||
|
export * as listSearch from './listSearch';
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { ILoadOptionsFunctions, INodeListSearchItems, INodeListSearchResult } from 'n8n-workflow';
|
||||||
|
import { sortLoadOptions } from '../helpers/utils';
|
||||||
|
import { googleApiRequest } from '../transport';
|
||||||
|
|
||||||
|
export async function searchProperties(
|
||||||
|
this: ILoadOptionsFunctions,
|
||||||
|
): Promise<INodeListSearchResult> {
|
||||||
|
const returnData: INodeListSearchItems[] = [];
|
||||||
|
|
||||||
|
const { accounts } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'https://analyticsadmin.googleapis.com/v1alpha/accounts',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const acount of accounts || []) {
|
||||||
|
const { properties } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{ filter: `parent:${acount.name}` },
|
||||||
|
'https://analyticsadmin.googleapis.com/v1alpha/properties',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (properties && properties.length > 0) {
|
||||||
|
for (const property of properties) {
|
||||||
|
const name = property.displayName;
|
||||||
|
const value = property.name.split('/')[1] || property.name;
|
||||||
|
const url = `https://analytics.google.com/analytics/web/#/p${value}/`;
|
||||||
|
returnData.push({ name, value, url });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
results: sortLoadOptions(returnData),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchViews(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||||
|
const returnData: INodeListSearchItems[] = [];
|
||||||
|
const { items } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'https://www.googleapis.com/analytics/v3/management/accounts/~all/webproperties/~all/profiles',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
returnData.push({
|
||||||
|
name: `${item.name} [${item.websiteUrl}]`,
|
||||||
|
value: item.id,
|
||||||
|
url: `https://analytics.google.com/analytics/web/#/report-home/a${item.accountId}w${item.internalWebPropertyId}p${item.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
results: sortLoadOptions(returnData),
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
|
||||||
|
import { sortLoadOptions } from '../helpers/utils';
|
||||||
|
import { googleApiRequest } from '../transport';
|
||||||
|
|
||||||
|
export async function getDimensions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const { items: dimensions } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'https://www.googleapis.com/analytics/v3/metadata/ga/columns',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const dimesion of dimensions) {
|
||||||
|
if (dimesion.attributes.type === 'DIMENSION' && dimesion.attributes.status !== 'DEPRECATED') {
|
||||||
|
returnData.push({
|
||||||
|
name: dimesion.attributes.uiName,
|
||||||
|
value: dimesion.id,
|
||||||
|
description: dimesion.attributes.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortLoadOptions(returnData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMetrics(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const { items: metrics } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'https://www.googleapis.com/analytics/v3/metadata/ga/columns',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const metric of metrics) {
|
||||||
|
if (metric.attributes.type === 'METRIC' && metric.attributes.status !== 'DEPRECATED') {
|
||||||
|
returnData.push({
|
||||||
|
name: metric.attributes.uiName,
|
||||||
|
value: metric.id,
|
||||||
|
description: metric.attributes.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortLoadOptions(returnData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getViews(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const { items } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'https://www.googleapis.com/analytics/v3/management/accounts/~all/webproperties/~all/profiles',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
returnData.push({
|
||||||
|
name: item.name,
|
||||||
|
value: item.id,
|
||||||
|
description: item.websiteUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortLoadOptions(returnData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
|
||||||
|
const { accounts } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
'https://analyticsadmin.googleapis.com/v1alpha/accounts',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const acount of accounts || []) {
|
||||||
|
const { properties } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{},
|
||||||
|
{ filter: `parent:${acount.name}` },
|
||||||
|
'https://analyticsadmin.googleapis.com/v1alpha/properties',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (properties && properties.length > 0) {
|
||||||
|
for (const property of properties) {
|
||||||
|
const name = property.displayName;
|
||||||
|
const value = property.name.split('/')[1] || property.name;
|
||||||
|
returnData.push({ name, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortLoadOptions(returnData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDimensionsGA4(
|
||||||
|
this: ILoadOptionsFunctions,
|
||||||
|
): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const propertyId = this.getNodeParameter('propertyId', undefined, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as string;
|
||||||
|
const { dimensions } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
`/v1beta/properties/${propertyId}/metadata`,
|
||||||
|
{},
|
||||||
|
{ fields: 'dimensions' },
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const dimesion of dimensions) {
|
||||||
|
returnData.push({
|
||||||
|
name: dimesion.uiName as string,
|
||||||
|
value: dimesion.apiName as string,
|
||||||
|
description: dimesion.description as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sortLoadOptions(returnData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMetricsGA4(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const propertyId = this.getNodeParameter('propertyId', undefined, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as string;
|
||||||
|
const { metrics } = await googleApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
`/v1beta/properties/${propertyId}/metadata`,
|
||||||
|
{},
|
||||||
|
{ fields: 'metrics' },
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const metric of metrics) {
|
||||||
|
returnData.push({
|
||||||
|
name: metric.uiName as string,
|
||||||
|
value: metric.apiName as string,
|
||||||
|
description: metric.description as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sortLoadOptions(returnData);
|
||||||
|
}
|
104
packages/nodes-base/nodes/Google/Analytics/v2/transport/index.ts
Normal file
104
packages/nodes-base/nodes/Google/Analytics/v2/transport/index.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import { OptionsWithUri } from 'request';
|
||||||
|
import { IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions } from 'n8n-core';
|
||||||
|
import { IDataObject, NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export async function googleApiRequest(
|
||||||
|
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
|
||||||
|
method: string,
|
||||||
|
endpoint: string,
|
||||||
|
body: IDataObject = {},
|
||||||
|
qs: IDataObject = {},
|
||||||
|
uri?: string,
|
||||||
|
option: IDataObject = {},
|
||||||
|
) {
|
||||||
|
const propertyType = this.getNodeParameter('propertyType', 0, 'universal') as string;
|
||||||
|
const baseURL =
|
||||||
|
propertyType === 'ga4'
|
||||||
|
? 'https://analyticsdata.googleapis.com'
|
||||||
|
: 'https://analyticsreporting.googleapis.com';
|
||||||
|
|
||||||
|
let options: OptionsWithUri = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
qs,
|
||||||
|
uri: uri ?? `${baseURL}${endpoint}`,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
options = Object.assign({}, options, option);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Object.keys(body).length === 0) {
|
||||||
|
delete options.body;
|
||||||
|
}
|
||||||
|
if (Object.keys(qs).length === 0) {
|
||||||
|
delete options.qs;
|
||||||
|
}
|
||||||
|
return await this.helpers.requestOAuth2.call(this, 'googleAnalyticsOAuth2', options);
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = (error.message || '').split(' - ')[1] as string;
|
||||||
|
if (errorData) {
|
||||||
|
const parsedError = JSON.parse(errorData.trim());
|
||||||
|
if (parsedError.error?.message) {
|
||||||
|
const [message, ...rest] = parsedError.error.message.split('\n');
|
||||||
|
const description = rest.join('\n');
|
||||||
|
const httpCode = parsedError.error.code;
|
||||||
|
throw new NodeApiError(this.getNode(), error, { message, description, httpCode });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NodeApiError(this.getNode(), error, { message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function googleApiRequestAllItems(
|
||||||
|
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||||
|
propertyName: string,
|
||||||
|
method: string,
|
||||||
|
endpoint: string,
|
||||||
|
body: IDataObject = {},
|
||||||
|
query: IDataObject = {},
|
||||||
|
uri?: string,
|
||||||
|
) {
|
||||||
|
const propertyType = this.getNodeParameter('propertyType', 0, 'universal') as string;
|
||||||
|
const returnData: IDataObject[] = [];
|
||||||
|
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
if (propertyType === 'ga4') {
|
||||||
|
let rows: IDataObject[] = [];
|
||||||
|
query.limit = 100000;
|
||||||
|
query.offset = 0;
|
||||||
|
|
||||||
|
responseData = await googleApiRequest.call(this, method, endpoint, body, query, uri);
|
||||||
|
rows = rows.concat(responseData.rows);
|
||||||
|
query.offset = rows.length;
|
||||||
|
|
||||||
|
while (responseData.rowCount > rows.length) {
|
||||||
|
responseData = await googleApiRequest.call(this, method, endpoint, body, query, uri);
|
||||||
|
rows = rows.concat(responseData.rows);
|
||||||
|
query.offset = rows.length;
|
||||||
|
}
|
||||||
|
responseData.rows = rows;
|
||||||
|
returnData.push(responseData);
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
responseData = await googleApiRequest.call(this, method, endpoint, body, query, uri);
|
||||||
|
if (body.reportRequests && Array.isArray(body.reportRequests)) {
|
||||||
|
(body.reportRequests as IDataObject[])[0].pageToken =
|
||||||
|
responseData[propertyName][0].nextPageToken;
|
||||||
|
} else {
|
||||||
|
body.pageToken = responseData.nextPageToken;
|
||||||
|
}
|
||||||
|
returnData.push.apply(returnData, responseData[propertyName]);
|
||||||
|
} while (
|
||||||
|
(responseData.nextPageToken !== undefined && responseData.nextPageToken !== '') ||
|
||||||
|
responseData[propertyName]?.[0].nextPageToken !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
Loading…
Reference in a new issue