Like Jitsu? Give us a star on ā­ GitHub!

šŸ“œ Configuration

Configuration UI

šŸ‘©ā€šŸ”¬ Extending Jitsu

Overview
Destination Extensions
Source Extensions
API Specs

Jitsu Internals

Destination Extensions (Plugins)

  1. Overview
  2. Quickstart
  3. Project Structure
  4. Destination Config
  5. Pushing to the Destination
  6. Providing Meta Information
  7. Writing a Test
  8. Adding to Jitsu

Overview

Jitsu Destination Plugins allow anyone to implement a new destination type for Jitsu and publish it to make it available for all users of Jitsu.

Jitsu Destination Plugins are designed to work with HTTP APIs in stream mode.

A plugin receives a Jitsu event and returns objects describing HTTP requests necessary to push data to destinations.

Quickstart

We need to use Jitsu SDK's CLI tool to bootstrap a project for new destination plugin:

npx jitsu-cli extension create --type destination

nodejs and npx must be installed on your system.

jitsu-cli creates a functioning project for a destination plugin. All parts are working, but they are placeholder implementation and don't do anything meaningful.

If you are an experienced developer, you can start replacing placeholder logic with your own right away.

This article will explain the creation of a Jitsu destination plugin step by step.
As an example, we will implement a simple destination that will post a message to a Slack webhook on receiving a Jitsu event with provided types.

Project Structure

jitsu-cli generates project directory structure with a set of files typical for Typescript node.js project:

ā”œā”€ā”€ package.json
ā”œā”€ā”€ src
ā”‚   ā”œā”€ā”€ destination.ts
ā”‚   ā””ā”€ā”€ index.ts
ā”œā”€ā”€ __test__
ā”‚   ā””ā”€ā”€ destination.test.ts
ā””ā”€ā”€ tsconfig.json
  • package.json ā€“ file contains meta-information about npm project including name, version
  • src/index.ts ā€“ file contains the instance of ExtensionDescriptor that Jitsu uses to collect info about the destination: id, icon, name, description, configuration parameters.
  • src/destination.ts ā€“ file where must be implemented main logic of destination along with config object and config validator
  • __test__/destination.test.ts ā€“ test for destination logic must be written here
  • tsconfig.json ā€“ settings for Typescript compiler. No need to change that

We recommend working on the project in an integrated development environment (IDE) like Visual Studio Code or WebStorm.

Destination Config

No destination can work without configuration. Let's open src/destination.ts file and prepare the DestinationConfig object for our Slack destination. Config will consist of webhook URL, list of event types to react on, and message template for Slack message:

export type DestinationConfig = {
  webhookUrl: string, //url of slack webhook https://hooks.slack.com/services/ABC/XYZ/etc
  eventTypes: string, //comma-separated list of event types to trigger Slack message
  messageTemplate: string //message template that will be filled with data from event e.g. `New event: ${event_type}!`
}

It is nice to tell users in advance if they make a mistake in their config. That is why destination plugins have ConfigValidator

Validating Config

Once we have the destination config, we can implement the validator. It is an optional part, but we highly recommend implementing it.

ConfigValidator is the only part of the plugin that can access internet using the fetch method. The main logic of plugins relies on Jitsu Server pipelines for sending HTTP requests.

We replace placeholder implementation with to following code

export const validator: ConfigValidator<DestinationConfig> = async (config: DestinationConfig) => {
  //check that all config parameters are present
  if (!config.webhookUrl) {
    return "Missing required parameter: webhookUrl";
  }
  if (!config.eventTypes) {
    return "Missing required parameter: eventTypes";
  }
  if (!config.messageTemplate) {
    return "Missing required parameter: messageTemplate";
  }
  //check validness of provided webhookUrl.
  try {
    //validator must not send any real messages, so we intentionally miss request body
    let response = await fetch(config.webhookUrl, { method: 'post' });
    let responseText = await response.text()
    if (responseText == "invalid_payload") {
      //invalid_payload - is success case because we haven't sent any. It means that webhookUrl is correct
      //otherwise that would be other kind of error response
      return true
    } else {
      return "Error: " + responseText
    }
  } catch (error) {
    return "Error: " + error.toString()
  }
}

Now we can test our validator with validate-config action that jitsu-cli already added to the project

yarn build && yarn validate-config -c '{"webhookUrl":"https://hooks.slack.com/services/ABC/XYZ/etc", "eventTypes":"registration,error", "messageTemplate": "Important event of type: ${event_type}"}'

alternatively config may be stored in JSON-file:

yarn build && yarn validate-config -c config.json

If everything is fine, we should get the following output:

[info ] - šŸ¤” Validating configuration {"webhookUrl":"https://hooks.slack.com/services/ABC/XYZ/etc", "eventTypes":"registration,error", "messageTemplate": "Important event of type: ${event_type}"}
[info ] - āœ… Config is valid. Hooray!
[info ] - āœØ Done

Pushing to the Destination

