Testing and Debugging

As mentioned at the end of the last section, Reactor scripts can be challenging to debug as their size and complexity grows. Using a recommended pattern can systematically reduce this complexity, but other issues are also common. This page contains a collection of topics and strategies that can be useful when debugging a Reactor script.


Triggering the Script

If you're unsure if the Reactor script is running in response to application-related events, ensure the following criteria are met:

  • The request (for example, action creation) is authenticated by an application-scoped key (Trusted Application API Key, Application User API Key), or the project query parameter specifies the application’s project.
  • The resource involved in the request (that is, the Thng that's the action target or that owns the property being updated) is in the same project scope specified by the API key or project parameter in the request.

If in doubt, simplify the event handler to log a statement and call done(), which indicates whether the script is executing as expected. You could also remove any Reactor filters to be sure. For example:

const onActionCreated = async (event) => {
  // Test to check if Reactor script is executing, then exit early
  logger.info('Running script!');
  done();
  return;
  
  // This will now no longer run
  try {
    const product = await getThngProduct(event.thng);
    await updateProduct(product);
    logger.info('Done!');
  } catch (e) {
    logger.error(e.message || e.errors[0]);
  }

  done();
};

No Logs Output

When no logs are written, the script can be hard to debug. In these cases, you don't know where the error occurs or whether the script is running at all. This happens if the script enters a code path or throws an uncaught error and done() isn't called.

A strategy to prevent that scenario is to ensure that all promise chains include at least one catch block that calls done(). This way, if they catch an error, the script exits, and logs are written for debugging. For example:

const onActionCreated = async (event) => {
  try {
    const thngs = await getThngs();
    await updateThngs(thngs);
    logger.info('Done!'));
  } catch (e) {
    // All errors caught here
    logger.error(e.message || e.errors[0]);
  }

  // Always called
  done();
};

Similarly, for other JavaScript errors, include try/catch blocks that do the same things around code that performs I/O or parsing of JSON and nested fields of such objects. For example, accessing nested fields that might not be present in an object received from the API:

const isThngActive = (thng) => {
  try {
    return thng.customFields.is_active;
  } catch(e) {
    // Exit and receive logs
    logger.error('Thng has no customFields!');
    done();
  }
};

An alternative for this particular error is to examine each accessed field before accessing a nested one. For example:

const isThngActive = (thng) => {
  if (!thng.customFields) {
    logger.error('Thng has no customFields!');
    done();
    return;
  }

  return thng.customFields.is_active;
};

Execution Limits

There are some built-in limits on how a script can behave. These include limits on runtime, build time, size of the scripts, and recursion. Crossing these limits can cause a Reactor script to behave in unpredictable ways that are hard to debug.

In most cases, these limits don't present a problem, with the exception of recursion. For example, if an action is created in response to some other action and the onActionCreated() event handler isn't using a Reactor filter, resulting in a callback loop.

For full details on these limits, see the Limits section of the API Reference.


Local Testing

The reactor-testing repository contains a Node.js application that allows local testing of Reactor scripts by emulating the event and calling the relevant event handler function, providing all the expected global variables (done(), EVT, app, logger, and so on). It also includes options to write to the Node.js console as soon as logs are added, instead of waiting for done() and other debugging features.

For full details on the local testing project, see the README.md file in the repository.


Logging to a Thng

If a Reactor script doesn't call done() after execution, an alternative strategy to get logs is to add log messages as properties of a Thng. In this way, the messages are immediately available, have a history of values, and are updated automatically from the Thng’s resource page on the Dashboard.

One example implementation of this is shown below, using a ThngLogger class to capture the Thng ID and return a Promise after the property has been created/updated. In this example, the Thng ID is captured on the action being performed, but it could be a dedicated Thng for logging purposes:

function ThngLogger(id) {
  this.log = msg => app.thng(id).property('logs').update(msg);
}

// @filter(onActionCreated) action.type=_CountMe
const onActionCreated = async (event) => {
  const thngLogger = new ThngLogger(event.action.thng);

  let counter = 0;
  const handle = setInterval(() => {
    counter++;
    if (counter > 10) {
      clearInterval(handle);
      thngLogger.log('Count complete!');
      done();
      return;
    }
    
    thngLogger.log(`Counter now ${counter}`);
  }, 2000);
  thngLogger.log('onActionCreated!');
};