HomeGuidesAPI Reference
Submit Documentation FeedbackJoin Developer CommunityOptimizely GitHubOptimizely NuGetLog In

src/

This topic describes the src/ directory as it pertains to your application in Optimizely Connect Platform (OCP).

channel/

Channel apps allow you to create a new channel available within campaigns in ODP.
SDK Docs can be found here

app.yml Linkage

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

Details

status should be defined as 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, etc.

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

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 as follows:
 * 1. Check if the channel is `ready` to use
 * 2. Dynamically determine the `target` (may be provided statically in `app.yml` 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. Reasonable caching should be utilized 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, this should be specified statically via `channel.targeting`
   * in `app.yml`. If targeting is based on selections made in the content settings form, this method must be
   * implemented and the value in `app.yml` 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,
   * appropriate error messages should be provided using {@link ChannelContentResult.addError}. Any errors that are
   * not linked to a specific field should be provided using {@link ChannelContentResult.addToast}. If no errors of
   * either type are returned, the validation is considered successful, and the operation will be 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 the content must be stored in an external system, this is also the time to do that. If the content must instead
   * be known in `prepare` or `deliver`, it should be placed in the document store for future use.
   * <p>
   * This method may be called multiple times with the same content key. But for a given content key, it will always
   * be called with the same content and options. As such, once successful, it should be treated 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();

    return result;
  }

  /**
   * Prepares for a campaign run. This can be used 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 will be 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. This method may be called 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, it may be retried (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> {
  
    // Because we report soft_bounces, we do not want to handle retries.
    // Even if we didn't deliver everything, 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;
  }

}

Best Practices

For best practices for channel apps, view Channel documentation.

lifecycle/

Functions that will be called throughout the lifecycle of an app live here.
SDK Docs.

Details

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

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};
  }
}

Best Practices

For best practices for lifecycle, view Lifecycle.

liquid-extensions/

Liquid Extensions are small, executable blocks of code, similar in concept to OCP functions. They allow apps to integrate with the content rendering pipeline, exposing custom functionality into the OCP templating and personalization language which is based on the Shopify Liquid templating language.
SDK Docs

app.yml Linkage

liquid_extensions:
  generate_coupon: #<-- function reference in liquid
    entry_point: GenerateCoupon #<-- File and Class name
    description: Generates a unique coupon code
    input:
      rule:
        type: string
        required: true
        description: The coupon rule to create the coupon from
      email:
        type: string
        required: false
        description: Email address to tie the coupon code to

Details

FunctionCalledParametersReturn
performper rendercontext, inputLiquidExtensionResult

Example - GenerateCoupons.ts

interface GenerateCouponInput extends LiquidExtensionInput {
  rule: string;
  email?: string;
}

export class GenerateCoupon extends LiquidExtension {
  public async perform(
    context: LiquidExtensionContext,
    input: GenerateCouponInput
  ): Promise<LiquidExtensionResult> {
    const ruleDefinition = loadRule(input.rule);
    if (!ruleDefinition) {
      return LiquidExtensionResult.error(`Invalid rule: ${input.rule}`);
    }
    const couponCode = generateCode(ruleDefinition);
    if (context.mode === 'live' && input.email) {
      associateCode(input.email);
    }
    return LiquidExtensionResult.success(couponCode);
  }
}

Best Practices

For best practices for liquid extensions, view Liquid Extensions.

Liquid Usage

Within the OCP templating language, extensions are accessed in content for campaigns via special syntax of the form:

{{ app.<app_id>.<extension>(<input>: <value>, ...) }}

With our example above, if the app's id is my_coupon_app, this extension would be called:

{{ app.my_coupon_app.generate_coupon(rule: 'foo', email: customer.email) }}

This would result in the generated coupon code being inserted into the rendered content at this location.

functions/

Functions are small, executable blocks of code typically triggered off of a real-time event. An app can have any number of functions stored in the src/functions directory. Common examples of functions include:
SDK Docs.

app.yml Linkage

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

Details

status should be defined as 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, etc.

FunctionCalledParametersReturn
performon requestRequestApp.Response()

Example - GetEspLists.ts

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

/**
 * Returns the lists within Zaius
 */
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/

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.
SDK Docs.

app.yml Linkage

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

Details

status should be defined as 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, etc.

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

Example - Import.ts

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 can be used 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;
  }
}

Best Practices

For best practices for jobs, view Jobs documentation.