Lets get to the main part of plugin ā€“ pushing data to the target destination. We need to write proper version of DestinationFunction instead of placeholder:

export const destination: DestinationFunction = (event: DefaultJitsuEvent, dstContext: JitsuDestinationContext<DestinationConfig>) => {
  return { url: "https://test.com", method: "POST", body: { a: (event.a || 0) + 1 } };
};

DestinationFunction receives two parameters:

  • event ā€“ type: DefaultJitsuEvent ā€“ event received and enriched by Jitsu Server
  • dstContext - type: JitsuDestinationContext - destination context containing: destinationId, destinationType and what is more important: config - config object that we described at the beginning filled by Jitsu Server

Return values

The main plugin code doesn't support async execution and doesn't have access to any external resources. So instead, DestinationFunction needs to return:

  • a single instance of DestinationMessage
  • an array of DestinationMessage's - to produce multiple pushes to destination
  • null - null means that Jitsu must skip this event.

DestinationMessage is an object that tells Jitsu Server what HTTP request it needs to make to push data to the destination:

export declare type DestinationMessage = {
  url: string;
  method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
  headers?: { [key: string]: string };
  body: any;
};

Jitsu server is in charge of queuing, executing, and retrying HTTP requests, the same as with built-in destinations like webhook.

Writing DestinationFunction

Now we can write DestinationFunction implementation that prepares DestinationMessage with HTTP request that creates a new Slack Message. DestinationFunction must skip all events whose types differ from those provided in the config. Let's get back to the src/destination.ts file and write some code:

//Function that fill string template with values from obj
function renderTemplateMessage(str, obj) {
    const get = (obj: any, key: string | string[]) => {
        if (typeof key == 'string')
            key = key.split('.');

        if (key.length == 1)
            return obj[key[0]];
        else if (key.length == 0)
            return obj;
        else
            return get(obj[key[0]], key.slice(1));
    }
    return str.replace(/\$\{(.+)\}/g, (match, p1) => {
        return get(obj, p1)
    })
}

export const destination: DestinationFunction = (event: DefaultJitsuEvent, dstContext: JitsuDestinationContext<DestinationConfig>) => {
    const eventTypes = dstContext.config.eventTypes.split(",")
    if (!eventTypes.includes(event.event_type)) {
        return null
    }
    let messageText = renderTemplateMessage(dstContext.config.messageTemplate, event)
    if (!messageText) {
        return null;
    }
    //Using Block Kit to build message. See: https://api.slack.com/block-kit
    let blocks = [];
    blocks.push({
        "type": "section",
        "text": {
            "type": "plain_text",
            "text": messageText
        }
    })
    if (event.slack_destinantion_message_blocks) {
        //extensibility with javascript transformation
        blocks.push(...event.slack_destinantion_message_blocks)
    }
    return {
        url: dstContext.config.webhookUrl,
        method: "POST",
        body: {
            "blocks": blocks
        }
    };
};

That is it!

Testing destination

We need Jitsu Server to run the plugin, but jitsu-cli created our project with the execute action that allows executing the plugin for a single event. exec action performs HTTP requests from returned DestinationMessage.

You need to prepare JSON file event.json with a sample of a Jitsu event first.

yarn build && yarn execute --file event.json -c '{"webhookUrl":"https://hooks.slack.com/services/ABC/XYZ/etc", "eventTypes":"registration,error", "messageTemplate": "Important event of type: ${event_type}"}'

or with config from file:

yarn build && yarn execute --file event.json -c config.json

Since exec action actually performs HTTP request, it makes changes in the destination. We not always can afford to put test data to a destination. In that case, it is good to write an automated test that checks the correctness of returned DestinationMessage's in most possible cases. See Writing a Test section

Providing Meta Information

Let's open file src/index.ts Here we can find a placeholder object that jitsu-cli made for us. Let's fill it with some real data

const descriptor: ExtensionDescriptor = {
  id: "jitsu-slack-destination",
  displayName: "Slack",
  icon: "<svg enable-background=\"new 0 0 2447.6 2452.5\" viewBox=\"0 0 2447.6 2452.5\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><g clip-rule=\"evenodd\" fill-rule=\"evenodd\"><path d=\"m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z\" fill=\"#36c5f0\"\/><path d=\"m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z\" fill=\"#2eb67d\"\/><path d=\"m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z\" fill=\"#ecb22e\"\/><path d=\"m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0\" fill=\"#e01e5a\"\/><\/g><\/svg>",
  description: "Destination that posts messages to Slack Webhook on receiving Jitsu evens with specified event_types",
  configurationParameters: [
    {
      id: "webhookUrl",
      type: "string",
      required: true,
      displayName: "Webhook URL",
      documentation: "Url of slack webhook https://hooks.slack.com/services/ABC/XYZ/etc"
    },
    {
      id: "eventTypes",
      type: "string",
      required: true,
      displayName: "Event Types",
      documentation: "comma separated list of event types to trigger Slack message"
    },
    {
      id: "messageTemplate",
      type: "string",
      required: true,
      displayName: "Message Template",
      documentation: "Template of Slack message.<br/>You can use ${parameter} expressions to add values from incoming event, e.g.:<br/>Welcome ${user.id}<br/>Received event ${event_type}"
    }
  ],
};

configurationParameters's parameters can have type from the list:

  • string
  • int
  • boolean
  • password
  • json
  • dashDate - Date formatted like YYYY-MM-DD, e.g: 2022-01-31
  • isoUtcDate - Date and time formatted according to ISO_8601 standard, e.g.: 2022-01-31T10:28:13Z

If you want Jitsu to display a nice graphic icon along with destination name you need to provide svg code of icon to "icon" parameter of ExtensionDescriptor.

Writing a Test

Tests allow to check plugin logic without actually posting anything to the destination.

Let's open __test__/destination.test.ts To write a test we simply need to make a call to the function testDestination. testDestination accepts a single parameter - object of DestinationTestParams type:

export declare type DestinationTestParams = {
  name: string;                                    //name of the test
  context: JitsuDestinationContext;                //JitsuDestinationContext containing: destinationId, destinationType and config
  destination: DestinationFunction;                //DestinationFunction we are going to test
  event: DefaultJitsuEvent;                        //JitsuEvent
  expectedResult: ObjectSet<DestinationMessage>;   //Result object we expect to get after processing event with provided DestinationFunction
};

To implement the test we need to fill DestinationTestParams properties and pass it to the function testDestination

testDestination({
    name: "proper case",

    context: {
        destinationId: "test",
        destinationType: "slack",
        config: {
            "webhookUrl": "https://hooks.slack.com/services/ABC/XYZ/etc",
            "eventTypes": "registration,error",
            "messageTemplate": "Important event of type: ${event_type}"
        }
    },

    destination: destination,

    event: {
        event_type: 'registration'
    },

    expectedResult: {
        method: "POST",
        url: "https://hooks.slack.com/services/ABC/XYZ/etc",
        body: {
            "blocks": [
                {
                    "type": "section",
                    "text": {
                        "type": "plain_text",
                        "text": "Important event of type: registration"
                    }
                }
            ]
        }
    },
})

This is a test for "proper case". We provide testDestination function with:

  • properly set context with a config,
  • Jitsu event - very short one with fields that may be used in plugin code
  • our expectation of HTTP request jitsu should make to a Slack webhook URL in case of receiving such an event.

Now let's run tests to make sure that everything works fine.

yarn test

Output must contain the following lines.

PASS  __test__/destination.test.ts
 āœ“ proper case (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

One test for "proper case" probably won't be enough in a real plugin for production usage. It would be great to add test cases when event_type is not "registration" to make sure that nothing wrong happens in that cases, and we get null result.

Adding to Jitsu

To add our plugin to jitsu we need to build and publish it.

To build plugin code use:

yarn build

Publishing to NPM Repository

Publishing plugin to public npm repository to make it available for other users. You need to have an account in https://www.npmjs.com

The following commands in the project directory will publish the package to the npmjs repository:

npm login
npm publish

npm will ask to provide some additional details to complete the publishing.

Setting up Jitsu Server

Users of a standalone jitsu server can setup a destination based on plugin since version 1.38. Add a new destination of type npm, information about plugin package, and config to eventnative.yaml config file:

destinations:
...
  my_slack_destination:
    only_tokens:
      - my_token
    type: npm
    package: jitsu-slack-destination@^1.0.0
    mode: stream
    config:
      webhookUrl: "https://hooks.slack.com/services/ABC/XYZ/etc"
      eventTypes: "registration,error"
      messageTemplate: "Important event of type: ${event_type}"

package can be:

  • npm package name - if a plugin is published to npm repository. We recommend providing package name with version expression to prevent backward compatibility issues: jitsu-slack-destination@^1.0.0
  • HTTP URL - e.g.: https://my-site.com/plugins/jitsu-slack-destination.tgz
  • filesystem path - in case of a docker image, provided path needs to be reachable inside of docker image filesystem. /home/eventnative/data/plugins/ needs to be mounted to host filesystem directory where plugin's .tgz is located, e.g. following param may be added to docker run command: -v /Users/testaccount/projects/:/home/eventnative/data/plugins/

Setting up Jitsu Joint Image or Configurator UI

UI support for adding plugin based destinations is not ready yet. To make your destination plugin appear in Jitsu Configurator UI please create a new ticket or pull request in the jitsu repository

Publishing plugin locally

If there is no intention to publish plugin for other users, you can keep it for yourself.

You need to build .tgz package manually:

Use a command like that to make compressed .tgz package of destination project directory:

tar -cvzf package_name.tgz -C /workspace_directory/ destination_project_directory

In case of our destination command will look like:

tar -cvzf jitsu-slack-destination.tgz -C /Users/testaccount/projects/ jitsu-slack-destination

jitsu-slack-destination.tgz file will appear in the current directory.

See Setting Up Jitsu Server to check how to pass local package to Jitsu Server