Files
firecrawl/apps/api/src/scraper/WebScraper/scrapers/fireEngine.ts
T

207 lines
7.7 KiB
TypeScript
Raw Normal View History

2024-07-03 18:01:17 -03:00
import axios from "axios";
import { Action, FireEngineOptions, FireEngineResponse } from "../../../lib/entities";
2024-07-03 18:01:17 -03:00
import { logScrape } from "../../../services/logging/scrape_log";
import { generateRequestParams } from "../single_url";
import { fetchAndProcessPdf } from "../utils/pdfProcessor";
import { universalTimeout } from "../global";
2024-07-23 17:30:46 -03:00
import { Logger } from "../../../lib/logger";
2024-08-22 23:37:23 +02:00
import * as Sentry from "@sentry/node";
2024-07-03 18:01:17 -03:00
2024-07-03 18:01:54 -03:00
/**
* Scrapes a URL with Fire-Engine
* @param url The URL to scrape
* @param waitFor The time to wait for the page to load
* @param screenshot Whether to take a screenshot
* @param fullPageScreenshot Whether to take a full page screenshot
2024-07-03 18:01:54 -03:00
* @param pageOptions The options for the page
* @param headers The headers to send with the request
* @param options The options for the request
* @returns The scraped content
*/
2024-07-03 18:01:17 -03:00
export async function scrapWithFireEngine({
url,
actions,
2024-08-19 16:41:54 -03:00
pageOptions = { parsePDF: true, atsv: false, useFastMode: false, disableJsDom: false },
2024-07-12 22:02:08 -04:00
fireEngineOptions = {},
2024-07-03 18:01:17 -03:00
headers,
options,
2024-08-15 19:04:46 +02:00
priority,
2024-08-19 16:41:54 -03:00
teamId,
2024-07-03 18:01:17 -03:00
}: {
url: string;
actions?: Action[];
2024-08-19 16:41:54 -03:00
pageOptions?: { scrollXPaths?: string[]; parsePDF?: boolean, atsv?: boolean, useFastMode?: boolean, disableJsDom?: boolean };
2024-07-12 22:02:08 -04:00
fireEngineOptions?: FireEngineOptions;
2024-07-03 18:01:17 -03:00
headers?: Record<string, string>;
options?: any;
2024-08-15 19:04:46 +02:00
priority?: number;
2024-08-19 16:41:54 -03:00
teamId?: string;
2024-07-03 18:01:17 -03:00
}): Promise<FireEngineResponse> {
const logParams = {
url,
scraper: "fire-engine",
success: false,
response_code: null,
time_taken_seconds: null,
error_message: null,
html: "",
startTime: Date.now(),
};
try {
const reqParams = await generateRequestParams(url);
2024-09-09 21:06:23 -03:00
let engineParam = reqParams["params"]?.engine ?? reqParams["params"]?.fireEngineOptions?.engine ?? fireEngineOptions?.engine ?? "chrome-cdp";
2024-08-19 16:41:54 -03:00
let fireEngineOptionsParam : FireEngineOptions = reqParams["params"]?.fireEngineOptions ?? fireEngineOptions;
2024-07-12 23:20:26 -04:00
let endpoint = "/scrape";
if(options?.endpoint === "request") {
endpoint = "/request";
}
2024-07-18 13:19:44 -04:00
let engine = engineParam; // do we want fireEngineOptions as first choice?
2024-07-12 23:20:26 -04:00
2024-08-19 16:41:54 -03:00
if (pageOptions?.useFastMode) {
fireEngineOptionsParam.engine = "tlsclient";
engine = "tlsclient";
}
2024-09-05 14:24:10 -03:00
Logger.info(
`⛏️ Fire-Engine (${engine}): Scraping ${url} | params: { actions: ${JSON.stringify((actions ?? []).map(x => x.type))}, method: ${fireEngineOptionsParam?.method ?? "null"} }`
2024-09-05 14:24:10 -03:00
);
2024-08-19 16:41:54 -03:00
// atsv is only available for beta customers
const betaCustomersString = process.env.BETA_CUSTOMERS;
const betaCustomers = betaCustomersString ? betaCustomersString.split(",") : [];
2024-08-20 21:38:11 -03:00
2024-08-19 16:41:54 -03:00
if (pageOptions?.atsv && betaCustomers.includes(teamId)) {
fireEngineOptionsParam.atsv = true;
} else {
pageOptions.atsv = false;
}
2024-08-02 19:25:15 -04:00
const axiosInstance = axios.create({
headers: { "Content-Type": "application/json" }
});
const startTime = Date.now();
2024-08-22 23:37:23 +02:00
const _response = await Sentry.startSpan({
name: "Call to fire-engine"
}, async span => {
2024-09-05 14:24:10 -03:00
2024-08-22 23:37:23 +02:00
return await axiosInstance.post(
process.env.FIRE_ENGINE_BETA_URL + endpoint,
{
url: url,
headers: headers,
disableJsDom: pageOptions?.disableJsDom ?? false,
priority,
engine,
instantReturn: true,
...fireEngineOptionsParam,
2024-09-05 14:16:31 -03:00
atsv: pageOptions?.atsv ?? false,
scrollXPaths: pageOptions?.scrollXPaths ?? [],
actions: actions,
2024-08-22 23:37:23 +02:00
},
{
headers: {
"Content-Type": "application/json",
...(Sentry.isInitialized() ? ({
"sentry-trace": Sentry.spanToTraceHeader(span),
"baggage": Sentry.spanToBaggageHeader(span),
}) : {}),
}
}
2024-08-22 23:37:23 +02:00
);
});
2024-07-03 18:01:17 -03:00
2024-09-18 21:34:09 +02:00
const waitTotal = (actions ?? []).filter(x => x.type === "wait").reduce((a, x) => (x as { type: "wait"; milliseconds: number; }).milliseconds + a, 0);
let checkStatusResponse = await axiosInstance.get(`${process.env.FIRE_ENGINE_BETA_URL}/scrape/${_response.data.jobId}`);
while (checkStatusResponse.data.processing && Date.now() - startTime < universalTimeout + waitTotal) {
2024-09-13 16:41:27 +02:00
await new Promise(resolve => setTimeout(resolve, 250)); // wait 0.25 seconds
checkStatusResponse = await axiosInstance.get(`${process.env.FIRE_ENGINE_BETA_URL}/scrape/${_response.data.jobId}`);
}
if (checkStatusResponse.data.processing) {
2024-08-21 10:35:50 -03:00
Logger.debug(`⛏️ Fire-Engine (${engine}): deleting request - jobId: ${_response.data.jobId}`);
axiosInstance.delete(
process.env.FIRE_ENGINE_BETA_URL + `/scrape/${_response.data.jobId}`, {
validateStatus: (status) => true
}
).catch((error) => {
Logger.debug(`⛏️ Fire-Engine (${engine}): Failed to delete request - jobId: ${_response.data.jobId} | error: ${error}`);
});
2024-08-20 15:42:39 -03:00
Logger.debug(`⛏️ Fire-Engine (${engine}): Request timed out for ${url}`);
logParams.error_message = "Request timed out";
2024-09-18 20:39:25 +02:00
return { html: "", pageStatusCode: null, pageError: "" };
}
if (checkStatusResponse.status !== 200 || checkStatusResponse.data.error) {
2024-07-23 17:30:46 -03:00
Logger.debug(
2024-09-18 21:34:09 +02:00
`⛏️ Fire-Engine (${engine}): Failed to fetch url: ${url} \t status: ${checkStatusResponse.status}\t ${checkStatusResponse.data.error}`
2024-07-03 18:01:17 -03:00
);
2024-07-12 23:20:26 -04:00
logParams.error_message = checkStatusResponse.data?.pageError ?? checkStatusResponse.data?.error;
logParams.response_code = checkStatusResponse.data?.pageStatusCode;
2024-07-12 14:59:49 -04:00
if(checkStatusResponse.data && checkStatusResponse.data?.pageStatusCode !== 200) {
Logger.debug(`⛏️ Fire-Engine (${engine}): Failed to fetch url: ${url} \t status: ${checkStatusResponse.data?.pageStatusCode}`);
2024-07-12 14:59:49 -04:00
}
const pageStatusCode = checkStatusResponse.data?.pageStatusCode ? checkStatusResponse.data?.pageStatusCode : checkStatusResponse.data?.error && checkStatusResponse.data?.error.includes("Dns resolution error for hostname") ? 404 : undefined;
2024-07-03 18:01:17 -03:00
return {
html: "",
pageStatusCode,
pageError: checkStatusResponse.data?.pageError ?? checkStatusResponse.data?.error,
2024-07-03 18:01:17 -03:00
};
}
2024-08-20 21:16:33 -03:00
const contentType = checkStatusResponse.data.responseHeaders?.["content-type"];
2024-07-03 18:01:17 -03:00
if (contentType && contentType.includes("application/pdf")) {
const { content, pageStatusCode, pageError } = await fetchAndProcessPdf(
url,
pageOptions?.parsePDF
);
logParams.success = true;
2024-07-03 18:06:53 -03:00
logParams.response_code = pageStatusCode;
logParams.error_message = pageError;
2024-09-18 20:39:25 +02:00
return { html: content, pageStatusCode, pageError };
2024-07-03 18:01:17 -03:00
} else {
const data = checkStatusResponse.data;
2024-07-03 18:01:17 -03:00
logParams.success =
(data.pageStatusCode >= 200 && data.pageStatusCode < 300) ||
data.pageStatusCode === 404;
logParams.html = data.content ?? "";
logParams.response_code = data.pageStatusCode;
logParams.error_message = data.pageError ?? data.error;
2024-07-03 18:01:17 -03:00
return {
html: data.content ?? "",
2024-09-18 20:39:25 +02:00
screenshots: data.screenshots,
2024-07-03 18:01:17 -03:00
pageStatusCode: data.pageStatusCode,
pageError: data.pageError ?? data.error,
2024-07-03 18:01:17 -03:00
};
}
} catch (error) {
if (error.code === "ECONNABORTED") {
2024-07-23 17:30:46 -03:00
Logger.debug(`⛏️ Fire-Engine: Request timed out for ${url}`);
2024-07-03 18:01:17 -03:00
logParams.error_message = "Request timed out";
} else {
2024-07-23 17:30:46 -03:00
Logger.debug(`⛏️ Fire-Engine: Failed to fetch url: ${url} | Error: ${error}`);
2024-07-03 18:01:17 -03:00
logParams.error_message = error.message || error;
}
2024-09-18 20:39:25 +02:00
return { html: "", pageStatusCode: null, pageError: logParams.error_message };
2024-07-03 18:01:17 -03:00
} finally {
const endTime = Date.now();
2024-07-03 18:06:53 -03:00
logParams.time_taken_seconds = (endTime - logParams.startTime) / 1000;
2024-07-03 20:18:11 -03:00
await logScrape(logParams, pageOptions);
2024-07-03 18:01:17 -03:00
}
}
2024-07-03 18:06:53 -03:00