Dev Guide
Dev GuideUser GuidesLegal TermsGitHubDevCommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

src/

Describes the src/ directory for applications in Optimizely Connect Platform (OCP).

The src/ directory contains source code for your app and is where you will spend most of your time developing your app. You can organize your code however you prefer, but some folders have a specific purpose in Optimizely Connect Platform (OCP):

You can add any other directory and reference files from there in your code. A common practice is to create a src/lib/ directory for your own libraries and utilities.

channel/ folder

Channel apps allow you to create a new channel that you can use within campaigns in ODP.

Channel app resources:

Below is an example of the channel section in the app.yml file:

channel:
  type: sms
  targeting:
    - identifier: phone #<-- field for reachability/consent
  options:
    prepare: false
  events:
    send: true
    delivery: true
    delivery_unknown: true
    hard_bounce: true
    soft_bounce: true
    spam_report: false
    active_actions:
      - engage
      - disengage

channel/ functions

FunctionCalledParametersReturn
readyis channel readynoneboolean
targetn/acontentSettingsChannelTargetResult
validatecontent, on campaign save or campaign runcontent, optionsChannelValidateOptions
publishto publish content to providercontentKey, content, optionsChannelContentResult
preparebefore campaign runcontentKey, tracking, optionsChannelPrepareResult
previewon previewcontent, batchChannelPreviewResult
deliverdeliver batchcontentKey, tracking, options, batch, previousResultChannelDeliverResult

Example Channel.ts file

import * as App from '@zaius/app-sdk';
import {Batcher, CampaignContent, CampaignDelivery, CampaignTracking, ChannelContentResult, ChannelDeliverOptions, ChannelDeliverResult, ChannelPreviewResult, ChannelPublishOptions, ChannelTargetResult, ChannelValidateOptions, KVHash, logger, storage} from '@zaius/app-sdk';
import {z} from '@zaius/node-sdk';
import * as URL from 'url';

/**
 * Defines the interface of a channel. The typical channel flow in a campaign run is:
 * 1. Check if the channel is `ready` to use
 * 2. Dynamically determine the `target` (may be provided statically in the `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 ensures 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 using `channel.targeting`
   * in `app.yml`. If targeting is based on selections made in the content settings form, you must implement this method
   * and set the value in `app.yml` 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 ensures 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}. 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 is 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 you have already processed and successfully stored the given key, 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();

    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, 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 fails.
   * @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 can call this method many times for the same content key, tracking parameters,
   * and options, each with a unique batch of recipients and substitutions. A batch either succeeds
   * or fails as a whole. There is no partial delivery handling.
   * <p>
   * If a batch fails, you can 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 is never 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> {
  
    // Because OCP reports soft_bounces, retries are not supported.
    // Even if everything was not delivered, the delivery call was completed.
    return {success: true};
  }

  /**
   * 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();
 
    return result;
  }

}

lifecycle/ folder

Define functions that you call throughout the lifecycle of an app in the lifecycle/ folder.

Lifecycle resources:

📘

Note

There is no lifecycle section in the app.yml file. OCP reads the lifecycle/Lifecycle.ts file directly and executes methods from it.

lifecycle/ functions

FunctionCalledParametersReturn
onSettingsFormon settings form actionpage, action, formDataApp.Response()
onUpgradeon version upgradefromVersion{success: }
onFinalizeUpgradeafter onUpgradefromVersion{success: }
onInstallon user install of appnone{success: }
onUninstallon user uninstall of appnone{success: }

Example Lifecycle.ts file

import * as App from '@zaius/app-sdk';
import {logger, storage} from '@zaius/app-sdk';

export class Lifecycle extends App.Lifecycle {
  public async onInstall(): Promise<App.LifecycleResult> {
    try {
      logger.info(`Performing Install`);
      // TODO: any operation you need to perform during installation
      return {success: true};
    } catch (error) {
      logger.error('Error during installation:', error);
      return {success: false, retryable: true};
    }
  }

  public async onSetingsForm(page: string, action: string, formData: App.ValueHash): Promise<App.Response> {
    try {
      // TODO: any logic you need to perform when a setup form section is submitted
      // When you are finished, save the form data to the settings store
      storage.settings.put(page, formData);
      return new App.Response(200);
    } catch (e) {
      return new App.Response(500);
    }
  }

  public async onUpgrade(fromVersion: string): Promise<App.LifecycleResult> {
    // TODO: any logic required when upgrading from a previous version of the app
    // Note: `fromVersion` may not be the most recent version or could be a beta version
    return {success: true};
  }

  public async onFinalizeUpgrade(fromVersion: string): Promise<App.LifecycleResult> {
    // TODO: any logic required when finalizing an upgrade from a previous version
    // At this point, new webhook URLs have been created for any new functions in this version
    return {success: true};
  }

  public async onUninstall(): Promise<App.LifecycleResult> {
    // TODO: any logic required to properly uninstall the app
    return {success: true};
  }
}

liquid-extensions/ folder

Define the code for Liquid extensions in the liquid-extensions/ folder. Each liquid extension is a separate file in the liquid-extensions/ folder that contains a class extending the LiquidExtension class. Both file and class names must match the value of the entry_point property from the app.yaml file.

Liquid extension resources:

functions/ folder

Functions are small, executable blocks of code typically triggered from a real-time event. An app can have any number of functions stored in the src/functions directory.

Functions resources:

Below is an example of the functions section in the app.yml file:

functions:
  get_esp_lists: #<-- name used for form sources and reference within the app
    entry_point: GetEspLists #<-- File and Class name
    description: Pulls the lists from the ESP to select which ones you want imported

functions/ functions

FunctionCalledParametersReturn
performon requestRequestApp.Response()

Example GetEspLists.ts file

import * as App from '@zaius/app-sdk';
import {logger} from '@zaius/app-sdk';
import {z} from '@zaius/node-sdk';

/**
 * Returns the lists within ODP
 */
