serverless twitter bot

Create a Serverless Twitter Bot with Airbrake and AWS Lambda – Part 2

In Part 1 we setup our development environment, created our twitter-bot project, integrated it with the Twitter API so it could send out programmatic tweets, and performed a basic Atom feed retrieval for some actual content to tweet.

Today, we’ll continue refining our Twitter bot by integrating automatic error handling with Airbrake’s Node.js software. This will ensure our Twitter bot is always stable and executing as expected, since we’ll be alerted immediately if something goes wrong. We’ll then begin the process of getting our application into a serverless environment with AWS Lambda, before we finish everything up in Part 3 later this week. You can also view the full source code for this project at anytime on GitHub, so with that let’s jump right in!

Getting Started with Airbrake

To begin using the Airbrake-JS module we need to Create an Airbrake account, sign in, and make a new project.

Now we just need to locally install Airbrake-JS package. We’ll do this in a console from our project root directory using the npm install --save airbrake-js command:

Alternatively, we could also install it by adding airbrake-js to our package.json dependencies:

Handling Secret Keys

We need to instantiate the Airbrake client object in our application by passing the projectId and projectKey values obtained from the twitter-bot project we created on the Airbrake dashboard. However, we don’t want to publicly expose these values, so we’ll implement the same technique we used in Part 1 to hide the Twitter API secret keys.

Let’s start by adding airbrake-credentials.js to our .gitignore file, since this is the file we’ll use to hold our private projectId and projectKey values. Our .gitignore file should look something like this:

Now, let’s create the airbrake-credentials.js file, then copy and paste the projectId and projectKey values into the appropriate properties, similar to what we did in twitter-api-credentials.js:

Integrating Airbrake-JS

We can now require the airbrake-credentials.js file in our application code and pass the exported object to the AirbrakeClient constructor. We’ll start by requiring airbrake-js and airbrake-credentials.js. We’ll then pass the credentials to new AirbrakeClient(...) to create the actual client object we can use in our code:

That’s all there is to using airbrake-js with default settings! All thrown Errors will be detected by airbrake-js and will be instantly, automatically reported to you via the Airbrake project dashboard (and also via email or other service integrations you may have setup). For our purposes we’ll add a simple tweet(message) function that attempts to tweet the passed message argument:

We’ve also started to clean up the code a bit by creating the tweetCallback(error, tweet, response) function, which is invoked as the callback for the method call used throughout our code:

The error object passed to tweetCallback(error, tweet, response) is actually a one-dimensional array, so we explicitly throw a new Error and pass the message property of the underlying error object that was caught, if applicable. Otherwise a successful tweet was posted, so we log that to the console.

We can test this by calling tweet(message):

This produces a successful tweet:

However, let’s see what happens if we try to send the same tweet again:

Something has gone wrong. The console shows the following error array object:

More importantly, airbrake-js immediately picked up on the error and reported the issue to me via the Airbrake project dashboard (and via email, which is the default behavior). The Airbrake dashboard shows a great deal of useful information about the error:

  • First Seen: 8 seconds ago
  • Last Seen: 8 seconds ago
  • Occurrences: 1
  • Error Type: Error
  • Severity: error
  • File: index.js:99
  • Error Message: Status is a duplicate.

Clicking on the specific Occurrence tab in the dashboard shows the full details of the caught error. For example, airbrake-js picks up the full backtrace, which shows us that the error occurred in the tweetCallback(error, tweet, response) function in our index.js file on line 99:

It also reports a great deal of contextual info, including all recent console.log messages. With Airbrake-JS added to our project we can now ensure all future errors, whether during development or in production, are instantly reported.

We’ll finish up our Airbrake-JS integration by committing the recent changes to Git:

AWS Lambda

Everything is working so far as expected. Our application is able to successfully retrieve a random Airbrake article, connect to Twitter, and submit a new tweet. However, we want to be able to let our bot go and have it execute its own code when appropriate, such as on a regular schedule. To accomplish this we’ll be using AWS Lambda, which allows you to run code without worrying about servers or infrastructure.

Unlike the typical scenario, where server hardware is paid for and configured to host application code, AWS Lambda is a serverless technology that hosts your code within tiny, self-contained functions, which can be triggered by a wide variety of events. Instead of paying for servers, you just pay for compute time. This sort of setup is perfect for infrequent, small applications like our little Twitter bot.

