This post is the last post of a series of three posts about how I created isitblackfridayyet.app.
Full story here

As I explained earlier, isitblackfridayyet.app product needed a way to tweet once a day if it is black friday or not as an image (that makes it more challenging than just text.). @isitblackfriday was created.

Setup Firebase

Create the project on the Firebase console page

Create your firebase project

On the project is set up, install Firebase CLI on your machine:

$ npm install -g firebase-tools

Sign in to Google to have access to your Firebase account.

$ firebase login

Initiate your Firebase project locally and select Functions.

$ firebase init

? Which Firebase CLI features do you want to set up for this folder?
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
❯◉ Functions: Configure and deploy Cloud Functions
 ◯ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules

Selecte whether you want to use JavaScript or TypeScript to write functions (please select TypeScript), then folow the rest of the wizard.

? What language would you like to use to write Cloud Functions?
  JavaScript
❯ TypeScript

once the installation is over, you will get the following functions folder:

functions filesystem

Run the hello world function locally

uncomment the following from functions/src/index.ts

export const helloWorld = functions.https.onRequest((request, response) => {
  response.send("Hello from Firebase!");
});

run npm run serve, you should have the hello world functions available on http://localhost:5000/<Firebase-project-id>/us-central1/helloWorld

Deploy the hello world function on the cloud

$ npm run deploy

now your function runs on Firebase servers on https://us-central1-<Firebase-project-id>.cloudfunctions.net/helloWorld!

Let’s create something fancier now shall we?

Create an image from a Web page

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium.

We are going to use it to create an image (png or jpg) from an online page.

Install Puppeteer

npm i puppeteer @types/puppeteer

Create a function to take a screenshot

We want that function to open https://isitblackfridayyet.app, take a screenshot of the page, save it and return the image path.

Here is the code with comments:

import * as puppeteer from "puppeteer";
import * as os from "os";
import * as fs from "fs";
import * as path from "path";

async function takeScreenshot(): Promise<string> {
  // #1 create a browser instance
  const browser = await puppeteer.launch({
    headless: true,
    args: ["--no-sandbox", "--disable-setuid-sandbox"]
  });

  // #2 create a browser page with the required viewport
  const page = await browser.newPage();
  await page.setViewport({ width: 512, height: 512 });
  await page.goto("https://isitblackfridayyet.app/", {
    // consider navigation to be finished when there are
    // no more than 0 network connections for at least 500 ms.
    waitUntil: "networkidle0"
  });

  // #3 create a new file path
  const localFile = path.join(os.tmpdir(), `${Date.now()}.png`);

  // #4 generate the screenshot using this path
  await page.screenshot({
    type: "png",
    path: localFile
  });

  return localFile;
}

Create a function to download the image

we can reuse the helloWorld function to download the isitblackfridayyet.app screenshot.

export const helloWorld = functions
  .runWith({ memory: '1GB', timeoutSeconds: 60 })
  .https.onRequest(async (request, response) => {
    try {
      // #1 call takeScreenshot function
      const localFile = await takeScreenshot();

      // #2 download the image
      return response.download(localFile, () => {
        // #3 once the image is downloaded we can erase it.
        fs.unlinkSync(localFile);
      });
    } catch (error) {
      return response.json({ error });
    }
  }
);

now re-run the functions locally and open http://localhost:5000/<Firebase-project-id>/us-central1/helloWorld function. You should be able to download the image.

functions filesystem

The real functions for isitblackfridayyet.app are open sourced here

Trigger functions via a CRON job

We can now create images from any webpage but we still need to trigger the function manually! To fix that problem on a unix system, we would use a Cron job. But how to use a Cron job on Firebase functions ?

Firebase supports scheduling function calls via the Cloud Scheduler and pubsub.

The following two functions will run every 2 minutes. As you can see you can use the crontab way */2 * * * * or jsut plain english.

export const scheduledFunction = functions.pubsub
  .schedule("*/2 * * * *")
  .onRun(context => {
    console.log("This will be run every 2 minutes!");
  });

export const scheduledFunctionPlainEnglish = functions.pubsub
  .schedule("every 2 minutes")
  .onRun(context => {
    console.log("This will be run every 2 minutes!");
  });

When you will try to deploy that you will get the following error message: Error: HTTP Error: 400, Billing must be enabled for activation of service '[cloudscheduler.googleapis.com]' in project 'XXXXXXXX' to proceed.

Your project must be on the Blaze payment plan, as Cloud Scheduler and Pub/Sub require billing information. Each Cloud Scheduler job costs $0.10 (USD) per month.

To enable Blaze payment plan open the Firebase console of your project and go check the bottom left corner of the page:

firebase default plan

Click upgrade and select Pay as you go on next screen:

firebase different plans

Re-deploy your code and open GCP Cloud Scheduler page to see the two jobs:

firebase different plans

Now go to the Firebase functions logs page to see that the functions are already running!

firebase different plans

We now have everything that we need to schedule image creation on Firebase functions. To know exactly how @isitblackfriday does it, the code is open sourced here!