Screenshots - a perfect task to automate!

Screenshots - a perfect task to automate!

Do you also have to create or update screenshots of web products all the time? Or at least over and over again and then you think: oh man, does this have to be again? Welcome to the club! It takes me about 40 seconds per screenshot, so that’s almost 7 minutes for 10 screenshots alone, just looking at the pure creation. So five iterations means over 30 minutes. What if I told you that you could automate and standardize the whole process in 30 minutes with Camunda Cloud and Websiteshot?

Maybe it’s not immediately obvious why you would need such a process. But there are some use cases that lend themselves to it:

Here’s the scenario I’d like to walk you through in this tutorial:

So for this tutorial, you’ll need an account for Websiteshot and AWS. The free tier offerings are perfectly sufficient for what we have in mind :)

First iteration

Let’s start with the first iteration. This is the most minimal process that meets our requirements.

workflow

As you can see, the process consists of three very simple steps that correspond to the scenario described above:

  1. Trigger screenshot job
  2. Wait 60 seconds for the screenshots to be generated (Websiteshot doesn’t offer webhooks yet, but it’s on the roadmap)
  3. Upload screenshots

Step 1 and 3 are service tasks, for each of which we again implement a simple worker. The second step is a timer event.

Let’s build the workers

We need workers that interact with two services. What simplifies things enormously: both Websiteshot and AWS offer NodeJS SDKs that make integration very easy.

Create screenshots

The worker is quite simple, as the actual screenshot configuration takes place within Websiteshot. Templates can be created there, which contain the parameterization and all URLs.

templates

So that the service task can be used quite flexibly, we pass the TemplateId to be used as a service task header. With this approach we don’t have to touch the worker if we want to use different templates.

export class WebsiteshotWorker {
  constructor(private zeebeController: ZeebeController) {}

  public create() {
    this.zeebeController.getZeebeClient().createWorker({
      taskType: Worker.WEBSITESHOT_CREATE_JOB,
      taskHandler: async (job: any, complete: any, worker: any) => {
        const templateId = job.customHeaders.templateid;

        if (!templateId) {
          complete.failure("Template Id not set as header <templateid>");
          return;
        }

        logger.info(`Creating Screenshot Job for Template Id ${templateId}`);

        const screenshotController = new ScreenshotController({
          projectId: ConfigController.get(
            ConfigParameter.WEBSITESHOT_PROJECT_ID
          ),
          apikey: ConfigController.get(ConfigParameter.WEBSITESHOT_API_KEY),
        });

        try {
          const response = await screenshotController.create(templateId);
          complete.success({ jobId: response.jobId });
        } catch (error) {
          logger.error(error);
          complete.failure("Failed to create screenshot job via websiteshot");
        }
      },
    });
  }
}

Websiteshot integration is not worth mentioning with the Library:

const response: CreateResponse = await this.websiteshotController.create({
  templateId,
});

Upload created screenshots

After the first worker has started the screenshot job, the second worker takes care of the next steps:

For this reason the worker is a bit more extensive:

export class BucketWorker {
  constructor(private zeebeController: ZeebeController) {}