Lambda Function Handler Basics (for Node.js)

To actually exceute your code, AWS Lambda invokes the Node function you specify as the handler. This handler is simply a function in your code that should be exported, using the standard exports statement. The typical syntax for an exported handler is:

The event parameter is used by AWS Lambda to pass event data to the handler. The context parameter is used by AWS Lambda to provide runtime information, such as logging info, timeout duration, request id, and so forth. The context object also includes succeed and fail methods, which can be explicitly invoked in your own code to inform Lambda that a function call has succeeded or failed, respectively.

Since you must explicitly tell AWS Lambda which particular handler to invoke within any given Lambda function, it’s a smart practice to export and expose multiple handler functions, and then you can invoke whatever handler is relevant.

Coding Lambda-Compatible Handler Functions

We now need to modify our existing application code so that it can effectively export the handler functions we want to expose to AWS Lambda. These functions need to have a few capabilities. The first capability our function needs is a way to ensure that execution of a handler function is completely self-contained, meaning that any outside libraries can also be accessed by AWS Lambda.

The second requirement is that the code is contextually sound, meaning that it will make proper use of the AWS Lambda-specific context parameter we briefly discussed above. Since AWS Lambda is priced based on computation time and memory, it’s critical that our code only executes for as long as absolutely necessary. If something goes wrong or a request is successful, we want to immediately inform AWS Lambda of the result so we can quickly halt execution. It may seem minor, but a savings of even a few milliseconds for each function call will add up quickly for computations that are performed hundreds or thousands of times.

Therefore, the first change we need to make to our application code is to streamline the tweet(message) function and allow it to provide context information during the underlying method callback. Here is the modified tweet(...) function:

The first change is at the top when we check if message is valid. If no message is provided we invoke the fail(...) function, which you’ll see in a moment is the function provided by AWS Lambda. Invoking this will immediately halt execution of our function and inform Lambda of the stop.

We’ve also reverted the callback function to an anonymous function, as this allows us to use the built-in Function.prototype.bind() method, so we can include the two new succeed and fail parameters in the callback function context. Since we don’t want to modify the twitter package code, this is the next best option for being able to invoke the succeed and fail functions within the callback code block.

Within said callback function we check if an error parameter exists. If so, we output it to the log and then explicitly invoke the airbrake.notify(...) method and pass in the generated error. Since airbrake.notify(...) returns a promise instance, we can chain the then() method, which will invoke the returned fail(...) promise after airbrake.notify(...) processes the error. The result of this code is that we are able to immediately report the generated error via Airbrake while this code is running remotely on AWS Lambda, and once the asynchronous Airbrake notification has completed, we then halt our Lambda function execution and report the error to Lambda.

On the other hand, if no error exists we merely output the tweet to the log and invoke the succeed(...) function, informing Lambda that the function has completed.

The first AWS Lambda handler function we’re exporting is a simple test function called tweetHelloWorld(event context), which does just what the name suggests:

As mentioned, it’s a good idea to export multiple handler functions that you think you might want to use for testing or what not, since we can explicitly choose which handler is executed by AWS Lambda. As such, let’s also create the tweetTime(event, context) function:

Both of these basic handler functions will be useful for testing, since one of them will attempt to tweet the same value each time, while the other will always be something new.

The final handler function is, of course, tweetRandomArticle(event, context), which is the primary function we’re looking to have AWS Lambda execute. This function needs to be able to perform the entire process of retrieving articles, selecting a random one, and tweeting the selected article. As such, we’ve moved a lot of the event handler code that previously existed outside the context of a function, and stuck it directly inside this function:

We also have to move the FeedParser() and request(...) calls into this function context, since we don’t want these calls to be invoked at any other time except when tweetRandomArticle(event, context) is called. Other than that, most of the code is the same as before — we start by gathering article from the feed, select a random one, and then pass it to tweetArticle(article, succeed, fail):

That’ll do for Part 2 of this series. Stay tuned for Part 3 later this week, where we’ll finish up by packaging and integrating our application code into AWS Lambda by creating functions and handlers, testing everything, and then finally getting a fully automated, error-managed, serverless Twitter bot up and running!