Skip to content

Lighthouse User Flow doens't work in Docker #15796

@FabianSchoenfeld

Description

@FabianSchoenfeld

FAQ

URL

https://wikipedia.org

What happened?

The user flow doesn't work in docker image:

File to reproduce:

package.json

  "name": "lighthouse-userflow",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "dependencies": {
    "chrome-launcher": "^1.1.0",
    "cron": "^2.3.0",
    "express": "^4.18.2",
    "lighthouse": "^11.4.0",
    "prom-client": "^14.2.0",
    "puppeteer": "^21.7.0"
  },
  "scripts": {
    "start": "node index.js"
  }
}

index.js

import { LighthouseMetricsExporter } from "./lighthouseMetricsExporter.js";

// WEB VITALS and more
const newLighthouseMetrics = new LighthouseMetricsExporter();
await newLighthouseMetrics.init();

lighthouseMetricsExporter.js

import { startFlow } from "lighthouse";
import puppeteer, { KnownDevices } from "puppeteer";
import express from "express";

const config = [
  {
    url: "https://wikipedia.org",
    label: "startpage",
    devices: ["mobile"],
    click: ".lang-list-button-text",
    wait: "#js-lang-lists",
  },
];
export class LighthouseMetricsExporter {
  isRunning = false;
  gaugeOne;
  cronJob;
  server;

  constructor() {
    this.fireTick = this.fireTick.bind(this);
    this.onTick = this.onTick.bind(this);
    this.onComplete = this.onComplete.bind(this);
  }

  async init() {
    const labelNames = [
      "audit",
      "score",
      "page",
      "group",
      "country",
      "device",
      "asset_url",
      "url",
    ];

    this.server = express();
    this.server.get("/", (_, res) => {
      res.status(200).end("ok");
    });

    this.server.get("/metrics", async (req, res) => {
      try {
        res.set("Content-Type", register.contentType);
        res.end(await register.metrics());
      } catch (ex) {
        console.log("Error: ", ex);
        res.status(500).end(ex.message);
      }
    });
    this.server.listen(3000);
    this.fireTick(); // starts immediately
  }

  async fireTick() {
    if (this.isRunning) {
      console.warn("This CRON is already in execution");
      return;
    }
    this.isRunning = true;
    try {
      await this.onTick.bind(this)();
    } catch (exception) {
      console.error("Tick stopped due to an error");
      console.error(exception);
    }
    this.isRunning = false;
  }

  async onTick() {
    const browser = await puppeteer.launch({
      headless: "new",
      executablePath: process.env.CHROME_PATH
        ? process.env.CHROME_PATH
        : undefined,
      args: [
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-dev-shm-usage",
        "--disable-accelerated-2d-canvas",
        "--no-first-run",
        "--no-zygote",
      ],
      timeout: 10_000, // 10 seconds
      protocolTimeout: 30_000, // 30 seconds
    });

    const flagsShared = {
      logLevel: "error",
      output: ["html"],
      onlyCategories: ["performance"],
      disableFullPageScreenshot: true,
    };

    const iPhone = KnownDevices["iPhone 6"];

    for (const { url, label, devices, click, wait } of config) {
      for (const device of devices) {
        const page = await browser.newPage();

        await page.emulate(iPhone);

        await page.goto(url);

        const flow = await startFlow(page, flagsShared);

        await flow.startTimespan();

        await page.click(click);
        await page.waitForSelector(wait);

        await flow.endTimespan();

        const flowResult = await flow.createFlowResult();

        this.printLighthouseMetric(flowResult.steps[0].lhr, label, device);
      }
    }
    await browser.close();
  }

  printLighthouseMetric(lighthouseReport, page, device) {
    const standardLabels = { page, device };
    const auditedItems = Object.keys(lighthouseReport.audits);
    auditedItems.forEach((auditItem) => {
      if (
        typeof lighthouseReport.audits[auditItem]?.numericValue === "number"
      ) {
        console.log(
          { audit: auditItem, ...standardLabels },
          lighthouseReport.audits[auditItem].numericValue
        );
      }
    });
    this.onComplete();
  }

  onComplete() {
    console.log("done");
    process.exit();
  }
}

Dockerfile

# Docker now supports running x86-64 (Intel) binaries on Apple silicon with Rosetta 2.
# https://www.docker.com/blog/docker-desktop-4-25/
#
# on arm you have to build for linux/amd64, e.g.: 
# docker build --platform linux/amd64 -f Dockerfile -t lighthouse-metrics-exporter .
# docker run --rm -p 3000:3000 --platform linux/amd64 -it --cap-add=SYS_ADMIN lighthouse-metrics-exporter:latest
#
# https://developer.chrome.com/blog/chrome-for-testing?hl=de
# https://github.com/puppeteer/puppeteer/blob/main/docker/Dockerfile

FROM node:21-bookworm-slim

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
ENV CHROME_DEBUG_PORT=9222
ENV CHROME_PATH=/usr/bin/google-chrome

RUN apt-get update && apt-get install gnupg wget -y && \
  wget --quiet --output-document=- https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor > /etc/apt/trusted.gpg.d/google-archive.gpg && \
  sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' && \
  apt-get update && \
  apt-get install google-chrome-stable -y --no-install-recommends && \
  rm -rf /var/lib/apt/lists/*

WORKDIR /usr/src/app

COPY index.js lighthouseMetricsExporter.js package.json package-lock.json ./

RUN npm install

EXPOSE 3000

CMD ["node", "index.js"]

What did you expect?

Working like "node index.js". A complete user flow, with report, but at point await flow.startTimespan(); it stuck

What have you tried?

  • ChromeLauncher
  • different docker images
  • different configurations for chromeLauncher, puppeteer

How were you running Lighthouse?

node, Other

Lighthouse Version

10.x

Chrome Version

latest

Node Version

21

OS

docker / debian

Relevant log output

*nodejs:*
> node index.js
{ audit: 'total-blocking-time', page: 'startpage', device: 'mobile' } 0
{
  audit: 'cumulative-layout-shift',
  page: 'startpage',
  device: 'mobile'
} 0
{
  audit: 'interaction-to-next-paint',
  page: 'startpage',
  device: 'mobile'
} 88
{
  audit: 'mainthread-work-breakdown',
  page: 'startpage',
  device: 'mobile'
} 178.5600000000001
{ audit: 'bootup-time', page: 'startpage', device: 'mobile' } 5.212
{ audit: 'uses-long-cache-ttl', page: 'startpage', device: 'mobile' } 0
{ audit: 'total-byte-weight', page: 'startpage', device: 'mobile' } 0
done
^C
> node index.js
{ audit: 'total-blocking-time', page: 'startpage', device: 'mobile' } 0
{
  audit: 'cumulative-layout-shift',
  page: 'startpage',
  device: 'mobile'
} 0
{
  audit: 'interaction-to-next-paint',
  page: 'startpage',
  device: 'mobile'
} 88
{
  audit: 'mainthread-work-breakdown',
  page: 'startpage',
  device: 'mobile'
} 202.50999999999993
{ audit: 'bootup-time', page: 'startpage', device: 'mobile' } 6.053
{ audit: 'uses-long-cache-ttl', page: 'startpage', device: 'mobile' } 0
{ audit: 'total-byte-weight', page: 'startpage', device: 'mobile' } 0
done

*docker:*
Tick stopped due to an error
ProtocolError: Runtime.evaluate timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.
    at <instance_members_initializer> (file:///usr/src/app/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:92:14)
    at new Callback (file:///usr/src/app/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:96:16)
    at CallbackRegistry.create (file:///usr/src/app/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:19:26)
    at Connection._rawSend (file:///usr/src/app/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/Connection.js:77:26)
    at CdpCDPSession.send (file:///usr/src/app/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/CDPSession.js:63:33)
    at #evaluate (file:///usr/src/app/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/ExecutionContext.js:178:18)
    at ExecutionContext.evaluateHandle (file:///usr/src/app/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/ExecutionContext.js:166:36)
    at file:///usr/src/app/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/ExecutionContext.js:55:29
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async LazyArg.get (file:///usr/src/app/node_modules/puppeteer-core/lib/esm/puppeteer/common/LazyArg.js:20:16)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions