From bb794b6da319810e0e3e944a1ab2f0523d420d63 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 14 Feb 2021 11:56:34 -0500 Subject: [PATCH] :sparkles: Add PostHog Node (#1440) * :sparkles: PostHog Node * :zap: Minor improvements to PostHog Node Co-authored-by: Jan Oberhauser --- .../credentials/PostHogApi.credentials.ts | 23 ++ .../nodes/PostHog/AliasDescription.ts | 126 +++++++++ .../nodes/PostHog/EventDescription.ts | 126 +++++++++ .../nodes/PostHog/GenericFunctions.ts | 80 ++++++ .../nodes/PostHog/IdentityDescription.ts | 113 ++++++++ .../nodes-base/nodes/PostHog/PostHog.node.ts | 248 ++++++++++++++++++ .../nodes/PostHog/TrackDescription.ts | 175 ++++++++++++ packages/nodes-base/nodes/PostHog/postHog.svg | 1 + packages/nodes-base/package.json | 2 + 9 files changed, 894 insertions(+) create mode 100644 packages/nodes-base/credentials/PostHogApi.credentials.ts create mode 100644 packages/nodes-base/nodes/PostHog/AliasDescription.ts create mode 100644 packages/nodes-base/nodes/PostHog/EventDescription.ts create mode 100644 packages/nodes-base/nodes/PostHog/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/PostHog/IdentityDescription.ts create mode 100644 packages/nodes-base/nodes/PostHog/PostHog.node.ts create mode 100644 packages/nodes-base/nodes/PostHog/TrackDescription.ts create mode 100644 packages/nodes-base/nodes/PostHog/postHog.svg diff --git a/packages/nodes-base/credentials/PostHogApi.credentials.ts b/packages/nodes-base/credentials/PostHogApi.credentials.ts new file mode 100644 index 0000000000..cce3f99cf6 --- /dev/null +++ b/packages/nodes-base/credentials/PostHogApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class PostHogApi implements ICredentialType { + name = 'postHogApi'; + displayName = 'PostHog API'; + properties = [ + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: 'https://app.posthog.com', + }, + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/PostHog/AliasDescription.ts b/packages/nodes-base/nodes/PostHog/AliasDescription.ts new file mode 100644 index 0000000000..072a450dd7 --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/AliasDescription.ts @@ -0,0 +1,126 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const aliasOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'alias', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an alias', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const aliasFields = [ + + /* -------------------------------------------------------------------------- */ + /* alias:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Alias', + name: 'alias', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'alias', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'The name of the alias.', + }, + { + displayName: 'Distinct ID', + name: 'distinctId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'alias', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The user's distinct ID.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'alias', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Context', + name: 'contextUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Context', + name: 'contextValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + description: `If not set, it'll automatically be set to the current time.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/PostHog/EventDescription.ts b/packages/nodes-base/nodes/PostHog/EventDescription.ts new file mode 100644 index 0000000000..920f636c87 --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/EventDescription.ts @@ -0,0 +1,126 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an event', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + + /* -------------------------------------------------------------------------- */ + /* event:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event', + name: 'eventName', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'The name of the event.', + }, + { + displayName: 'Distinct ID', + name: 'distinctId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The user's distinct ID.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Properties', + name: 'propertiesUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'propertyValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + description: `If not set, it'll automatically be set to the current time.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/PostHog/GenericFunctions.ts b/packages/nodes-base/nodes/PostHog/GenericFunctions.ts new file mode 100644 index 0000000000..ad64cc006d --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/GenericFunctions.ts @@ -0,0 +1,80 @@ +import { + OptionsWithUrl, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function posthogApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('postHogApi') as IDataObject; + + const base = credentials.url as string; + + body.api_key = credentials.apiKey as string; + + const options: OptionsWithUrl = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + url: `${base}${path}`, + json: true, + }; + + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + return await this.helpers.request!(options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + + const message = error.response.body.message; + // Try to return the error prettier + throw new Error( + `PosHog error response [${error.statusCode}]: ${message}`, + ); + } + throw error; + } +} + +export interface IEvent { + event: string; + properties: { [key: string]: any }; // tslint:disable-line:no-any +} + +export interface IAlias { + type: string; + event: string; + properties: { [key: string]: any }; // tslint:disable-line:no-any + context: { [key: string]: any }; // tslint:disable-line:no-any +} + +export interface ITrack { + type: string; + event: string; + name: string; + messageId?: string; + distinct_id: string; + category?: string; + properties: { [key: string]: any }; // tslint:disable-line:no-any + context: { [key: string]: any }; // tslint:disable-line:no-any +} + + +export interface IIdentity { + event: string; + messageId?: string; + distinct_id: string; + properties: { [key: string]: any }; // tslint:disable-line:no-any +} diff --git a/packages/nodes-base/nodes/PostHog/IdentityDescription.ts b/packages/nodes-base/nodes/PostHog/IdentityDescription.ts new file mode 100644 index 0000000000..4f9aff6138 --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/IdentityDescription.ts @@ -0,0 +1,113 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const identityOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'identity', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const identityFields = [ + + /* -------------------------------------------------------------------------- */ + /* identity:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Distinct ID', + name: 'distinctId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'identity', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The identity's distinct ID.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'identity', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Properties', + name: 'propertiesUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'propertyValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + description: `If not set, it'll automatically be set to the current time.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/PostHog/PostHog.node.ts b/packages/nodes-base/nodes/PostHog/PostHog.node.ts new file mode 100644 index 0000000000..f56860731a --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/PostHog.node.ts @@ -0,0 +1,248 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + IAlias, + IEvent, + IIdentity, + ITrack, + posthogApiRequest, +} from './GenericFunctions'; + +import { + aliasFields, + aliasOperations, +} from './AliasDescription'; + +import { + eventFields, + eventOperations, +} from './EventDescription'; + +import { + trackFields, + trackOperations, +} from './TrackDescription'; + +import { + identityFields, + identityOperations, +} from './IdentityDescription'; + +import * as moment from 'moment-timezone'; + +export class PostHog implements INodeType { + description: INodeTypeDescription = { + displayName: 'PostHog', + name: 'postHog', + icon: 'file:postHog.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume PostHog API.', + defaults: { + name: 'PostHog', + color: '#000000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'postHogApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Alias', + value: 'alias', + }, + { + name: 'Event', + value: 'event', + }, + { + name: 'Identity', + value: 'identity', + }, + { + name: 'Track', + value: 'track', + }, + ], + default: 'event', + description: 'The resource to operate on.', + }, + ...aliasOperations, + ...aliasFields, + ...eventOperations, + ...eventFields, + ...identityOperations, + ...identityFields, + ...trackOperations, + ...trackFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + if (resource === 'alias') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + const distinctId = this.getNodeParameter('distinctId', i) as string; + + const alias = this.getNodeParameter('alias', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const context = (additionalFields.contextUi as IDataObject || {}).contextValues as IDataObject[] || []; + + const event: IAlias = { + type: 'alias', + event: '$create_alias', + context: context.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + properties: { + distinct_id: distinctId, + alias, + }, + }; + + Object.assign(event, additionalFields); + + if (additionalFields.timestamp) { + additionalFields.timestamp = moment(additionalFields.timestamp as string).toISOString(); + } + + responseData = await posthogApiRequest.call(this, 'POST', '/batch', event); + + returnData.push(responseData); + } + } + } + + if (resource === 'event') { + if (operation === 'create') { + const events: IEvent[] = []; + for (let i = 0; i < length; i++) { + const eventName = this.getNodeParameter('eventName', i) as string; + + const distinctId = this.getNodeParameter('distinctId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const properties = (additionalFields.propertiesUi as IDataObject || {}).propertyValues as IDataObject[] || []; + + const event: IEvent = { + event: eventName, + properties: properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + }; + + event.properties['distinct_id'] = distinctId; + + Object.assign(event, additionalFields); + + if (additionalFields.timestamp) { + additionalFields.timestamp = moment(additionalFields.timestamp as string).toISOString(); + } + //@ts-ignore + delete event.propertiesUi; + + events.push(event); + } + + responseData = await posthogApiRequest.call(this, 'POST', '/capture', { batch: events }); + + returnData.push(responseData); + } + } + + if (resource === 'identity') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + const distinctId = this.getNodeParameter('distinctId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const properties = (additionalFields.propertiesUi as IDataObject || {}).propertyValues as IDataObject[] || []; + + const event: IIdentity = { + event: '$identify', + properties: properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + distinct_id: distinctId, + }; + + Object.assign(event, additionalFields); + + if (additionalFields.timestamp) { + additionalFields.timestamp = moment(additionalFields.timestamp as string).toISOString(); + } + //@ts-ignore + delete event.propertiesUi; + + responseData = await posthogApiRequest.call(this, 'POST', '/batch', event); + + returnData.push(responseData); + } + } + } + + if (resource === 'track') { + if (operation === 'page' || operation === 'screen') { + for (let i = 0; i < length; i++) { + const distinctId = this.getNodeParameter('distinctId', i) as string; + + const name = this.getNodeParameter('name', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const context = (additionalFields.contextUi as IDataObject || {}).contextValues as IDataObject[] || []; + + const properties = (additionalFields.propertiesUi as IDataObject || {}).propertyValues as IDataObject[] || []; + + const event: ITrack = { + name, + type: operation, + event: `$${operation}`, + context: context.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + distinct_id: distinctId, + properties: properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}), + }; + + Object.assign(event, additionalFields); + + if (additionalFields.timestamp) { + additionalFields.timestamp = moment(additionalFields.timestamp as string).toISOString(); + } + //@ts-ignore + delete event.propertiesUi; + + responseData = await posthogApiRequest.call(this, 'POST', '/batch', event); + + returnData.push(responseData); + } + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/PostHog/TrackDescription.ts b/packages/nodes-base/nodes/PostHog/TrackDescription.ts new file mode 100644 index 0000000000..fb45d329f6 --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/TrackDescription.ts @@ -0,0 +1,175 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const trackOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'track', + ], + }, + }, + options: [ + { + name: 'Page', + value: 'page', + description: 'Track a page', + }, + { + name: 'Screen', + value: 'screen', + description: 'Track a screen', + }, + ], + default: 'page', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const trackFields = [ + + /* -------------------------------------------------------------------------- */ + /* track:page */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'track', + ], + operation: [ + 'page', + 'screen', + ], + }, + }, + default: '', + }, + { + displayName: 'Distinct ID', + name: 'distinctId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'track', + ], + operation: [ + 'page', + 'screen', + ], + }, + }, + default: '', + description: `The user's distinct ID.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'track', + ], + operation: [ + 'page', + 'screen', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Category', + name: 'category', + type: 'string', + default: '', + }, + { + displayName: 'Context', + name: 'contextUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Context', + name: 'contextValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + }, + { + displayName: 'Properties', + name: 'propertiesUi', + type: 'fixedCollection', + placeholder: 'Add Property', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Property', + name: 'propertyValues', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + description: `If not set, it'll automatically be set to the current time.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/PostHog/postHog.svg b/packages/nodes-base/nodes/PostHog/postHog.svg new file mode 100644 index 0000000000..e62492556c --- /dev/null +++ b/packages/nodes-base/nodes/PostHog/postHog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index f3d2dfe3a9..438f6ebf9a 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -171,6 +171,7 @@ "dist/credentials/PipedriveOAuth2Api.credentials.js", "dist/credentials/PhilipsHueOAuth2Api.credentials.js", "dist/credentials/Postgres.credentials.js", + "dist/credentials/PostHogApi.credentials.js", "dist/credentials/PostmarkApi.credentials.js", "dist/credentials/ProfitWellApi.credentials.js", "dist/credentials/PushbulletOAuth2Api.credentials.js", @@ -422,6 +423,7 @@ "dist/nodes/Pipedrive/PipedriveTrigger.node.js", "dist/nodes/PhilipsHue/PhilipsHue.node.js", "dist/nodes/Postgres/Postgres.node.js", + "dist/nodes/PostHog/PostHog.node.js", "dist/nodes/Postmark/PostmarkTrigger.node.js", "dist/nodes/ProfitWell/ProfitWell.node.js", "dist/nodes/Pushbullet/Pushbullet.node.js",