  public create() {
    this.zeebeController.getZeebeClient().createWorker({
      taskType: Worker.AWS_BUCKET_UPLOAD,
      taskHandler: async (job: any, complete: any, worker: any) => {
        const jobId = job.variables.jobId;
        if (!jobId) {
          complete.failure("Job Id not found on process context: <jobId>");
          return;
        }

        const screenshotController = new ScreenshotController({
          projectId: ConfigController.get(
            ConfigParameter.WEBSITESHOT_PROJECT_ID
          ),
          apikey: ConfigController.get(ConfigParameter.WEBSITESHOT_API_KEY),
        });

        const bucketController = new BucketController(
          {
            id: ConfigController.get(ConfigParameter.AWS_SECRET_ID),
            secret: ConfigController.get(ConfigParameter.AWS_SECRET_KEY),
          },
          ConfigController.get(ConfigParameter.AWS_BUCKET)
        );

        try {
          const getResponse: GetResponse = await screenshotController.get(
            jobId
          );
          const files: Array<{
            url: string;
            name: string;
          }> = getResponse.jobs.map((screenshotJob) => {
            return {
              url: screenshotJob.data,
              name: `${screenshotJob.url.name}.png`,
            };
          });
          files.forEach((file) => logger.info(`name: ${file.name}`));
          const downloadPromises = files.map((file) =>
            DownloadController.download(file.url, file.name)
          );
          await Promise.all(downloadPromises);

          logger.info(`Uploading Screenshots to Cloud Bucket`);

          const uploadPromises = files.map((file) =>
            bucketController.upload(
              Path.resolve(__dirname, `../..`, DOWNLOAD_FOLDER, file.name),
              file.name
            )
          );
          await Promise.all(uploadPromises);

          complete.success({ screenshots: uploadPromises.length });
        } catch (error) {
          complete.failure("Failed to send slack message");
        }
      },
    });
  }
}

Let’s take the Worker apart a bit.

Which job?

As a parameter, the worker gets the JobId from the process context. The first worker has written the JobId returned from Websiteshot to the process context at the end. So easy game!

Which screenshots?

We are using the Websiteshot NodeJS client again for this. Easy peasy. Somehow it doesn’t get more sophisticated…

Intermediate step

In order for us to upload the screenshots to the cloud bucket we need to have them available. We take the easy way and save the screenshots temporarily before uploading them again. For this, we don’t need to do anything more than execute a few GET requests. In NodeJS this is done with a few lines of code :)

Finale Grande

This is the central task of the worker. The previous three steps were just the preparation for this step. But even this part is pretty manageable with the help of the AWS SDK.

Yikes, are we done already? Yes! In fact, with this process and the associated workers, we’ve done everything we need to take screenshots of pre-configured URLs.

And now?

Now comes the concrete example: Camunda Cloud provides a console through which users can manage clusters and clients. Now I want to have screenshots taken from the Console using a test account. For this purpose I have created the following template:

screenshots

I use the process shown above exactly the same way to deploy and run it in Camunda Cloud. To start a new instance you can use Restzeebe again. Once the workers are registered the service tasks are processed.

process instance

The results can be viewed via the Websiteshot Console:

websiteshot overview

And our screenshots end up in S3:

aws

So, without further ado, in the last few minutes we built a process that automatically takes screenshots from the Cloud Console. I don’t need to mention that the URLs can be replaced quite easily. We can create as many other templates as we want and just reuse the same process. We just need to adjust the header parameter. Pretty cool I think!

You can also view, fork and modify the complete implementation in this repo: https://github.com/websiteshot/camunda-cloud-example

As with the last blog posts in this series: the process can easily be extended or the flow changed. For example, if you want to use the screenshots to automatically update the documentation, you can add an approval process. If you have read the tutorial with the Trello Cards you can for example create a new Trello Card on a specific board. A responsible person can then first look at the screenshots and either approve them for upload or reject them. In case of rejection, a specific message can be sent to a Slack channel because a view is not rendered correctly.

workflow

Another nice use case is the automated generation of social share images of conference speakers: at a conference there are many speakers who like to be announced via social media. Here, a template based on HTML and CSS can be parameterized so that only the parameters need to be changed. A process could eventually generate the social share images and publish them to various social media platforms. Create the template once and sit back!

Maybe this tutorial inspired you to automate the generation of your screenshots with the help of processes. If so, I look forward to your reports! And with the time gained, you can now take care of more important things, such as:

footer


Let me know if the article was helpful! And if you like the content follow me on Twitter, LinkedIn or GitHub :)


Header Photo by ShareGrid on Unsplash, last Photo by Rémi Bertogliati on Unsplash.

github node typescript github-actions
Published on 2021-02-14, last updated on 2024-04-21 by Adam
Comments or questions? Open a new discussion on github.
Adam Urban

Adam Urban is fullstack engineer, loves serverless and generative art, and is building side projects like weeklyfoo.com, flethy.com and diypunks.xyz in his free time.