1.0.25 • Published 1 year ago

journey-state-machine v1.0.25

Weekly downloads
-
License
-
Repository
-
Last release
1 year ago

Table of Contents

Getting Started

Install

npm i journey-state-machine

// or if using yarn
yarn add journey-state-machine

Usage

import { StateMachine, journeys as defaultJourneys } from "journey-state-machine";

const stateMachine = new StateMachine(defaultJourneys, true);

// toggle verbose logging
stateMachine.verbose = false;

// reset state machine with new journey definitions
stateMachine.updateJourneys(updatedJourneys);

// transition journey states with given inputs
stateMachine.getNewState(eventInput, eventPayload, eventDate);

Documentation

Journey Definitions

Journey definitions are JSON type representations of directed acyclic graph, where each node represents a step in a journey.

Example Journey Visualization

image

JSON Format

Journey

JSON fieldTypeDefinition
namestringunique identifier for a given journey e.g. “myUserJourney”
nodesarrayordered list of journey nodes

Node

JSON fieldTypeDefinition
idnumberunique identifier for a given node
parentNode?boolean(root node) these nodes are always active entry points into a journey
namestringhuman readable node definition e.g. “User clicks button X”
trigger?stringevent input indicator that will trigger a state change
childrenarrayunordered list of numbers representing the children’s node ids
outcomestringresulting state change of the journey after a given input
check?stringglobally scoped javascript function body

Node Outcomes

OutcomeTypeDefinition
advancestringjourney progressed: deactivate current node and active all children nodes
abortstringintentionally aborted: deactivate current node, terminate current journey
failstringunintentionally failed: deactivate current node, terminate current journey
succeedstringcompleted successfully: deactivate current node, terminate current journey
ignorestringinsignificant trigger: maintain current active node

Example JSON Journey Definition

{
    "name": "myJourneyName",
    "nodes": [
      {
        "id": 0,
        "parentNode": true,
        "name": "First step of my journey",
        "trigger": "UNIQUE_START_TRIGGER_STRING",
        "children": [2, 3, 4],
        "outcome": "advance"
      },
      {
        "id": 1,
        "parentNode": true,
        "name": "Alternate first step of my journey",
        "trigger": "SECONDARY_START_TRIGGER_STRING",
        "children": [2, 3, 4],
        "outcome": "advance"
      },
      {
        "id": 2,
        "parentNode": false,
        "name": "Intermediate happy path step in journey",
        "trigger": "HAPPY_PATH_TRIGGER",
        "children": [3, 5],
        "outcome": "advance",
        "check": "return [\"advance\", `${data.nodeId}:${data.payloadVariable}`]"
      },
      {
        "id": 3,
        "parentNode": false,
        "name": "Final step in journey - failure",
        "trigger": "FAILURE_TRIGGER",
        "children": [],
        "outcome": "fail",
        "check": "return data.isFailure ? [\"fail\", `${data.nodeId}:${data.errorMessage}`] : [\"ignore\"];"
      },
      {
        "id": 4,
        "parentNode": false,
        "name": "Final step in journey - abort",
        "trigger": "ABORT_TRIGGER",
        "children": [],
        "outcome": "abort"
      },
      {
        "id": 5,
        "parentNode": false,
        "name": "Final step in journey - success",
        "trigger": "TERMINAL_SUCCESS_TRIGGER",
        "children": [],
        "outcome": "succeed"
      }
    ]
  }

Journey Nodes

Definitions

  • Active Nodes: Nodes that are actively listening to state inputs
  • Triggered Nodes: Active nodes that have received a triggering input
  • Parent Nodes: Root nodes of a journey. These nodes are default always active

Node API

isRootNode() => boolean() Returns true if the node is a root or parent node.

isTriggered(trigger: string) => boolean Returns true if the given trigger argument is considered a triggering event of the node. NOTE: isTriggered only checks the trigger field and triggerless nodes. It doesn not perform node checks.

Parent Nodes

Parent Nodes are always active nodes in a journey. This allows the ability to track journeys of the same type running in parallel.

image image

Triggerless Nodes