export class GetEspLists extends App.Function {

  public async perform(): Promise<App.Response> {
      try {
        return new App.Response(200, await z.lists.getLists().data);
      } catch (e) {
        logger.error(e);
        return new App.Response(500, `An unexpected error occurred: ${e}`);
      }
    }
  }
}

jobs/ folder

Jobs are asynchronous functions with an undefined completion time. Jobs are typically used for historical imports of data or recurring imports of data from APIs that do not support real-time functions.

Jobs resources:

Below is an example of the jobs section in the app.yml file:

jobs:
  historical_import: #<-- used to reference within app
    entry_point: Import #<-- File/Class name
    description: Performs a one-time historical import when triggered

jobs/ functions

FunctionCalledParametersReturn
preparebefore performparams, statusstatus
performafter perform, until status.complete == truestatusstatus

📘

Note

status is an interface that contains the information needed to complete a unit of work. Often, this includes the last page called from an API, number of records imported, and so on.

Example Import.ts file

import * as App from '@zaius/app-sdk';
import {logger, ValueHash} from '@zaius/app-sdk';

interface ImportJobStatus extends App.JobStatus {
  state: {
    // TODO: define your job state here
  };
}

/**
 * Performs an import
 */
export class Import extends App.Job {

  /**
   * Prepares to run a job. Prepare is called at the start of a job
   * and again only if the job was interrupted and is being resumed.
   * Use this function to read secrets and establish connections to simplify the job loop (perform).
   * @param params a hash if params were supplied to the job run, otherwise an empty hash
   * @param status if job was interrupted and should continue from the last known state
   */
  public async prepare(params: ValueHash, status?: ImportJobStatus): Promise<ImportJobStatus> {
    logger.info('Preparing Import Job with params:', params, 'and status', status);
    // TODO: Prepare for a job run
    return {state: {}, complete: false};
  }

  /**
   * Performs a unit of work. Jobs should perform a small unit of work and then return the current state.
   * Perform is called in a loop where the previously returned state will be given to the next iteration.
   * Iteration will continue until the returned state.complete is set to true or the job is interrupted.
   * @param status last known job state and status
   * @returns The current JobStatus/state that you can use to perform the next iteration or resume a job if interrupted.
   */
  public async perform(status: ImportJobStatus): Promise<ImportJobStatus> {
    // TODO: perform a small unit of work, then return your state

    // When you are finished, set complete to true
    status.complete = true;

    // Return your status. If complete is false, perform will be called again imediately with the status you returned
    return status;
  }
}

schema/ folder

The src/schema folder contains YAML files with declarative, static schema definitions for an app. OCP reads these files when users install or upgrade an app and makes sure the ODP account where the app is installed contains all declared objects and fields.

For more information about schema, see Schema for objects and fields.