Channel apps
In Optimizely Connect Platform (OCP), channels enable you to deliver content from the Activation feature in Optimizely Data Platform (ODP) through any third-party provider.
After a user installs a channel app, they can add it to any Optimizely Data Platform (ODP) campaign touchpoint, shown below:
The typical channel interface flow is as follows:
Note
For channel app implementation details, see the OCP App SDK documentation.
- Check if the channel is ready to use. For example, ensure the app user has configured their credentials before you attempt to send touchpoints through the channel.
- (Optional) Dynamically determine the target (customer identifier fields such as email address) for a piece of content. If this information is statically known ahead of time, provide it in the
app.yml
file instead. - Validate the content settings and template. This may happen at any time and is not strictly associated with the lifecycle of the content itself. You can provide error messages and toast notifications to the user when they attempt to launch the campaign.
- Publish the content template. This happens when the campaign content changes. You should pre-process campaign content if necessary (for example, if the system you are delivering to uses a different templating system) and store it for use during the campaign deliveries.
- (Optional) Prepare for a campaign run, allowing any setup required by a third party to deliver a set of content. If you do not need this step, you can leave the method unimplemented and turned off in the
app.yml
file withchannel.options.prepare
. - Deliver content to batches of recipients with substitutions. During a campaign run, the target identifier and substitutions are provided in batches of 100 and delivered to the user through the channel.
In addition to implementing the channel interface, a channel app must also have:
- The category
channel
specified in theapp.yml
file. - The
channel
section of theapp.yml
file. - The
forms/content-settings.yml
andforms/content-template.yml
files. These forms display to the user when creating content for a campaign to send to this channel. Provide the data from these forms to thevalidate
andpublish
channel methods.
Form validation
Channel apps are powered by their forms, which contain user input. Apps have two mechanisms to validate user input before a customer is able to go live with a campaign containing the channel app.
Inline validation
In the following forms/content-settings.yml
file example, the URL must start with http://
or https://
. If the user enters anything else, the form displays the following error message before a user can go live: "Must be an http or https url." With this example, the app assumes that when it accesses the form data, the URL is a string value that starts with http://
or https://
.
sections:
- key: request
label: Request
elements:
- key: url
type: text
label: URL
help: HTTP URL to deliver to
required: true
validations:
- regex: "^https?:\\/\\/"
message: Must be an http or https url
Caution
Be careful about making validation changes with new versions of your app. Updates to your forms must be compatible with older campaigns to ensure a smooth user experience. Additionally, you may not enforce form validation added to a newer version of your app on a campaign that was set live with a previous version of your app. You are responsible for migrating the form data at runtime.
Runtime validation
You should handle more complicated validation at runtime with the validate
method, which is called before a user can go live with their campaign. The app can return errors and toast notifications for each form value that is invalid. If the app returns any errors, publish
is not called and the campaign cannot go live.
Note
You do not need any code to enforce validations.
Example channel app
The following files are an example of implementing an API-based channel app. First, the app.yml
file:
meta:
app_id: api_channel
display_name: API Channel
version: 1.0.16
vendor: optimizely
summary: Combine the power of ODP campaigns with any API to deliver content or updates through other integrations.
support_url: https://apps.optimizely.com/api-channel
contact_email: [email protected]
categories:
- Channel
availability:
- us
runtime: node18
functions:
list_identifiers:
entry_point: ListIdentifiers
description: Lists messaging identifiers for the channel form
channel:
type: api
targeting: dynamic
options:
prepare: false
metrics:
delivery:
- sent
reachability:
- soft_bounce
The channel/metrics
section defines event types that are sent to ODP by the app. ODP uses this configuration to determine which events to expect from the app and how to process them. For more information about possible event types, see Campaign and channel events.
Next, the forms/content-settings.yml
file:
sections:
- key: target
label: Targeting
elements:
- key: identifier
label: Target Identifier
type: select
help: Select an ODP Identifier for targeting
required: true
dataSource:
type: app
function: list_identifiers
- key: headers
label: Headers
elements:
- key: h1_key
label: Header Name 1
help: Header name (for example, Content-Type)
type: text
hint: Content-Type
defaultValue: Content-Type
- key: h1_value
label: Header Value 1
help: Header value (for example, application/json)
type: text
hint: application/json
defaultValue: application/json
- key: h2_key
label: Header Name 2
help: Header name (for example, Content-Type)
type: text
hint: Content-Type
- key: h2_value
label: Header Value 2
help: Header value (for example, application/json)
type: text
hint: Content-type
Finally, the forms/content-template.yml
file:
sections:
- key: request
label: Request
elements:
- key: method
label: API Method
help: API Call Method (for example, POST or GET)
type: select
options:
- value: GET
text: GET
- value: POST
text: POST
- value: PUT
text: PUT
- value: PATCH
text: PATCH
- value: DELETE
text: DELETE
hint: Content-Type
defaultValue: Content-Type
- key: url
type: text
label: URL
help: HTTP URL to deliver to
required: true
validations:
- regex: "^https?:\\/\\/"
message: Must be an http or https url
- key: body
type: text
label: Body Content
help: Post Body
multiline: true
defaultValue: "{}"
visible:
operation: any
comparators:
- key: method
equals: 'POST'
- key: method
equals: 'PUT'
- key: method
equals: 'PATCH'
The image below is an example of how the above configuration displays in an ODP campaign:
Number, type, and names of the fields in both forms depends on what your third-party service requires to process the requests. The app's channel interface must support preview, test sends, and delivery. The following is a simplified example of the src/channel/Channel.ts
file:
import * as App from '@zaiusinc/app-sdk';
import {CampaignContent, CampaignDelivery, CampaignTracking, ChannelContentResult, ChannelDeliverOptions, ChannelDeliverResult, ChannelPreviewResult, ChannelPublishOptions, ChannelTargetResult, ChannelValidateOptions, KVHash, storage} from '@zaiusinc/app-sdk';
import {ContentSettingsFormData} from '../lib/ContentSettingsFormData';
import {ContentTemplateFormData} from '../lib/ContentTemplateFormData';
import {previewTemplate} from '../lib/previewTemplate';
/**
* Defines the interface of a channel. The typical channel flow in a campaign run is as follows:
* 1. Check if the channel is `ready` to use
* 2. Dynamically determine the `target` (may be provided statically in `app.yml` file instead)
* 3. `publish` the content template (in the future, this will instead happen when the campaign is modified)
* 4. `prepare` for the run (if the method is implemented)
* 5. `deliver` the content to batches of recipients with substitutions
*
* Outside of the campaign run flow, a channel must also be able to `preview` a given piece of content for a batch
* of recipients.
*/
export class Channel extends App.Channel {
/**
* Checks if the channel is ready to use. This should ensure that any required credentials and/or other configuration
* exist and are valid. Utilize reasonable caching to prevent excessive requests to external resources.
* @async
* @returns true if the channel is ready to use
*/
public async ready(): Promise<boolean> {
return true;
}
/**
* Dynamically determines campaign targeting requirements. It should also perform any necessary validations on the
* input data. If targeting is always known ahead of time, specify this statically in `channel.targeting`
* in the `app.yml` file. If targeting is based on selections made in the content settings form, you must implement this method
* and the value in the `app.yml` file must be set to `dynamic`.
* @async
* @param contentSettings data from the content settings form
* @returns result of the operation
*/
public async target(contentSettings: ContentSettingsFormData): Promise<ChannelTargetResult> {
const result = new ChannelTargetResult();
result.addTargeting({identifier: contentSettings.target.identifier});
return result;
}
/**
* Validates the given content. This should ensure that the content is suitable for use in the current mode, as
* specified in the options (see {@link ChannelValidateOptions.mode}). If specific fields are missing or invalid,
* provide appropriate error messages using {@link ChannelContentResult.addError}. You should provide any errors that are
* not linked to a specific field using {@link ChannelContentResult.addToast}. If no errors of
* either type are returned, the validation is considered successful, and the operation is allowed to proceed.
* @async
* @param content the content with translated tempaltes
* @param options additional options
* @returns result of the operation
*/
public async validate(
content: CampaignContent, options: ChannelValidateOptions
): Promise<ChannelContentResult> {
const result = new ChannelContentResult();
this.validateSettings(content.settings as ContentSettingsFormData, result);
await this.validateTemplate(content.template as ContentTemplateFormData, result);
return result;
}
/**
* Publishes the given content. This is the place to perform any necessary transformations between the given template
* format and the external system's format. It can be assumed that {@link Channel.validate} has already been called,
* but additional errors may still be detected in this phase and returned in the same way as during validation.
* <p>
* If you must store the content in an external system, this is also the time to do that. If the content must instead
* be known in `prepare` or `deliver`, place it in the document store for future use.
* <p>
* You may call this method multiple times with the same content key. But for a given content key, it is always
* called with the same content and options. As such, when successful, treat it as an idempotent
* operation at the content key level. So if the given key has already been processed and successfully stored, there
* is no need to process and store it again.
* @async
* @param contentKey unique key for the content
* @param content the content with translated templates
* @param options additional options
* @returns result of the operation
*/
public async publish(
contentKey: string, content: CampaignContent, options: ChannelPublishOptions
): Promise<ChannelContentResult> {
const result = new ChannelContentResult();
if (
this.validateSettings(content.settings as ContentSettingsFormData, result)
&& await this.validateTemplate(content.template as ContentTemplateFormData, result)
) {
await storage.kvStore.put(`content:${contentKey}`, content as CampaignContent & KVHash);
}
return result;
}
/**
* Prepares for a campaign run. You can use this to set up an external entity for use in `deliver` (or perform any
* other processing that should only be performed once per run). If this step is unnecessary, simply do not implement
* the method.
* <p>
* If implemented, this method is called exactly once per content key involved in a campaign run. If any one of
* these fails, the campaign run will fail.
* @async
* @param contentKey unique key of the content
* @param tracking campaign tracking parameters
* @param options additional options
* @returns result of the operation
*/
// public async prepare?(
// contentKey: string, tracking: CampaignTracking, options: ChannelPrepareOptions
// ): Promise<ChannelPrepareResult>;
/**
* Delivers a batch of messages. You may call this method many times for the same content key, tracking parameters,
* and options, each with a unique batch of recipients and substitutions. It is assumed that a batch either succeeds
* or fails as a whole. There is no partial delivery handling.
* <p>
* If a batch fails, you may retry it (as controlled by the return value). When that happens, the subsequent call(s)
* for that batch will be given the previous result for reference to enable proper recovery logic.
* <p>
* Once a batch succeeds, it will never be given again.
* @async
* @param contentKey unique key of the content
* @param tracking campaign tracking parameters
* @param options additional options
* @param batch of recipients and substitutions
* @param previousResult previous result of the operation, if this is a retry
* @returns result of the operation
*/
public async deliver(
contentKey: string,
tracking: CampaignTracking,
options: ChannelDeliverOptions,
batch: CampaignDelivery[],
previousResult?: ChannelDeliverResult
): Promise<ChannelDeliverResult> {
// TODO: Deliver the batch of content here. See the example API Channel app for details.
}
/**
* Renders a batch of messages into HTML previews. Each preview must be a full HTML page containing a user-friendly
* representation of the message as it would be delivered.
* @async
* @param content the content with translated templates
* @param batch of recipients and substitutions
* @returns result of the operation
*/
public async preview(content: CampaignContent, batch: CampaignDelivery[]): Promise<ChannelPreviewResult> {
const result = new ChannelPreviewResult();
result.setPreviews(batch.map((delivery) => {
const template = content.template as ContentTemplateFormData;
const {method} = template.request;
const [url, body] = this.performSubstitutions(
delivery.substitutions, template.request.url, template.request.body || ''
);
const settings = content.settings as ContentSettingsFormData;
return previewTemplate(this.formatHeaders(settings.headers), `${method} ${url}`, body);
}));
return result;
}
// ...
}
ContentSettingsFormData
and ContentTemplateFormData
are TypeScript interfaces that represent the data from the content settings and content template forms, respectively. OCP fills these interfaces with the data provided by ODP users in campaign forms and passes them to your channel app in callback methods.
import {Schema} from '@zaiusinc/app-forms-schema';
export interface ContentSettingsFormData extends Schema.FormData {
target: {
identifier: string;
};
headers: {
h1_key: string;
h1_value: string;
h2_key: string;
h2_value: string;
h3_key: string;
h3_value: string;
h4_key: string;
h4_value: string;
};
}
import {Schema} from '@zaiusinc/app-forms-schema';
export interface ContentTemplateFormData extends Schema.FormData {
request: {
method: string;
url: string;
body: string | null;
};
}
The previewTemplate
function is a helper function that renders a preview of the content template.
/* eslint-disable max-len */
export function previewTemplate(headers: string, url: string, body?: string) {
return `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Template</title>
<meta name="description" content="Template">
<meta name="author" content="Zaius">
<style>
html {
background: transparent;
}
body {
font-family: sans-serif;
color: rgba(0,0,0,.9);
margin: 20px 0;
display: flex;
justify-content: center;
background: transparent;
}
* {
box-sizing: border-box;
}
h5 {
text-transform: uppercase;
color: rgba(0,0,0,.6);
font-size: 11px;
}
.api {
min-width: 350px;
background: white;
padding: 20px;
border-radius: 5px;
}
.info {
display: flex;
align-items: center;
font-size: 12px;
background: rgba(20, 172, 228, 0.16);
color: #14ACE4;
padding: 10px;
border-radius: 3px;
}
.icon {
flex-shrink: 0;
width: 18px;
height: 18px;
margin-right: 10px;
}
pre {
background: rgba(0,0,0,.05);
border-radius: 3px;
padding: 10px;
white-space: pre-wrap;
overflow: auto;
}
</style>
</head>
<body>
<div class="api">
<div class="info">
<svg class="icon" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="info-circle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-info-circle fa-w-16 fa-9x"><path fill="currentColor" d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" class=""></path></svg>
This is only a preview of the API request. No API calls were made.
</div>
<h5>Headers</h5>
<pre class="headers">${headers}</pre>
<h5>URL</h5>
<pre class="url">${url}</pre>
${body === '' ? '' : `<h5>Body</h5><pre class="body">${body}</pre>`}
</div>
</body>
</html>
`;
}
Updated about 1 year ago