A node without a trigger parameter will be triggered by every input event. This can be useful for gathering common data from input events or using a node check to determine how the node should advance. Triggerless nodes allow you to inspect the event payload or time to determine if a journey should advance.

Example Triggerless Node Check This example only advances nodes in a given timeframe.

// only advance this journey once, do not allow parallel journeys
if (data.journeyId > 1) {
  return ["ignore"];
}

if (data.timestamp < 300000 && data.timestamp > 1000) {
  let captureMetaData = this.captureMetaData.bind(this)
  captureMetaData(data.journeyId, {
    timestamp,
    counter,
    uniqueNodeData: `uniqueNodeData-${data.nodeId}`
  }, {
    timestamp: data.timestamp,
    eventType: data.EventType,
    counter: 1, // counter variable can be passed to downstream nodes
    uniqueNodeData: data.uniqueData
  });
  return ["advance"];
} else {
  return ["ignore"];
}

Node Children and Outcomes

Terminal node outcomes ("abort" | "fail" | "succeed") explicitly ignore any children defined on the node. Terminal nodes will end the journey regardless of downstream children nodes. image

Node Checks

Node checks are stringified, globally scoped javascript function bodies. Node checks allow you to capture data at a given node, print logs within a node check, inspect captured metadata and modify the node's outcome.

/**
 * @returns [outcome, optionalMessage]
 *  outcome: string "advance" | "abort" | "fail" | "succeed" | "ignore"
 *  optionalMessage: string
 *
 * e.g. return ["advance"] || ["fail", "invalid-driver"]
 *
 * data contains:
 * 1. all data in the event payload
 * 2. the journey id which represents the number of journeys started
 * 3. the node id
 * 4. any captured upstream metadata
 *
 * capture metadata:
 * const captureMetaData = this.captureMetaData.bind(this);
 * captureMetaData(data.journeyId, { variableNameInPayload: "myUniqueVarName" }, payload)
 *
 * inspect metadata: this.journeyMetaData[data.journeyId]
 *
 * save data to raw results
 * const addDataToJourneyPath = this.addDataToJourneyPath.bind(this);
 * addDataToJourneyPath(data.journeyId, {
 *    payload: { journeyStartTime: data.happenedAtMs }
 * })
 */

Example Node Check note: parameters on the data object are unique to the node and passed in the event payload

`let captureMetaData = this.captureMetaData.bind(this)
captureMetaData(data.journeyId, {
  uuid: "uuid",
  source: "source"
}, {
  uuid: data.uuid,
  source: data.source
});

const previouslyActiveJourneyData = this.journeyMetaData[data.journeyId - 1];
if (previouslyActiveJourneyData) {
  if (previouslyActiveJourneyData.source !== data.source ) {
    return ["fail", "source-change"];
  }

  if (previouslyActiveJourneyData.uuid !== data.uuid) {
    return ["fail", "uuid-change"];
  }
}

return ["advance"];`

Node checks expose the data and functions on a given journey. See Journey API for more details on the functions available within a node check.

Journeys

Journey API

captureMetaData(journeyId, dataCapture, payload) Captures metadata at any active node of a journey and saved for comparison on downstream nodes.

VariableTyepDescription
journeyIdnumberthe journey id for the metadata to be saved on. the first activated journey id is 1. Subsequent activations of the same journey will increment the journey id.
dataCaptureobjectkey: value found in payloadvalue: variable name to save captured value ase.g. { source: "sourceDataForFirstNode" }
payloadobjectpayload to be saved to metadata

Example:

captureMetaData(data.journeyId, {
  uuid: `uuid-${data.nodeId}`
}, {
  uuid: data.uuid
});

/**
 * the uuid variable is now saved on the current journey and can be
 * inspected under the uuid-${data.nodeId} variable name
 * i.e. journeyMetaData[data.journeyId]["uuid-3"]
 */

addDataToJourneyPath(journeyId, data) Adds payload data to the raw journey summary output

Example:

 addDataToJourneyPath(data.journeyId, {
    payload: { journeyStartTime: data.happenedAtMs }
 })

getActivatedNodes(trigger, callback) Checks if trigger activates any currently active journey nodes and returns all activated nodes.

VariableTyepDescription
triggerstringthe event trigger to test against the node
callbackfunctioncallback for handling activated nodes(node: JourneyNode, journeyName: string, index: number) => void

Example:

const activeJourneys: Record<string, Record<number, {}>> = {};
this.journeys.forEach(journey => {
  journey.getActivatedNodes(
    trigger,
    (node: JourneyNode, journeyName: string, id: number) => {
      if (!activeJourneys[journeyName]) {
        activeJourneys[journeyName] = {};
      }
      activeJourneys[journeyName][id] = node.toString();
    }
  );
});
console.log(JSON.stringify(activeJourneys));

getNode(nodeId) Returns the node for a given journey by id, if present

let journeyMetaData: Record<journeyId, Record<string, any>> Data captured during a journey

Example:

// this data can be inspected by any active node
let journeyMetaData = {
   1: {
       thirdNodeRoute: "homepage",
       userId: 1234
    },
   2: {
       userCancelled: true,
       userId: 1234
    },
}

let activeJourneys: Map<number, number[]>

  /**
   * a single journey can be triggered multiple times and multiple journeys of the same type
   * can be active at once.
   * activeJourneys represents the active nodes for each triggered journey of the same type
   *  e.g. {
   *         1: [E, D, C],
   *         2: [B, C],
   *         // journey 3 ended (either failed, aborted or succeeded)
   *         4: [B, C],
   *         5: [F],
   *         6: [A]
   *     }
   *   this represents 5 active journeys
   */

let verbose: boolean Toggles verbose logging of the state machine. Verbose true logs things such as triggers, captures, checks, outcomes and active journeys

let journeySummary: JourneySummary Snaphot summary of journey history. See journey summary.

export interface OutcomeSummary {
  count: number;
  /** a map of message to count */
  messages?: Map<string, number>;
}

/** the specific path for a single journey instance  */
export interface JourneyPath {
  journey: string;
  journeyId: number;
  nodePath: number[]; // ordered list of node path
  payload: any;
  outcome?: string;
}

export interface JourneySummary {
  succeed: OutcomeSummary;
  abort: OutcomeSummary;
  fail: OutcomeSummary;
  pending: OutcomeSummary;
  advance: OutcomeSummary;
  ignore: OutcomeSummary;
  total: number;
  raw: Map<number, JourneyPath>;
}

Journey Summaries

Journey summaries are the summary of outcomes after running a list of event triggers. They contain a list of all possible node outcomes with a count of how many journey paths reached the outcome, as well as an optional message and count for each message. The journey summary also contains the raw outcome data.

Node Paths

Node paths are ordered lists of the node path a journey took. For Example, 0, 3, 5 would be the node path for a single journey path of the image below.

image

State Machine

State Machine API

updateJourneys(journeyData: Record<string, JourneyData>) Replace state machine journeys with updated journey data. All previous journey data will be lost.

getNewState(trigger: string, payload: any, timestamp: any) Entry point to state transitions of journeys. Can terminate or advance nodes in a journey.

  /**
   * Goes through all active nodes for all active journeys and:
   * 1. checks if trigger activates the active node
   * 2. captures metadata
   * 3. checks logic
   * 4. handles result (advance, fail, succeed, abort)
   */

printActivatedNodes(trigger: string) Debugging function to print all journeys activated by given trigger.

printInProgressJourneys() Debugging function to print all journeys not on a parent node.

printActiveJourneyNodes() Debugging function to print all currently active journeys and the active nodes within them

CI

Parse CSV logs directly from the terminal

ts-node ./tools/parse-logs.ts

1.0.18

1 year ago

1.0.17

1 year ago

1.0.16

1 year ago

1.0.22

1 year ago

1.0.21

1 year ago

1.0.20

1 year ago

1.0.25

1 year ago

1.0.24

1 year ago

1.0.23

1 year ago

1.0.15

1 year ago

1.0.14

2 years ago

1.0.13

2 years ago

1.0.12

2 years ago

1.0.11

2 years ago

1.0.10

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago