Skip to content

Browser disconnects unexpectedly while scraping #1183

@yassinbahri

Description

@yassinbahri

So I am trying to cloud connection to scrape TradingView, but each time I run the script it fails, should I be doing this with a locally run lightpanda browser or what is the issue?

 * ------------------------------------------------
 *
 * Description:
 *  - Uses Puppeteer Core connected to a remote browser (Lightpanda Cloud or similar)
 *  - Captures TradingView chart screenshots with optional indicators
 *  - Manages a browser session with auto-timeout and a request queue
 *
 * Environment Variables:
 *  - LIGHTPANDA_TOKEN: Token for remote browser connection (required)
 *  - TV_SESSION_ID: (optional) TradingView session cookie
 *  - TV_SESSION_ID_SIGN: (optional) TradingView session signature cookie
 *  - IGNORE_INDICATORS_CHARTS: (optional) Comma-separated chart IDs to skip indicator loading
 *  - BASE_URL: (optional) Override default TradingView base URL
 */

const puppeteer = require("puppeteer-core");

const BASE_URL = process.env.BASE_URL || "https://www.tradingview.com";

let browserPage = undefined;
let browser = undefined;
let browserLastUsed = Date.now();
let isLightpanda = false;

const BROWSER_IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
const screenshotQueue = [];
let isProcessingScreenshot = false;

// Get or create Puppeteer browser connection
const getBrowser = async () => {
  console.log("[Puppeteer] Setup start:", new Date().toISOString());

  try {
    if (!browser) {
      const lightpandaToken = process.env.LIGHTPANDA_TOKEN;
      if (!lightpandaToken) throw new Error("LIGHTPANDA_TOKEN is required");

      console.log("[Puppeteer] Connecting to remote browser (Lightpanda)...");
      const wsEndpoint = `wss://euwest.cloud.lightpanda.io/ws?token=${lightpandaToken}`;

      browser = await puppeteer.connect({
        browserWSEndpoint: wsEndpoint,
        defaultViewport: null
      });

      isLightpanda = true;
      console.log("[Puppeteer] Connected successfully");

      browser.on("disconnected", () => {
        console.error("[Puppeteer] Browser disconnected unexpectedly");
        browser = null;
        browserPage = null;
        isLightpanda = false;
      });
    }

    // Create new page if needed
    if (!browserPage || browserPage.isClosed()) {
      console.log("[Puppeteer] Creating new page...");
      browserPage = await browser.newPage();
      await browserPage.setViewport({ width: 1920, height: 1080 });

      // Optional cookies
      if (process.env.TV_SESSION_ID && process.env.TV_SESSION_ID_SIGN) {
        await browserPage.setCookie(
          { name: "sessionid", value: process.env.TV_SESSION_ID, domain: new URL(BASE_URL).hostname, path: "/" },
          { name: "sessionid_sign", value: process.env.TV_SESSION_ID_SIGN, domain: new URL(BASE_URL).hostname, path: "/" }
        );
        console.log("[Puppeteer] Added TradingView cookies");
      }

      // Clipboard permissions
      try {
        const context = browser.defaultBrowserContext();
        await context.overridePermissions(BASE_URL, ["clipboard-read", "clipboard-write"]);
      } catch {
        console.log("[Puppeteer] Clipboard permissions not supported (non-critical)");
      }

      // Request interception (skip ads/tracking)
      await browserPage.setRequestInterception(true);
      browserPage.on("request", (request) => {
        const url = request.url();
        const blockList = ["adzerk", "doubleclick", "google-analytics", "mixpanel", "zedo"];
        if (blockList.some((e) => url.includes(e))) request.abort();
        else request.continue();
      });
    }

    console.log("[Puppeteer] Browser ready");
    return browserPage;
  } catch (error) {
    console.error("[Puppeteer] Setup failed:", error.message);
    throw error;
  }
};

// Close browser cleanly
const closeBrowser = async () => {
  try {
    if (browserPage) await browserPage.close();
    if (browser) await browser.disconnect();
    browser = undefined;
    browserPage = undefined;
    console.log("[Puppeteer] Browser closed");
  } catch (error) {
    console.error("[Puppeteer] Error closing browser:", error.message);
  }
};

// Auto-close after idle timeout
setInterval(() => {
  if (browserPage && Date.now() - browserLastUsed > BROWSER_IDLE_TIMEOUT) {
    console.log("[Puppeteer] Idle timeout reached — closing browser");
    closeBrowser();
  }
}, 2 * 60 * 1000);

// Process queue
const processScreenshotQueue = async () => {
  if (isProcessingScreenshot || screenshotQueue.length === 0) return;
  isProcessingScreenshot = true;

  const { chart, ticker, timeframe, indicator, resolve, reject } = screenshotQueue.shift();
  console.log(`[Queue] Processing screenshot (remaining: ${screenshotQueue.length})`);

  try {
    const result = await screenshotInternal(chart, ticker, timeframe, indicator);
    resolve(result);
  } catch (error) {
    reject(error);
  } finally {
    isProcessingScreenshot = false;
    if (screenshotQueue.length > 0) setImmediate(processScreenshotQueue);
  }
};

// Public screenshot API
const screenshot = async (chart, ticker, timeframe = null, indicator = null) => {
  return new Promise((resolve, reject) => {
    screenshotQueue.push({ chart, ticker, timeframe, indicator, resolve, reject });
    console.log(`[Queue] Queued screenshot (length: ${screenshotQueue.length})`);
    processScreenshotQueue();
  });
};

// Internal screenshot function
const screenshotInternal = async (chart, ticker, timeframe = null, indicator = null) => {
  const id = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
  console.log(`[Screenshot:${id}] Start for ${chart}/${ticker}`);

  const url = `${BASE_URL}/chart/${chart}?symbol=${ticker}${timeframe ? `&interval=${timeframe}` : ""}`;
  try {
    browserPage = browserPage || (await getBrowser());
    browserLastUsed = Date.now();

    await browserPage.goto(url, { waitUntil: "domcontentloaded" });
    await browserPage.waitForSelector("#header-toolbar-screenshot", { visible: true, timeout: 15000 });

    console.log(`[Screenshot:${id}] Chart loaded successfully`);

    // Optional: add indicator
    const ignoredCharts = (process.env.IGNORE_INDICATORS_CHARTS || "").split(",");
    if (indicator && !ignoredCharts.includes(chart)) {
      try {
        await browserPage.keyboard.press("Slash");
        await browserPage.waitForSelector("#indicators-dialog-search-input", { visible: true });
        await browserPage.type("#indicators-dialog-search-input", indicator, { delay: 20 });
        await browserPage.waitForSelector(`[data-role="list-item"][data-title*="${indicator}"]`, { visible: true });
        await browserPage.click(`[data-role="list-item"][data-title*="${indicator}"]`);
        await browserPage.keyboard.press("Escape");
        console.log(`[Screenshot:${id}] Indicator '${indicator}' added`);
      } catch {
        console.log(`[Screenshot:${id}] Failed to add indicator (non-critical)`);
      }
    }

    // Trigger TradingView screenshot
    await browserPage.keyboard.down("Alt");
    await browserPage.keyboard.press("KeyS");
    await browserPage.keyboard.up("Alt");

    await browserPage.waitForFunction(
      () => document.body.innerText.includes("copied to clipboard"),
      { timeout: 30000 }
    );

    const imageUrl = await browserPage.evaluate("navigator.clipboard.readText()");
    const imageId = imageUrl.split("/").reverse()[1];
    const finalUrl = `${BASE_URL.replace("www", "s3")}/snapshots/${imageId[0].toLowerCase()}/${imageId}.png`;

    console.log(`[Screenshot:${id}] Completed: ${finalUrl}`);
    return finalUrl;
  } catch (error) {
    console.error(`[Screenshot:${id}] Failed: ${error.message}`);
    return undefined;
  }
};

module.exports = { screenshot, closeBrowser };

output:

[QUEUE] Screenshot queued. Queue length: 1
[QUEUE] Processing screenshot request. Queue length: 0
[SCREENSHOT:1761576969880] START
[SCREENSHOT:1761576969880] Chart: *******, Ticker: ********, Timeframe: 30, Indicator: none
[SCREENSHOT:1761576969880] URL: https://www.tradingview.com/chart/*******?symbol=******&interval=30
[SCREENSHOT:1761576969880] Getting browser page...
[INFO] Setup start: 2025-10-27T14:56:09.880Z
[INFO] Connecting to remote browser...
[INFO] Connected to remote browser successfully
[INFO] Creating new page...
[INFO] New page created
[INFO] Cookies added (sessionid + sessionid_sign)
[INFO] Clipboard permissions not supported - continuing anyway
[INFO] Skipping request interception for remote browser
[INFO] Setup complete: 2025-10-27T14:56:11.019Z
[SCREENSHOT:1761576969880] Browser page ready
[SCREENSHOT:1761576969880] Navigating to chart URL...
[ERROR] Browser disconnected unexpectedly
[SCREENSHOT:1761576969880] SCREENSHOT FAILED
[ERROR] Navigating frame was detached
    at LifecycleWatcher.js
    at CdpFrame.goto
    at screenshotInternal
    at processScreenshotQueue
[SCREENSHOT:1761576969880] Full error: { "cause": {} }
[INFO] Screenshot failed - returned undefined
[INFO] Screenshot request processing complete (partial failure)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions