Testing and Debugging

As mentioned at the end of the last section of the walkthrough, 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. To this end, this page contains a collection of topics and strategies that can be useful when debugging a Reactor script.


Triggering the Script

If you are unsure if the Reactor script is being run in response to some application-related events, ensure the following criteria are met:

  • The request (e.g.: 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 (Thng that is the action target, or 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 simply log some statement and call done(), which will tell you for sure if the script is executing as expected or not. You should also remove any Reactor filters to be completely 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

One frequent cause of frustration can be that no logs are output, which makes the script hard to debug. In these cases it is not known where the error is occuring or even if the script is running at all. This happens if the script enters a code path or throws an uncaught error and done() is not called.

A strategy to prevent a scenario where done() isn’t called 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 will exit and logs will be output 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();
};

An analog for other JavaScript errors is to 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 may 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 check 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. Falling foul of these can cause a Reactor script to behave in unpredictable and difficult to debug ways. These includes limits on runtime, build time, size of the scripts, and recursion.

In most cases these will not present a problem, with the exception of recursion - for instance, if an action is created in response to some other action and the onActionCreated() event handler is not 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, etc.). It also includes options to log 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

An alternative strategy to receive logs from a Reactor script that may not be calling done() after execution is to add log messages as properties of a Thng. In this way the messages will be immediately available, have a history of values, and will be updated automatically from the Thng’s resource page in the Dashboard.

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

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!');
};